ExamplesWebhook Processing

Webhook Processing

Webhooks connect Causet timelines to existing systems. Adoption guide: Use Webhooks to Update Existing Flows.

This example covers two scenarios:

  1. Ingesting incoming webhooks from external services into Causet (Available now)
  2. 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

  1. Webhook arrives at your HTTP endpoint
  2. Immediately return 200 (acknowledge delivery)
  3. Submit a Causet intent to record the webhook payload
  4. Causet rules process, validate, emit domain events
  5. 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.ts

Idempotency 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:

  1. Causet emits domain events (e.g., access_approved, TICKET_PURCHASED)
  2. Events publish to Kafka causet.ledger-events.v1
  3. Your consumer (or a webhook delivery service) subscribes
  4. Consumer updates your existing DB / projections idempotently
  5. 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:   int
projections:
  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