Skip to main content

Webhook Triggers

Webhooks allow external systems (n8n, Zapier, Twilio, custom apps) to activate agents via HTTP POST.

Authentication

  • Read endpoints (GET): JWT + x-tenant-id or API Key
  • Write endpoints (POST/PATCH/DELETE): JWT required
  • Execution endpoints: Own validation (HMAC, API Key, or none)

CRUD Endpoints

These endpoints apply to all trigger types (webhook, email, db_event, schedule).

POST /api/triggers

Create a new trigger.

Request Body:

{
"agent_id": "uuid",
"name": "ERP Chat Integration",
"trigger_type": "webhook",
"trigger_config": {
"auth": { "type": "api_key" },
"query_extraction": { "mode": "field", "field": "chatInput" },
"context_mapping": {
"userId": "$.userId",
"userName": "$.userName"
},
"response_adapter": { "format": "raw", "split_messages": true, "max_messages": 10 },
"session_strategy": { "mode": "derive", "fields": ["userId", "companyId"] }
}
}

Response (201):

{
"success": true,
"data": {
"id": "uuid",
"agent_id": "uuid",
"name": "ERP Chat Integration",
"trigger_type": "webhook",
"webhook_secret": "whs_example_abc123...",
"enabled": true
},
"webhook_url": "https://llm.zihin.ai/api/triggers/webhook/uuid"
}
caution

The webhook_secret is only shown at creation time. Store it securely for HMAC validation.

Other CRUD

EndpointMethodAuthDescription
GET /api/triggersGETHybridList triggers
GET /api/triggers/:idGETHybridGet trigger details
PATCH /api/triggers/:idPATCHJWTUpdate trigger
DELETE /api/triggers/:idDELETEJWTDelete trigger
POST /api/triggers/:id/enablePOSTJWTEnable trigger
POST /api/triggers/:id/disablePOSTJWTDisable trigger
GET /api/triggers/:id/executionsGETHybridExecution history
GET /api/triggers/:id/executions/:executionIdGETHybridEnriched execution details
POST /api/triggers/:id/testPOSTJWTDry-run test
GET /api/triggers/statsGETHybridAggregated statistics
GET /api/triggers/typesGETHybridAvailable trigger types

Webhook Execution

POST /api/triggers/webhook/:id

Execute a webhook trigger. Uses validation configured in trigger_config.auth.

Auth by type:

auth.typeHeader RequiredDescription
signatureX-Webhook-Signature: sha256=<hmac>HMAC-SHA256/SHA512 with webhook_secret
api_keyX-Api-Key: YOUR_API_KEYTenant API Key
noneNo validation

Sync Response (default):

{
"success": true,
"trigger_id": "uuid",
"agent_id": "uuid",
"agent_response": "Found 5 active contracts...",
"execution_id": "uuid",
"usage": { "total_tokens": 1250 },
"latency_ms": 15234
}

ERP Format Response (when response_adapter.format = "ebarn"):

[
{ "message": "Found 5 active contracts:", "type": "text" },
{ "message": "1. Contract ABC - $50,000\n2. Contract DEF - $30,000", "type": "text" }
]

Async Execution with Callback

Triggers can execute in async mode: the webhook responds immediately (202 Accepted) and delivers the agent response via HTTP callback.

Configuration

Add execution inside trigger_config:

{
"execution": {
"mode": "async",
"timeout_ms": 120000,
"ack_response": {
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>",
"content_type": "text/xml"
},
"callback": {
"url": "https://api.twilio.com/2010-04-01/Accounts/{{accountSid}}/Messages.json",
"method": "POST",
"content_type": "application/x-www-form-urlencoded",
"auth": { "type": "basic", "secret_ref": "TWILIO_AUTH" },
"body_template": {
"To": "{{phoneNumber}}",
"From": "whatsapp:+14155238886",
"Body": "{{agent_response}}"
},
"response_transform": {
"strip_markdown": true,
"max_length": 1550
}
}
}
}

Execution Fields

FieldTypeDefaultDescription
modestringsyncsync or async
timeout_msnumber30000Agent execution timeout (1s-10min)
ack_response.bodyanyImmediate response body
callback.urlstringCallback URL (supports {{placeholders}})
callback.methodstringPOSTPOST, PUT, or PATCH
callback.auth.typestringnonebasic, bearer, api_key, or none
callback.auth.secret_refstringVault secret name
callback.body_templateobjectTemplate with {{agent_response}}, etc.
callback.response_transformobjectTransform rules (strip_markdown, max_length)

Auto-derived Transform

When response_transform is omitted, it is derived from response_adapter.format:

Formatstrip_markdownmax_length
ebarnfalse
twimltrue4096
rawfalse

Response Splitting

Long agent responses can be automatically split into multiple chunks for channels with message size limits (WhatsApp ~1600 chars, SMS 160 chars, etc.).

Split is domain-agnostic — it works with any trigger type and any callback endpoint. The agent generates its best response; the infrastructure adapts to channel constraints.

Configuration

Add split_config inside execution:

{
"execution": {
"mode": "async",
"callback": { "..." : "..." },
"split_config": {
"enabled": true,
"max_chunk_size": 1500,
"max_chunks": 10,
"chunk_strategy": "paragraph",
"numbering": true,
"inter_chunk_delay_ms": 500
}
}
}

Split Config Fields

FieldTypeDefaultDescription
enabledbooleanfalseEnable response splitting
max_chunk_sizenumber1500Maximum characters per chunk (100-50000)
max_chunksnumber10Maximum number of chunks (1-50)
chunk_strategystringparagraphSplit strategy: paragraph, sentence, or length
numberingbooleantrueAdd (1/N) header to each chunk
inter_chunk_delay_msnumber500Delay between chunk deliveries in ms (0-10000)

Chunk Strategies

StrategyDescriptionBest For
paragraphSplits on \n\n boundaries, regroups small paragraphsGeneral text, reports
sentenceSplits on sentence endings (.!?), regroups short sentencesConversational text
lengthHard split at exact character countRaw data, code blocks

Behavior

  • If response fits in a single chunk, no splitting occurs
  • Each chunk is delivered via the configured callback sequentially
  • If a chunk delivery fails, remaining chunks are not sent (fail-fast)
  • Numbering format: (1/3)\nFirst chunk text...
Sync Mode

Split works best with mode: "async". In sync mode, the response is returned directly — split only affects adapters that support arrays (e.g., ERP format). For most channels, use async mode with callback.

Channelmax_chunk_sizeinter_chunk_delay_msNotes
WhatsApp (Twilio)1500500Sandbox truncates at ~1600
SMS140300160 char limit with encoding overhead
Slack4000200Block Kit has 3000 char limit per block
ERP30000Array format, no delay needed

Message Buffering (Popcorn)

When users send multiple short messages in rapid succession ("popcorn messages"), each message would normally trigger a separate agent execution. Message buffering consolidates these into a single execution.

How It Works

User sends:    "Olá"          →  buffered (timer starts: 5s)
User sends: "preciso de" → buffered (timer resets: 5s)
User sends: "ajuda" → buffered (timer resets: 5s)
... 5 seconds pass ...
Agent receives: "Olá\npreciso de\najuda" → single execution

The buffer uses a sliding window: each new message resets the timer. After the window expires without new messages, all accumulated messages are concatenated and sent as a single agent execution.

Configuration

Add message_buffer inside execution:

{
"execution": {
"mode": "async",
"callback": { "..." : "..." },
"message_buffer": {
"enabled": true,
"window_ms": 5000,
"max_messages": 10,
"concat_separator": "\n"
}
}
}

Message Buffer Fields

FieldTypeDefaultDescription
enabledbooleanfalseEnable message buffering
window_msnumber5000Debounce window in milliseconds (100-60000)
max_messagesnumber10Maximum messages in buffer before forced execution (1-100)
concat_separatorstring\nSeparator used when joining buffered messages

Behavior

  • Requires Redis — if Redis is unavailable, messages execute immediately (fail-safe)
  • Buffer is per-session (uses the same session key from session_strategy)
  • During the buffer window, the webhook responds with 202 Accepted (no agent execution)
  • After the window expires, the concatenated message is processed and the response is delivered via callback
  • Compatible with split_config — buffered input can produce split output
  • The _buffered_messages count is added to webhookContext for telemetry
Combining Split + Buffer

Split and buffer solve complementary problems. Use both together for channels like WhatsApp:

{
"execution": {
"mode": "async",
"message_buffer": { "enabled": true, "window_ms": 5000 },
"split_config": { "enabled": true, "max_chunk_size": 1500 },
"callback": { "..." : "..." }
}
}

The flow becomes: multiple user messages → buffer → single agent execution → split response → multiple callback deliveries.


Webhook Config Reference

FieldOptionsDescription
auth.typesignature, api_key, noneValidation method
query_extraction.modefield, jmespath, template, full_bodyHow to extract the query
context_mappingobjectJSONPath mapping ($.field) from payload to agent context
response_adapter.formatebarn, slack, teams, twiml, rawResponse format
session_strategy.modederive, ephemeralderive = deterministic session, ephemeral = new each call
timeout_msnumberTimeout in ms (1000-300000, default: 30000)

Context Mapping

Maps fields from the webhook payload into the agent's execution context via JSONPath expressions. These fields are available to the LLM in the system prompt and to tools at runtime.

Naming Conventions

PatternVisibilityUse for
fieldNameVisible in LLM prompt + toolsData the LLM needs (IDs, names, locations)
_fieldNameTools only (hidden from prompt)Internal metadata (message SIDs, internal IDs)

Auto-excluded Fields

These fields are always excluded from the LLM prompt regardless of name — they remain accessible to tools via ${context.field}:

FieldReason
tokenAPI credential — used by tools for authentication
hostAPI base URL — used by tools for requests
userNameAlready rendered in the session context section

Examples

ERP Integration:

{
"context_mapping": {
"userId": "$.user.id",
"companyId": "$.company.id",
"companyName": "$.company.name",
"city": "$.company.address.city",
"state": "$.company.address.state",
"userName": "$.userName",
"token": "$.token",
"host": "$.host"
}
}

The LLM sees: userId, companyId, companyName, city, state. Excluded: userName (session section), token and host (credentials).

WhatsApp via Twilio:

{
"context_mapping": {
"_waId": "$.WaId",
"_messageSid": "$.MessageSid",
"phoneNumber": "$.From",
"profileName": "$.ProfileName"
}
}

The LLM sees: phoneNumber, profileName. Excluded: _waId and _messageSid (prefixed with _).

Safety Net: defaults_from_context

MCP Servers support defaults_from_context in their config to inject context values authoritatively into tool parameters — even if the LLM hallucinates a different value:

{
"config": {
"defaults_from_context": {
"body[].requesterId": "requesterId"
}
}
}

This maps webhookContext.requesterIdbody[].requesterId in every tool call, overriding whatever the LLM sends.


Sender Access Control

Control who can trigger a webhook before agent execution — zero LLM cost for unauthorized senders.

{
"sender_access": {
"mode": "members",
"identity_field": "From",
"match_column": "phone",
"resolve_user": true
}
}
ModeDescription
anyAny sender (default)
membersOnly active tenant members (lookup in tenant_users)
whitelistOnly listed identifiers

Phone numbers are normalized before comparison: whatsapp:+5511999990000 becomes +5511999990000.


WhatsApp via Twilio

Key points for WhatsApp integration:

  • Twilio sends application/x-www-form-urlencoded payloads (accepted automatically)
  • Message field: Body (not chatInput)
  • Use execution.mode: "async" to avoid Twilio's ~15s timeout
  • Format twiml: auto strips markdown + 4096 char limit
  • WhatsApp Sandbox truncates messages > ~1600 chars — use max_length: 1550
  • Markdown tables are converted to bullet-lists automatically

Execution Inspection

GET /api/triggers/:id/executions/:executionId

Returns enriched execution details with correlated session, LLM calls, and aggregated metrics.

Response:

{
"success": true,
"execution": {
"id": "uuid",
"trigger_id": "uuid",
"status": "success",
"started_at": "2026-02-18T14:30:00Z",
"finished_at": "2026-02-18T14:30:12Z",
"execution_time_ms": 12340,
"normalized_query": "List active contracts",
"agent_response": "Found 5 active contracts...",
"response_channel": "webhook_sync",
"tokens_used": 1250,
"session_id": "a1b2c3d4-...",
"webhook_context": {
"userId": "123",
"companyId": "456",
"company": "Acme Corp"
}
},
"session": {
"session_id": "a1b2c3d4-...",
"agent_id": "uuid",
"agent_name": "Sales Assistant",
"status": "active",
"created_at": "2026-02-18T14:00:00Z",
"updated_at": "2026-02-18T14:30:12Z",
"message_count": 8
},
"llm_calls": [
{
"model": "openai.gpt-4.1",
"provider": "openai",
"input_tokens": 850,
"output_tokens": 400,
"cached_input_tokens": 200,
"cost_usd": 0.0125,
"response_time_ms": 3200,
"tools_executed": ["search_contracts"],
"finish_reason": "stop",
"timestamp": "2026-02-18T14:30:02Z"
}
],
"tool_calls": [
{
"id": "uuid",
"tool_name": "search_contracts",
"tool_call_id": "call_abc123",
"tool_input": { "status": "active", "limit": 10 },
"tool_output": "{\"rows\": [...], \"count\": 5}",
"output_preview": "5 rows returned",
"success": true,
"duration_ms": 450,
"error_message": null,
"iteration": 1,
"call_index": 0,
"execution_id": "uuid",
"created_at": "2026-02-18T14:30:01Z"
}
],
"metrics": {
"total_calls": 2,
"total_tokens": 1250,
"total_cost_usd": 0.018,
"avg_latency_ms": 2800,
"models_used": ["openai.gpt-4.1"],
"providers_used": ["openai"]
}
}
tip

The tool_calls array contains granular details for each tool invocation. Older executions without tool call logs return an empty array.


Session → Trigger Navigation

GET /api/v1/sessions/:sessionId/trigger-context

Reverse navigation: from a session, discover which trigger and execution originated it. Returns has_trigger: false if the session was not created by a trigger.

Response (session from trigger):

{
"success": true,
"has_trigger": true,
"trigger": {
"id": "uuid",
"name": "ERP Chat Integration",
"trigger_type": "webhook",
"agent_id": "uuid",
"agent_name": "Sales Assistant",
"enabled": true
},
"execution": {
"id": "uuid",
"status": "success",
"started_at": "2026-02-18T14:30:00Z",
"finished_at": "2026-02-18T14:30:12Z",
"execution_time_ms": 12340
},
"navigation": {
"execution_detail": "/api/triggers/uuid/executions/uuid"
}
}

Error Codes

CodeHTTPDescription
invalid_input400Missing or invalid field
trigger_not_found404Trigger not found
trigger_disabled400Trigger is disabled
invalid_signature401HMAC/Svix validation failed
webhook_sender_denied403Webhook sender not authorized (sender_access)
execution_failed500Agent execution error