Workspace webhooks
Route lifecycle events to organization endpoints by event, agent, and session tags, then inspect every delivery attempt.
Workspace webhooks are managed independently from agents. Configure them in Settings → Webhooks or through /v1/webhooks. One endpoint can subscribe to several lifecycle events and either every agent or an explicit agent set.
Create an endpoint
const endpoint = await speko.webhooks.create({
name: 'Production call events',
url: 'https://hooks.example.com/speko',
events: ['call.status', 'call.report', 'call.analysis', 'call.recording'],
allAgents: true,
filterTags: { environment: 'production' },
headers: { 'x-source': 'speko' },
authHeaders: [{ name: 'Authorization', value: 'Bearer secret-token' }],
timeoutMs: 4000,
});Supported voice-session events are:
| Event | When it fires | Automatic retries |
|---|---|---|
call.pre_call | Before a phone worker starts; the response may override supported call configuration | No |
call.status | When a call lifecycle event is recorded | No; best effort |
call.report | When the report is finalized, and again when a recording becomes ready | Yes |
call.analysis | When post-call analysis completes | Yes |
call.recording | When recording reaches ready or failed | Yes |
Call control events
These describe a human call — a broker on a browser softphone talking to a phone — leg by leg, rather than an AI voice session. None of them are retried: they are a live projection of the call's event history, which stays readable at GET /v1/voice/calls/{callId}/events, so a redelivery would arrive too late to be worth anything.
| Event | When it fires | Automatic retries |
|---|---|---|
call.initiated | A call is created, before anyone is reachable | No |
call.ringing | A leg starts ringing (an outbound INVITE is out, or a broker is being rung) | No |
call.answered | A leg answers; the first one to answer also starts the call | No |
call.bridged | A leg is pulled into another leg's conversation | No |
call.hold / call.unhold | A leg is parked or resumed | No |
call.mute / call.unmute | A leg's own audio is muted or restored, server-side | No |
call.dtmf.sent | Speko sent tones towards a PSTN leg | No |
call.transfer.initiated / call.transfer.completed / call.transfer.failed | A blind or warm transfer progresses | No |
call.leg.hangup | One leg drops | No |
call.hangup | The whole call ends | No |
Every call-control payload carries call_id, control_id (null for the two call-scoped events, call.initiated and call.hangup), event_id and occurred_at alongside the event's own fields. Human calls have no agent, so an endpoint scoped to specific agentIds never receives them — subscribe with allAgents.
Inbound DTMF (a key the far end pressed) is deliberately absent from this table. It arrives as an in-room data packet addressed to room participants, not to the webhook receiver, so it is observable by a connected softphone and not by the server — and a subscription that can never deliver is worse than no subscription.
Every delivery includes a stable webhook_id for that event occurrence and webhook_tags from the session. A report's initial and recording-ready emissions are separate occurrences with different IDs; retries and manual redeliveries of one occurrence reuse its ID.
Agent and tag routing
allAgents defaults to true. Set it to false and provide agentIds to narrow the endpoint. New agents automatically match all-agent endpoints.
Tags are free-form, case-sensitive string pairs. Attach them when creating an agent-backed browser, realtime, or phone session:
await speko.voice.dial({
to: '+12015551234',
agentId: 'agent_123',
webhookTags: {
environment: 'production',
customer: 'acme',
},
});An endpoint matches when all of its filterTags exist on the session with equal values. If at least one tagged endpoint matches, Speko sends to every matching tagged endpoint and does not use untagged defaults. If none match, Speko falls back to all matching untagged endpoints. Agentless sessions reject webhookTags because they cannot resolve an agent-scoped route.
Only one effective call.pre_call endpoint may exist for an agent/tag route. Conflicting endpoint configuration is rejected, and session creation fails explicitly if inconsistent stored data would still make the route ambiguous.
Signing and secrets
Requests use Standard Webhooks headers and default to the workspace signing secret. Set signingSecretSource: 'custom' with a write-only signingSecret to override it for one endpoint. Secret auth-header values are encrypted and returned only as { name, configured: true }. On update, omit an existing auth-header value to keep it or supply a value to rotate it.
Query values, signatures, authorization, cookies, API keys, and credential-shaped headers are redacted from delivery logs. Response bodies are capped at 64 KiB and expose responseTruncated.
Extraction fields
Endpoints subscribed to call.report or call.analysis can request extractionFields. Speko executes one extraction pass using the union required by all matching endpoints, then includes only each endpoint's requested fields in its custom_data. Identical definitions deduplicate; conflicting definitions are rejected.
Delivery logs and redelivery
Use the Logs tab in Settings or the SDK:
const page = await speko.webhooks.deliveries.list({
endpointId: endpoint.id,
event: 'call.report',
status: 'failed',
limit: 50,
});
const detail = await speko.webhooks.deliveries.get(page.data[0]!.id);
await speko.webhooks.deliveries.redeliver(detail.id);Logs contain the endpoint-specific request, sanitized URL and headers, HTTP result, duration, bounded response, error, routing tags, and chronological attempts. They are retained for 30 days. Private phone-number API deliveries for imessage.received, imessage.reaction_received, imessage.sent, imessage.delivered, and imessage.delivery_failed appear in the same Logs tab and API, using the stable Inkbox event ID.
Manual redelivery is available for unexpired workspace lifecycle events except call.pre_call. It uses the stored original payload and webhook ID with the endpoint's current URL and credentials, and adds an attempt to the existing delivery. A manual failure does not create a new automatic retry or cancel one that was already pending; after an earlier success, the delivery remains successful. Inkbox owns retry scheduling for imessage.* subscriber deliveries, so those records are view-only.
Migrating agent webhooks
Existing agent webhook configuration is backfilled to central endpoints and dispatch reads only those endpoints, avoiding duplicates. agent.webhooks reads and writes remain supported through the current SDK major version; legacy writes synchronize legacy-managed central endpoints. New integrations should use speko.webhooks exclusively.