Webhook Processing
Webhooks connect Causet timelines to existing systems. Adoption guide: Use Webhooks to Update Existing Flows.
This example covers two scenarios:
- Ingesting incoming webhooks from external services into Causet (Available now)
- Delivering updates outbound to existing apps via managed HTTP webhooks, Kafka, or queries (Available now)
Status labels: What runs today?.
Retry and idempotency (both directions)
- Include event IDs (provider ID inbound; Causet event ID outbound)
- Make handlers idempotent
- Store last processed event (or a processed-ID set)
- Safely ignore duplicates
- Retry failed deliveries with backoff
Pass the provider event ID as the Causet idempotency key on inbound emit().
Scenario A: Ingesting incoming webhooks
External services send webhooks to your application (e.g., Stripe payment confirmations, Ticketmaster ticket sale events).
The pattern
- Webhook arrives at your HTTP endpoint
- Immediately return 200 (acknowledge delivery)
- Submit a Causet intent to record the webhook payload
- Causet rules process, validate, emit domain events
- Projection worker materializes — and/or your app updates its own DB from those events
# events/webhook.events.causet
events:
WEBHOOK_RECEIVED:
state: webhook_ingestion
entity_expr: event.webhook_id
payload:
webhook_id: string
source: string # "stripe", "ticketmaster", etc.
event_kind: string # NOT "type" — reserved
raw_payload: string # JSON string of original payload
received_at: string
WEBHOOK_PROCESSED:
state: webhook_ingestion
entity_expr: event.webhook_id
payload:
webhook_id: string
result: string
WEBHOOK_FAILED:
state: webhook_ingestion
entity_expr: event.webhook_id
payload:
webhook_id: string
error_code: string
error_msg: string# actions/webhook.actions.causet
actions:
INGEST_WEBHOOK:
state: webhook_ingestion
input:
webhook_id: { type: string, required: true }
source: { type: string, required: true }
event_kind: { type: string, required: true }
raw_payload: { type: string, required: true }
preflight:
rules:
- name: check_not_duplicate
when: {}
then:
- op: if
expr: "entity.processed == true"
then:
- op: reject
code: DUPLICATE_WEBHOOK
core:
rules:
- name: mark_received
when: {}
then:
- op: set
path: /status
value: "received"
side_effects:
rules:
- name: emit_received
then:
- op: emit
event_type: WEBHOOK_RECEIVED
payload:
webhook_id: intent.webhook_id
source: intent.source
event_kind: intent.event_kind
raw_payload: intent.raw_payload
received_at: event.tsIdempotency via entity state
The webhook entity tracks whether a given webhook_id has been processed. Duplicate webhook deliveries from the external service are rejected by preflight with DUPLICATE_WEBHOOK.
This requires the webhook_id to be deterministic. Use the external service’s event ID (e.g., Stripe’s evt_xxx) as the webhook_id.
Scenario B: Delivering updates to existing apps
Your existing app needs Causet state changes (access approved, dispute won, ticket escalated, AI decision emitted).
The pattern (Available now via Kafka)
HTTP delivery must not live inside Causet core rules — rules must stay deterministic and replay-safe. Instead:
- Causet emits domain events (e.g.,
access_approved,TICKET_PURCHASED) - Events publish to Kafka
causet.ledger-events.v1 - Your consumer (or a webhook delivery service) subscribes
- Consumer updates your existing DB / projections idempotently
- Optionally track delivery state back in Causet
Managed outbound HTTP webhook delivery from Causet is Available now. You can also use Kafka consumers or Causet queries/projections.
# Optional: track webhook delivery state in Causet
events:
WEBHOOK_DELIVERY_SUCCEEDED:
state: webhook_delivery
entity_expr: event.delivery_id
payload:
delivery_id: string
consumer_id: string
event_kind: string
response_status: int
WEBHOOK_DELIVERY_FAILED:
state: webhook_delivery
entity_expr: event.delivery_id
payload:
delivery_id: string
consumer_id: string
error_code: string
retry_count: intprojections:
webhook_delivery_log:
source_events: [WEBHOOK_DELIVERY_SUCCEEDED, WEBHOOK_DELIVERY_FAILED]
target:
table: webhook_delivery_log
primary_key: [delivery_id]
fields:
delivery_id: TEXT
consumer_id: TEXT
event_kind: TEXT
status: TEXT
response_status: INT
error_code: TEXT
retry_count: INT
updated_at: BIGINT
derive:
delivery_id: event.delivery_id
consumer_id: event.consumer_id
event_kind: event.event_kind
updated_at: event.ts
mutations:
WEBHOOK_DELIVERY_SUCCEEDED: { op: upsert }
WEBHOOK_DELIVERY_FAILED: { op: upsert }
indexes:
- columns: [consumer_id, status]Why external delivery
The HTTP call to deliver a webhook is a side effect. If you put it inside Causet rules:
- It breaks determinism (HTTP responses vary)
- It breaks replay safety (you’d re-deliver on every replay)
- It couples delivery reliability to rule evaluation
The right pattern is: Causet emits the event durably → external service delivers → tracks success/failure back into Causet.
Projection for monitoring
projections:
webhook_ingestion_status:
source_events: [WEBHOOK_RECEIVED, WEBHOOK_PROCESSED, WEBHOOK_FAILED]
target:
table: webhook_ingestion_status
primary_key: [webhook_id]
fields:
webhook_id: TEXT
source: TEXT
event_kind: TEXT
status: TEXT
error_code: TEXT
received_at: BIGINT
updated_at: BIGINT
derive:
webhook_id: event.webhook_id
source: event.source
updated_at: event.ts
mutations:
WEBHOOK_RECEIVED: { op: upsert }
WEBHOOK_PROCESSED: { op: upsert }
WEBHOOK_FAILED: { op: upsert }
indexes:
- columns: [source, status]
- columns: [received_at]
direction: desc