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- Verification: If a
verify_header+verify_algorithm(hmac-sha256orhmac-sha1) +verify_secretare configured on theWebhookSource, Ironflow validates the HMAC signature withsubtle.ConstantTimeCompare. Hex-encoded signatures only. - Deduplication: Automatic idempotency by reading the top-level
idorevent_idfield from the JSON payload (no header support, no caller override). - Event name: Built server-side as
{event_prefix}.{payload.type}— e.g. anevent_prefixofstripewithpayload.type=payment_intent.succeededproduces the eventstripe.payment_intent.succeeded. The event’ssourcemetadata column is set towebhook. - 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 FunctionsThe 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 withprev_setto 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().