Webhook Relay guide
Use Webhook Relay when your app already has a backend, users, subscriptions, and entitlement logic. web2app handles funnel checkout, payment webhooks, success links, AppsFlyer OneLink, and outbound events; your backend decides who gets access.
Flow
- Visitor pays in the funnel.
- Stripe/Paddle confirms the payment to web2app.
- web2app sends your backend
purchase.completed. - Success step sends the user to your app through AppsFlyer OneLink.
- Your app receives
purchaseToken/accessCode. - Your app sends those values to your backend.
- Your backend grants or denies access.
In this mode, web2app's claim-purchase endpoint returns access_not_managed; use your own claim endpoint instead.
Configure relay mode
In Funnel settings:
- Open Web-to-App Access.
- Select Webhook Relay.
- Configure AppsFlyer OneLink.
- Configure Success delivery fallbacks.
- In Advanced, add your Event Webhook URL and signing secret.
Verify webhook security
Every outbound webhook includes:
W2A-Webhook-Secret: YOUR_SECRET
Content-Type: application/json
Reject requests when the header is missing or incorrect. Process id idempotently.
Events to handle
purchase.completed
Create or update a local purchase. Store data.purchase_token and data.access_code; the same values are also sent through the Success URL / OneLink so your app can claim the matching purchase after install.
{
"api_version": "1",
"id": "evt_...",
"type": "purchase.completed",
"created_at": "2026-04-04T12:00:00.000Z",
"livemode": true,
"session_id": "1700000000-abcd",
"funnel": {
"id": "funnel_uuid",
"slug": "my-funnel",
"name": "My Funnel"
},
"data": {
"processor": "stripe",
"purchase_token": "pur_...",
"access_code": "K7MZQ",
"amount_minor": 1999,
"currency": "usd",
"plan_id": "monthly",
"customer_email": "[email protected]",
"stripe_payment_intent_id": "pi_...",
"stripe_subscription_id": "sub_..."
}
}
subscription.renewed
Treat a new paid invoice/transaction as a renewal. purchase.completed is also sent for paid renewal events, so you can use either the lifecycle event or the purchase event depending on your backend model.
subscription.canceled
Revoke access immediately or schedule revocation at period end according to your product policy.
{
"type": "subscription.canceled",
"data": {
"processor": "paddle",
"paddle_subscription_id": "sub_..."
}
}
subscription.payment_failed
Mark the account as past due, start a grace period, or temporarily deny access depending on your product policy.
{
"type": "subscription.payment_failed",
"data": {
"processor": "stripe",
"stripe_subscription_id": "sub_...",
"status": "past_due"
}
}
refund.created
Revoke access for full refunds or downgrade/keep access for partial refunds. Partial refunds include full_refund: false.
{
"type": "refund.created",
"data": {
"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 instead:
{
"type": "refund.created",
"data": {
"processor": "paddle",
"paddle_transaction_id": "txn_...",
"paddle_adjustment_id": "adj_...",
"full_refund": true
}
}
purchase.claimed
Sent only when Managed Access claim API is used. In pure Relay mode, your own backend should emit and store the equivalent event.
Your app/backend claim endpoint
Your app should call your backend:
POST https://api.yourapp.com/web-purchase/claim
Content-Type: application/json
{
"purchaseToken": "pur_abc123",
"accessCode": "K7MZQ",
"appUserId": "user_123",
"platform": "ios",
"appsFlyerId": "1700000000000-123456789"
}
Your backend should:
- Find the purchase by token/code.
- Confirm it came from a trusted
purchase.completedwebhook. - Ensure it is not refunded/canceled.
- Link it to the app user.
- Return the entitlement.
Email options
For relay mode, send customer email from your own backend or automation tool after receiving purchase.completed. This is recommended when you already use SendGrid, Resend, Customer.io, Braze, or your own transactional email service.
web2app sends the signed event with the purchase token, access code, amount, currency, and processor identifiers. Your backend owns access, copy, localization, templates, deliverability, retries, consent, and unsubscribe handling.
Testing
Run the full flow with a mobile test device:
- Complete payment in Stripe test mode or Paddle sandbox.
- Confirm your backend receives
purchase.completed. - Click the AppsFlyer OneLink from email/QR/mobile message, not by pasting into the browser address bar.
- Install/open the app.
- Confirm AppsFlyer UDL returns
deep_link_sub1anddeep_link_sub2. - Claim the purchase on your backend.
- Refund/cancel in the payment provider and confirm your backend receives the lifecycle event.