# API: webhooki, zdarzenia i podpisy

_API · 7 min czytania · Zaktualizowano 2026-10-22 · https://lanachat.ai/guides/pl/api-webhooks/_

**Webhook** wysyła HTTPS `POST` do Twojego serwera, gdy coś się dzieje w workspace: nowa rozmowa, sprzedaż, gotowe media, konto, które utraciło połączenie. Wybierasz zdarzenia; LanaChat podpisuje każdą dostawę, abyś mógł sprawdzić, że pochodzi od nas, i ponawia próby, gdy Twój serwer nie odpowiada.

## Endpoints

| Method | Path | Description | Permission | Status |
|---|---|---|---|---|
| GET | `/webhooks` | Lista endpointów webhooków | `owner` | Planned |
| POST | `/webhooks` | Dodaj endpoint: URL i zdarzenia (sekret podpisu pokazany raz) | `owner` | Planned |
| GET | `/webhooks/{id}` | Jeden endpoint i jego wskaźnik dostarczalności za 7 dni | `owner` | Planned |
| PATCH | `/webhooks/{id}` | Zmień URL lub zdarzenia, wstrzymaj lub wznow | `owner` | Planned |
| DELETE | `/webhooks/{id}` | Usuń endpoint | `owner` | Planned |
| POST | `/webhooks/{id}/rotate-secret` | Nowy sekret podpisu; stary działa jeszcze 24 godziny | `owner` | Planned |
| POST | `/webhooks/{id}/test` | Wyślij teraz zdarzenie `ping` | `owner` | Planned |
| GET | `/webhooks/{id}/deliveries` | Ostatnie dostawy, ich status i odpowiedź | `owner` | Planned |
| POST | `/webhooks/{id}/deliveries/{delivery_id}/retry` | Wyślij ponownie jedną dostawę | `owner` | Planned |

## Zdarzenia

| Event | Wysyłane gdy | Wymagane uprawnienie |
|---|---|---|
| `conversation.started` | Fan pisze po raz pierwszy (z `source_link_id` jeśli znany) | `can_fan_read` |
| `conversation.needs_review` | Rozmowa otrzymuje status Review lub Safety hold | `can_fan_read` |
| `conversation.taken_over` | Ktoś przejmuje rozmowę lub ją oddaje | `can_fan_read` |
| `message.received` | Fan wysyła wiadomość | `can_fan_read` |
| `sale.completed` | Zweryfikowany zakup (Stars lub link płatności) | `can_sales_read` |
| `sale.refunded` | Zwrot potwierdzony | `can_sales_read` |
| `media.ready` | Przesłane media przetworzone lub nieudane | `can_setup` |
| `account.health_changed` | Konto zmienia status na connected, pending lub expired | `can_setup` |
| `pool.saturated` | Pool osiąga 95% swojej godzinowej pojemności lub limit workspace | `can_tracking` |
| `brain.published` | Szkic braina opublikowany | `can_engine` |
| `billing.low_balance` | Saldo spada poniżej kwoty na kolejne 7 dni licencji | `owner` |
| `ping` | Naciśnięto Test | brak |

Endpoint otrzymuje zdarzenie tylko jeśli token, który go utworzył, ma wymagane uprawnienia do danych.

## Obiekt endpointu webhook

```json
{
  "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"
}
```

| Pole | Typ | Opis |
|---|---|---|
| `id` | string | Endpoint |
| `url` | string | Twój HTTPS URL |
| `events` | tablica stringów | Zdarzenia, które odbiera (patrz tabela wyżej) |
| `status` | string | `active` lub `paused` (przez Ciebie lub po 3 dniach błędów) |
| `delivery_rate_7d` | number | Udział dostaw z odpowiedzią `2xx` w ciągu 7 dni, od 0 do 1 |
| `created_at` | string | ISO 8601, UTC |

## Zarządzanie endpointami

Wszystkie te wywołania wymagają tokena **owner**.

### Dodaj endpoint

`POST /webhooks` · `owner`

| Parametr | Typ | Wymagany | Opis |
|---|---|---|---|
| `url` | string | Tak | HTTPS URL Twojego serwera |
| `events` | tablica stringów | Tak | Zdarzenia do odbierania |

```bash
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"]}'
```

Odpowiedź to endpoint (`201`) i jego **signing secret**, pokazany tylko w tej odpowiedzi: zapisz go od razu, obok tokena. Nieznana nazwa zdarzenia zwraca `400`.

### Lista, odczyt, zmiana i usuwanie

| Wywołanie | Co robi |
|---|---|
| `GET /webhooks` | Wszystkie endpointy workspace |
| `GET /webhooks/{id}` | Jeden endpoint i wskaźnik dostarczalności za 7 dni |
| `PATCH /webhooks/{id}` | Zmień `url` lub `events`; wstrzymaj lub wznow przez `status` |
| `DELETE /webhooks/{id}` | Usuń; nic więcej nie jest wysyłane |

```bash
curl -X PATCH https://api.lanachat.ai/v1/webhooks/wh_12 \
  -H "Authorization: Bearer $LANACHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'
```

### Rotacja sekretu

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

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

Odpowiedź zawiera nowy sekret, raz. Stary działa jeszcze 24 godziny, więc możesz wdrożyć nowy bez utraty dostawy.

### Wyślij test

`POST /webhooks/{id}/test` · `owner`

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

Twój serwer otrzyma zdarzenie `ping` w ciągu kilku sekund, podpisane jak każda inna dostawa.

### Dostawy i ponawianie

`GET /webhooks/{id}/deliveries` pokazuje ostatnie dostawy, ich status i odpowiedź Twojego serwera. `POST /webhooks/{id}/deliveries/{delivery_id}/retry` wysyła ponownie jedną z nich, np. po naprawieniu błędu po Twojej stronie.

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

## Dostawa

```http
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
```

```json
{
  "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"
  }
}
```

Każde zdarzenie ma tę samą kopertę: `id`, `type`, `created_at`, `workspace_id` i `data`. `data` zawiera obiekt, którego dotyczy zdarzenie, z polami zwracanymi przez odpowiadający endpoint API.

## Payload każdego zdarzenia

`conversation.started`: [rozmowa](https://lanachat.ai/guides/pl/api-conversations/), z linkiem, z którego pochodzi.

```json
{ "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`: rozmowa i jej nowy status.

```json
{ "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`: kto teraz prowadzi rozmowę (`state` to `human` gdy osoba przejmuje, `ai` gdy oddaje).

```json
{ "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`: wiadomość fana.

```json
{ "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` i `sale.refunded`: sprzedaż, jak w powyższej dostawie. Zwrot ma ten sam `sale_id`, co sprzedaż, którą anuluje.

`media.ready`: [media](https://lanachat.ai/guides/pl/api-vault/), `status` `ready` lub `failed`.

```json
{ "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`: [konto Telegram](https://lanachat.ai/guides/pl/api-telegram-accounts/) i jego nowy `health`.

```json
{ "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`: [pool](https://lanachat.ai/guides/pl/api-telegram-accounts/) i jego obciążenie.

```json
{ "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](https://lanachat.ai/guides/pl/api-ai-brains/) i jego nowa wersja.

```json
{ "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`: Twoje [saldo](https://lanachat.ai/guides/pl/api-billing/).

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

`ping`: wysłane przez **Test**; `data` jest puste.

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

## Sprawdź podpis

`LanaChat-Signature` zawiera znacznik czasu `t` i `v1`, HMAC-SHA256 z `t + "." + raw body` z sekretem podpisu endpointu, w hex.

W Node (dowolny framework dający surowe ciało):

```js
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)));
}
```

Pełny odbiorca z Express:

```js
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);
```

W Python, z Flask:

```python
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
```

- Obliczaj na surowym ciele, przed parsowaniem JSON.
- Odrzucaj znaczniki czasu starsze niż 5 minut: zapobiega to powtórkom.
- Podczas rotacji sekretu dostawy mają dwa `v1`; zaakceptuj którykolwiek.

## Ponawianie

- Odpowiadaj dowolnym `2xx` w ciągu 10 sekund. Inne odpowiedzi lub brak odpowiedzi to błąd.
- Nieudana dostawa jest ponawiana 8 razy przez około 24 godziny (1 min, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 24 h).
- To samo zdarzenie może przyjść dwa razy: używaj `id` do ignorowania duplikatów. Kolejność nie jest gwarantowana: używaj `created_at`.
- Po 3 dniach błędów endpoint jest wstrzymany, a właściciel dostaje e-mail; wznow go przez `PATCH /webhooks/{id}`.

## Powiązane artykuły

- [LanaChat API: szybki start, uwierzytelnianie i błędy](https://lanachat.ai/guides/pl/api-overview/)
- [Szybkie przepisy API: kopiuj, wklej, uruchom](https://lanachat.ai/guides/pl/api-recipes/)
- [API: fani, konwersacje i wiadomości](https://lanachat.ai/guides/pl/api-conversations/)
