Skip to content

Webhook Ingestion

Ironflow provides two ways to receive external webhooks (Stripe, GitHub, etc.) and transform them into native Ironflow events.

How It Works

Ironflow supports two ingress paths:

Path A — Server-managed source (register via Dashboard / CLI / RPC):

Provider → POST {ironflow}/api/v1/webhooks/:provider → Verify → Deduplicate → Emit Event → Trigger Functions
  1. Verification: If a verify_header + verify_algorithm (hmac-sha256 or hmac-sha1) + verify_secret are configured on the WebhookSource, Ironflow validates the HMAC signature with subtle.ConstantTimeCompare. Hex-encoded signatures only.
  2. Deduplication: Automatic idempotency by reading the top-level id or event_id field from the JSON payload (no header support, no caller override).
  3. Event name: Built server-side as {event_prefix}.{payload.type} — e.g. an event_prefix of stripe with payload.type=payment_intent.succeeded produces the event stripe.payment_intent.succeeded. The event’s source metadata column is set to webhook.
  4. Execution: The resulting event triggers matching functions.

Path B — SDK-managed webhook (createWebhook + serve()):

Provider → POST {your-app}/.../webhooks/:provider → SDK runs verify() → SDK runs transform() → POST {ironflow}/api/v1/events → Trigger Functions

The SDK mounts its handler on any URL path that contains /webhooks/:provider (regex /\/webhooks\/([^/]+)/). Your verify() runs locally; your transform() produces { name, data, idempotencyKey }; the SDK forwards it to Ironflow’s /api/v1/events endpoint. The Quick Start below uses Path B.


Quick Start (Node.js)

1. Define the webhook source:

import { createWebhook } from "@ironflow/node";
const stripeWebhook = createWebhook({
id: "stripe",
// Verification logic (HMAC, etc.)
verify: (req) => { /* ... */ },
// Transform payload into Ironflow event
transform: (payload) => ({
name: `stripe.${payload.type}`,
data: payload.data.object,
idempotencyKey: payload.id,
}),
});

2. Register with the server:

import { serve } from "@ironflow/node";
export const POST = serve({
webhooks: [stripeWebhook],
functions: [myWorkflow],
});

3. Point your provider URL:

Any path on your app that ends with /webhooks/stripe works — the SDK matches /webhooks/:provider anywhere in the URL. For a Next.js Route Handler at app/api/webhooks/[provider]/route.ts, the URL is https://your-app.com/api/webhooks/stripe.


Registration Models

Model Setup Use Case
Dashboard Dashboard → Webhooks → New Source Add a server-managed source via UI (provider ID, event prefix, optional verify header/algorithm/secret).
CLI ironflow webhook list, ironflow webhook deliveries, ironflow webhook test List server-managed sources, browse deliveries, and POST a test payload to /api/v1/webhooks/:provider.
SDK (Code) createWebhook({ ... }) + serve({ webhooks }) Custom verification or payload mapping that lives next to your application code (Path B).

Managing Sources

Path A sources support live edits without losing delivery history. Four endpoints split the surface so secret rotation, lifecycle changes, and configuration edits stay separately auditable:

Endpoint Edits Notes
UpdateWebhookSource name, verify_header, verify_algorithm, metadata event_prefix, source_type, and id are immutable post-create. verify_secret is not touched — use rotate.
RotateWebhookSecret verify_secret, grace_seconds New secret is required. Promotes the prior current secret to a prev slot for the grace window.
DisableWebhookSignatureVerification grace_seconds Clears the current secret; preserves the prior secret as prev for the grace window. Source operates unsigned after the window.
ExpireWebhookSecretPrev (none) Force-expires prev immediately. Idempotent.

Rotation lifecycle (dual-secret with grace window — ADR 0024). Every rotation and disable carries a grace window during which the prior secret stays valid. The verify path tries the current secret first; on mismatch it falls back to the previous secret if it has not expired. Providers retrying with the old signature continue to land in webhook_deliveries.status = accepted until the grace window ends.

Grace window options (dashboard dropdown; same values accepted via SDK):

Option grace_seconds
Immediate (no grace) 0
1 hour 3600
24 hours (default) 86400
7 days (cap) 604800

Values above 604800 are rejected with InvalidArgument. The default can be overridden at the cluster level with the IRONFLOW_WEBHOOK_SECRET_GRACE_HOURS_DEFAULT env var (integer hours, also capped at 168 / 7 d — boot-time fatal if exceeded).

The WebhookSource response carries:

  • verify_secret_set bool — true when the current slot is configured.
  • verify_secret_prev_set bool — true when a previous secret is still set (the raw value is never returned).
  • verify_secret_prev_expires_at — timestamp when the grace window ends. Combine with prev_set to determine whether the window is active.

Dashboard: Open Webhooks → {source}. The verification card surfaces a Rotate and a Disable verification button. Both open a dialog with a grace-window dropdown. While a window is active, a banner displays the countdown and a Force expire now button.

Go SDK:

// Edit: full-replace semantics — fetch first, then submit the full intended state.
current, err := client.Webhooks().GetSource(ctx, sourceID)
if err != nil { /* ... */ }
_, err = client.Webhooks().UpdateSource(ctx, ironflow.UpdateWebhookSourceInput{
ID: current.ID,
Name: "Stripe production",
VerifyHeader: current.VerifyHeader,
VerifyAlgorithm: current.VerifyAlgorithm,
Metadata: current.Metadata,
})
// Rotate with the default 24 h grace.
_, err = client.Webhooks().RotateSecret(ctx, ironflow.RotateWebhookSecretInput{
ID: sourceID,
VerifySecret: "whsec_new",
})
// Rotate with a 1 h grace.
grace := time.Hour
_, err = client.Webhooks().RotateSecret(ctx, ironflow.RotateWebhookSecretInput{
ID: sourceID,
VerifySecret: "whsec_new",
GracePeriod: &grace,
})
// Instant cutover (matches the pre-#997 behavior).
zero := time.Duration(0)
_, err = client.Webhooks().RotateSecret(ctx, ironflow.RotateWebhookSecretInput{
ID: sourceID,
VerifySecret: "whsec_new",
GracePeriod: &zero,
})
// Force-expire the previous secret slot.
_, err = client.Webhooks().ExpireSecretPrev(ctx, sourceID)
// Disable signature verification with a 24 h grace.
_, err = client.Webhooks().DisableSignatureVerification(ctx, sourceID, nil)

Monitoring & Audit

Ironflow records every Path A delivery in the webhook_deliveries table and retains rows for 30 days (hardcoded — a background sweep runs hourly). In the Dashboard → Webhooks section, you can:

  • Browse all registered webhook sources and create/delete them from the UI.
  • Open the Deliveries view to see status (accepted, deduplicated, rejected, failed), external ID, timestamp, and linked event ID.
  • Expand a delivery row to inspect raw request headers and body.

Path B deliveries are not written to webhook_deliveries — they emit straight to /api/v1/events, so they appear in the Events stream rather than the Deliveries view.

Idempotency

On Path A, Ironflow deduplicates by the top-level id or event_id field in the payload (composite unique index on (source_id, external_id)). If the payload has neither field, no dedup occurs. On Path B, dedup is whatever idempotencyKey you return from transform().