Skip to navigation

Webhooks

Signed, real-time event notifications for calls, conversations, campaigns, contacts and more.

View as Markdown

Jelliu delivers workspace events to your endpoints as signed POST requests. Deliveries implement the Standard Webhooks specification: each request is signed with a secret unique to the endpoint, carries a stable event id for deduplication, and can be verified with the official standardwebhooks libraries without modification.

Webhooks v2 (2026-09-27). The headers, signature scheme, payload envelope and several event names changed. Receivers built before this date must be migrated; see the changelog.

Managing webhooks

Manage endpoints in the dashboard under Settings → Webhooks (https://app.jelliu.co/settings?tab=webhooks) or through the API:

MethodPathRequired access
GET/api/webhooks/event-typesAny member · read key
GET/api/webhooksAny member · read key
GET/api/webhooks/{webhookId}Any member · read key
POST/api/webhooksOwner or admin · full key
PATCH/api/webhooks/{webhookId}Owner or admin · full key (changing url or headers: signed-in session only)
DELETE/api/webhooks/{webhookId}Owner or admin · full key
POST/api/webhooks/{webhookId}/rotate-secretOwner or admin, signed-in session only (API keys are refused)
POST/api/webhooks/{webhookId}/testOwner or admin · full key
POST/api/webhooks/{webhookId}/replayOwner or admin · full key
POST/api/webhooks/{webhookId}/enableOwner or admin · full key
POST/api/webhooks/{webhookId}/disableOwner or admin · full key
GET/api/webhooks/{webhookId}/deliveriesAny member · read key
GET/api/webhooks/deliveries/{deliveryId}Any member · read key
POST/api/webhooks/deliveries/{deliveryId}/retryOwner or admin · full key

Responses wrap the payload in data; the delivery list also returns meta.next_cursor. Validation errors return 400 VALIDATION_FAILED with a single, specific message in error.message (in Spanish, as in the dashboard). GET /api/webhooks/{webhookId}/delivery-logs remains available as a deprecated alias of /deliveries.

Outbound webhooks are available on every plan. Write operations (create, update, rotate, test, replay, retry, enable, disable) require an active subscription; without one they return 403 BILLING_ERROR with metadata.kind: "subscription_required". Listing, reading, the delivery log and deletion remain available. See Without an active subscription.

Create a webhook

curl -sS -X POST "https://api.jelliu.co/api/webhooks" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/jelliu",
"events": ["call.completed", "call.failed", "campaign.completed"],
"description": "CRM sync"
}'

The 201 response contains the endpoint and data.secret, for example whsec_o2N0WsUq4lyziZDhqOxfc90Dyn/Dhz/NK0x2lS6imYM=. The secret is returned only in this response. Store it with your other credentials.

url
stringRequired

The delivery URL. https only, up to 2000 characters, without embedded credentials. It must resolve to a public address; private, internal, loopback and link-local targets are rejected when the endpoint is saved and again before every attempt.

events
string[]Required

One to 100 event types from the catalog, or "*" for every type that is not opt-in only. Opt-in-only types must be listed explicitly and can be combined with "*": ["*", "contact.created"]. GET /api/webhooks/event-types returns the types available for subscription.

description
string

Up to 500 characters.

filters
object

Restricts deliveries to matching events. See Filters.

headers
object

Up to 20 custom headers sent with every delivery, as name/value strings (values up to 2000 characters, no line breaks). Names reserved by Jelliu are rejected: webhook-*, x-jelliu-*, host, content-length, content-type, connection, transfer-encoding, te, upgrade, keep-alive, proxy-*. A custom user-agent is not sent. Values are encrypted at rest and never returned; responses list each header name with the value "[configured]".

Unknown fields are rejected with 400. A workspace can have up to 20 endpoints.

Updating. PATCH /api/webhooks/{webhookId} accepts the same fields; all are optional, and at least one is required. events, filters and headers replace the stored value; null clears filters, headers and description. The headers object returned by a read can be sent back as is: a header whose value is "[configured]" keeps its stored value. Changing url or headers redirects a live integration and therefore requires a signed-in owner or admin session; API keys receive 403. Use /disable and /enable to deactivate and reactivate an endpoint.

Visibility. Secrets and header values are never returned. Owners, admins and full API keys see the complete url; other members and read keys see only its origin (https://example.com/…), because paths and query strings often carry tokens.

Payload

Every delivery is a POST with Content-Type: application/json and this envelope:

{
"id": "evt_01K6B3X0Q9Z4T7M2N5P8R1S3V6",
"type": "call.completed",
"api_version": "2026-09-27",
"created_at": "2026-09-27T21:04:05.123Z",
"tenant_id": "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"data": {
"call_id": "1679091c-5a88-4faf-8b3a-6c5d4e3f2a10",
"campaign_id": "a87ff679-a2f3-4e71-8c9e-1b2c3d4e5f60",
"contact_id": "e4da3b7f-bbce-4345-9d77-2f1e0c9b8a70",
"agent_id": "3c59dc04-8f1a-4e2b-9d6c-7a1b2c3d4e5f",
"phone_number": "+573001234567",
"outcome": "appointment_booked",
"sentiment_score": 0.62,
"duration": 184,
"summary": "La clienta pidió una cita para el jueves a las 10 a. m. y confirmó su correo.",
"data_collection": { "email": "laura.gomez@example.com", "preferred_day": "jueves" },
"kpi_data": { "appointments_booked": 1 }
}
}
FieldMeaning
idEvent id: evt_ followed by a ULID, so ids sort chronologically. Equal to the webhook-id header, and identical across retries, replays and every endpoint that receives the event.
typeEvent type, from the catalog.
api_versionVersion of the envelope and payload schemas; currently 2026-09-27.
created_atWhen the event occurred (ISO 8601, UTC). Unchanged across attempts.
tenant_idYour workspace id.
dataEvent-specific fields, in snake_case. The example of each type in GET /api/webhooks/event-types shows its fields.

data never contains secrets, agent instructions (prompts), lesson text, raw provider payloads or cost internals. It does contain personal data when the event concerns a person (phone numbers, emails, names, AI summaries, and message text for message.* types); the catalog identifies which types include it. A field that was not produced is null (for example, sentiment_score for an unscored call; 0 is a valid neutral score). New fields and event types may be added at any time: ignore unrecognized fields and respond 2xx to types you do not handle.

Delivery headers

HeaderValue
webhook-idEvent id (evt_…), equal to the body’s id. Identical across attempts and replays. Use it for deduplication.
webhook-timestampUnix time, in seconds, at which this attempt was sent. Each attempt is re-signed with a new timestamp.
webhook-signatureOne or more space-separated signatures, each v1,<base64>. Two are present during the 24 hours following a secret rotation.
x-jelliu-event-typeEvent type, for example call.completed.
x-jelliu-delivery-idDelivery id (dlv_…), identifying the delivery of this event to this endpoint. Use it to look up the delivery in the delivery log.
x-jelliu-attemptAttempt number, starting at 1.
user-agentJelliu-Webhooks/2
content-typeapplication/json

Custom headers are sent alongside these. The v1 X-Webhook-* headers are no longer sent.

Verifying signatures

Each signature is a base64-encoded HMAC-SHA256 of the following content:

<webhook-id>.<webhook-timestamp>.<raw request body>

The HMAC key is the base64-decoded portion of the secret following whsec_, not the secret string itself. The official Standard Webhooks libraries perform this decoding when given the full whsec_… value, and are the recommended way to verify deliveries.

To verify manually:

  1. Read the raw request body before any JSON parsing. Re-serialized JSON does not match the signature.
  2. Reject the request if webhook-timestamp differs from your clock by more than 5 minutes. Each attempt carries a new timestamp, so legitimate retries are never rejected as stale.
  3. Derive the key: base64-decode the portion of the secret following whsec_.
  4. Compute base64(HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>")).
  5. Split webhook-signature on spaces. Accept the request if any v1,<signature> entry matches your result, using a constant-time comparison.
// npm install standardwebhooks express
import express from 'express';
import { Webhook } from 'standardwebhooks';
const app = express();
// The whole "whsec_..." value: the library decodes it.
const wh = new Webhook(process.env.JELLIU_WEBHOOK_SECRET);
// Use the raw body: re-serialized JSON will not match the signature.
app.post('/webhooks/jelliu', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
// Checks webhook-id, webhook-timestamp (5-minute tolerance) and webhook-signature.
event = wh.verify(req.body.toString('utf8'), req.headers);
} catch {
return res.status(401).send('Invalid signature');
}
// Hand off to a queue, deduplicating on event.id (= webhook-id), and acknowledge quickly.
console.log('Received', event.type, event.id);
res.sendStatus(204);
});
app.listen(3000);

Test vector. A correct implementation reproduces the Standard Webhooks specification’s test vector: secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, webhook-id msg_p5jXN8AQM9LWM0D4loKWxJek, webhook-timestamp 1614265330, body {"test": 2432232314} → v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=.

Deduplicate on webhook-id: record processed ids and respond 2xx to a repeated id without processing it again. The same event can be delivered more than once: after a lost response, a manual retry or a replay. The id is identical in every case and across every endpoint that receives the event; if two endpoints feed the same system, track processed ids per endpoint.

Respond with any 2xx status as soon as the signature is verified, and process the event asynchronously to stay within the 10-second timeout. The response body is ignored; its first 1 KB is stored in the delivery log for debugging. Jelliu reads at most 64 KB of a response and closes the connection beyond that; the status code still determines the outcome.

Retries

SuccessAny 2xx status.
Timeout10 seconds per attempt, including DNS resolution.
RetriedTimeouts, network errors, 408, 425, 429 and 5xx.
ScheduleDelays after attempts 1 through 11: 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h, 24 h, 24 h, 24 h, each with ±10% jitter. 12 attempts in total, over approximately 4 days.
Retry-AfterHonored (delta-seconds or an HTTP date, capped at 24 h) when it requests a longer delay than the schedule.
Not retried410 Gone (which also disables the endpoint), any other 4xx, every 3xx (redirects are not followed), and a URL that resolves to a private or internal address (url_not_allowed).

Each attempt uses the endpoint’s current configuration: an updated URL, new headers or a rotated secret apply to pending retries, and deleting or disabling the endpoint cancels them. At most one request per endpoint is in flight at any time. Delivery order is not guaranteed; use created_at or the event id (both sort chronologically) when order matters.

Endpoint health

consecutive_failures counts consecutive deliveries that ended failed; all attempts of one delivery count once. A successful delivery resets the counter.

  • At 3 consecutive failed deliveries, the workspace receives an in-app warning.
  • At 20, provided at least 24 hours have elapsed since the first failure in the streak, the endpoint is disabled (is_active: false, disabled_reason: "consecutive_failures") and the workspace is notified.
  • A 410 Gone response to an automatic delivery disables the endpoint immediately (disabled_reason: "gone").

Tests, manual retries and replays do not affect endpoint health, and neither do failures on Jelliu’s side (for example, a stored secret that cannot be decrypted). After fixing the endpoint, call POST /api/webhooks/{webhookId}/enable, which also resets the counter. Events continue to be stored while an endpoint is disabled and can be replayed afterward.

Without an active subscription

While the workspace has no active subscription (status active, trialing or past_due), no requests are sent: each delivery is canceled when due, with last_error: "subscription_inactive". Cancellations do not affect endpoint health, and the endpoint remains active. Events are still stored for 30 days; after the subscription is reactivated, the missed period can be replayed. See Without an active subscription.

Delivery log

GET /api/webhooks/{webhookId}/deliveries lists the endpoint’s deliveries, newest first: one record per event, updated after each attempt.

curl -sS "https://api.jelliu.co/api/webhooks/$WEBHOOK_ID/deliveries?status=failed&limit=20" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": [
{
"id": "dlv_01K6B2W7M4P1R8T5V2X9Z6B3D0",
"event_id": "evt_01K6B2W7M1N8P5R2T9V6X3Z0B7",
"event_type": "call.failed",
"status": "failed",
"attempt_count": 12,
"next_attempt_at": null,
"last_attempt_at": "2026-09-27T20:46:10.004Z",
"last_status_code": 503,
"last_error": "http_503",
"last_duration_ms": 96,
"manual": false,
"created_at": "2026-09-23T20:45:39.870Z"
}
],
"meta": { "next_cursor": null }
}

Query parameters: status (pending, delivering, succeeded, failed, canceled), type (an event type), limit (1–100, default 50) and cursor (the meta.next_cursor of the previous page; null on the last page). Events excluded by the endpoint’s events or filters never create a delivery; an empty log for an expected event usually indicates a subscription or filter mismatch.

GET /api/webhooks/deliveries/{deliveryId} returns a single delivery with all of its attempts (attempt, at, status_code, duration_ms, error, response_snippet), the event envelope as sent, and the request headers (webhook-id, webhook-timestamp, x-jelliu-*; the signature and custom headers are never returned).

error and last_error contain stable, machine-readable codes:

CodeMeaning
http_<status>The endpoint returned that status, for example http_500.
timeoutNo response within 10 seconds.
connection_error, connection_error:<errno>The connection failed, for example connection_error:ECONNREFUSED.
url_not_allowedThe URL resolves to a private or internal address, or does not use https.
signing_secret_unreadable, custom_headers_unreadable, custom_headers_invalidJelliu could not build the request, and nothing was sent. Never counts against the endpoint.
attempt_abandonedThe last allowed attempt was interrupted on Jelliu’s side.
endpoint_deleted, endpoint_disabled, event_missing, subscription_inactiveCanceled without sending: the endpoint was deleted or disabled, the event is no longer stored, or the workspace has no active subscription.

Events and deliveries are retained for 30 days.

Testing and replay

Test an endpoint. POST /api/webhooks/{webhookId}/test sends a single signed delivery immediately and returns the endpoint’s response:

curl -sS -X POST "https://api.jelliu.co/api/webhooks/$WEBHOOK_ID/test" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "call.completed" }'
{
"data": {
"delivery_id": "dlv_01K6B3X0R2A5C8E1G4J7K0M3N6",
"success": true,
"status_code": 200,
"duration_ms": 184,
"error": null,
"response_snippet": "{\"received\":true}"
}
}

Without a body, it sends a webhook.test event (data: { "message": "Este es un evento de prueba de Jelliu.", "webhook_id": "<the endpoint id>" }). With type, it sends that type with its catalog example data, to exercise a specific handler. A test is a single attempt with no retries, works on a disabled endpoint, appears in the delivery log with manual: true, and does not affect endpoint health.

Retry one delivery. POST /api/webhooks/deliveries/{deliveryId}/retry queues the same event (same webhook-id) for an immediate attempt, using the endpoint’s current URL and secret. It returns 202 with { "data": { "delivery_id": "...", "status": "pending" } }, or 409 while an attempt of that delivery is in flight.

Replay a period. POST /api/webhooks/{webhookId}/replay re-sends every stored event in a time range that matches the endpoint’s current events and filters:

curl -sS -X POST "https://api.jelliu.co/api/webhooks/$WEBHOOK_ID/replay" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "2026-09-26T00:00:00Z", "to": "2026-09-27T12:00:00Z", "types": ["call.completed"] }'

It returns 202 with { "data": { "count": 42 } }, where count is the number of deliveries queued. from must fall within the last 30 days, and a range containing more than 1000 events is rejected (narrow the range or pass types). Replayed events keep their original id, so existing deduplication applies. Test events are never replayed. Events are stored only while the workspace has at least one endpoint, active or disabled. Replays and manual retries follow the standard retry schedule and are marked manual: true; a disabled endpoint cancels them, so enable it first.

Rotating the secret

Rotate a secret from the dashboard, which calls POST /api/webhooks/{webhookId}/rotate-secret. The response contains the new secret, returned only once, and previous_valid_until:

{ "data": { "secret": "whsec_75PxSqBz8FMKgLrHx+vDA4EMu5O7/tsz2FttmQtaDT0=", "previous_valid_until": "2026-09-28T16:45:12.000Z" } }

For the next 24 hours, webhook-signature carries two signatures, one per secret, so a receiver configured with either secret continues to verify requests while the new value is deployed. After 24 hours, only the new secret signs. Rotating again within the window replaces the previous secret: at most two secrets are valid at any time.

API keys cannot rotate secrets or change an endpoint’s url or headers, even with the full scope. This prevents a leaked key from redirecting deliveries or re-signing them with a secret the receiver does not know.

Filters

Filters restrict which events are delivered to the endpoint. All configured filters must match. Keys are snake_case.

FilterTypeMatches when
campaign_idsuuid[] (max 50)data.campaign_id is in the list.
agent_idsuuid[] (max 50)data.agent_id is in the list.
outcomesstring[] (max 20)data.outcome is in the list.
min_sentimentnumber, -1 to 1data.sentiment_score is at least this value. Unscored events (null) are not delivered.
audit_actionsstring[] (max 50)For audit.log_recorded only: data.action starts with one of these prefixes (with or without its FAILED_MUTATION: / FAILED_READ: prefix), for example payments. or FAILED_. Ignored for every other type.
conditionsarray (max 10)Every condition matches. Each is { field, operator, value }: field is a dot path inside data (for example kpi_data.score), operator is eq, neq, gt, lt, gte, lte (numbers only), contains (strings only) or exists.

Filters fail closed: when a filter targets a field that the event’s data does not contain (for example, agent_ids on an event without agent_id), the event is not delivered. For this reason, an endpoint subscribed to audit.log_recorded cannot use campaign_ids, agent_ids, outcomes or min_sentiment; the API rejects the combination. Use audit_actions or a separate endpoint.

Filters saved before 2026-09-27 with camelCase keys (campaignIds, agentIds, minSentiment, auditActions) remain valid and are returned in snake_case. New writes must use snake_case.

Event catalog

"*" covers every type marked No under Opt-in only. Opt-in-only types are high volume and must be listed explicitly in events.

All types below are available. GET /api/webhooks/event-types returns the same list, with English and Spanish descriptions and an example data object for each type.

TypeCategoryOpt-in onlyDescription
call.startedcallNoSent when a call starts, inbound or outbound. For an outbound call, it is sent when the call is queued for dialing, and started_at is that moment; call.failed may follow without call.answered. Includes the contact’s phone number.
call.answeredcallNoSent when an inbound or outbound call is answered. answered_at is the most precise time available (the provider’s conversation start, when present). For inbound calls it may arrive after the call has ended, immediately before call.completed. Includes the contact’s phone number.
call.completedcallNoSent when a call ends after a conversation. Includes the outcome, duration in seconds, sentiment (-1 to 1), the AI-generated summary, the fields the agent extracted and the contact’s phone number. kpi_data holds the campaign KPIs the call counted toward (numbers or labels), or null for a call outside a campaign. Any analysis field is null when it was not produced.
call.failedcallNoSent when a call ends without a conversation, including a call the carrier completed in which the agent never spoke. reason is no_answer, busy, failed, canceled, voicemail or error; outcome equals reason when the call has no analyzed outcome. Sent instead of call.completed, never both. Includes the contact’s phone number.
call.transferredcallNoSent when a call is transferred to a person or another number. reason is warm_transfer (a person took the call) or agent_transfer (the agent transferred it); transferred_to is null when the number is unknown.
call.analysis_updatedcallNoSent when the analysis of an ended call changes (a late or repeated analysis). Includes outcome, sentiment, summary and extracted fields; any of them may be null.
conversation.createdconversationNoSent when a conversation starts on WhatsApp, email, web chat or social channels, or when a website visitor starts a voice session in the widget (channel webchat, contact_id null).
conversation.handoff_requestedconversationNoSent when the agent hands a conversation off to a person. reason is the agent’s short explanation, in the conversation’s language.
conversation.handoff_resolvedconversationNoSent when a person resolves a conversation that was handed off.
conversation.closedconversationNoSent when a conversation is closed, including a widget visitor’s voice session.
conversation.analysis_updatedconversationNoSent when the analysis of a conversation changes. Includes outcome, sentiment and summary.
message.receivedmessageYesSent when a contact sends a message. Includes the message text. High volume: not covered by the wildcard, and recorded only while an endpoint subscribes to it by name.
message.sentmessageYesSent when the agent or a person sends a message. sender is agent or human. Includes the message text. High volume: not covered by the wildcard, and recorded only while an endpoint subscribes to it by name.
message.status_updatedmessageYesSent when a WhatsApp message sent by the agent or a person is delivered, read or fails. High volume: not covered by the wildcard, and recorded only while an active endpoint subscribes to it by name.
contact.createdcontactYesSent when a contact is created, one event per contact (imports included). Includes name, phone number and email. High volume: not covered by the wildcard.
contact.updatedcontactYesSent when a value of a contact actually changes. changed_fields lists the changed fields, never their values: any of name, email, phone_number, whatsapp_number, metadata, crm_provider and crm_external_id. High volume: not covered by the wildcard.
contact.opted_outcontactNoSent once, when a contact opts out of further contact. channel is voice, whatsapp, email or social; basis is request (the person asked) or suppression_list (the number was already on the suppression list). Includes name, phone number and email.
contact.convertedcontactNoSent once per contact, when the contact converts (a sale or the campaign goal) in a call or a text conversation.
contact.erasedcontactNoSent when a contact is erased at the data subject’s request. Contains only contact_id and erased_at; no personal data.
campaign.createdcampaignNoSent when a campaign is created.
campaign.startedcampaignNoSent when a campaign is activated for the first time.
campaign.pausedcampaignNoSent when a campaign is paused. reason is manual (paused by a person), agent_deleted (its agent was deleted) or blocked (the workspace cannot run it: no phone number, no minutes, a withdrawn template or a disconnected mailbox, among others). Only when reason is blocked, blocked_reason carries the explanation shown in the dashboard, in Spanish, and may be null. campaign_name may be null.
campaign.resumedcampaignNoSent when a paused campaign is activated again.
campaign.completedcampaignNoSent when every contact in a campaign has been processed. campaign_name may be null.
campaign.deletedcampaignNoSent when a campaign is deleted.
campaign_contact.failedcampaignNoSent when an attempt to reach a campaign contact fails; further attempts may follow, and the last one is followed by campaign_contact.exhausted. channel is voice, whatsapp or email. For a voice call that did not reach the person, reason is no_answer, busy, failed, canceled or voicemail, and call_id identifies the call. When the contact could not be dialed or messaged, reason is dial_error, outside_schedule, invalid_schedule, compliance_check_error, consent_check_error, concurrency_limit, compliance_window, attempt_cap, compliance_refused, deferral_limit or send_error, with no call_id. attempt is the number of calls to this contact so far, or null when not counted (WhatsApp and email).
campaign_contact.exhaustedcampaignNoSent when a campaign contact runs out of call attempts without being reached, only while the campaign is active and retries are enabled. attempts is the number of calls made in this retry round; last_reason is the reason of the last call, in the call.failed vocabulary: no_answer, busy or failed.
agent.createdagentNoSent when an agent is created. Describes the agent, never its instructions.
agent.updatedagentNoSent when an agent is modified. changed_fields lists the fields, never their values.
agent.deletedagentNoSent when an agent is deleted.
agent.pausedagentNoSent when an agent is paused and stops responding.
agent.resumedagentNoSent when a paused agent resumes responding.
agent.version_publishedagentNoSent when a new agent version is recorded because something that affects its behavior changed. change_source is update, approved_update, workflow_put, workflow_delete, base_resync, tool_surface or restore. changed_fields lists what changed (for example prompt.system_prompt, config.voice_id, tools.allowed_toolkits, workflow), never the values, and may be empty. restored_from_version is the restored version, or null otherwise. Not sent for the version written when the agent is created or deleted.
approval.requestedapprovalNoSent when an agent action is awaiting a person’s approval.
approval.approvedapprovalNoSent when a person approves a pending agent action.
approval.rejectedapprovalNoSent when a person rejects a pending agent action.
approval.executedapprovalNoSent when an approved agent action runs successfully.
approval.execution_failedapprovalNoSent when an approved agent action fails during execution. reason is refused (a check stopped it and nothing was sent) or provider_rejected (the connected app returned an error); the app’s own error text is never included.
approval.expiredapprovalNoSent when a pending agent action expires without a decision.
agent_action.recordedagent_actionYesSent when an agent uses a tool, one event per tool call. Includes redacted parameters, the outcome and a result summary. tool_source is mcp_gateway, integration, engine or internal; action_kind is read, write or unknown; outcome is success, error, denied, timeout or indeterminate; origin is agent, user, system or campaign. High volume: not covered by the wildcard.
appointment.bookedappointmentNoSent when an agent books an appointment in a connected calendar, once per booking even if the same booking is recorded twice. starts_at is the start time exactly as the agent sent it to the calendar (ISO 8601, possibly with a UTC offset), or null when unknown; provider is the calendar app, or null when unknown.
payment.link_createdpaymentNoSent when a payment link is created for a contact.
payment.link_failedpaymentNoSent when the payment provider refuses to create a payment link. reason is provider_error or provider_rejected.
payment.confirmedpaymentNoSent once per payment, when it is confirmed by the provider (cobre, dlocal) or, for a receipt, by a person or a bank credit (provider receipt; payment_id is the receipt ID). amount is in major currency units; currency is uppercase, or null when the source recorded none.
payment.expiredpaymentNoSent when a payment link expires unpaid.
receipt.receivedpaymentNoSent when a contact submits a payment receipt. The image is never included.
receipt.verifiedpaymentNoSent when a payment receipt is approved by a person or matched to a bank credit. Image checks alone never trigger it.
receipt.rejectedpaymentNoSent when a person rejects a payment receipt. The reviewer’s note is never included.
knowledge.document_readyknowledgeNoSent when a knowledge document finishes processing and becomes available to agents.
knowledge.document_failedknowledgeNoSent once per document, when a knowledge document cannot be processed. reason is failed (the provider could not index it) or indexing_error (processing failed).
knowledge.document_deletedknowledgeNoSent when a knowledge document is deleted.
lesson.learnedlessonNoSent when an agent adopts a new behavior: learned from its conversations, taught by the owner or applied from shared learning. kind is the behavior (for example invented_fact, unanswered_question, one_question_per_turn, doubts_before_price). The lesson text is never included.
lesson.retiredlessonNoSent when a learned behavior is retired. reason is expired (it reached its expiry), local_harm (results worsened after it was applied) or unvalidated (it did not show a benefit). kind is included when the lesson expired and omitted otherwise.
lesson.revertedlessonNoSent when a person reverts a learned behavior, one event per reverted lesson: reverting one also reverts the active lesson of the same kind. reverted_by is the user, or null when unknown.
whatsapp_template.status_updatedwhatsappNoSent when the status of a WhatsApp template changes: draft, pending, approved, rejected, paused or disabled.
whatsapp_sender.status_updatedwhatsappNoSent when the status of a WhatsApp number changes: creating, verifying, online, offline or failed.
integration.connectedintegrationNoSent when an integration is connected.
integration.disconnectedintegrationNoSent when an integration is disconnected.
integration.expiredintegrationNoSent when the authorization of an integration expires and the integration must be reconnected.
phone_number.provisionedphone_numberNoSent when a phone number is purchased for the workspace.
phone_number.connectedphone_numberNoSent when a phone number is connected to an agent.
phone_number.deactivatedphone_numberNoSent when a phone number is deactivated.
usage.threshold_reachedbillingNoSent once per billing period, when voice-minute usage reaches 80% of the minutes included in the plan (bonus minutes count toward included).
usage.overage_startedbillingNoSent when usage exceeds the amount included in the plan and overage begins.
execution_budget.warningbillingNoSent when the monthly agent execution budget is running low.
execution_budget.exhaustedbillingNoSent when the monthly agent execution budget is exhausted.
subscription.updatedbillingNoSent when the workspace’s plan or subscription status changes.
crm_sync.completedcrmNoSent when the outcome of a call or conversation is written to at least one connected CRM. results has one entry per CRM, where status is written (the note was written now; external_id is its ID in the CRM), already_written (an earlier attempt wrote it), no_contact (the contact does not exist in that CRM) or failed.
crm_sync.failedcrmNoSent when writing the outcome of a call or conversation fails in every connected CRM, or is skipped because the contact does not exist in them. results has the same shape as in crm_sync.completed; the CRM’s own error text is never included.
audit.log_recordedauditYesSent for each audited action that a person or an API key performs in the workspace, for SIEM ingestion. action is the operation (create, update, delete, …, prefixed with FAILED_MUTATION: when it was refused); changes holds the redacted request body, or null once redacted; user_agent is a coarse {browser, os, device} summary. High volume: not covered by the wildcard.
security.event_recordedauditYesSent for each decision a security control makes about an agent, for SIEM ingestion: a deny rule that blocked a tool (tool_rule_denied) or would have in audit mode (tool_rule_audit), a stored key refused because the request was headed to a host it is not bound to (credential_destination_mismatch), and an access request from an agent raised or decided (access_proposal_created, access_proposal_decided). ocsf is the same event as an OCSF 1.9.0 Detection Finding. Never contains a credential, tool arguments or results. Not covered by the wildcard.
webhook.testmetaNoSent only on demand, from the dashboard or the API, to test an endpoint. Never sent automatically.

Notes on frequently used types:

  • One terminal event per call. Each call ends with exactly one of call.completed (a conversation took place) or call.failed (no conversation). call.failed carries reason: no_answer, busy, failed, canceled, voicemail or error (the call was never placed, for example because the provider refused it).
  • campaign.started and campaign.resumed. The first activation of a campaign sends campaign.started; reactivation after a pause sends campaign.resumed. Both include campaign_id, campaign_name, agent_id, channel, previous_status and status.
  • campaign.paused includes reason: manual (paused by a person), agent_deleted or blocked (the workspace cannot run the campaign; blocked_reason describes the required action).
  • contact.created is sent once for every new contact, regardless of origin: the API or the dashboard (individually, in bulk or from a CSV), an agent, a CRM import or push, a first inbound message on WhatsApp, web chat, Instagram, Messenger or email, an outbound message or call to an unsaved recipient, or an automation. It is not sent when an existing contact is matched or a removed contact is restored. source identifies the origin (see Contact sources).
  • contact.opted_out is sent once, when a contact becomes opted out: the person asks during a call, sends an opt-out keyword on WhatsApp or a social inbox, unsubscribes by email, or a campaign reaches a contact whose number is already on the suppression list (basis: "suppression_list"). Repeated requests send nothing. When one opt-out applies to several contacts (for example, every contact with the same WhatsApp number), each contact receives its own event.
  • agent_action.recorded includes the agent’s in-call handoffs: tool_name transfer_to_number and transfer_to_agent, with tool_source: "engine" and action_kind: "write".
  • Escalations have no dedicated webhook event: a call handed off to a person appears as outcome: "escalated_to_human" on call.completed. The Zapier app provides a call.escalated trigger; see Zapier.
  • Never sent: conversations of Jelliu’s own sales assistant, agent instructions, lesson text, sealed approval arguments, raw receipts or bank data, and cost internals.

Contact sources

source in contact.created identifies the channel through which the contact was created. It never names the underlying tool or provider.

sourceOrigin
api:manualOne contact added through the API or the dashboard form.
api:bulkPOST /api/campaigns/{campaignId}/contacts/bulk.
agent:toolAn agent saving a prospect during a conversation.
csvA CSV import.
crm_importAn import from a connected CRM.
crm_webhookA connected CRM pushing records to Jelliu.
whatsappA first message from an unknown number on your WhatsApp line.
webchatA visitor identifying in the web chat widget.
socialA first direct message on a connected Instagram or Messenger inbox.
emailA first email from an unknown address to the connected mailbox.
outbound_messageA message sent to a recipient who was not saved as a contact.
outbound_callAn outbound call to a number with no contact.
automationAn automation that had to message a number with no contact.
otherAny other origin. New values may be added; treat unknown values as other.

Retired events

On 2026-09-27, two types were renamed: contact.dnc is now contact.opted_out, and agent.action_recorded is now agent_action.recorded. Existing subscriptions to the old names were migrated to the new ones. An endpoint that still lists a name the catalog no longer offers can be saved with it, but such a name cannot be added to an endpoint that does not already list it.

The names retired on 2026-09-15 (call.recording_ready, call.transcript_ready, contact.status_changed, subscription.changed, integration.sync_completed, integration.sync_failed, integration.error) are not part of the catalog. call.started, contact.updated and usage.threshold_reached have been reinstated and are available.