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

  1. Visitor pays in the funnel.
  2. Stripe/Paddle confirms the payment to web2app.
  3. web2app sends your backend purchase.completed.
  4. Success step sends the user to your app through AppsFlyer OneLink.
  5. Your app receives purchaseToken / accessCode.
  6. Your app sends those values to your backend.
  7. 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:

  1. Open Web-to-App Access.
  2. Select Webhook Relay.
  3. Configure AppsFlyer OneLink.
  4. Configure Success delivery fallbacks.
  5. 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:

  1. Find the purchase by token/code.
  2. Confirm it came from a trusted purchase.completed webhook.
  3. Ensure it is not refunded/canceled.
  4. Link it to the app user.
  5. 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:

  1. Complete payment in Stripe test mode or Paddle sandbox.
  2. Confirm your backend receives purchase.completed.
  3. Click the AppsFlyer OneLink from email/QR/mobile message, not by pasting into the browser address bar.
  4. Install/open the app.
  5. Confirm AppsFlyer UDL returns deep_link_sub1 and deep_link_sub2.
  6. Claim the purchase on your backend.
  7. Refund/cancel in the payment provider and confirm your backend receives the lifecycle event.