Use Webhooks to Update Existing Flows
Webhooks are the bridge between Causet and existing apps.
- Causet can emit (or publish) updates after important workflow state changes
- Existing apps use those updates to refresh their own DB / projections
- The existing frontend can keep reading existing tables
- Async workflows do not require a full app rewrite
Start with Retrofit an Existing App if you are wrapping an endpoint first.
Availability: Inbound webhook → intent and managed outbound HTTP webhook delivery are both Available now. You can also bridge with Kafka ledger consumers or Causet queries/projections. See What runs today?.
Two directions
| Direction | Role | Status |
|---|---|---|
| Inbound | Provider → your app → Causet intent | Available now |
| Outbound bridge | Causet event → your app DB/UI | Available now via HTTP webhooks, Kafka, or queries |
Why webhooks matter for retrofitting
Existing apps already receive async updates. Handlers often become mini workflow engines — verify, update state, email, notify, retry.
Causet moves the durable workflow out of the handler. The handler stays thin.
Before:
POST /api/webhooks/stripe
→ verify signature
→ update subscription
→ send receipt
→ notify Slack
→ maybe fail halfwayAfter:
POST /api/webhooks/stripe
→ verify signature
→ submit RECORD_PAYMENT intent
→ return 200 quickly
Causet
→ update subscription state
→ emit events
→ timeline records each stepYour app can then update its own tables from those events (consumer or future HTTP webhook).
Inbound: thin adapters
Webhook handlers should usually:
- Verify the signature
- Parse and normalize the payload
- Extract a correlation / entity ID
- Submit a Causet intent (with provider event ID as idempotency key)
- Return 2xx quickly
They should not send five emails, call three APIs, or hide retry logic in the route.
Webhooks can start a workflow
Stripe checkout.session.completed → RECORD_PAYMENT → fulfillment timeline starts.
Webhooks can advance a workflow
DocuSign signed → RECORD_DOCUMENT_SIGNED → onboarding saga resumes on the same entity.
Outbound: update the existing app
After Causet commits meaningful events, your existing system needs the new status.
Pattern A — Causet projections / queries (Available now)
Your API reads Causet for workflow status; frontend may still use your tables for other data.
Pattern B — Kafka / event consumer (Available now)
Subscribe to ledger events and update your DB:
async function handleCausetEvent(event: {
eventId: string
type: string
requestId: string
status?: string
}) {
if (await alreadyProcessed(event.eventId)) return // ignore duplicates
switch (event.type) {
case 'access_approved':
await db.accessRequests.update(event.requestId, { status: 'approved' })
break
case 'payment_dispute_opened':
await db.disputes.update(event.requestId, { status: 'open' })
break
case 'ticket_escalated':
await db.tickets.update(event.requestId, { status: 'escalated' })
break
case 'ai_decision_emitted':
await db.agentActions.insert({
ticketId: event.requestId,
decision: event.status,
})
break
}
await markProcessed(event.eventId)
}Pattern C — Outbound HTTP webhooks (Available now)
Same handler shape as above, delivered as HTTP callbacks from Causet’s webhook delivery service.
Examples
Access request status update
Causet emits: access_approved
Your app: UPDATE access_requests SET status = 'approved' WHERE id = …
Frontend: still reads access_requestsPayment dispute update
Causet emits: payment_dispute_opened → evidence_requested → dispute_won
Your app: updates disputes table per eventSupport ticket escalation update
Causet emits: ticket_escalated
Your app: sets tickets.status = 'escalated', notifies on-callAI decision emitted as a business event
Causet: op: decision → emits TICKET_AI_TRIAGED
Your app: stores triage label / confidence on the ticket row
Timeline: decision + tool calls remain inspectable in CausetRetry and idempotency
Treat deliveries as at-least-once.
| Practice | Why |
|---|---|
| Include event IDs in payloads | Stable dedupe key |
| Make handlers idempotent | Safe retries |
| Store last processed event (or set of IDs) | Ignore duplicates |
| Safely ignore duplicates | Same event twice must not double-update |
| Retry failed deliveries | Transient HTTP/DB errors |
Inbound intents: pass the provider event ID as the Causet idempotency key.
await client.emit(
'access_request_stream',
requestId,
'RECORD_CRM_ENRICHMENT',
{ …payload, providerEventId: event.id },
event.id, // idempotency key
)Outbound consumers: key off Causet eventId / cursor, not “process every message blindly.”
Correlation IDs
Use the same entity ID across the original intent and later webhooks (requestId, orderId, stripeSessionId, etc.). See Correlation and Causation.
Conceptual inbound handler
export async function POST(req: Request) {
const rawBody = await req.text()
verifySignature(rawBody, req.headers.get('x-signature'))
const event = JSON.parse(rawBody)
const requestId = await lookupRequestId(event.external_id)
const client = createServerCausetClient()
await client.init()
try {
await client.emit(
'access_request_stream',
requestId,
'RECORD_EXTERNAL_UPDATE',
{ …normalize(event), providerEventId: event.id },
event.id,
)
} finally {
client.destroy()
}
return Response.json({ received: true })
}Full DSL example: Webhook Processing.