Docs/API Reference/Webhooks & Callbacks

Webhooks & Callbacks

Inbound base
https://api.example.com/webhook/** and /api/billing/webhook/** (public URL that providers call).
Spring Security
permitAll by design: third-party servers cannot present a Keycloak token. Each controller authenticates the payload itself. See Authentication.
Outbound
Tenant-managed subscriptions at /api/webhooks (HMAC-signed, retried).
Delivery model
Providers are at-least-once; core de-duplicates on provider ids or content hashes before acting.

Inbound: channel and provider callbacks

All are POST unless a GET is noted. "Credential" is the header or field the handler checks.

PathControllerCredentialNotes
/webhook/meta/whatsapp (GET, POST)MetaWhatsAppWebhookControllerPOST: X-Hub-Signature-256. GET: hub.mode=subscribe + hub.verify_token (constant-time match with app.meta.webhook-verify-token)GET echoes hub.challenge as text/plain, else 403.
/webhook/meta/social (GET, POST)MetaSocialWebhookControllerX-Hub-Signature-256 (POST); same GET handshakeMessenger / Instagram.
/webhook/twilio/whatsappTwilioWebhookControllerX-Twilio-Signature validated with the tenant's own auth tokenapplication/x-www-form-urlencoded.
/webhook/twilio/smsTwilioSmsWebhookControllerX-Twilio-SignatureForm-encoded.
/webhook/twilio/statusTwilioStatusCallbackControllerX-Twilio-SignatureDelivery status callbacks.
/webhook/twilio/voice, /voice/collect, /voice/menu-collectTwilioVoiceWebhookControllerTwilio signature (same validator)Voice and DTMF menu flow.
/webhook/telegram/{botUserId}TelegramWebhookControllerX-Telegram-Bot-Api-Secret-Token, SHA-256 hashed and compared in constant time with the tenant's stored digest; mismatch 403Unknown bot is ignored with 200. De-duped on update_id.
/webhook/slackSlackWebhookControllerX-Slack-Request-Timestamp + X-Slack-SignatureAnswers Slack's url_verification handshake (no team_id, no signature check: it echoes a short challenge token and changes nothing); every event_callback resolves the tenant from team_id and must carry a valid signature.
/webhook/teams/{appId}TeamsWebhookControllerAuthorization header (verification scheme not confirmed here)Bot activity delivery.
/webhook/discordDiscordWebhookControllerX-Internal-Secret (shared with the Discord gateway service); mismatch 403Internal hop from the Node gateway, not from Discord directly.
/webhook/razorpayRazorpayWebhookController (core/tenancy)X-Razorpay-Signature; bad or missing signature 403 via GlobalExceptionHandlerTenant-state machine for the optional Razorpay gateway (hosted billing; off by default). Dedupe on X-Razorpay-Event-Id. Profile security.
/api/billing/webhook/{gateway}BillingWebhookController (core/billing)X-Razorpay-Signature verified by the resolved gateway client; unknown gateway 404; bad signature 401Gateway-agnostic. Always 200 after verification (failures are logged, not re-raised) so the gateway does not retry-storm.
/webhook/alerts/alertmanagerPlatformAlertWebhookControllerAuthorization: Bearer <shared-secret> (constant-time compare)Fails closed: 503 if the secret is unset, 401 on mismatch, else {"recorded": n}. Own filter chain.
Not covered
DemoWhatsAppWebhookController and DemoTelegramWebhookController (demo sandbox channels, core/omnichannel/demo), the OAuth redirect callbacks (/oauth/{google,x,atlassian}/callback) and /scim/v2 are listed in Authentication but their payload contracts are not documented here.

Inbound processing pattern

sequenceDiagram
  participant P as Provider
  participant W as Webhook controller
  participant T as TenantResolutionService
  participant D as Dedup (Redis)
  participant S as AgentChatService / workflow
  P->>W: POST /webhook/... (signature or secret)
  W->>T: resolve tenant from provider account id
  W->>W: verify signature with tenant credential
  W->>D: claim delivery id
  D-->>W: new or duplicate
  W->>S: run agent turn / trigger workflow
  W-->>P: 200 (fast)

Tenant is resolved from the provider's account identifier (bot id, phone-number id, team id, and so on) through the control plane, then TenantContext is set to that schema. Deep behaviour per channel is in Channels & Omnichannel.

Inbound: workflow webhook triggers

PathCredentialBehaviour
POST /webhook/workflows/{token}The secret {token} itself (SHA-256 hashed for lookup); one token per Trigger nodeUnknown token or disabled node: 404. Node not in shared-secret auth mode: 409. Workflow not ACTIVE: 409. Redelivery: 409. Success: 200 with the run (WorkflowRunResponse). JSON body is optional; it becomes the run input.
POST /webhook/workflows/oidc/{workflowId}Authorization: Bearer <jwt> with a tenant JWT whose realm_access.roles contains workflow-trigger503 if no JWT decoder (security profile off). Missing bearer or invalid token: 401. Role or tenant problems: 401/403 per the controller.

De-duplication on the token path uses the first of X-Delivery-Id, X-Webhook-Id, X-Request-Id that the sender provides, else a SHA-256 of the body, scoped by trigger node. The workflow-trigger role is a realm role that is not auto-provisioned; it must be assigned to a service account in Keycloak. Tokens are created and revoked through GET/POST/DELETE /api/workflows/{workflowId}/nodes/{nodeId}/webhook (PERM_MANAGE_WORKFLOWS_ADMIN). See Workflow Engine.

Outbound: tenant webhook subscriptions

WebhookSubscriptionController, base /api/webhooks. Writes (POST, PATCH, DELETE) need PERM_MANAGE_INTEGRATIONS; GETs need any authenticated tenant member.

Verb and pathPurpose
GET /api/webhooksList subscriptions.
GET /api/webhooks/event-typesEvent catalogue (WebhookEventCatalog).
GET /api/webhooks/{id}One subscription.
POST /api/webhooksCreate. Body {name (max 120), url (max 2048), eventTypes[]}. The signing secret is returned in plaintext only here and on rotate.
PATCH /api/webhooks/{id}Update name, url, eventTypes, enabled.
DELETE /api/webhooks/{id}Delete.
POST /api/webhooks/{id}/rotate-secretNew signing secret (returned once).
POST /api/webhooks/{id}/testEnqueue a synthetic webhook.test delivery; returns {"deliveryId": ...}.
GET /api/webhooks/{id}/deliveriesNewest 100 deliveries: id, eventType, runId, status, attemptCount, responseCode, lastError, createdAt, deliveredAt, durationMs.

Event types (catalogue)

workflow.run.started|completed|failed|cancelled|resumed, workflow.node.completed, workflow.output.emitted, approval.requested, approval.resolved, approval.execution.recorded, document.generation.completed, an agent-reply-completed event (payload includes agentId, sessionId), inbound-channel-message events (Discord, X, Telegram, email and Teams entries were read; earlier entries in the catalogue were not), and webhook.test. Use GET /api/webhooks/event-types for the authoritative list on a deployment.

Delivery contract

# verify (pseudo): reject if abs(now - t) is large, then compare
expected = hex(hmac_sha256(secret, t + "." + raw_body))
constant_time_equal(expected, v1)

Evidence & references