VZ Chat API

Быстрый старт

User API делает от вашего имени всё, что вы делаете в приложении VZ Chat: пишет сообщения, заводит чаты и группы, меняет профиль. Адрес API — https://api.vz-chat.com/v1.

  1. Войдите в кабинет по номеру телефона и выпустите ключ.
  2. Передавайте его в каждом запросе: Authorization: Bearer vzc_…
  3. Напишите себе в «Избранное»:
CHAT=$(curl -s https://api.vz-chat.com/v1/chats/saved -H "Authorization: Bearer $KEY" | jq -r .data.id)

curl -X POST https://api.vz-chat.com/v1/chats/$CHAT/messages \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Привет из API"}'

Ключи и права

Ключ начинается с vzc_ и показывается один раз — при выпуске. У нас хранится только его хеш: восстановить ключ нельзя, можно отозвать и выпустить новый. Заводите отдельный ключ на каждую интеграцию — тогда любую можно отключить, не трогая остальные.

ПравоЧто даёт
readЧитать чаты, сообщения, людей, события; скачивать вложения
writeПисать и удалять сообщения, заводить и менять чаты, отмечать прочитанное
profileМенять свой профиль и фотографию, записную книжку
webhooksЗаводить и настраивать вебхуки

Прав у ключа не больше, чем у вас самих: чужие чаты он не видит, чужую группу не переименует.

Ответы, ошибки, лимиты

Всё — JSON, поля в snake_case, время — ISO 8601 с поясом. Объект приходит в data, список — массивом в data. Идентификаторы — ULID: строки, которые растут со временем, поэтому их можно сравнивать.

Ошибки

HTTP/1.1 422 Unprocessable Content
{
  "message": "Поле Текст обязательно для заполнения.",
  "errors": { "text": ["Поле Текст обязательно для заполнения."] }
}
401Ключа нет, он отозван или истёк
403У ключа нет нужного права — или действие запрещено вам самим
404Не найдено. Чужой чат тоже «не найден»: по ответу нельзя узнать, что он есть
422Ошибка в данных; подробности в errors
429Превышен лимит; подождите столько секунд, сколько в Retry-After

Лимиты

300 запросов в минуту на ключ, из них не больше 60 отправок сообщений.

Профиль

GET /me · read

Ваш профиль — с телефоном и датой рождения, которых другим не видно.

PATCH /me · profile

Передайте только то, что меняете: first_name, last_name, bio (до 200 знаков), birthday (ГГГГ-ММ-ДД), nickname (латиница, цифры, «_», 5–32 знака).

curl -X PATCH https://api.vz-chat.com/v1/me -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"bio": "Строю дома"}'

POST /me/avatar · profile

Фотография JPEG, PNG или WebP до 8 МБ — файлом (file), ссылкой (url) или base64. DELETE /me/avatar убирает её.

Чаты

GET /chats · read

Все ваши чаты, свежие сверху, с последним сообщением и непрочитанным.

GET /chats/saved · read

«Избранное» — чат с самим собой. Есть у каждого.

GET /chats/{id} · read

POST /chats · write

Личка с человеком — второй раз не заводится, вернётся существующая. Себе — это «Избранное».

{"type": "direct", "user_id": "01k…"}

Группа: вы — владелец, остальные — участники.

{
  "type": "group",
  "title": "Стройка на Кленовой",
  "member_ids": ["01k…", "01k…"],
  "avatar": {"url": "https://example.com/house.jpg"}
}

PATCH /chats/{id} · write

Только группа: title, description, avatar (url или base64), remove_avatar.

POST /chats/{id}/read · write

Отмечает прочитанным всё до message_id включительно; без него — всё. Собеседник увидит две синие галочки.

DELETE /chats/{id} · write

У себя; с {"for_everyone": true} — у всех (группу так удаляет только владелец). «Избранное» не удаляется, а очищается.

Участники группы

Управляют составом владелец и админы, назначать админов может только владелец.

GET /chats/{id}/members · read

POST /chats/{id}/members · write

{"user_ids": ["01k…", "01k…"]}

DELETE /chats/{id}/members/{user_id} · write

PUT /chats/{id}/members/{user_id}/role · write

{"role": "admin"}   // или "member"

Сообщения

GET /chats/{id}/messages · read

Лента по времени. Без параметров — последние 50. Листайте курсором: before=<id> — старше, after=<id> — новее, limit — до 100. has_more скажет, есть ли ещё.

curl "https://api.vz-chat.com/v1/chats/$CHAT/messages?before=01k…&limit=100" -H "Authorization: Bearer $KEY"

GET /chats/{id}/messages/{message_id} · read

POST /chats/{id}/messages · write

Текст, ответ, вложения, стикер или голосовое. Нужно хотя бы что-то одно.

{
  "text": "Смета во вложении",
  "reply_to_id": "01k…",
  "client_id": "7b3f1c2e-…",
  "attachments": [
    {"url": "https://example.com/smeta.pdf"},
    {"base64": "iVBORw0KGgo…", "name": "план.png"},
    {"url": "https://example.com/photo.jpg", "as_file": true}
  ]
}
  • client_id — ваш UUID против дублей: повтор с тем же ключом вернёт то же сообщение, а не второе. Ставьте его всегда, когда запрос могут повторить.
  • as_file — картинку прислать документом, без сжатия.
  • sticker — идентификатор стикера вместо текста.
  • voice — голосовое: {"url"|"base64": …, "duration_ms": 3200, "waveform": [0…100]}, звук m4a/aac/mp3.

DELETE /chats/{id}/messages/{message_id} · write

У себя; с {"for_everyone": true} — у всех, но только своё.

Файлы

Любой файл — вложение, аватар — можно передать тремя способами:

  • Загрузкой (multipart): attachments[0][file]=@smeta.pdf.
  • Ссылкой: {"url": "https://…"} — сервер скачает файл сам. Только http и https на публичные адреса, без перенаправлений; ждём до 30 секунд.
  • Строкой base64: {"base64": "…", "name": "план.png"}; можно с приставкой data:image/png;base64,.

До 10 файлов в сообщении, каждый до 10 МБ. Тип определяем по содержимому, а не по имени. Не прошёл один файл — ошибка придёт с его номером: attachments.2.

curl -X POST https://api.vz-chat.com/v1/chats/$CHAT/messages -H "Authorization: Bearer $KEY" \
  -F "text=Фото с объекта" -F "attachments[0][file]=@photo.jpg"

Скачать вложение

preview_url открыт всем и годится для показа. Оригинал — по download_url с ключом: API проверит, что вы в этом чате, и перенаправит на ссылку, которая живёт 15 минут.

curl -L -H "Authorization: Bearer $KEY" -o smeta.pdf "https://api.vz-chat.com/v1/attachments/01k…"

Люди и контакты

GET /people?q= · read

Поиск по имени, никнейму или номеру, от двух знаков.

GET /people/{id} · read

«В сети» и время последнего визита видно только тем, у кого с человеком есть общий чат.

GET /contacts · profile

POST /contacts · profile

{"user_id": "01k…", "name": "Прораб Игорь", "note": "Кленовая, 12"}

PUT /contacts · profile

Выгрузка книжки целиком: {"contacts": [{"phone": "+7…", "name": "…"}]}. Заменяет прежнюю, кроме добавленных вручную.

Объекты

Сообщение

{
  "id": "01k8…",
  "chat_id": "01k7…",
  "type": "text",               // text, image, file, sticker, voice, system
  "text": "Смета во вложении",
  "sticker": null,
  "from": {"id": "01k5…", "display_name": "Марина Соколова"},
  "reply_to_id": null,
  "client_id": "7b3f1c2e-…",
  "attachments": [{
    "id": "01k8…", "kind": "file", "name": "smeta.pdf", "mime": "application/pdf",
    "size": 182044, "width": null, "height": null, "duration_ms": null, "waveform": null,
    "preview_url": null, "download_url": "https://api.vz-chat.com/v1/attachments/01k8…"
  }],
  "is_out": true,
  "status": "read",             // sent, delivered, read — только у своих
  "created_at": "2026-09-30T12:04:11+00:00",
  "edited_at": null
}

Чат

{
  "id": "01k7…",
  "type": "direct",             // direct, group, saved
  "title": "Марина Соколова",   // у лички — имя собеседника, как оно записано у вас
  "description": null,
  "avatar_url": "https://…",
  "companion": { /* человек — только у лички */ },
  "members_count": 2,
  "my_role": "member",          // owner, admin, member
  "unread_count": 3,
  "notifications_muted": false,
  "last_message": { /* сообщение */ },
  "last_message_at": "2026-09-30T12:04:11+00:00",
  "created_at": "2026-09-01T08:00:00+00:00"
}

Человек

{
  "id": "01k5…",
  "display_name": "Марина Соколова",  // как записан у вас в книжке
  "first_name": "Марина",
  "last_name": "Соколова",
  "nickname": "vz_a7k2m9",
  "avatar_url": null,
  "bio": null,
  "is_staff": true,
  "is_bot": false,
  "is_online": false,                 // null, если общего чата нет
  "last_seen_at": "2026-09-30T11:58:00+00:00"
}

События

Всё, что происходит в ваших чатах, пишется в журнал событий. Забирать его можно опросом или получать вебхуком — это один и тот же журнал, в одном порядке. События хранятся 7 дней.

{
  "update_id": "01k8…",       // растёт со временем — по нему offset
  "type": "message.new",
  "created_at": "2026-09-30T12:04:11+00:00",
  "data": { … }
}
typeКогдаdata
message.new Новое сообщение — входящее или ваше, отправленное с другого устройства {message}
message.status Участник получил или прочитал сообщения. Всё, что не новее отметки, — доставлено или прочитано {chat_id, user_id, delivered_up_to, read_up_to}
message.deleted Сообщение удалили у всех {chat_id, message_id}
chat.created Завели личку или группу, вас позвали в группу {reason, chat}
chat.updated Название, фото, состав, роли; очистка «Избранного» {reason, chat}
chat.deleted Чат удалён или вас убрали из группы {chat_id, reason}

Статус своего сообщения по message.status: если read_up_to ≥ id — прочитано, если delivered_up_to ≥ id — доставлено. id сравниваются как строки. В группе событие приходит от каждого участника.

Опрос

GET /updates · read

Как getUpdates в Telegram. Передайте offset — next_offset из прошлого ответа, и придёт только новое. С timeout (до 25 секунд) запрос подождёт, пока что-нибудь не случится. types[] — только нужные события, limit — до 100.

offset=""
while true; do
  r=$(curl -s "https://api.vz-chat.com/v1/updates?timeout=25&offset=$offset" -H "Authorization: Bearer $KEY")
  echo "$r" | jq -c '.data[]'
  offset=$(echo "$r" | jq -r '.next_offset // empty')
done

Сохраняйте next_offset у себя: после перезапуска продолжите с того же места и ничего не потеряете, если уложитесь в 7 дней.

Вебхуки

Заведите вебхук в кабинете или через API — и события будут приходить POST-запросом на ваш адрес. До 10 вебхуков, у каждого свой набор событий.

Запрос

POST /your/hook HTTP/1.1
Content-Type: application/json
User-Agent: VZChat-Webhooks/1.0
X-VZ-Event: message.new
X-VZ-Update-Id: 01k8…
X-VZ-Timestamp: 1790000000
X-VZ-Signature: sha256=5d41402abc4b2a76b9719d911017c592…

{"update_id": "01k8…", "type": "message.new", "created_at": "…", "data": {…}}

Проверка подписи

Подпись — HMAC-SHA256 от строки «timestamp.тело» секретом вебхука. Проверяйте её на каждом запросе и отбрасывайте те, у которых время старше пяти минут, — так перехваченный запрос нельзя повторить. Считайте подпись от сырого тела, до разбора JSON.

// PHP
$body = file_get_contents('php://input');
$timestamp = (int) $_SERVER['HTTP_X_VZ_TIMESTAMP'];
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_VZ_SIGNATURE'] ?? '') || abs(time() - $timestamp) > 300) {
    http_response_code(401);
    exit;
}
// Node.js
const crypto = require('crypto');

function verify(rawBody, headers, secret) {
  const timestamp = headers['x-vz-timestamp'];
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`).digest('hex');
  const given = headers['x-vz-signature'] || '';

  return given.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
    && Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}
# Python
import hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers["X-VZ-Timestamp"]
    expected = "sha256=" + hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers.get("X-VZ-Signature", "")) and abs(time.time() - int(timestamp)) < 300

Ответ и повторы

  • Отвечайте любым кодом 2xx в течение 10 секунд. Тяжёлую работу делайте после ответа — в очереди.
  • Не дошло — повторим через 10 секунд, минуту, 5, 15 минут и час.
  • Одно событие может прийти дважды — сверяйте update_id.
  • Порядок доставки не гарантирован: сортируйте по update_id.
  • После 50 неудач подряд вебхук выключается. Починили приёмник — включите его в кабинете, а пропущенное заберите опросом.
  • Адрес — только публичный http(s), перенаправлениям не следуем.

Управление через API · webhooks

GET /webhooksСписок
POST /webhooks{"url": "…", "events": ["message.new"]} — ответ с secret
GET /webhooks/{id}Вебхук с секретом
PATCH /webhooks/{id}url, events, is_active, rotate_secret
DELETE /webhooks/{id}Удалить
POST /webhooks/{id}/testПроверочная доставка события webhook.test
GET /webhooks/{id}/deliveriesПоследние 100 доставок: код ответа, ошибка, время