Skip to main content

What this lets you do

Send Traceten your Dodo Payments revenue and see which AI sources drive paying customers, not just pageviews. Dodo has no built-in Traceten integration, so there is no key to paste and no connect flow: Dodo sends the payment to Traceten directly, either from its own dashboard or from your server.

Before you start

  • A Traceten account with at least one site tracked.
  • A Dodo Payments account.
  • The Traceten snippet installed and verified on the site your checkout runs from.
  • A Traceten API key with the ingest:write permission. Create one in Settings → API keys. It is shown once.

Two ways to forward payments

Pick one. Both call the same Payment API endpoint and attribute the same way; they differ only in where the payload gets reshaped.

Method A: send directly from Dodo (no server needed)

Dodo’s webhook endpoints support a transformation function that runs on Dodo’s own servers before dispatch. You can point it straight at Traceten’s Payment API, so no code runs on your side at all.
1

Create a webhook endpoint in Dodo

Open Dodo Dashboard → Webhooks → + Add Endpoint. Set the endpoint URL to Traceten’s Payment API:
In the integrations dropdown, pick Custom rather than one of the named presets. Make sure the endpoint is Enabled. A disabled endpoint stops receiving events entirely, which is not something you’ll notice from the transform or headers being correct.Subscribe it to these eight events, then create it:The transform below posts two of these to Traceten: payment.succeeded and refund.succeeded. Those are the two that move money. Subscribe to the rest so the whole subscription lifecycle lands on one endpoint, which is what you need if you handle it in your own server (method B). They change no revenue figure on their own: a cancellation stops future charges, it does not take back the ones already paid.
Subscribing to payment.succeeded alone means a refund can never reach Traceten, so your revenue only ever goes up. If you already have an endpoint set up that way, add refund.succeeded to it.
2

Open the endpoint and go to Advanced

Click into the endpoint you just created and open its Advanced tab. Both the transformation code and the custom headers live here.
3

Set the transformation code

Replace the transformation code with:
Three things that are easy to get wrong here:
  • The refund’s transaction_id is the original payment_id. Sending the refund’s own id would create a second, unrelated payment instead of reducing the first one.
  • The divisor is not always 100. JPY, KRW and ISK have no minor unit, so a ¥5000 charge sent as 5000 / 100 records as ¥50 and no error is raised. toMajor is what keeps those three right.
  • Never send a negative amount. The endpoint rejects it. A refund is a positive number with refunded: true.
A payment refunded in two or more parts needs method B. refunded_amount is the running total refunded against a payment, and a transform runs on one event with no memory of the ones before it, so it can only ever report the refund in front of it. The first partial refund of a payment is therefore exact, and a second one subtracts too little or nothing at all, with a 202 either way. Full refunds are unaffected: they send no refunded_amount, and Traceten takes back everything still outstanding. If you issue more than one partial refund against the same payment, forward refunds from your own server instead, where you can read the payment back and sum its refunds array.
Replace site_id with your own site key before saving. Dodo’s transform has no access to environment variables, so it has to be written in directly.
4

Add the Authorization header

In the same Advanced tab, under Custom headers, add one header:The header name has to be exactly Authorization. A header named after your key (for example TRACETEN_API_KEY) is a header Traceten never looks at, and the request will 401.
5

Save and test

Save the endpoint. Dodo’s own Testing tab sends a sample payload through the transform if you want to check it before going live.
Dodo now posts successful payments and refunds straight to Traceten. Nothing runs on your infrastructure, and there is nothing to keep running.

Method B: forward from your own server

If you’d rather keep the transform in your own codebase (versioned, tested, deployed with the rest of your backend), point Dodo’s webhook at your own server instead and forward from there. See the Payment API’s Dodo example for a complete handler. This is also the method to use if you issue more than one partial refund against the same payment. Traceten’s refunded_amount is the running total refunded against a payment, and a refund.succeeded event states only the refund that just happened. A handler on your own server can call Dodo back for the payment and sum its refunds array, which is the running total; Dodo’s transform cannot, because it has no network access and no memory of earlier events. Dodo follows the Standard Webhooks specification, so your handler gets three headers: Verify the signature against the raw request body, before any JSON parsing. Re-serialising the body changes the bytes and the signature will not match. Every event arrives in the same envelope:
data.payload_type tells you which object you have: Payment, Subscription, Refund or Dispute. Branch on it, or on the top-level type, rather than guessing from which fields are present.

Passing the visitor ID through checkout

Both methods depend on the same thing: traceten_visitor_id in the payment’s metadata, set when you create the Dodo checkout session. Without it, the payment either records as unattributed (method B) or is never sent at all (method A’s transform skips dispatch on purpose, to avoid a pointless API call).
In the dashboard, open Settings → Integrations, pick Dodo Payments and click Set up with AI. It copies a prompt for your coding assistant that wires the visitor ID into your checkout and forwards payments to the Payment API (method B). The same card shows this site’s Visitor cookie name.
The robust way to get the value is window.traceten.getVisitorId(), called client-side and forwarded to whatever creates the checkout session:
If you’d rather not add a client-side round trip, read the cookie directly server-side instead:
Do not hardcode this cookie name. It is per-site and only correct on your configured domain (not on localhost). Find the real one under Sites → Settings → Cookies → Visitor cookie name.
Dodo echoes metadata back on the payment.succeeded webhook as a top-level object on the Payment payload, so whichever method reads it, it reaches Traceten unchanged. Dodo also appends its own query parameters to your return_url after checkout: payment_id on a one-time purchase, subscription_id on a subscription, status on both, plus license_key and email. You do not add template variables for these. Traceten uses payment_id on its own, which is what the next section is about. If you sell through a Dodo checkout link that never touches your server, there is nowhere to set metadata.traceten_visitor_id. There is nothing to configure for this: set your return_url to a page on your site that has the Traceten snippet installed, and Dodo appends payment_id to it by itself. The snippet reads that id and reports it, and Traceten matches it to the browser that completed the purchase.
Dodo turns that into https://yourdomain.com/thanks?payment_id=pay_ts2ySpzg07phGeBZqePbH&status=succeeded on redirect.
Method A’s transform skips these payments by default. The transform above returns null when metadata.traceten_visitor_id is missing, so a checkout-link payment is never sent to Traceten at all, and a redirect signal has no payment to attach to. To use this path with method A, delete these two lines from the transform:
and change the visitor_id line to send an empty value when there is no metadata:
Method B is unaffected: if your own handler forwards every payment.succeeded, the payment is already reaching Traceten.
More things worth knowing:
  • Your server API key must be active. Traceten keeps a return-page report only for a site whose account has an active server API key with the ingest:write scope, the key your payments already reach Traceten with. With no such key, or once it is revoked, reports are discarded rather than held. A report can also be discarded for up to a minute after you create the key.
  • The order of events does not matter. Dodo’s webhook usually reaches Traceten before the buyer’s browser finishes redirecting. When that happens the payment is recorded first and re-attributed the moment the browser reports in, on the original payment’s own date.
  • The return page must load within 30 minutes of payment. Traceten pairs a report with a payment only when the two are no more than 30 minutes apart. Dodo’s redirect happens straight after payment, so this only matters for a return page opened again much later.
  • Subscriptions are not covered. Dodo appends subscription_id rather than payment_id on a subscription checkout, and a subscription id is not what the payment is recorded under. Pass metadata.traceten_visitor_id for those.
This path matches a browser, not a person. A buyer who completes checkout on their phone and opens the confirmation link on a laptop is not matched, and neither is a confirmation URL forwarded to somebody else. Checkout metadata, where you can use it, is exact.

How attribution works

Traceten tries to tie the payment back to the originating session, in priority order:
  1. metadata.traceten_visitor_id: matches the payment to the originating session exactly.
  2. Return URL redirect: Dodo appends payment_id to your return_url, and the Traceten snippet on that page reports it. Traceten matches it to the browser that completed the purchase. See Checkout links above.
  3. Email: if the buyer’s session called traceten.identify({ email }), Traceten matches the payment’s customer email (hashed, never stored) to that visitor.
  4. No match: the payment is still recorded so revenue totals stay complete, but it shows as unattributed (no AI source).
There is no automatic history import for Dodo. Because nothing here is a Traceten-managed webhook, there is no endpoint whose creation date bounds what can be backfilled. Only payments you actively forward (by either method) are recorded, starting from whenever you set it up.

Refunds and net revenue

Attributed revenue in Traceten is net of refunds. When a refund.succeeded event reaches Traceten, it writes a matching entry that subtracts from whichever AI source earned the original payment, so the source’s revenue goes down by the amount refunded. Four things follow from that, and they apply to every revenue source in Traceten, not only Dodo:
  • A refund is dated on the day it happened, not on the day of the original payment. Yesterday’s revenue figure never changes after the fact. A day on which you refunded more than you took in reads as a negative number, which is correct.
  • Conversion counts stay gross. A refunded payment still counts as a conversion, because it still happened. The money is reported separately as a refunded figure beside the net one.
  • refunded_amount is the running total, not one refund’s amount. Send the total refunded against the payment so far. A payment refunded 12.50andthenafurther12.50 and then a further 10.00 reports 22.5 on the second post, and Traceten subtracts the 10.00difference.Post10.00insteadandnothingissubtracted,because10.00 difference. Post `10.00` instead and nothing is subtracted, because 10.00 is less than the $12.50 already taken back. Dodo’s transform can only report the refund in front of it, which is why a payment refunded in parts more than once belongs in method B.
  • A cancelled subscription is not a refund. subscription.cancelled, subscription.expired and subscription.on_hold stop future charges. The charges already paid were earned, so they stay in your totals.
If you never subscribe the endpoint to refund.succeeded, none of this happens: Traceten never hears about the refund, and the revenue stays in your totals permanently.

Verify it worked

  1. Complete a real payment through your checkout.
  2. Check the endpoint you configured (Dodo’s dashboard shows delivery attempts and responses for method A; your own logs for method B). A 202 response means Traceten accepted it.
  3. Open Revenue in the Traceten dashboard and select today. The payment appears within a minute or two, either against an AI source or under “Unattributed.”

Troubleshooting

Got a 401 from Traceten. Check the header name is exactly Authorization, not the name of your API key, and that the value is Bearer followed by the key with no extra characters. Got a 422 from Traceten. The payload shape is wrong. Compare your transform’s output against the Payment API’s request body. Dodo’s own field names (payment_id, total_amount) do not match Traceten’s (transaction_id, amount), so sending Dodo’s raw payload through unmodified always fails here. A negative amount is a 422 too: send a refund as a positive amount with refunded: true. Nothing arrives at Dodo’s endpoint at all. For method A, confirm webhook.eventType === "payment.succeeded" matches what your account actually sends, and that the endpoint is enabled and subscribed to that event. Revenue only ever goes up. The endpoint is not subscribed to refund.succeeded, so refunds never reach Traceten. Add it under the endpoint’s events and refunds from that point on will subtract. A refund subtracted more than you refunded. The transform sent refunded: true without refunded_amount on a partial refund, which takes back everything outstanding on the transaction. Check the refund.is_partial branch. A second partial refund on the same payment subtracted nothing. Expected with method A. refunded_amount is the running total, and the transform reported only the latest refund, which is smaller than what has already been taken back. Move refund forwarding to method B, where you can sum the payment’s refunds array. A JPY, KRW or ISK sale landed at a hundredth of its value. The divisor was 100 for a currency with no minor unit. Check toMajor is in the transform and is applied to both payment.total_amount and refund.amount. Everything shows as unattributed. metadata.traceten_visitor_id is not being set at checkout, or it is empty. Confirm the value your checkout-creation code sends is not an empty string.

Next