Sign up

Revenue

Connect Paddle

On this page

Paddle Billing sends completed transactions and refund adjustments to a notification destination signed with its secret key. Checkout custom data links each transaction to a visit.

This guide is for Paddle Billing. Paddle Classic webhooks are not supported.

Set up the notification destination

  1. Copy the webhook URL

    In MetricRaven, open Settings → Integrations → Paddle and copy the webhook URL.

  2. Create a destination in Paddle

    In Paddle, open Developer tools → Notifications → New destination. Choose Webhook, paste the URL, and select transaction.completed, adjustment.created and adjustment.updated. Also select customer.created and customer.updated to match payments by the buyer's email, and the subscription.* events (created, activated, trialing, updated, past_due, paused, resumed, canceled) for subscription goals.

  3. Paste the secret key in MetricRaven

    Open the saved destination, copy its secret key (pdl_ntfset_…), paste it into the Paddle card and choose Connect Paddle.

Pass the visitor ID at checkout

Paddle keeps custom_data on the transaction and copies it to the subscription and its renewals. Use these keys:

KeyValue
metricraven_visitor_idThe visitor ID from getVisitorId() (24 or 32 hex characters).
metricraven_user_idOptional. Your signed-in user's ID, set on the server, matching the Identify API.
metricraven_planOptional. A plan name for reports; otherwise the product from the transaction's first price is used.
  1. Read the visitor ID in the browser

    The MetricRaven script exposes window.analytics.getVisitorId(). Read it when checkout starts and send it to your checkout code:

    script.js
    // Read this in the visitor's browser when they start checkout.
    const visitorId = window.analytics?.getVisitorId() ?? null;
    // Send visitorId to your own checkout endpoint alongside its usual request data.
    // Use the authenticated user ID from your backend; do not trust a browser-supplied user ID.
  2. Add it to the checkout

    With Paddle.js:

    Paddle.js
    // Browser: Paddle.js overlay or inline checkout.
    Paddle.Checkout.open({
      items: [{ priceId: "pri_YOUR_PRICE_ID", quantity: 1 }],
      customData: {
        metricraven_visitor_id: window.analytics?.getVisitorId() ?? "",
      },
    });

    Or create the transaction on your server, which can also set your trusted user ID:

    Transactions API
    // Server: create a transaction with the Paddle API, then open it with Paddle.js.
    const response = await fetch("https://api.paddle.com/transactions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.PADDLE_API_KEY}`,
      },
      body: JSON.stringify({
        items: [{ price_id: "pri_YOUR_PRICE_ID", quantity: 1 }],
        custom_data: {
          ...(visitorId ? { metricraven_visitor_id: visitorId } : {}),
          ...(user ? { metricraven_user_id: user.id } : {}),
        },
      }),
    });
    const { data } = await response.json();
    // In the browser: Paddle.Checkout.open({ transactionId: data.id })

MetricRaven links each payment to a visit using the first of these that is available:

  1. The metricraven_visitor_id passed through checkout.
  2. The metricraven_user_id (or the provider's customer external ID), matched to a visitor you linked with the Identify API.
  3. An earlier payment from the same provider customer, so renewals keep the original attribution.
  4. The checkout email, matched to an email you sent to the Identify API.
  5. The checkout email, matched to a browser that called payment({ email }) within an hour of the payment.

Can't set custom data on your checkout? See Other Paddle methods to match by the buyer's email.

Verify it works

After a delivery, the Paddle card in Settings → Integrations shows Connected, the number of verified events and the last event type. A live payment appears in Revenue in its own currency; with a visitor ID it is credited to that visit's source in Traffic.

If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement.

Details

  • Revenue is the transaction's grand_total (including tax), converted from the lowest denomination. Trials and fully credited transactions are not revenue.
  • Transactions with origin: subscription_recurring are renewals. Approved refund and chargeback adjustments reduce the original transaction once, even when both created and updated events arrive.
  • Paddle transactions carry only a customer ID. With customer.created and customer.updated selected, MetricRaven keeps a keyed hash of each customer's email to match payment({ email }) calls; otherwise pass metricraven_user_id or the visitor ID.
  • Signature: Paddle-Signature carries ts and one or more h1 values, the hex HMAC-SHA256 of ts:body. Deliveries older than five minutes are rejected.
  • Paddle's sandbox is a separate account; connect it only to a test site.
  • Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with 401 and never change revenue.
  • Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it.
  • The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports.
  • Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays.
  • Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only.
  • Use one revenue path per purchase: don't also send the same orders through the Payments API.