LanaChat

API: вебхуки, події та підписи

Читання 7 хвОновлено Markdown

Webhook надсилає HTTPS POST на ваш сервер, коли в робочому просторі відбувається якась подія: нова розмова, продаж, готовий медіафайл, обліковий запис втратив з’єднання. Ви обираєте події; LanaChat підписує кожну доставку, щоб ви могли перевірити, що вона від нас, і повторює спроби, якщо ваш сервер не відповідає.

Endpoints #

MethodPathDescriptionPermissionStatus
GET/webhooksСписок webhook-ендпоінтівownerPlanned
POST/webhooksДодати ендпоінт: URL і події (секрет підпису показується один раз)ownerPlanned
GET/webhooks/{id}Один ендпоінт і його коефіцієнт доставки за 7 днівownerPlanned
PATCH/webhooks/{id}Змінити URL або події, призупинити або відновитиownerPlanned
DELETE/webhooks/{id}Видалити ендпоінтownerPlanned
POST/webhooks/{id}/rotate-secretНовий секрет підпису; старий працює ще 24 годиниownerPlanned
POST/webhooks/{id}/testВідправити подію ping заразownerPlanned
GET/webhooks/{id}/deliveriesОстанні доставки, їх статус і відповідьownerPlanned
POST/webhooks/{id}/deliveries/{delivery_id}/retryВідправити одну доставку зновуownerPlanned

Events #

EventSent whenPermission the data needs
conversation.startedФан вперше пише (з source_link_id, якщо відомо)can_fan_read
conversation.needs_reviewРозмова отримує статус Review або Safety holdcan_fan_read
conversation.taken_overХтось бере розмову в роботу або повертає їїcan_fan_read
message.receivedФан надсилає повідомленняcan_fan_read
sale.completedПідтверджена покупка (Stars або платіжне посилання)can_sales_read
sale.refundedПідтверджено повернення коштівcan_sales_read
media.readyЗавантажене медіа оброблено або не вдалосяcan_setup
account.health_changedОбліковий запис став підключеним, очікує або простроченийcan_setup
pool.saturatedПул досяг 95% годинної потужності або ліміту робочого просторуcan_tracking
brain.publishedЧернетка brain опублікованаcan_engine
billing.low_balanceБаланс впав нижче суми на наступні 7 днів ліцензійowner
pingВи натиснули Testnone

Ендпоінт отримує подію лише якщо токен, що його створив, має дозвіл, потрібний для цих даних.

The webhook endpoint object #

{
  "id": "wh_12",
  "url": "https://hooks.mia-agency.com/lanachat/webhook",
  "events": ["sale.completed", "sale.refunded", "conversation.needs_review"],
  "status": "active",
  "delivery_rate_7d": 0.998,
  "created_at": "2026-10-01T09:00:00Z"
}
FieldTypeDescription
idstringЕндпоінт
urlstringВаш HTTPS URL
eventsarray of stringsПодії, які він отримує (див. таблицю вище)
statusstringactive або paused (від вас або після 3 днів помилок)
delivery_rate_7dnumberЧастка доставок з відповіддю 2xx за 7 днів, від 0 до 1
created_atstringISO 8601, UTC

Manage endpoints #

Всі ці виклики потребують токен owner.

Add an endpoint #

POST /webhooks · owner

ParameterTypeRequiredDescription
urlstringТакHTTPS URL вашого сервера
eventsarray of stringsТакПодії для отримання
curl -X POST https://api.lanachat.ai/v1/webhooks \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://hooks.mia-agency.com/lanachat/webhook", "events": ["sale.completed", "sale.refunded"]}'

Відповідь, це ендпоінт (201) і його секрет підпису, показаний лише тут: збережіть його одразу поруч із токеном. Невідоме ім’я події повертає 400.

List, read, change and remove #

CallWhat it does
GET /webhooksВсі ендпоінти робочого простору
GET /webhooks/{id}Один ендпоінт і його коефіцієнт доставки за 7 днів
PATCH /webhooks/{id}Змінити url або events; призупинити або відновити через status
DELETE /webhooks/{id}Видалити; більше нічого не надсилається
curl -X PATCH https://api.lanachat.ai/v1/webhooks/wh_12 \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Rotate the secret #

POST /webhooks/{id}/rotate-secret · owner

curl -X POST https://api.lanachat.ai/v1/webhooks/wh_12/rotate-secret \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"

Відповідь містить новий секрет, один раз. Старий працює ще 24 години, щоб ви могли оновити без втрати доставок.

Send a test #

POST /webhooks/{id}/test · owner

curl -X POST https://api.lanachat.ai/v1/webhooks/wh_12/test \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"

Ваш сервер отримає подію ping за кілька секунд, підписану як будь-яка інша доставка.

Deliveries and retry #

GET /webhooks/{id}/deliveries показує останні доставки, їх статус і відповідь вашого сервера. POST /webhooks/{id}/deliveries/{delivery_id}/retry повторно надсилає одну доставку, наприклад, після виправлення помилки.

curl -X POST https://api.lanachat.ai/v1/webhooks/wh_12/deliveries/dlv_5f21a9/retry \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"

A delivery #

POST /lanachat/webhook HTTP/1.1
Host: hooks.mia-agency.com
Content-Type: application/json
LanaChat-Event: sale.completed
LanaChat-Delivery: dlv_5f21a9
LanaChat-Signature: t=1759762800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
  "id": "evt_31877",
  "type": "sale.completed",
  "created_at": "2026-10-06T15:00:00Z",
  "workspace_id": "ws_7Hq2",
  "data": {
    "sale_id": "sale_8812", "fan_id": "fan_921", "creator_id": "cr_lea", "account_id": "acc_1001",
    "conversation_id": "conv_1001_88231", "stars": 300, "net_cents": 390, "sent_by": "ai", "kind": "offer"
  }
}

Кожна подія має однакову структуру: id, type, created_at, workspace_id і data. data містить об’єкт, про який подія, з полями, які повертає відповідний API.

Payload of each event #

conversation.started: розмова з посиланням, звідки вона прийшла.

{ "id": "evt_31801", "type": "conversation.started", "created_at": "2026-10-06T14:01:12Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "conv_1001_88231", "fan_id": "fan_921", "creator_id": "cr_lea", "account_id": "acc_1001", "state": "ai", "source_link_id": "lnk_3" } }

conversation.needs_review: розмова і її новий статус.

{ "id": "evt_31840", "type": "conversation.needs_review", "created_at": "2026-10-06T14:20:00Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "conv_1001_88231", "creator_id": "cr_lea", "account_id": "acc_1001", "state": "off", "status": { "tag": "review", "reason": "The fan asked for a refund." } } }

conversation.taken_over: хто зараз веде розмову (state, human, коли людина взяла, ai, коли повернула).

{ "id": "evt_31852", "type": "conversation.taken_over", "created_at": "2026-10-06T14:04:00Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "conv_1001_88231", "state": "human", "taken_by": "tok_31", "taken_at": "2026-10-06T14:04:00Z" } }

message.received: повідомлення від фана.

{ "id": "evt_31860", "type": "message.received", "created_at": "2026-10-06T14:02:10Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "msg_77119", "conversation_id": "conv_1001_88231", "from": "fan", "text": "show me more", "at": "2026-10-06T14:02:10Z" } }

sale.completed і sale.refunded: продаж, як у прикладі вище. Повернення має той самий sale_id, що і продаж, який скасовує.

media.ready: медіа, status, ready або failed.

{ "id": "evt_31870", "type": "media.ready", "created_at": "2026-10-06T14:22:05Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "med_5521", "creator_id": "cr_lea", "type": "photo", "status": "ready", "folder_id": "vf_3" } }

account.health_changed: Telegram-акаунт і його новий health.

{ "id": "evt_31881", "type": "account.health_changed", "created_at": "2026-10-06T15:10:00Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "acc_1004", "creator_id": "cr_lea", "username": "lea_backup", "health": "expired" } }

pool.saturated: пул і його навантаження.

{ "id": "evt_31890", "type": "pool.saturated", "created_at": "2026-10-06T21:00:00Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "pool_lea_fr_instagram", "creator_id": "cr_lea", "sent_last_hour": 381, "capacity_per_hour": 400, "status": "saturated" } }

brain.published: brain і його нова версія.

{ "id": "evt_31901", "type": "brain.published", "created_at": "2026-10-06T14:20:03Z", "workspace_id": "ws_7Hq2",
  "data": { "id": "br_lea_main", "version": 13, "published_at": "2026-10-06T14:20:03Z" } }

billing.low_balance: ваш баланс.

{ "id": "evt_31910", "type": "billing.low_balance", "created_at": "2026-10-06T08:00:00Z", "workspace_id": "ws_7Hq2",
  "data": { "balance_cents": 2900 } }

ping: відправлено через Test; data порожнє.

{ "id": "evt_31920", "type": "ping", "created_at": "2026-10-06T16:00:00Z", "workspace_id": "ws_7Hq2", "data": {} }

Check the signature #

LanaChat-Signature містить часову позначку t і v1, HMAC-SHA256 від t + "." + raw body з секретом підпису вашого ендпоінта, у шістнадцятковому форматі.

У Node (будь-який фреймворк, що дає сире тіло):

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body as received (Buffer or string), before any JSON parsing
export function verify(rawBody, header, secret) {
  const parts = (header || '').split(',').map((p) => p.trim().split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);   // two during a secret rotation
  if (!t || !(Math.abs(Date.now() / 1000 - Number(t)) < 300)) return false;  // older than 5 minutes: refuse
  const want = createHmac('sha256', secret).update(t + '.').update(rawBody).digest('hex');
  return signatures.some((v) => v.length === want.length && timingSafeEqual(Buffer.from(v), Buffer.from(want)));
}

Повний приймач з Express:

import express from 'express';
import { verify } from './verify.js';

const app = express();
const seen = new Set();   // use your database in production

app.post('/lanachat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body, req.get('LanaChat-Signature'), process.env.LANACHAT_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  res.sendStatus(200);                       // answer fast, work after
  if (seen.has(event.id)) return;            // the same event can arrive twice
  seen.add(event.id);
  if (event.type === 'sale.completed') console.log('New sale', event.data.stars, 'Stars for', event.data.creator_id);
});

app.listen(3000);

У Python з Flask:

import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["LANACHAT_WEBHOOK_SECRET"].encode()

def verify(raw_body: bytes, header: str) -> bool:
    parts = [p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p]
    t = next((v for k, v in parts if k == "t"), None)
    signatures = [v for k, v in parts if k == "v1"]  # two during a secret rotation
    if not t or abs(time.time() - int(t)) >= 300:    # older than 5 minutes: refuse
        return False
    want = hmac.new(SECRET, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(want, v) for v in signatures)

@app.post("/lanachat/webhook")
def webhook():
    raw = request.get_data()
    if not verify(raw, request.headers.get("LanaChat-Signature")):
        abort(400)
    event = json.loads(raw)
    if event["type"] == "sale.completed":
        print("New sale", event["data"]["stars"], "Stars for", event["data"]["creator_id"])
    return "", 200
  • Обчислюйте на сирому тілі, до JSON-парсингу.
  • Відхиляйте часову позначку старшу за 5 хвилин: це зупиняє повторну відправку.
  • Під час ротації секрету доставки мають два v1; приймайте будь-який.

Retries #

  • Відповідайте будь-яким 2xx протягом 10 секунд. Все інше або відсутність відповіді, це помилка.
  • Помилкову доставку повторюють 8 разів протягом приблизно 24 годин (1 хв, 5 хв, 30 хв, 2 год, 5 год, 10 год, 14 год, 24 год).
  • Одна й та сама подія може прийти двічі: використовуйте id для ігнорування дублікатів. Порядок не гарантовано: використовуйте created_at.
  • Після 3 днів помилок ендпоінт призупиняється, а власник отримує листа; відновіть через PATCH /webhooks/{id}.

Останні новини про Lana в Telegram

Нові релізи та запрошення в бету, першими в каналі.