Event webhooks
Receive signed consent changes and inspect outgoing deliveries, retries, and attempt history.
Event webhooks notify your application when Keiki records a communications state change.
The first supported event is keiki.conversation.opt_status_changed, independent of the
underlying messaging provider. Consent notifications do not start an agent turn.
Configure a destination
In Settings → Webhooks, add or edit a destination and enable Consent change events. Use a publicly reachable HTTPS URL. Leave Agent IDs blank to receive events for all agents in your organization, or select specific agents. Subscriptions apply to future changes; enabling a subscription does not backfill historical STOP messages.
Choose Destination signing secret for a standalone receiver. Saving a newly enabled subscription shows its signing secret once; store it securely in your receiver. Use Signing secret to issue a replacement. Rotation includes both the new and previous signature for 24 hours. Only owners and admins can change destinations, issue secrets, send tests, or resend deliveries; organization members can inspect history.
For an external agent in push mode, choose that agent as the signing identity. Events then use its current dispatch URL and existing harness signature (HMAC and/or Keiki's Ed25519 signature). No second signing secret is required. This subscription does not add consent events to inbox polling or relay mode.
Event delivery enabled controls this event subscription, not existing handoff or Slack workspace-event producers. Those existing producers are not migrated into this ledger.
Consent change event
{
"type": "keiki.conversation.opt_status_changed",
"version": 1,
"eventId": "evt_b03bb4cd633e4a7d8e425051e6a93c29",
"agentId": "agent_123",
"conversationId": "+14155550100",
"occurredAt": "2026-10-29T12:00:01.000Z",
"sourceOccurredAt": "2026-10-29T12:00:00.000Z",
"channel": "imessage",
"line": "+14155550101",
"previousOptedOut": false,
"optedOut": true,
"reason": "keyword",
"sourceMessageId": "provider_message_123"
}occurredAtis when Keiki persisted the state transition, not when your receiver got it.sourceOccurredAtis the source signal's timestamp, ornullwhen unavailable. A health check discovering suppression does not prove when the person originally opted out.channeldescribes the source messaging service, such asimessage,sms,rcs, oramb, not the provider. It andlinecan benullwhen unknown.- Events can arrive out of order. Event IDs are opaque; no platform-wide counter is exposed.
reasoniskeyword,provider,health, orverified_restoration.
Repeated STOP signals do not create another transition. STOP blocking still happens before an external agent receives a turn. Suppression is shared by conversations using the same messaging credential and contact identity; affected agents with conversation history can each receive a scoped event.
START does not automatically resubscribe someone. An explicit, verified restoration
of a keyword suppression emits the inverse transition (optedOut: false). Provider and
health suppressions cannot be cleared by this restoration path. This release does not
expose a self-service restoration endpoint or infer missing historical timestamps.
Consent is not send eligibility: an opted-in contact can still be blocked for other reasons.
Verify and acknowledge
Verify the raw request body, before decoding or acting on it. Standalone destinations
receive Keiki-Signature: t=<unix-seconds>,v1=<hmac>, using HMAC-SHA256 over
<timestamp>.<raw-body>. During secret overlap the header can contain multiple v1 values.
The channels SDK checks signatures and replay tolerance:
import { } from '@onkeiki/channels'
export async function (: ) {
const = ..
if (!) throw new ('WEBHOOK_SIGNING_SECRET is required')
const = ({
: await .(),
: .,
}, {
,
: false,
})
if (. === 'opt_status_changed') {
// Atomically deduplicate event.event.eventId and persist the state change.
await (.)
}
return new (null, { : 204 })
}Any HTTP 2xx acknowledges the delivery; redirects are not followed. Return a non-2xx
status if processing failed, or persist work durably before acknowledging. Delivery is
at least once, not exactly once: a receiver can commit successfully and lose the
acknowledgment. Deduplicate completed work by eventId, not by attempt ID.
The request also includes Keiki-Event-Id, Keiki-Event-Type, Keiki-Delivery-Id, and
Keiki-Attempt-Id. These are useful for diagnostics, but the signature authenticates the
body, so use the verified body's event ID for application decisions.
AI SDK receivers
import { } from '@onkeiki/ai-sdk'
export const = ({
: async () => {
await ()
},
: async () => (),
})The handler waits for the consent callback and returns 200 on success, 500 if it
throws, or 501 if the callback is missing. It never calls onTurn for consent events.
Keiki's Flue and Eve adapters accept these non-turn events without starting model work;
use your own receiver when your application needs to react to consent changes.
Inspect deliveries and retry
Settings → Webhooks → Event deliveries shows each destination's independent status:
pending, delivered, failed, or canceled. Open a delivery for its immutable event
payload and attempt timeline, including HTTP status, duration, automatic/manual origin,
and the next scheduled retry. The API also supports event type, agent, and time filters.
Failures and timeouts retry automatically for up to 72 hours, with exponential backoff and jitter (approximately one minute initially, capped around six hours). The worker checks due deliveries every 30 seconds. Expired leases recover interrupted attempts; process failure does not create a different event.
Resend preserves the event ID and payload and queues another attempt with a fresh retry window. It does not override deduplication at your receiver. An active delivery lease, expired retention, unavailable agent, or disabled/deleted destination prevents resend. URL changes use the current destination on subsequent attempts; each attempt records a redacted destination. An already-started HTTP request cannot be recalled.
Terminal history is retained for 30 days. Deleting a destination preserves its delivery history. Transferring an agent leaves historical events in the source organization and prevents pending delivery to its former destinations.
Send test queues a signed keiki.webhook.test event. This is synthetic, not a real
consent transition. Organizations can have up to 25 pending test deliveries across all
destinations, including manual resends. Additional tests return HTTP 429 with a
test_queue_full code; wait for pending tests to finish before trying again. Real consent
events are not subject to this test limit.
The channels verifier reports kind: 'webhook_test'; the AI SDK, Flue,
and Eve accept it without a model turn.
Response previews are bounded and best-effort redacted. Never put credentials or personal data in webhook responses. Keiki does not store request authorization headers or raw signing secrets in the attempt log, and hides destination paths and query strings there.