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

# Webhooks

> Send positive monitor results to your system to update customer histories automatically.

Connect a public HTTPS receiver under **Settings → Integrations → Webhooks**.
When a selected [monitor](/docs/monitors) flags a new call, Zelto sends a
`monitor.matched` event. Your receiver can save the result in your database,
CRM, or customer timeline. Zelto sends the event to your HTTP endpoint; your
system controls the database write.

## Set up a destination

Owners and administrators can manage destinations and delivery history.

1. Create the monitors whose positive results you want to receive.
2. Choose **Add destination**, enter a name and a public HTTPS URL on port 443,
   and select monitors (up to 100), enable completed monitor exports, or both. **Select current monitors (up to 100)**
   selects up to 100 from the current list; future monitors are not subscribed automatically.
3. Save the destination. It starts **Paused**. Copy the generated signing secret,
   which is displayed only once, and store it securely in your receiver.
4. Choose **Send test**. Handle `webhook.test` as described below and acknowledge
   it with any 2xx response. Tests can run while the destination is paused.
5. Choose **Enable deliveries** in the destination’s setup steps, or enable
   deliveries from **Edit destination**. For `monitor.matched`, only calls ingested
   after activation are eligible. Check **Last test** and the history to confirm delivery;
   background scheduling normally picks up pending events within one minute.

For `monitor.matched`, activation does not send events for existing calls.
Adding another monitor preserves coverage for unchanged subscriptions. Removing
and later re-adding a monitor, or pausing and reactivating the destination, starts
coverage from the new activation time. A provider's original call start time can
precede ingestion. These activation and subscription rules do not limit completed
export contents; an explicitly requested export may include older calls.

Destinations show only the receiver hostname; credentials, paths and query
parameters stay hidden. When editing, leave the URL blank to keep it. **Generate a new signing secret**
rotates the secret and pauses the destination. Update your receiver, send a test,
and enable deliveries again. Changing the URL or secret cancels pending deliveries; set
up your receiver before enabling delivery. An HTTP request already started may
still arrive after a destination is paused or edited.

## Events

### `monitor.matched`

One event is recorded for the first positive result of each call and monitor
while an eligible destination exists. Reprocessing or changing the monitor's
version does not create another event for that pair. A previously negative
result can produce its first event when it becomes positive. Negative, unscored,
failed, preview, benchmark, and isolated experiment results do not emit events.
A later correction does not retract an already delivered detection.

This is a post-analysis event, not live keyword detection during a call. It
contains the monitor's existing typed outputs and explanations. A list of
matched keywords is available only if your monitor actually extracts that list.

Synthetic example:

```json theme={null}
{
  "id": "019a0000-0000-7000-8000-000000000001",
  "type": "monitor.matched",
  "schemaVersion": 1,
  "occurredAt": "2026-10-14T15:30:00.000Z",
  "data": {
    "conversation": {
      "id": "019a0000-0000-7000-8000-000000000002",
      "externalId": "your-call-123",
      "endUserId": null,
      "startedAt": "2026-10-14T15:25:00.000Z",
      "url": "https://dashboard.zelto.ai/org/example/conversations/019a0000-0000-7000-8000-000000000002"
    },
    "agent": {
      "id": "019a0000-0000-7000-8000-000000000003",
      "name": "Collections agent"
    },
    "monitor": {
      "id": "019a0000-0000-7000-8000-000000000004",
      "name": "Complaint",
      "version": 3
    },
    "result": {
      "id": "019a0000-0000-7000-8000-000000000005",
      "affected": true,
      "overallScore": 100,
      "evaluatedAt": "2026-10-14T15:30:00.000Z",
      "items": [
        {
          "stageId": "complaint",
          "label": "Complaint",
          "kind": "boolean",
          "value": true,
          "reasoning": "The caller requests to file a complaint."
        }
      ]
    }
  }
}
```

Use `data.conversation.externalId`, the call ID you originally supplied, to join
the event to your call records. `endUserId` is an optional opaque person ID you
can supply when [uploading calls](/docs/integrations/api-call-upload). Either may
be null when absent. `company_id` identifies a company or creditor and must not
be treated as the called person's ID.

`occurredAt` is when the event was recorded; `evaluatedAt` identifies the
captured result. `startedAt` may be null. `overallScore` is a number or null;
`items[].value` preserves its original JSON type. The snapshot stays unchanged
across retries. Full transcripts, recordings, phone numbers, prompts, and
free-form call metadata are not included. Monitor outputs can themselves contain
customer information, so store them with the same controls as your call results.

Filter-only monitors can have an empty `items` list: passing their filters is
itself the positive result. No extra keyword extraction is performed.

### `webhook.test`

Tests contain no real call or customer data:

```json theme={null}
{ "id": "019a0000-0000-7000-8000-000000000099", "type": "webhook.test", "schemaVersion": 1, "occurredAt": "2026-10-14T15:30:00.000Z", "data": { "test": true } }
```

Verify the signature and return 2xx for this type. Do **not** insert a customer
history entry. Tests are explicitly requested from the destination screen,
appear separately by event type in history, and do not reserve a monitor match.

## Verify the signature

Every request contains:

| Header | Meaning |
| - | - |
| `X-Zelto-Event-Id` | Stable event ID; use it to deduplicate customer history writes. |
| `Idempotency-Key` | The same event ID. |
| `X-Zelto-Delivery-Id` | Delivery ID for this destination. |
| `X-Zelto-Timestamp` | Unix timestamp in seconds for this attempt. |
| `X-Zelto-Signature` | `v1=` followed by the hexadecimal HMAC-SHA256 digest. |

The signed message is `timestamp + "." + rawBody`. Verify the **original raw
request body**, before parsing JSON or changing whitespace. The timestamp and
signature change for each attempt; the event ID remains the same. The body also remains the same for `monitor.matched` and `webhook.test`; export download links are renewed on every attempt.
This Node.js verifier rejects signatures older than five minutes:

```javascript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody, timestamp, signature, secret, now = Date.now()) {
  if (!/^\d+$/.test(timestamp ?? "")) return false;
  const age = Math.abs(now / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  if (!/^v1=[a-f0-9]{64}$/.test(signature ?? "")) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest();
  return timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"));
}
```

Read the header values from your HTTP framework and pass the raw UTF-8 body,
then parse JSON only after verification. Reject failed verification with a
non-2xx response. Keep the signing secret server-side.

## Save once, acknowledge, and retry

After verification, branch on `type`. For `monitor.matched`, identify the person
through your call's external ID or supplied `endUserId`, then commit the event ID
and the history entry in one database transaction. Enforce a unique constraint
on the event ID. Return 2xx only after durable processing, or after durably
queuing the work in your system. If the ID was already processed, return 2xx
without inserting another history entry.

Delivery is **at least once**, with no ordering guarantee. A response can be lost
after your database commit, so automatic and manual retries may repeat an event.
Do not deduplicate by monitor name or assume only one monitor can match a call.

Zelto allows up to six automatic attempts. Base delays between attempts are 5,
10, 20, 40, and 80 minutes, plus up to 30 seconds of jitter. Non-2xx responses,
network failures, and timeouts are retried; a valid `Retry-After` on 429 or 503
postpones the next attempt if later. Invalid or private destinations fail without
repeated delivery attempts. DNS has a 5-second timeout and HTTP has a 15-second
timeout. Redirects are not followed. The scheduler can add a short delay to a due
attempt. Deliveries run separately from call analysis.

**Delivery history** shows the latest 50 deliveries, statuses, attempts, bounded
error summaries, and up to 100 recent attempt outcomes. Monitor detections show
the monitor and a link to the retained call, including its external reference
when present. Search and status filters apply to these latest 50 deliveries.
Technical event IDs and attempt logs are available under **Technical details
and attempts**. Pending deliveries refresh every 15 seconds while the page is
visible; automatic updates pause while editing or viewing a one-time secret.
Use **Update** to refresh manually.
Use **Resend** for a terminal delivery while its content is retained and the
destination is enabled (test events can also be resent while paused). Resend
uses the current destination configuration, the same event ID and manifest (with a renewed download link for exports), and a
new automatic-attempt budget. An active or still-leased delivery cannot be
restarted; wait for it to finish or its lease to expire. There is no bulk replay
of historical calls.

Event content is retained for at most 30 days from call ingestion, or the
organization's shorter effective retention policy. Deleting the call, monitor,
or organization removes the related event content. Expired content cannot be
sent again. A minimal event identifier remains while the call and monitor
exist to prevent re-emitting the same first match after content cleanup.

## Other webhook features

These per-call webhooks are separate from the aggregate
[weekly monitor report webhook](/docs/reports#saved-monitor-reports), whose
`monitor_report.weekly` payload and delivery-ID deduplication are unchanged.
Provider webhooks in [voice integrations](/docs/integrations/index) send calls
**into** Zelto; this integration sends detections **out** to your system.

## Completed monitor exports

Owners/admins can enable **Receive completed monitor exports** for a destination.
It authorizes complete files from any monitors selected by an organization member;
monitor subscriptions do not restrict export contents. Existing destinations default
to off. In **Monitors → Exports → Create export**, choose an active approved
receiver under **Notify when complete**, or leave **No notification** selected.
Destinations can receive only exports without subscribing to individual monitors.

Zelto records one `monitor_export.completed` event per export and sends it only
after the private file is complete. Its envelope uses the same signed headers:

```json theme={null}
{
  "id": "019a0000-0000-7000-8000-000000000011",
  "type": "monitor_export.completed",
  "schemaVersion": 1,
  "occurredAt": "2026-10-15T12:00:00.000Z",
  "data": {
    "exportId": "019a0000-0000-7000-8000-000000000012",
    "format": "csv",
    "rowCount": 150,
    "bytes": 18000,
    "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "completedAt": "2026-10-15T12:00:00.000Z",
    "fileExpiresAt": "2026-11-14T12:00:00.000Z",
    "downloadUrl": "https://private-storage.example/signed-download",
    "downloadExpiresAt": "2026-10-15T13:00:00.000Z"
  }
}
```

Verify HMAC against the received raw bytes, then GET `downloadUrl` directly;
no Zelto login or API key is required. Treat it as a temporary credential and
never log it. Verify file size and SHA-256, and deduplicate work using event ID.
The URL can be used more than once before expiration. Return 2xx to acknowledge
the event; that does not confirm the file was downloaded or processed.

Links last **up to one hour**, capped by file lifetime, webhook retention and
signing credentials. Always use `downloadExpiresAt`. Each retry or manual resend
renews **only `downloadUrl` and `downloadExpiresAt`**, with the same event ID and
manifest. This is an explicit exception to immutable-body retries for the other
event types. Do not reject a valid retry because its download URL changed.

A failed notification does not fail the completed export. Six delivery attempts,
backoff, history and manual resend apply as above. Admins can resend while the
original destination grant, creator membership, event and file remain valid.
Files remain available for 30 days, but current webhook retention may stop
resends earlier. Expired files require a new export with current data.

Pausing or editing URL/secret, removing export approval, or loss of creator
membership stops new links; reactivation does not restore a revoked grant.
Already authorized HTTP requests may arrive and issued links expire naturally.
After revocation, create a new export and explicitly select an approved receiver.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.