RU
EN

Webhooks

Smartbtn can push a signed JSON POST to a URL you register whenever a subscribed event happens. This page mirrors the ACTUAL committed App\Domain\Emitter / config/domain_events.php / App\Domain\Subscribers\WebhookSubscriber / App\Jobs\WebhookDeliveryJob code, not just the original design proposal -- see the notes under the catalog below for anywhere the two differ. Manage subscriptions via /v1/webhook-subscriptions (see Endpoints).

Delivery envelope

Every delivery is POST <your url>, Content-Type: application/json, with these headers:

HeaderMeaning
X-Smartbtn-EventThe event type, e.g. message.created.
X-Smartbtn-Signaturesha256=v1:<hex hmac> -- see Signature verification below.
X-Smartbtn-Idempotency-KeyOne value per underlying event, shared by every endpoint subscribed to it and stable across retries of your delivery. Use this to dedupe -- see Idempotency below.
X-Smartbtn-DeliveryOne value per (endpoint, event) delivery attempt. Useful for your own logging; changes on manual redelivery even though the idempotency key does not.
X-Smartbtn-TimestampUnix timestamp the request was sent. Informational only -- it is NOT part of the signed body (see below), so it changes on every retry even though the body does not.

The body is:

{
  "id": "<event id -- same value as X-Smartbtn-Idempotency-Key>",
  "type": "<event type>",
  "data": { ... event-specific fields, see catalog below ... }
}

The body's bytes are identical across every retry attempt of the same delivery (no timestamp or nonce is embedded inside the signed body itself -- only in the separate, unsigned X-Smartbtn-Timestamp header), so re-verifying the signature on a retried delivery always checks the exact same bytes you saw the first time, if you received it.

Signature verification

Compute an HMAC-SHA256 of the raw request body (the exact bytes as received, before any JSON parsing) using your endpoint's signing secret, and compare it against X-Smartbtn-Signature using a constant-time comparison (e.g. PHP's hash_equals) -- never a plain ==/=== string compare.

// Example only -- REDACTED_EXAMPLE_SECRET is a placeholder, never a real secret.
$secret = 'REDACTED_EXAMPLE_SECRET';
$rawBody = file_get_contents('php://input'); // raw bytes, not $_POST / json_decode()

$expected = 'sha256=v1:' . hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $_SERVER['HTTP_X_SMARTBTN_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

Formula, provider-agnostic: X-Smartbtn-Signature: sha256=v1:HMAC-SHA256(raw_request_body, your_webhook_secret). Your signing secret is shown to you exactly once, at subscription-creation time (POST /v1/webhook-subscriptions) -- Smartbtn stores it encrypted and never displays it again afterwards. See Authentication & Scopes for the secret-rotation overlap window and immediate-revocation behavior.

Event catalog

Status below is the actual, current rollout state, not the original design intent: every event in the table has real code wired to emit it, but only events marked Live are actually enabled in config/domain_events.php today. A subscription can be created for any event marked Wired, canary-disabled -- the API accepts it -- but no delivery will actually happen for it until Smartbtn operations enables that type in the canary rollout. If you need one of those event types for a production integration, contact support to ask about its rollout timeline before you rely on it firing.

EventStatusFires whenPayload data fields (besides button_guid)
message.created Live (canary-enabled by default) A chat message is persisted, from either the client or an agent. chat_id, message_id, client_id, author_type (USER|OPERATOR), source
chat.created Wired, canary-disabled by default A new chat room is created for a client contacting this widget for the first time. chat_id, client_id, source
chat.transferred Wired, canary-disabled by default A chat is transferred from one agent to another. chat_id, client_guid, from_operator_id, to_operator_id, author_type, source
chat.closed Wired, canary-disabled by default A chat is closed -- either via POST /v1/chats/{id}/close, or automatically when a chat transitions into the expired state (idle timeout). chat_id, reason (e.g. "expired", or the caller-supplied close reason)
rating.created Wired, canary-disabled by default A client submits a chat rating for the first time. chat_client_id, operator_id, author_type, source
agent.online Wired, canary-disabled by default An agent's socket connects to this widget. operator_id
agent.offline Wired, canary-disabled by default An agent's last connected socket disconnects. operator_id
chat.accepted Wired, canary-disabled by default A human operator claims a previously-unowned chat -- an explicit accept, an assign action, or simply the first reply into an unclaimed room. chat_id, operator_id, client_id
offline_message.created Wired, canary-disabled by default A client submits the offline/callback form while zero operators are connected. chat_id, client_id, message_id, name, email, phone
contact.updated Wired, canary-disabled by default A contact's fields are updated via PATCH /v1/contacts/{id}. client_id, changed_fields

Payload is intentionally minimal

Every event's data is an ids/discriminator payload, not a full copy of the underlying record. Two differences from an earlier, more detailed design draft are worth calling out explicitly so you don't build against fields that don't exist yet:

A note on naming: chat.updated

chat.accepted, offline_message.created, and contact.updated appeared in an earlier design draft of this catalog without a native emission point -- that gap is closed; all three are now in the table above, wired and canary-disabled like the rest. The one name from that earlier draft that still does not appear in the table is chat.updated -- it was never a distinct native concept on this side. It only exists as Jivo's own name for what this API calls chat.transferred (see Migrating from Jivo below); subscribing to the native chat.updated string itself returns 422, since that is not this API's vocabulary.

Retry & backoff policy

Up to 4 attempts total (the initial send plus 3 retries), with exponential backoff between attempts: min(60, 2^attempt * 5) seconds -- roughly 10s, 20s, 40s (capped at 60s beyond that). After the final attempt fails, the delivery is recorded internally as failed and is not retried automatically again.

Idempotency-key handling (for your receiver)

Delivery is at-least-once, so your endpoint should be able to safely process the same event more than once:

Migrating from Jivo

If you already have a Jivo integration speaking Jivo's own webhook wire format, you do not need to rewrite it. The compat/jivo/* routes (see Endpoints) are a day-one adapter: for inbound Bot/Chat API traffic, point your existing integration at Smartbtn's /compat/jivo/bot/<token> or /compat/jivo/chat/<secret> instead of Jivo's URL -- swap the base URL and the token/secret, no wire-format changes required on your side. For outbound webhooks, create a subscription with is_jivo_compat: true to receive Jivo's own event_name-keyed envelope instead of the native envelope above. All five Jivo event names now map to a native event with a real emission point in the code: chat_updatedchat.transferred, chat_finishedchat.closed, chat_acceptedchat.accepted, offline_messageoffline_message.created, client_updatedcontact.updated (chat_updated has no separate native concept of its own -- it is simply Jivo's name for chat.transferred). None of the five actually deliver in production yet, since their underlying native events are all still canary-disabled (see the Event catalog above) -- but subscribing to any of them now succeeds, accepted by the API the same way every other canary-disabled event already is.

Умная кнопка