Webhooks
Configure an Event Webhook URL (and optional signing secret) in Funnel settings → Advanced. The platform POSTs JSON to your HTTPS endpoint when something meaningful happens on the live published funnel — similar in spirit to FunnelFox webhooks (outbound POST, verify a secret header, respond quickly, expect possible duplicates without retries on our side).
Security
- URL: https:// only. Localhost and private IPs are rejected when we call your URL (SSRF guard).
- Signing: If you set a Webhook signing secret in funnel settings, every request includes header
W2A-Webhook-Secretwith exactly that value. Compare it on your server before trusting the body (like FunnelFox’sFox-Secret-Key). - The stored secret is never shown again in the dashboard; you can rotate it or remove it from the same form.
- Workspace safety: payment events are matched to the same workspace as your funnel before they update Subscribers or trigger
purchase.completed.
Delivery
| Item | Behavior |
|---|---|
| Method | POST |
Content-Type | application/json |
| Timeout | 10 seconds — return 2xx quickly |
| Retries | None — if your endpoint errors, we do not retry |
Payload shape (api_version: "1")
All events use one envelope:
{
"api_version": "1",
"id": "evt_…",
"type": "funnel.step_viewed | funnel.started | lead.captured | purchase.completed | purchase.claimed | subscription.renewed | subscription.payment_failed | subscription.canceled | refund.created",
"created_at": "2026-04-04T12:00:00.000Z",
"livemode": true,
"funnel": {
"id": "uuid",
"slug": "my-funnel",
"name": "My funnel"
},
"session_id": "string or null",
"data": {},
"legacy": {}
}
legacy— optional object with the older flat fields some automations may still expect (see below).livemode—falsewhen the triggering Stripe event was in test mode.
Event types
funnel.started
First step view in a browser session (maps from internal funnel_view). session_id is set.
data includes step_id plus fields from the tracker (step_type, step_index, UTM params when present).
legacy: { funnelId, stepId, sessionId, eventType: "funnel_view", properties, timestamp }.
funnel.step_viewed
Visitor opened another step (step_view). Same data / legacy pattern with eventType: "step_view".
Preview mode does not hit the tracker — no webhook.
lead.captured
After a successful email capture step on a published funnel.
data:
{
"email": "[email protected]",
"name": "string or null",
"step_id": "uuid or null",
"properties": {}
}
Use this to trigger welcome emails, CRM leads, etc. legacy mirrors the previous { event: "lead_captured", … } object.
purchase.completed
Fired from the server when Stripe or Paddle confirms a successful charge (workspace Payment webhooks in Settings — not the browser). Includes money for reporting and automation.
data (Stripe example):
{
"processor": "stripe",
"purchase_token": "pur_...",
"access_code": "K7MZQ",
"amount_minor": 1999,
"currency": "usd",
"plan_id": "plan_abc",
"customer_email": "[email protected]",
"stripe_payment_intent_id": "pi_…",
"stripe_invoice_id": "in_… or null",
"stripe_subscription_id": "sub_… or null",
"stripe_event_id": "evt_…"
}
amount_minor — integer in the smallest currency unit (e.g. cents), same convention as Stripe.
Paddle uses processor: "paddle", purchase_token, access_code, paddle_transaction_id, paddle_customer_id, paddle_subscription_id, paddle_notification_event_id, and the same amount_minor / currency (totals converted from Paddle’s decimal strings).
Subscription renewals each produce another purchase.completed when the processor sends a paid invoice / completed transaction.
Stripe timing: purchase.completed is sent for confirmed one-time payments and paid subscription invoices when Stripe includes enough information to match the payment to the funnel.
purchase.claimed
Fired when the Managed Access API links a web purchase to an app user.
{
"purchase_token": "pur_...",
"access_code": "K7MZQ",
"app_user_id": "user_123",
"platform": "ios",
"appsflyer_id": "1700000000000-123456789"
}
subscription.canceled
Fired when Stripe/Paddle tells web2app the subscription is canceled.
{
"processor": "stripe",
"stripe_subscription_id": "sub_..."
}
subscription.payment_failed
Fired when a recurring payment fails and the local access state becomes past_due. For Stripe this comes from invoice.payment_failed.
{
"processor": "stripe",
"stripe_subscription_id": "sub_...",
"status": "past_due"
}
refund.created
Fired when Stripe/Paddle reports a refund/adjustment. Full refunds revoke local purchase access. Partial refunds keep access active and include full_refund: false.
{
"processor": "stripe",
"amount_minor": 1999,
"full_refund": true,
"currency": "usd",
"stripe_charge_id": "ch_...",
"stripe_payment_intent_id": "pi_..."
}
Paddle refunds/adjustments use Paddle identifiers:
{
"processor": "paddle",
"paddle_transaction_id": "txn_...",
"paddle_adjustment_id": "adj_...",
"full_refund": true
}
What is not sent here
- Workspace Stripe/Paddle inbound webhooks (signing secrets in Settings → Payment) are separate — they update Subscribers and trigger these outbound funnel webhooks when appropriate.
- Client-only GTM / dataLayer / Pixel events are not duplicated to this URL.
- AppsFlyer OneLink clicks/installs are not sent here by AppsFlyer; configure AppsFlyer postbacks/S2S separately if you need those in your systems.
Best practices
- Verify
W2A-Webhook-Secret(if configured) before parsing the body. - Respond 200 immediately and queue work asynchronously (avoid timeouts).
- Use
idfor idempotency if you might process the same logical event twice (we do not retry, but you may change endpoints or replay). - Prefer
type+datafor new integrations; keeplegacyonly while migrating old Zaps.