Вебхуктер: оқиғаларды қабылдау

Вебхук тіркеу: PUT /webhook/. Жалпыға қолжетімді HTTPS URL мекенжайын және қажет ағындарды беріңіз. Жауаптағы hmac_secret арқылы барлық жеткізілген оқиғалардың қолтаңбасы жасалады; оны сақтаңыз. PUT жазылымды толық ауыстырады. subscribe_templates әдепкіде false, сондықтан шаблон оқиғалары керек әр PUT сұрауында subscribe_templates: true мәнін қайталаңыз. Әйтпесе келесі PUT бұл ағынды өшіреді:

curl -X PUT https://dialogs.tehprof.kz/api/v1/gw/webhook/ \
  -H "Authorization: Bearer gw_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-service.example/wahub",
    "subscribe_messages": true,
    "subscribe_statuses": true,
    "subscribe_templates": true,
    "channel_ids": [32]
  }'

200 {
  "hmac_secret": "gw_9a8b7c…",
  "url": "https://your-service.example/wahub",
  "subscribe_messages": true, "subscribe_statuses": true,
  "subscribe_templates": true, "channel_ids": ["32"]
}

Белгілі нөмірлерге жазылу. channel_ids берілсе, оқиғалар тек көрсетілген арналардан келеді. Әдепкі бос тізім компанияның барлық арналарына жазылуды білдіреді. Өзге компанияның немесе өшірілген арнаның channel_id мәні 404 қатесін қайтарады. Бір арнаға бірнеше сервис жазыла алады; оқиға бәріне жеткізіледі.

Жазылымды тексеру және өшіру. GET /webhook/ ағымдағы URL, ағындар мен арналарды көрсетеді; құпия кілтті қайтармайды. DELETE /webhook/ URL мекенжайын өшіріп, оқиғалардың жеткізілуін толық тоқтатады. Қайталанған DELETE те 204 жауабын береді.

URL мекенжайыңызға үш ағын жеткізіледі. messages ағыны жаңа хабарламаларды береді. Кіріс хабарламалар direction: "incoming" мәнімен келеді. Диалогтар мен CRM операторларының жауаптары, телефоннан жазылған хабарламалар және басқа кілттермен API арқылы жіберілгендер message.created ретінде direction: "outgoing" және source мәнімен келеді (wahub — Диалогтар не API, crm — CRM, channel_echo — телефон). Кілтіңіз POST /messages/ арқылы өзі жіберген хабарлама вебхукқа қайта келмейді:

{
  "messages": [
    {
      "event": "message.created",
      "message_id": "8841",
      "channel_id": "32",
      "chat_type": "whatsapp",
      "chat_id": "77010009988",
      "occurred_at": "2026-10-20T09:12:44.512000+00:00",
      "direction": "incoming",
      "source": "customer",
      "text": "Привет!",
      "content_type": "text",
      "media_url": "",
      "file_name": "",
      "file_mimetype": "",
      "reply_to": "",
      "sender_name": "Айгерим",
      "sender_phone": "77010009988",
      "metadata": {},
      "edited": false
    }
  ]
}

direction мәні incoming немесе outgoing болады. source хабарламаның қайдан келгенін көрсетеді: customer — клиент жазған; wahub — Dialogs-тен, соның ішінде осы API арқылы жіберілген; channel_echo — телефоннан тікелей жіберілген; crm — қосылған CRM-нен жіберілген. Бот өз хабарламасына жауап бермеуі үшін source және direction арқылы сүзгілеңіз. media_url, file_name, file_mimetype, reply_to және sender_name бос болса, null орнына бос жол беріледі.

Файлдар. media_url — серверіміздегі жол (/media/…). Оған API серверінің мекенжайын қосып, файлды Authorization: Bearer gw_… тақырыбымен жүктеңіз; тақырыпсыз браузерде ашылмайды. WhatsApp файлы хабарлама келгеннен кейін жүктеледі: алдымен бос media_url бар message.created, файл дайын болғанда сол message_id бар message.updated келеді. Екінші оқиға файл жолын, file_name және file_mimetype береді. Файл алынбаса, message.updated ішінде content_type: text және text өрісінде түсіндірме келеді. Хабарламаны message_id бойынша сақтап, message.updated кезінде объектіні толық ауыстырыңыз. Мәтін өзгерісі де message.updated болып, edited: true белгісімен келеді. Хабарлама бәріне өшірілсе, сол message_id бар message.deleted келеді; text, файл және metadata бос болады — өз сервисіңіздегі хабарламаны өшіріңіз. Өшірілген хабарламаның әлі жеткізілмеген оқиғалары жіберілмейді, өзі тарихта да берілмейді.

sender_phone жіберушіні көрсетеді: топта — қатысушы нөмірі, жеке чатта — чат нөмірі. metadata тек осы хабарламаға қатысты кілттерді қамтиды: audio {voice, duration}, дауыстық хабарламада voice: true; video {note, gif, duration}, бейне дөңгелекте note: true; location {lat, lon, name, address}; contact {name, phones, business}; album {id}, бір альбомдағы фотоларға ортақ; call {kind, outcome, duration_sec}; forwarded {many}; mentions — мәтіндегі id бойынша телефон нөмірі немесе null; reaction {emoji, message_id}, реакция қойылған хабарлама бізде болса, оның message_id мәні беріледі. Алдымен notice {kind} мәнін тексеріңіз: ол бар болса, бұл клиент мәтіні емес, жүйелік жазба. unsupported_type көрсетілмейтін WhatsApp хабарламасын, media_unavailable алынбаған файлды білдіреді. Белгісіз кілттерді өткізіп жіберіңіз: кейін жаңалары қосылуы мүмкін.

statuses ағыны сіз жіберген хабарламалардың жеткізілу күйін жаңартады: sent → delivered → read немесе failed. message_id — POST /messages/ қайтарған мән. Жеткізілімдер ретсіз келуі мүмкін: күйді status_rank сақталғаннан үлкен болғанда ғана қолданыңыз (pending 0, sent 1, delivered 2, read 3, played 4). failed рангі −1, ол жеке соңғы нәтижені білдіреді; онда error бар: code — себептің біздегі коды (recipient_not_registered — нөмірде WhatsApp жоқ, recipient_unreachable, file_too_large, type_unsupported, provider_unavailable, provider_error, unknown), provider_code — провайдер коды (мысалы, WhatsApp Cloud-тағы 131042 — Meta-дағы төлем мәселесі), description — провайдер мәтіні, reason — провайдер бас тартуының code-тан нақтырақ себебі (мысалы, marketing_per_user_limit, payment_method_problem; себеп танылмаса — бос жол; жаңа мәндер қосылуы мүмкін). Дәл осы өріс GET /messages/ ішіндегі failed хабарламада да бар. occurred_at — оқиғаны біз тіркеген уақыт; X-Wahub-Timestamp — тек HTTP әрекетінің уақыты:

{
  "statuses": [
    {
      "message_id": "msg_7f3c",
      "channel_id": "32",
      "chat_type": "whatsapp",
      "chat_id": "77010009988",
      "status": "delivered",
      "status_rank": 2,
      "occurred_at": "2026-10-20T09:13:05.104000+00:00"
    }
  ]
}

templates ағыны шаблондар тізіміндегі өзгерістерді береді. Ол әдепкіде өшірулі, subscribe_templates арқылы қосылады. Әр Meta шешімі оқиғамен келеді, сондықтан шешімді қайта-қайта сұраудың қажеті жоқ. change өрісі өзгерісті көрсетеді: status — мақұлданды не қабылданбады; components — мәтін өзгерді; quality — сапа бағасы; category — санат өзгерді. Оқиғаны осы өріске қарап өңдеңіз. status әрдайым ағымдағы күйді береді. WABA қызмет көрсететін әр арнаға жеке оқиға келеді:

{
  "templates": [
    {
      "template_id": "123",
      "channel_id": "32",
      "change": "status",
      "name": "order_shipped",
      "language": "ru",
      "status": "APPROVED",
      "category": "UTILITY",
      "quality_rating": "GREEN",
      "rejection_reason": ""
    }
  ]
}

Қолтаңбаны тексеру. Әр жеткізілімде X-Wahub-Timestamp (Unix секундтары), X-Wahub-Delivery-ID (жеткізілім UUID) және X-Wahub-Signature-V2: sha256=<hmac_sha256(hmac_secret, timestamp + "." + delivery_id + "." + бастапқы дене)> келеді. HMAC мәнін JSON талдауына дейін сұраудың бастапқы байттарынан есептеңіз. timestamp сіздің сағатыңыздан 5 минуттан артық айырмашылықта болса, жеткізілімді қабылдамаңыз: ұсталған сұрауды қайта жіберу мүмкін болмайды. Тақырыпты үтір бойынша бөліп, кемінде бір мәні сәйкес келсе қабылдаңыз. Құпия кілт ауыстырылғанда екі мән, қалыпты кезде бір мән беріледі. Бұрынғы X-Wahub-Signature тақырыбы (уақытсыз, тек дене бойынша HMAC) үйлесімділік үшін әлі келеді; жаңа өңдеушілер V2 тексереді.

import hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers["X-Wahub-Timestamp"]
    delivery_id = headers["X-Wahub-Delivery-ID"]
    if abs(time.time() - int(ts)) > 300:
        return False
    signed = f"{ts}.{delivery_id}.".encode() + raw_body
    expected = "sha256=" + hmac.new(
        secret.encode(), signed, hashlib.sha256
    ).hexdigest()
    sent = headers["X-Wahub-Signature-V2"].split(",")
    return any(hmac.compare_digest(expected, s) for s in sent)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, headers, secret) {
  const ts = headers["x-wahub-timestamp"];
  const deliveryId = headers["x-wahub-delivery-id"];
  if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
  const expected =
    "sha256=" +
    createHmac("sha256", secret)
      .update(`${ts}.${deliveryId}.`)
      .update(rawBody)
      .digest("hex");
  const want = Buffer.from(expected);
  return (headers["x-wahub-signature-v2"] ?? "").split(",").some((sent) => {
    const got = Buffer.from(sent);
    return got.length === want.length && timingSafeEqual(got, want);
  });
}

Құпия кілтті үзіліссіз ауыстыру. POST /webhook/rotate-secret/ жаңа hmac_secret береді және бұрынғы кілтті тағы 24 сағат жарамды қалдырады. Осы аралықта әр жеткізілім екі кілтпен қол қойылып, екі қолтаңба тақырыбы да екі мәнді үтірмен береді. Жаңа кілтке сол аралықта ауысыңыз. Idempotency-Key тақырыбы міндетті: осы ауыстыруға бірегей мән ойлап тауып, қайталағанда соны беріңіз — қайталау кілтті екінші рет ауыстырмай, сол кілтті қайтарады. Кілт жария болуы мүмкін болса, оны дереу ауыстырыңыз. 24 сағат ішінде қайта ауыстырсаңыз, соңғы ауыстыруға дейінгі ағымдағы кілт ауысады; одан бұрынғы кілттің жарамдылық мерзімі ұзармайды. Жаңа кілт тек осы жауапта беріледі, оны GET /webhook/ арқылы ала алмайсыз.

curl -X POST https://dialogs.tehprof.kz/api/v1/gw/webhook/rotate-secret/ \
  -H "Authorization: Bearer gw_xxx" \
  -H "Idempotency-Key: rotate-2026-10-20"

200 {
  "hmac_secret": "gw_new1a2b…",
  "previous_secret_valid_until": "2026-08-09T09:12:44+00:00"
}

# пока окно открыто:
X-Wahub-Signature-V2: sha256=<новым>,sha256=<прежним>

Қайталау және телнұсқалар. 2xx жауабын тез беріп, ұзақ жұмысты кейін орындаңыз. Серверіңіз 10 секунд ішінде 2xx жауабын бермесе, Диалогтар жіберуді шамамен 27 сағат ішінде тағы 9 рет қайталайды (~30 с, 2 мин, 10 мин, 30 мин, 1 сағ, 2 сағ, 4 сағ, 8 сағ және 12 сағ кейін) және ұзағырақ Retry-After мәнін ескереді. Тек 410 Gone жауабында бірден тоқтатамыз. Одан кейін оқиғаны жазылым лентасынан алып, қолмен қайталауға болады («Өткізілгенін алу» бөлімін қараңыз). Қайталау сол X-Wahub-Delivery-ID мәнімен келеді — телнұсқаларды соған қарап сүзіңіз: қабылданған ID мәндерін есте сақтап, қайталауға 2xx беріңіз. message_id бойынша сүзуге болмайды: message.updated және message.deleted сол message_id мәнімен келіп, хабарламаның жаңа күйін береді.