Webhook Triggers
Webhooks allow external systems (n8n, Zapier, Twilio, custom apps) to activate agents via HTTP POST.
Authentication
- Read endpoints (GET): JWT +
x-tenant-idor 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"
}
The webhook_secret is only shown at creation time. Store it securely for HMAC validation.
Other CRUD
| Endpoint | Method | Auth | Description |
|---|---|---|---|
GET /api/triggers | GET | Hybrid | List triggers |
GET /api/triggers/:id | GET | Hybrid | Get trigger details |
PATCH /api/triggers/:id | PATCH | JWT | Update trigger |
DELETE /api/triggers/:id | DELETE | JWT | Delete trigger |
POST /api/triggers/:id/enable | POST | JWT | Enable trigger |
POST /api/triggers/:id/disable | POST | JWT | Disable trigger |
GET /api/triggers/:id/executions | GET | Hybrid | Execution history |
GET /api/triggers/:id/executions/:executionId | GET | Hybrid | Enriched execution details |
POST /api/triggers/:id/test | POST | JWT | Dry-run test |
GET /api/triggers/stats | GET | Hybrid | Aggregated statistics |
GET /api/triggers/types | GET | Hybrid | Available trigger types |
Webhook Execution
POST /api/triggers/webhook/:id
Execute a webhook trigger. Uses validation configured in trigger_config.auth.
Auth by type:
auth.type | Header Required | Description |
|---|---|---|
signature | X-Webhook-Signature: sha256=<hmac> | HMAC-SHA256/SHA512 with webhook_secret |
api_key | X-Api-Key: YOUR_API_KEY | Tenant API Key |
none | — | No 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
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | sync | sync or async |
timeout_ms | number | 30000 | Agent execution timeout (1s-10min) |
ack_response.body | any | — | Immediate response body |
callback.url | string | — | Callback URL (supports {{placeholders}}) |
callback.method | string | POST | POST, PUT, or PATCH |
callback.auth.type | string | none | basic, bearer, api_key, or none |
callback.auth.secret_ref | string | — | Vault secret name |
callback.body_template | object | — | Template with {{agent_response}}, etc. |
callback.response_transform | object | — | Transform rules (strip_markdown, max_length) |
Auto-derived Transform
When response_transform is omitted, it is derived from response_adapter.format:
| Format | strip_markdown | max_length |
|---|---|---|
ebarn | false | — |
twiml | true | 4096 |
raw | false | — |
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
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable response splitting |
max_chunk_size | number | 1500 | Maximum characters per chunk (100-50000) |
max_chunks | number | 10 | Maximum number of chunks (1-50) |
chunk_strategy | string | paragraph | Split strategy: paragraph, sentence, or length |
numbering | boolean | true | Add (1/N) header to each chunk |
inter_chunk_delay_ms | number | 500 | Delay between chunk deliveries in ms (0-10000) |
Chunk Strategies
| Strategy | Description | Best For |
|---|---|---|
paragraph | Splits on \n\n boundaries, regroups small paragraphs | General text, reports |
sentence | Splits on sentence endings (.!?), regroups short sentences | Conversational text |
length | Hard split at exact character count | Raw data, code blocks |
Behavior
- If response fits in a single chunk, no splitting occurs
- Each chunk is delivered via the configured
callbacksequentially - If a chunk delivery fails, remaining chunks are not sent (fail-fast)
- Numbering format:
(1/3)\nFirst chunk text...
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.
Recommended Settings by Channel
| Channel | max_chunk_size | inter_chunk_delay_ms | Notes |
|---|---|---|---|
| WhatsApp (Twilio) | 1500 | 500 | Sandbox truncates at ~1600 |
| SMS | 140 | 300 | 160 char limit with encoding overhead |
| Slack | 4000 | 200 | Block Kit has 3000 char limit per block |
| ERP | 3000 | 0 | Array 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
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable message buffering |
window_ms | number | 5000 | Debounce window in milliseconds (100-60000) |
max_messages | number | 10 | Maximum messages in buffer before forced execution (1-100) |
concat_separator | string | \n | Separator 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_messagescount is added towebhookContextfor telemetry
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
| Field | Options | Description |
|---|---|---|
auth.type | signature, api_key, none | Validation method |
query_extraction.mode | field, jmespath, template, full_body | How to extract the query |
context_mapping | object | JSONPath mapping ($.field) from payload to agent context |
response_adapter.format | ebarn, slack, teams, twiml, raw | Response format |
session_strategy.mode | derive, ephemeral | derive = deterministic session, ephemeral = new each call |
timeout_ms | number | Timeout 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
| Pattern | Visibility | Use for |
|---|---|---|
fieldName | Visible in LLM prompt + tools | Data the LLM needs (IDs, names, locations) |
_fieldName | Tools 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}:
| Field | Reason |
|---|---|
token | API credential — used by tools for authentication |
host | API base URL — used by tools for requests |
userName | Already 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.requesterId → body[].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
}
}
| Mode | Description |
|---|---|
any | Any sender (default) |
members | Only active tenant members (verified against team directory) |
whitelist | Only listed identifiers |
Phone numbers are normalized before comparison: whatsapp:+5511999990000 becomes +5511999990000.
WhatsApp via Twilio
Twilio is a Business Solution Provider (BSP) for the WhatsApp Business API. The Zihin platform sends and receives WhatsApp messages through a Twilio number registered as a WhatsApp Sender. This guide covers the full integration end-to-end.
For lower cost and fewer moving parts, the Zihin platform also supports the official Meta WhatsApp Cloud API without going through a BSP. See Meta WhatsApp Cloud API setup below. Twilio is recommended when you already have Twilio infrastructure (SMS, voice) and want to centralize.
How the integration works
Lead sends WhatsApp message
↓
Twilio receives + POSTs to your webhook
↓
Zihin webhook endpoint (/api/triggers/webhook/:id)
↓
Agent loop processes (LLM + tools + persona)
↓
Zihin POSTs response back to Twilio Messages API
↓
Twilio delivers to the lead's WhatsApp
Prerequisites
| Item | Where | Notes |
|---|---|---|
| Twilio account, upgraded | twilio.com | Trial accounts have severe restrictions |
| Twilio Number (BR or other) | Phone Numbers > Buy a Number | Local commercial number recommended |
| Brazil only: Regulatory Bundle approved | Phone Numbers > Regulatory Compliance | Required for any BR number purchase |
| Meta Business Portfolio | business.facebook.com | Can be created during Self Sign-up |
| WhatsApp Business Account (WABA) | Created via Twilio Self Sign-up | Linked to your Meta Business Portfolio |
| Display Name approved by Meta | Set during WhatsApp Self Sign-up | Takes hours to ~24h to approve |
Step 1 — Buy a phone number on Twilio
In Twilio Console: Phone Numbers > Buy a Number.
For Brazilian numbers:
- Choose Local or Mobile
- Confirm the approved Regulatory Bundle is selected
- ⚠️ Local Brazilian numbers have only Voice capability, not SMS — this is normal and does not affect WhatsApp routing
Step 2 — Start WhatsApp Sender Self Sign-up
In Twilio Console: Messaging > Senders > WhatsApp Senders > Create new sender.
- Select the phone number you just purchased
- Click Continue with Facebook — popup opens
- Inside the Facebook popup:
- Log in with a Facebook account that is admin in your Meta Business Manager
- Select or create the Meta Business Portfolio
- Select Create a new WhatsApp Business Account (not "Use a display name only" — Twilio doesn't support that mode)
- Set Display Name (e.g., "MyCompany Atendimento") — this is what leads see
- Choose category and fill business profile
- On "Add your WhatsApp phone number":
- ⚠️ Click "Add a new phone number" (NOT "Use only a display name")
- Paste the number in international format:
+551150287190
Step 3 — Verify the number
Brazilian local commercial numbers on Twilio have no SMS capability. The only working verification method is voice (phone call).
In the Meta popup, choose Phone call verification.
Meta will call the Twilio number to deliver a 6-digit OTP. You need to capture this OTP somehow — three options:
Option A: Forward the call to your cellphone (recommended)
Configure the Twilio number's Voice webhook to a TwiML that dials your cellphone:
- Twilio Console: Phone Numbers > Active Numbers > select your number
- Voice Configuration > "A call comes in":
- Webhook URL:
https://twimlets.com/forward?PhoneNumber=%2B55XXXXXXXXXXX&Timeout=60&CallerId=%2B<YOUR_TWILIO_NUMBER> - HTTP:
HTTP POST - Replace
+55XXXXXXXXXXXwith your URL-encoded cellphone, and+<YOUR_TWILIO_NUMBER>with the Twilio number itself - Forcing
CallerIdto your Twilio number is critical — operators may filter the Meta POP number as spam (+551140402377etc.)
- Webhook URL:
- Click "Send code" in the Facebook popup
- Your cellphone rings showing
+<YOUR_TWILIO_NUMBER>as caller - Answer, listen to the 6-digit code, paste into the Facebook popup
Option B: Voicemail Twimlet (records and emails the transcription)
https://twimlets.com/voicemail?Email=you@example.com
Meta calls, the Twimlet records, transcribes (English), and emails. Less reliable for capturing exact digits (transcription sometimes truncates).
Option C: SIP softphone or Twilio Dev Phone
For ongoing operations. Overkill for one-time setup.
Step 4 — Complete the Self Sign-up
After verification succeeds, the popup closes. Twilio takes a few minutes to register the Sender. Status transitions: CREATING → ONLINE (or PENDING_REVIEW while Meta approves the Display Name).
Step 5 — Configure the trigger in Zihin
Create a webhook trigger via POST /api/triggers with the following config (real example used in production):
{
"agent_id": "YOUR_AGENT_UUID",
"name": "WhatsApp Twilio Production",
"trigger_type": "webhook",
"trigger_config": {
"channel": {
"type": "twilio_whatsapp",
"auth": { "type": "basic", "secret_ref": "TWILIO_CREDENTIALS" }
},
"execution": {
"mode": "async",
"callback": {
"url": "https://api.twilio.com/2010-04-01/Accounts/<ACCOUNT_SID>/Messages.json",
"method": "POST",
"content_type": "application/x-www-form-urlencoded",
"auth": { "type": "basic", "secret_ref": "TWILIO_CREDENTIALS" },
"body_template": {
"To": "{{phoneNumber}}",
"Body": "{{agent_response}}",
"From": "whatsapp:+<YOUR_TWILIO_NUMBER>"
},
"response_transform": {
"max_length": 8000,
"strip_markdown": true
}
},
"ack_response": {
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>",
"content_type": "text/xml"
},
"split_config": {
"enabled": true,
"numbering": false,
"max_chunks": 6,
"chunk_strategy": "paragraph",
"max_chunk_size": 1500,
"inter_chunk_delay_ms": 800
},
"message_buffer": {
"enabled": true,
"window_ms": 3000,
"max_messages": 8,
"concat_separator": "\n"
}
},
"sender_access": { "mode": "any" },
"context_mapping": {
"_waId": "$.WaId",
"_messageSid": "$.MessageSid",
"phoneNumber": "$.From",
"profileName": "$.ProfileName"
},
"query_extraction": { "mode": "field", "field": "Body" },
"response_adapter": { "format": "twiml" },
"session_strategy": { "mode": "derive", "fields": ["phoneNumber"] }
}
}
Field-by-field explanation:
| Field | Why this value |
|---|---|
channel.type: "twilio_whatsapp" | Tells Zihin to use the Twilio WhatsApp inbound adapter (extracts media, decodes Twilio-specific fields) |
channel.auth.secret_ref | Name of the secret containing <AccountSID>:<AuthToken> — store it via Tenant Secrets |
execution.mode: "async" | Required — Twilio's webhook timeout is ~15s, agent loops may take longer. Async returns ack immediately and delivers via callback |
callback.url | Twilio Messages API. Replace <ACCOUNT_SID> with your real SID |
body_template.From | Your Twilio WhatsApp number prefixed with whatsapp:. Must match the registered Sender |
response_transform.max_length | Per-chunk cap before the split_config runs. 8000 is safe — split breaks earlier at 1500 |
split_config (paragraph, 6×1500) | Long agent responses get split by paragraph into up to 6 messages. WhatsApp respects natural breaks |
message_buffer (3s window) | Users often send 2-3 short messages in a row. Buffer concatenates them into a single agent execution |
sender_access.mode: "any" | Accept messages from any phone number (typical for inbound lead campaigns) |
context_mapping | Maps Twilio's payload fields (Body, From, ProfileName, WaId, MessageSid) into the agent's context |
response_adapter.format: "twiml" | Returns TwiML XML acknowledgment to Twilio's POST |
session_strategy.derive(phoneNumber) | Same lead = same session across messages |
Step 6 — Wire the webhook in Twilio
After the trigger is created, Zihin returns the webhook_url:
https://llm.zihin.ai/api/triggers/webhook/<TRIGGER_ID>
In Twilio Console: Messaging > Senders > WhatsApp Senders > Edit Sender:
- "When a message comes in":
- URL:
<webhook_url> - Method:
HTTP POST
- URL:
- "Status callback URL" (optional, for delivery tracking): same URL or dedicated
Step 7 — Smoke test
Send a real WhatsApp message from your cellphone to the Twilio number. You should see in Zihin:
trigger_executionsrow withstatus=successagent_executionsrow withiterations >= 1- The lead receives the agent's response within 3-10s
Brazil-specific gotchas
Phone number digit count
Brazilian commercial numbers on WhatsApp have 12 digits (+55<area><8-digit-number>, e.g., +551150287190). Brazilian mobile numbers have 13 digits with the leading 9 (+55<area>9<8-digit-number>, e.g., +5562999999999).
⚠️ Outbound to BR mobile: WhatsApp delivery is inconsistent with the 13-digit format. The 12-digit format (without the 9) is universally accepted. Twilio normalizes inbound From values, but To in outbound should match what was received.
Local numbers are Voice-only
When you buy a local BR number on Twilio, the page shows:
"Messaging configuration is unavailable for this phone number."
This is expected and does not affect WhatsApp Business. WhatsApp uses its own infrastructure layer separate from SMS.
Verification method must be Voice
Because there's no SMS capability, Meta's SMS-based verification fails silently. Always choose Phone call in the Self Sign-up popup, and have a forwarding rule ready (Step 3).
Meta rate limit on verification retries
Meta exponentially backs off: 60s → 1h → 3h → 24h between retries. Plan to get the verification right on the first 1-2 attempts.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Status OFFLINE with error 63111 | Sender's phone number or WABA mismatched | Delete the broken sender (DELETE /v2/Channels/Senders/<SID>), restart Self Sign-up |
| "Voice call" verification not received | Voice webhook on the number points to demo URL (default) | Set webhook to a forward Twimlet pointing to your cell (Step 3) |
| Cellphone doesn't ring during verification | Operator filtering Meta's POP number as spam | Force CallerId parameter in the forward URL to your Twilio number |
| Verification SMS never arrives | BR number has no SMS capability | Use Voice verification only |
Outbound message error 63016 | "Outside the allowed window" (no inbound from lead in last 24h) | Use an approved Template HSM, OR wait for lead to message first |
Outbound returns 21617 ("body required") | body_template missing Body field | Verify trigger config has body_template.Body: "{{agent_response}}" |
Sender stays PENDING_REVIEW for days | Meta is reviewing Display Name | Allow up to 24h. If longer, contact Twilio support |
| Lead receives duplicated messages | Older Zihin server version (pre-2026-05-11) | Update to current — bug fixed |
| Agent narratives between tool calls don't reach lead | Older Zihin server version (pre-2026-05-11) | Update to current — extractTurnResponse now preserves all turn messages |
Validation queries (via Twilio API)
After setup, validate end-to-end via:
# 1. Sender state
curl -u $TWILIO_SID:$TWILIO_TOKEN \
"https://messaging.twilio.com/v2/Channels/Senders?Channel=whatsapp"
# 2. Last messages sent/received
curl -u $TWILIO_SID:$TWILIO_TOKEN \
"https://api.twilio.com/2010-04-01/Accounts/$TWILIO_SID/Messages.json?PageSize=10"
# 3. Trigger executions on Zihin side
curl -H "x-tenant-id: $TENANT_ID" -H "Authorization: Bearer $JWT" \
"https://llm.zihin.ai/api/triggers/$TRIGGER_ID/executions?limit=10"
Meta WhatsApp Cloud API (without Twilio)
If you prefer to remove the BSP layer:
- Set
channel.type: "meta_whatsapp"intrigger_config - Use Meta's
Permanent Access Tokeninstead of Twilio credentials ({ "type": "bearer", "secret_ref": "META_WHATSAPP_TOKEN" }) - Set
callback.url: "https://graph.facebook.com/v22.0/<PHONE_NUMBER_ID>/messages" body_templateuses JSON instead of form-urlencoded
The Zihin server handles both BSP (Twilio) and direct (Meta) channels — the only difference is in trigger_config. See Meta WhatsApp Cloud setup for obtaining the Permanent Access Token.
| Aspect | Twilio (BSP) | Meta Cloud API direct |
|---|---|---|
| Cost per message | Meta fee + Twilio markup (~$0.005/msg template) | Only Meta fee |
| Monthly fee | ~$1/number | $0 |
| Setup complexity | 3 systems (Twilio + Meta + Zihin) | 2 systems (Meta + Zihin) |
| Webhook latency | 2 hops | 1 hop |
| Recommended for | Existing Twilio investments | Most new integrations |
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"]
}
}
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
| Code | HTTP | Description |
|---|---|---|
invalid_input | 400 | Missing or invalid field |
trigger_not_found | 404 | Trigger not found |
trigger_disabled | 400 | Trigger is disabled |
invalid_signature | 401 | HMAC/Svix validation failed |
webhook_sender_denied | 403 | Webhook sender not authorized (sender_access) |
execution_failed | 500 | Agent execution error |