PilotFoxDocs

Dodo Payments

Connect Dodo Payments with a read-only key, which payments count, and linking payments, renewals and signed-in buyers.

PilotFox reads a site's Dodo Payments payments and refunds with a read-only API key, and links each payment to the visit that led to it. Connect it in Site settings → Revenue → Dodo Payments, or with PUT /api/v1/sites/{siteId}/integrations/dodo. The revenue overview explains connecting, syncing, Match again and disconnecting for every provider.

Create the key

In the Dodo Payments dashboard, switch to the mode you want to import (live or test), go to Developer → API Keys and select Create New Key. Set the access per resource:

ResourceAccessNeeded for
PaymentsReadRequired
RefundsReadRequired
SubscriptionsReadOptional: the subscription metrics

Leave every other resource at None. PilotFox lists payments and refunds once when you connect; a key without one of them is refused with a message that names it.

  • A Dodo key works in one mode only, and Dodo documents no key prefix that would tell which. PilotFox tries the key against the live API first and the test API second, so it finds out itself whether a key is live or test; a test key imports test-mode payments only. The settings show the key's last four characters.
  • If your business does not have live payments yet (account verification is not finished), a live key is refused with a message that says so: connect a test key until Dodo enables live payments. A key of an archived business is refused too.
  • Without Read access to Subscriptions, payments (renewals included) still import and the connection shows a notice. After changing the key's access in Dodo (Edit Permissions on the key), click Sync now: PilotFox asks again at once (otherwise every 6 hours).

By default Dodo allows each business 240 API requests a minute, shared with your own integration. PilotFox syncs with at most one request a second and checks every 5 minutes once it has caught up.

Which Dodo payments count

CountedNot counted
Succeeded payments, from checkout sessions, payment links or the APIFailed, cancelled, processing or abandoned payments (for example Requires payment method)
Subscription renewals, each as its own paymentPayments of 0, such as a payment-method update or a free trial's start
Bank debits (ACH, SEPA) once Dodo marks them succeededDisputes, which are not refunds
Full and partial refunds once they succeedRefunds still pending or in review (counted when they succeed)
  • A Dodo payment's time is when it was created: Dodo does not report when a payment completed, so a bank debit that clears days later keeps its checkout day.
  • The amount is total_amount, the total the customer paid, tax included. A refund is subtracted up to the payment's amount (with Adaptive Currency a refund in the customer's currency can come to more than they paid), and a full refund is always the whole payment.
  • A payment that succeeds days after checkout (ACH takes up to 4 business days, SEPA up to 6) is picked up by a re-check of the last 14 days every 6 hours.

Pass the reference to Dodo Payments

Put the checkout reference in the Checkout Session's metadata as pilotfox_ref, in any format (metadata values are at most 500 characters). Dodo stores a Checkout Session's metadata on the payment it creates. You can also send the session's own session_id (returned when you create the session) as the checkout event's ref: a payment is linked by its checkout session id too.

server.js (Node.js, the dodopayments package)
import DodoPayments from "dodopayments";

// A key that can create checkout sessions (not the read-only one you gave PilotFox):
const dodo = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: "live_mode", // "test_mode" with a test key
});

app.post("/create-checkout", async (req, res) => {
  const ref = String(req.body.ref ?? "");
  const session = await dodo.checkoutSessions.create({
    product_cart: [{ product_id: "pdt_123", quantity: 1 }],
    return_url: "https://example.com/thanks",
    metadata: { pilotfox_ref: ref.slice(0, 500) },
    // A signed-in buyer: metadata: { pilotfox_ref: ref, pilotfox_user_id: user.id },
  });
  res.json({ url: session.checkout_url });
});

The return URL needs nothing more: Dodo appends payment_id (or subscription_id) to it, which the script sends from your return page. Dodo also appends the buyer's email and any license_key there; the script leaves those two out of the URL it reports.

What PilotFox reads from a Dodo payment

FieldUsed as
metadata.pilotfox_refCheckout reference, any format
checkout_session_idCheckout reference
payment_id, subscription_idReturn-page ids, matched up to an hour after the payment
metadata.pilotfox_user_id, then the customer's metadata.pilotfox_user_idUser id
customer.emailCustomer email
customer.customer_idSame-customer link for later payments
subscription_idThe subscription link of renewals

With Payment links and return pages on (the default), the script adds a fresh reference to static payment links (https://checkout.dodopayments.com/buy/…, or https://test.checkout.dodopayments.com/buy/… in test mode) as they are clicked, in their metadata_pilotfox_ref parameter, and sends the matching checkout event (payment links).

To do it yourself: a payment link takes custom metadata as metadata_<key> URL parameters, so metadata_pilotfox_ref carries the reference as metadata.pilotfox_ref, in any format.

document.querySelectorAll('a[href^="https://checkout.dodopayments.com/buy/"]').forEach((a) => {
  a.addEventListener("click", () => {
    const ref = crypto.randomUUID();
    window.pilotfox?.event("checkout", { ref });
    const url = new URL(a.href);
    url.searchParams.set("metadata_pilotfox_ref", ref);
    a.href = url.toString();
  });
});

Renewals and signed-in buyers

A renewal is credited to the same visit as its subscription's first linked payment, so linking the checkout that starts a subscription links its renewals too. Later payments of the same Dodo customer that nothing else links reuse the link of their earlier payment.

A payment is also linked to an identified user by pilotfox_user_id in the payment's metadata or in its Dodo customer's metadata. Subscription renewals are charged without a checkout and carry the subscription's metadata, which may not include your checkout's. For a subscription whose first payment could not be linked, create the Dodo customer with the user id and attach that customer to each checkout:

const customer = await dodo.customers.create({
  email: user.email,
  name: user.name,
  metadata: { pilotfox_user_id: user.id },
});
// then, in dodo.checkoutSessions.create: customer: { customer_id: customer.customer_id },

Renewals without either still match by the customer's email when your server sent it for the user (option 3).

Dodo subscription metrics

With Read access to Subscriptions, PilotFox reads every subscription once when you connect (whatever its age), again every 6 hours, and a subscription right away when a new payment of it arrives. Dodo's statuses map to the metrics' as follows:

Dodo PaymentsSubscription metrics
activeactive (trialing during its trial)
past_duepast due
on_holdunpaid
pausedpaused
cancelled, expiredcanceled
failedincomplete
any other status (such as pending)not counted until it becomes one of the above
  • The plan price is Dodo's recurring_pre_tax_amount, the amount charged before tax for each recurring payment, with its quantity, per billing interval (its payment frequency). Dodo gives no price before discounts, so a discounted subscription counts at its discounted price; a tax-inclusive price keeps its tax.
  • Dodo has no trial status: a subscription is in its trial for its trial_period_days from its creation. A trial extended by moving the next billing date counts as active once the original trial period is over.
  • On-demand subscriptions (charged a variable amount whenever you ask) have no plan price and are left out.
  • A cancellation or a pause is dated by Dodo. A hold or a missed renewal on an old subscription with no new payment is seen within 6 hours, and dated when it is seen.

Known limits

  • Key access. Read access to Payments and Refunds is all PilotFox asks for. Dodo's documentation does not say whether reading a payment, which includes its customer's email and metadata, could also need Customers access; PilotFox checks both reads when you connect, so a key that is not enough is refused right away.
  • Payment-link parameters. Dodo documents metadata_<key> URL parameters with one example (metadata_orderId=123); metadata_pilotfox_ref follows that pattern. If your payments never show a checkout reference, use a Checkout Session instead, whose metadata is documented to reach the payment.
  • Renewals. Whether a Checkout Session's metadata is copied onto the subscription it starts is not documented, hence the customer metadata above.
  • Refund amounts. Dodo does not say whether a refund's amount includes tax. PilotFox subtracts the refund as Dodo reports it, never more than the payment, and a full refund is always the whole payment.
  • A payment for several subscriptions. A checkout that starts several subscriptions at once makes one payment that names none of them; its subscriptions' renewals are then linked on their own (customer, user id or email).
  • The provider SDK. The Node.js snippets use Dodo Payments' own dodopayments package; check its documentation for the current method names.

On this page