> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visitoai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Webhooks — Visito M2M API

All paths below are relative to `/m2m/v1`. Send `Authorization: Bearer <key>`. Tenant identity comes from the key.

## Create a subscription

Requires `webhooks:write` and the read permissions for every event type selected.

```json theme={null}
{"url":"https://integration.example.com/visito","events":["message.created","message.delivery.updated"]}
```

Send this body to `POST /webhooks`. Save the returned `secret`; it is returned only on creation or rotation. `GET /webhooks` lists subscriptions; `GET/PATCH/DELETE /webhooks/{subscriptionId}` reads, updates, or removes one. PATCH accepts `url`, `events`, and `active`. Rotate with `POST /webhooks/{subscriptionId}/rotate-secret`.

| Event                          | Required resource scope | Data                                                                   |
| ------------------------------ | ----------------------- | ---------------------------------------------------------------------- |
| `message.created`              | `conversations:read`    | messageId, conversationId, direction, channel, optional requestEventId |
| `message.delivery.updated`     | `messages:read`         | requestEventId, status, available reply/provider IDs                   |
| `conversation.handoff.updated` | `conversations:read`    | conversationId, caseId, action                                         |
| `crm.opportunity.created`      | `crm:sales:read`        | opportunityId, leadId, revision/stage/status                           |
| `crm.opportunity.updated`      | `crm:sales:read`        | opportunityId, leadId, changed revision/stage/status when present      |

The envelope contains `eventId`, `type`, `schemaVersion:"1.0"`, `tenantId`, `occurredAt`, and `data`. Fetch content through the scoped GET APIs. Internal system/tool turns and playground messages are excluded.

## Verify signatures

Headers: `X-Visito-Event-Id` and `X-Visito-Signature: t=<unix-seconds>,v1=<hex-hmac>`.

Compute HMAC-SHA256 over `timestamp + "." + rawBody` using the subscription secret. Compare using constant-time comparison, reject timestamps outside a five-minute tolerance, and deduplicate on eventId. Verify the raw bytes before JSON parsing.

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(signature, rawBody, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature || "");
  if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return false;
  const expected = createHmac("sha256", secret).update(match[1] + ".").update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(match[2], "hex"));
}
```

## Delivery and recovery

Delivery is at least once, with no ordering guarantee. Return any 2xx after durably accepting the event. Redirects are not followed. Destinations must be public HTTPS on port 443 with public IPv4 DNS answers; local/private/reserved addresses and IPv6-only destinations are unsupported. DNS is rechecked and the connection pinned on every attempt.

Attempts have a 10-second deadline. Failures retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, and 24 hours (seven attempts total). Inspect `GET /webhooks/{subscriptionId}/deliveries` or `GET /webhooks/{subscriptionId}/deliveries/{deliveryId}` with `webhooks:read`. Retry an exhausted failure with `POST /webhooks/{subscriptionId}/deliveries/{deliveryId}/retry`; this starts a new attempt cycle with the same event ID. Metadata is retained for 30 days after completion.

Revoked/expired/downscoped owning keys and disabled/deleted subscriptions cancel pending delivery. An already in-flight HTTP request may finish. Re-enabling does not replay canceled events. Updating a subscription associates it with the calling credential and requires the event read scopes again. Secret rotation takes effect immediately for future attempts.

For an automatic reply integration, act only on inbound `message.created` events. Persist the event ID and use a stable reply `Idempotency-Key` to prevent duplicate replies and outbound webhook loops.
