Skip to main content

Webhooks

Arbol uses Svix as its outbound webhook delivery platform. Internally, Arbol receives provider events from systems such as ElevenLabs and Kapso, normalizes them into Arbol domain models, persists the relevant state, and only then emits outbound webhook events to customer-defined endpoints through Svix. This guide documents:
  • How Arbol webhooks are designed
  • How webhook events should be named going forward
  • What the current public catalog looks like today
  • How integrators should consume webhook deliveries safely
  • How the Arbol team should add new events in the future

Table of Contents

  1. Platform Overview
  2. Current Public Catalog
  3. Event Naming Standard
  4. Payload Envelope
  5. Payload Design Rules
  6. Current Event Reference
  7. Planned Event Families
  8. Delivery Model
  9. Consumer Integration Guidelines
  10. How Arbol Should Add New Events
  11. Syncing Svix
  12. Versioning and Compatibility

Platform Overview

Arbol webhooks are domain events, not raw provider pass-through notifications. That distinction matters. Provider webhooks often contain provider-specific fields, partial state, or transport details that are useful internally but not ideal as the long-term public interface for customer integrations. Arbol’s webhook platform is designed to translate those raw inputs into stable business events. The intended flow is:
  1. A provider event or product action occurs.
  2. Arbol validates and normalizes that input.
  3. Arbol persists or updates its own database records.
  4. Arbol emits a stable outbound event to Svix.
  5. Svix handles delivery, retries, signatures, endpoint fanout, replay, and observability.

Why this design exists

This model lets Arbol:
  • Keep provider-specific details private unless explicitly useful
  • Emit payloads that match Arbol’s own data model
  • Avoid sending partially processed state
  • Maintain a cleaner, future-proof integration surface
  • Change internal implementation details without breaking customer integrations

What this means in practice

When possible, outbound events should be emitted after Arbol has:
  • resolved the organization
  • resolved or created the relevant conversation
  • resolved the contact
  • stored messages
  • stored evaluations, properties, or scorecards if those belong to the event
In other words, public webhook payloads should usually be assembled from Arbol’s own persisted state, or from a mixture of persisted state plus trusted runtime context.

Current Public Catalog

Arbol currently publishes exactly two public event types:
  • call.completed
  • whatsapp.message.received
These are the only active public events in the current catalog and the only ones that should be treated as implemented and available today.

Important note about naming

The current V1 public catalog started before the naming standard in this document was finalized. Future events should follow the naming convention described below. That means:
  • existing implemented names remain the source of truth for current integrations
  • future events should follow the new naming rules
  • if Arbol later renames an existing event for consistency, it should be done through a compatibility plan rather than a silent breaking change

Event Naming Standard

For future event types, Arbol should use this convention: domain.resource.action Use additional segments only when they add real meaning: domain.resource.subresource.action Examples:
  • crm.contact.created
  • crm.contact.updated
  • crm.contact.deleted
  • call.updated
  • call.completed
  • call.evaluated
  • call.scored
  • whatsapp.message.sent
  • whatsapp.message.delivered
  • whatsapp.channel.connected

Naming principles

Good event names are:
  • short
  • predictable
  • resource-oriented
  • action-oriented
  • free of redundant hierarchy
Bad event names are usually too verbose or repeat information already implied by the domain. Avoid names like:
  • call.status.updated
  • crm.contact.note.added.updated
  • whatsapp.channel.connection.connected
Prefer:
  • call.updated
  • crm.contact.updated
  • whatsapp.channel.connected

Model the thing that changed

A useful rule of thumb is: If the resource that changed is the contact, emit crm.contact.updated, even if the immediate cause was “a note was added” or “AI enrichment updated the contact”. The reason can live in the payload:
{
  "type": "crm.contact.updated",
  "timestamp": "2026-03-30T15:00:00.000Z",
  "data": {
    "contact": {
      "id": "cnt_123"
    },
    "changes": ["note_added"],
    "updatedFields": ["notes"]
  }
}
This keeps the catalog compact while still preserving useful detail. Prefer this vocabulary:
  • created
  • updated
  • deleted
  • connected
  • disconnected
  • received
  • sent
  • delivered
  • read
  • failed
  • completed
  • evaluated
  • scored
Avoid inventing verbs unless they represent a truly distinct domain event. For example:
  • prefer call.completed over call.finished_successfully
  • prefer call.updated over call.status.updated
  • prefer call.scored over call.scorecard.completed unless scorecards are intentionally exposed as a first-class public resource

Payload Envelope

Every outbound webhook delivery uses the same top-level envelope:
{
  "type": "call.completed",
  "timestamp": "2026-03-30T15:00:00.000Z",
  "data": {}
}

Envelope fields

  • type The event type identifier, such as call.completed or whatsapp.message.received.
  • timestamp The timestamp when Arbol emitted the webhook payload.
  • data The event-specific payload.

Why the envelope matters

This shape allows consumers to:
  • route by event type quickly
  • log or store a uniform structure
  • reuse common validation logic across events
  • support a single endpoint receiving many event types

Payload Design Rules

All future payloads should follow these rules.

1. Use Arbol IDs wherever possible

If Arbol has already persisted a record, payloads should prioritize Arbol identifiers:
  • org_*
  • cnt_*
  • cnv_*
  • agt_*
  • msg_*
External identifiers may also be included, but they should not replace Arbol IDs when Arbol IDs exist.

2. Include stable business context

When relevant, payloads should include:
  • organization
  • conversation
  • agent
  • contact
  • channel
  • message
This makes events self-contained enough for most integrations.

3. Avoid provider-only payloads

Provider-specific metadata is useful, but it should not dominate the public contract. If it is included, place it in a clearly scoped field such as:
  • provider
  • external
  • metadata

4. Prefer resource snapshots over partial fragments

For most public events, it is better to send a coherent snapshot of the relevant object than a tiny patch with no surrounding context. For example, whatsapp.message.received should include the normalized message plus conversation, contact, and channel context instead of only a raw message ID and text body.

5. Use updated plus payload detail instead of event-name explosion

If the same resource can change for many reasons, keep the event name broad and express the cause in payload fields. For example:
  • event name: crm.contact.updated
  • payload detail: changes, updatedFields, source, reason

6. Emit only after meaningful state is ready

If an event claims that a process is complete, the payload should reflect the finalized state. Examples:
  • call.completed should only fire after post-call processing is finished
  • call.scored should only fire after scorecard generation is done
  • whatsapp.message.received should only fire after the inbound message has been normalized and stored

Current Event Reference

call.completed

Sent when a voice call reaches a terminal state after Arbol finishes post-call processing. Included data:
  • organization identifiers
  • conversation metadata and terminal status
  • agent and contact context
  • transcript messages
  • extracted properties
  • evaluation results
Example:
{
  "type": "call.completed",
  "timestamp": "2026-03-30T15:00:00.000Z",
  "data": {
    "organization": {
      "id": "org_123",
      "identifier": "acme",
      "name": "Acme Inc."
    },
    "conversation": {
      "id": "cnv_123",
      "externalId": "conv_ext_123",
      "status": "SUCCESS",
      "direction": "OUTBOUND",
      "channel": "VOICE",
      "startedAt": "2026-03-30T14:56:10.000Z",
      "endedAt": "2026-03-30T15:00:00.000Z",
      "durationSeconds": 230,
      "title": "Call from +1 555 0100",
      "summary": "The customer asked to schedule a demo for Friday.",
      "language": "en",
      "senderTelephone": "+15550100",
      "recipientTelephone": "+15550200",
      "terminationReason": "call_completed"
    },
    "agent": {
      "id": "agt_123",
      "name": "Sales Agent"
    },
    "contact": {
      "id": "cnt_123",
      "givenName": "Bruce",
      "familyName": "Wayne",
      "name": "Bruce Wayne",
      "phone": "+15550200",
      "email": "bruce@example.com"
    },
    "messages": [
      {
        "role": "AGENT",
        "content": "Hi Bruce, thanks for taking the call.",
        "sequence": 0,
        "startTime": 0,
        "endTime": 4
      },
      {
        "role": "CONTACT",
        "content": "I want to book a demo this Friday.",
        "sequence": 1,
        "startTime": 5,
        "endTime": 11
      }
    ],
    "evaluations": [
      {
        "identifier": "qualified_lead",
        "status": "SUCCESS",
        "explanation": "The caller confirmed budget and timeline."
      }
    ],
    "properties": [
      {
        "identifier": "requested_demo_date",
        "values": ["2026-04-03"]
      }
    ]
  }
}

whatsapp.message.received

Sent when Arbol receives, normalizes, and stores an inbound customer WhatsApp message. Included data:
  • organization identifiers
  • WhatsApp channel metadata
  • conversation context
  • agent and contact context
  • normalized message content and message metadata
Example:
{
  "type": "whatsapp.message.received",
  "timestamp": "2026-03-30T15:00:00.000Z",
  "data": {
    "organization": {
      "id": "org_123",
      "identifier": "acme",
      "name": "Acme Inc."
    },
    "channel": {
      "id": "chc_123",
      "type": "WHATSAPP",
      "externalId": "1234567890",
      "phoneNumber": "+15550100",
      "displayName": "Acme Support"
    },
    "conversation": {
      "id": "cnv_456",
      "externalId": "kapso_conv_456",
      "channel": "WHATSAPP",
      "direction": "INBOUND",
      "status": "IN_PROGRESS"
    },
    "agent": {
      "id": "agt_123",
      "name": "Support Agent"
    },
    "contact": {
      "id": "cnt_123",
      "givenName": "Bruce",
      "familyName": "Wayne",
      "name": "Bruce Wayne",
      "phone": "+573001112233"
    },
    "message": {
      "id": "msg_789",
      "externalId": "wamid.HBgLNTczMDAxMTEyMjMzFQIAERgSODVBRDM1QTAyQjA2MTIyRDAA",
      "content": "Hola, necesito ayuda con mi pedido",
      "type": "text",
      "mediaUrl": null,
      "transcript": null,
      "receivedAt": "2026-03-30T15:00:00.000Z"
    }
  }
}

Planned Event Families

The following families are good candidates for future additions. These names are recommendations, not currently implemented public events.

CRM events

  • crm.contact.created
  • crm.contact.updated
  • crm.contact.deleted
Use these when the contact is the thing that changed, even if the immediate cause was:
  • manual editing
  • AI enrichment
  • note creation
  • contact point verification
The payload can carry more detail via:
  • changes
  • updatedFields
  • source
  • reason

Call events

  • call.updated
  • call.completed
  • call.evaluated
  • call.scored
Guidance:
  • Use call.updated for meaningful state changes
  • Use call.completed for finalized post-call state
  • Use call.evaluated when evaluation criteria have been persisted
  • Use call.scored when the scorecard has been computed and stored

WhatsApp message events

  • whatsapp.message.received
  • whatsapp.message.sent
  • whatsapp.message.delivered
  • whatsapp.message.read
  • whatsapp.message.failed
These are especially valuable for CRM sync, support tooling, delivery analytics, and customer engagement automations.

WhatsApp channel events

  • whatsapp.channel.connected
  • whatsapp.channel.disconnected
These should represent Arbol’s channel lifecycle, not just a raw provider callback.

WhatsApp conversation events

  • whatsapp.conversation.created
  • whatsapp.conversation.inactive
  • whatsapp.conversation.ended
  • whatsapp.conversation.completed
Use whatsapp.conversation.completed when you want to expose the point at which Arbol has already processed the conversation and the final normalized state is ready for external systems.

Delivery Model

Arbol webhook delivery should be treated as at-least-once. Consumers must assume:
  • the same event may be delivered more than once
  • delivery order is not guaranteed across different events
  • there may be retries after transient failures

Event identity

When available, Arbol should provide a stable event identifier through Svix’s eventId. Recommended patterns:
  • for call completion events, use the Arbol conversation ID
  • for inbound WhatsApp message events, use the upstream WhatsApp message ID if available, otherwise the stored Arbol message ID
  • for future update events, use a deterministic ID that reflects the actual state transition or mutation

Fast acknowledgment

Consumers should return a 2xx response quickly and do longer processing asynchronously.

Replay support

Svix replay is part of the operational contract. Consumers should make their handlers idempotent so replaying a message is safe.

Consumer Integration Guidelines

If you are consuming Arbol webhooks, your endpoint should:
  • accept POST requests
  • verify Svix signatures before processing
  • treat deliveries as at-least-once
  • use type for routing
  • store processed event IDs for idempotency
  • enqueue long-running work instead of blocking the webhook response
  1. Receive the request.
  2. Verify the Svix signature.
  3. Parse the JSON envelope.
  4. Check whether the event has already been processed.
  5. Route by type.
  6. Persist or enqueue downstream work.
  7. Return 2xx.

Idempotency strategy

Store one of the following:
  • Svix message ID
  • webhook eventId
  • a derived stable processing key
Do not rely only on timestamps.

How Arbol Should Add New Events

Every new public event should follow the same lifecycle.

1. Define the event in the shared catalog

Each catalog entry should include:
  • event type
  • display name
  • short description
  • JSON Schema
  • example payload
  • logical group name

2. Decide the correct emission point

Emit the event from the place where Arbol has the right level of truth. Usually that means:
  • after persistence
  • after normalization
  • after enrichment
  • after evaluation or scoring if those are part of the meaning of the event

3. Build the payload from Arbol’s domain context

Payloads should prefer:
  • database-backed identifiers
  • normalized conversation state
  • normalized contact data
  • channel metadata from Arbol records

4. Document the event

Every public event should be described in this guide and exposed through the Svix Event Catalog.

5. Sync Svix

After catalog changes, sync them to Svix so the App Portal stays aligned with the code.

Syncing Svix

Arbol keeps the Svix event catalog in code. When the catalog changes, sync it to Svix with:
bun sync:webhooks
This command updates:
  • event types
  • descriptions
  • logical group names
  • JSON Schemas
  • example payloads

Versioning and Compatibility

Webhook contracts should be treated as public APIs.

Safe changes

These are generally safe:
  • adding optional fields
  • adding new event types
  • improving descriptions and examples
  • adding non-breaking metadata fields

Breaking changes

These require a compatibility plan:
  • renaming an event type
  • removing existing fields
  • changing field semantics
  • changing ID strategy unexpectedly
Prefer one of these:
  • publish a new event name
  • support both old and new names temporarily
  • document the migration window clearly
Do not silently repurpose an existing event.

Summary

Arbol’s webhook platform should be built around stable domain events rather than raw provider callbacks. The most important long-term rules are:
  • keep the public catalog small and intentional
  • use clear event names such as crm.contact.updated or whatsapp.message.delivered
  • prefer normalized Arbol state over raw provider payloads
  • emit events only after meaningful state is ready
  • document every public event in code and in Svix
For current integrations, rely on the implemented public catalog shown above. For future webhook expansion, follow the naming and payload rules in this guide so new events stay consistent, stable, and easy to integrate with.