Revenue
Connect Paddle
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
Copy the webhook URL
In MetricRaven, open Settings → Integrations → Paddle and copy the webhook URL.
Create a destination in Paddle
In Paddle, open Developer tools → Notifications → New destination. Choose Webhook, paste the URL, and select
transaction.completed,adjustment.createdandadjustment.updated. Also selectcustomer.createdandcustomer.updatedto match payments by the buyer's email, and thesubscription.*events (created, activated, trialing, updated, past_due, paused, resumed, canceled) for subscription goals.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:
| Key | Value |
|---|---|
| metricraven_visitor_id | The visitor ID from getVisitorId() (24 or 32 hex characters). |
| metricraven_user_id | Optional. Your signed-in user's ID, set on the server, matching the Identify API. |
| metricraven_plan | Optional. A plan name for reports; otherwise the product from the transaction's first price is used. |
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.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:
- The
metricraven_visitor_idpassed through checkout. - The
metricraven_user_id(or the provider's customer external ID), matched to a visitor you linked with the Identify API. - An earlier payment from the same provider customer, so renewals keep the original attribution.
- The checkout email, matched to an email you sent to the Identify API.
- 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_recurringare renewals. Approved refund and chargeback adjustments reduce the original transaction once, even when bothcreatedandupdatedevents arrive. - Paddle transactions carry only a customer ID. With
customer.createdandcustomer.updatedselected, MetricRaven keeps a keyed hash of each customer's email to match payment({ email }) calls; otherwise passmetricraven_user_idor the visitor ID. - Signature:
Paddle-Signaturecarriestsand one or moreh1values, the hex HMAC-SHA256 ofts: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
401and 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.