Skip to content

Revenue attribution

Revenue is recorded by your server, from a confirmed payment, and reported against the source that produced it.

Report a payment

Send a revenue event when a payment is confirmed, never when a checkout starts. The request needs an API key with the revenue:write scope and the website id in the body.

terminalbash
curl -X POST https://abackanalytics.com/api/v1/revenue \
  -H "Authorization: Bearer abk_xxxxxxxx.secret" \
  -H "content-type: application/json" \
  -d '{
    "websiteId": "abk_xxxxxxxxxxxxxxxx",
    "idempotencyKey": "order_2026_10_00041",
    "provider": "dodo",
    "type": "payment",
    "amount": 1999,
    "currency": "USD",
    "customerRef": "cust_8f21",
    "productId": "pro_monthly"
  }'
responsejson
{
  "ok": true,
  "data": {
    "revenueEventId": "rev_3c9d5a1e7b2f4a6c8d0e",
    "status": "recorded",
    "attributed": true
  }
}

The browser never sends an amount. Revenue is only ever recorded from the server, so the amount in the report is the amount you confirmed.

Why the idempotency key matters

The idempotency key identifies one payment. A webhook that is delivered twice, a retry from your own code and a manual re-send all produce the same key, and the server stores the payment once. The second attempt comes back with the status duplicate and is not counted again.

Use the identifier of the order or the transaction in your own system. A value that is unique per payment is enough. Do not use a timestamp on its own, because two payments in the same second would then collide.

Linking a payment to a visitor

If the browser identifier is available on your server, send it as the visitor id and the payment is matched to the sessions of that visitor. If it is not available, the payment is still recorded, and it is reported as unmatched.

bodyjson
{
  "websiteId": "abk_xxxxxxxxxxxxxxxx",
  "idempotencyKey": "order_2026_10_00042",
  "amount": 1999,
  "currency": "USD",
  "visitorId": "4d1c2b7a-0f3e-4a51-9c88-1b6f0a2d5e73"
}

The value is the identifier from the visitor browser, and it is converted on the server in the same way as a browser event, so it can only be matched to the website it came from. It is not an email address and it is not a customer id from your database.

Linking a payment to a user

If your app knows who paid but not which browser they used, send your own user id instead. The payment is matched to the sessions of every visitor you linked to that user with identify, so a visit on a phone can be credited for a payment made on a laptop.

bodyjson
{
  "websiteId": "abk_xxxxxxxxxxxxxxxx",
  "idempotencyKey": "order_2026_10_00043",
  "amount": 1999,
  "currency": "USD",
  "userId": "usr_8f3k2"
}

Use the opaque id of your users table. A value that looks like an email address is refused. When neither a visitor nor a user is sent, a payment of a customer reference that paid before is credited to the visit that won that customer, which is how a renewal keeps its source.

How attribution works

Two models are supported, and both are named explicitly in the interface so you always know which one produced a number. Neither is a claim of perfect attribution.

  • First touch attributes the payment to the first session of the visitor inside the lookback window.
  • Last touch attributes the payment to the most recent session of the visitor inside the lookback window. This is the default.

The lookback window is 30 days by default and is configurable per website. A payment that happened after the window has passed cannot be attributed. It is still recorded as revenue, but it is stored without a source, a medium or a campaign, so it does not inflate the revenue of any channel.

Unmatched payments

A payment is unmatched when no visitor id was sent with it, or when no session of that visitor could be found inside the lookback window. The response reports the case with the attributed field set to false.

An unmatched payment is still counted as revenue, but it is stored without a source, a medium or a campaign. It never appears as the revenue of a campaign that did not produce it.

Refunds

A refund is a revenue event with the type refund. It reduces the net revenue of the day it happened and it never changes the original payment. The report shows gross, refunds and net side by side.

terminalbash
curl -X POST https://abackanalytics.com/api/v1/revenue \
  -H "Authorization: Bearer abk_xxxxxxxx.secret" \
  -H "content-type: application/json" \
  -d '{
    "websiteId": "abk_xxxxxxxxxxxxxxxx",
    "idempotencyKey": "refund_order_2026_10_00041",
    "transactionId": "order_2026_10_00041",
    "provider": "dodo",
    "type": "refund",
    "amount": 1999,
    "currency": "USD"
  }'

Use a different idempotency key for the refund, for example the original key with a refund prefix, so the two are stored separately. Set the transaction id to the idempotency key of the payment it reverses: the refund then takes revenue away from the source of that payment, instead of from no source at all. A chargeback works the same way, with the type chargeback.

Amounts and currencies

  • The amount is an integer in the minor unit of the currency. 1999 with USD is 19.99.
  • The currency is an ISO code with three letters.
  • Totals are kept per currency. Two currencies are never added together and no exchange rate is applied, so a converted number is never invented.
  • A refund is stored as a negative contribution to the net amount of its day.

Dodo Payments

Dodo Payments is the provider used to sell Aback Analytics subscriptions. If your own product also collects payments with Dodo Payments, there are two ways to report them.

The first is the revenue endpoint. Your backend sends the event and sets the provider field to dodo. Use the identifier of the payment in your own system as the idempotency key. A checkout page and a webhook that both report the same payment have to use the same key, otherwise the payment is counted twice.

The second is a direct connection. On the Integrations page you can connect your own Dodo account, give Aback Analytics a webhook URL and let it receive the payment events for you. The connection is separate from the Aback Analytics subscription: it uses your credentials, it never changes your plan, and the payments it records only appear in your own reports.

To attribute a connected payment to the visitor that produced it, put the value of window.aback.identifier() into the Dodo payment metadata under the key aback_visitor. The server turns it into the same pseudonymous identifier the browser events use, and the raw value never reaches the database.

The request and response shapes are documented in the API reference.

Other payment providers

Stripe, Paddle Billing, Lemon Squeezy, Polar, Shopify and WooCommerce connect through their signed webhooks, on the Integrations page of the site, on the Business plan. Aback Analytics gives you a private webhook address; you add it in the provider and paste the signing secret it shows. No API key of yours is stored. The connection says it is waiting until the first signed event arrives, so it never claims to work before it has.

Only payments the provider reports as paid are recorded, with their refunds and chargebacks. A dispute you win puts the revenue back. To attribute a payment, pass the visitor id to the checkout:

How to pass the visitor to each provider checkout
ProviderPass the visitor
StripeCreate the Checkout Session with metadata.aback_visitor set to aback.identifier(), and for a subscription set the same key in subscription_data.metadata. A Payment Link takes client_reference_id.
Paddle BillingOpen the checkout with customData.aback_visitor. Paddle copies it to the subscription and every renewal.
Lemon SqueezyAdd checkout[custom][aback_visitor] to the checkout link, or checkout_data.custom.aback_visitor when you create the checkout with the API.
PolarCreate the checkout with metadata.aback_visitor. A customer external_id is read as your user id.
ShopifySave the visitor id as a cart attribute named aback_visitor before checkout, for example with /cart/update.js. Shopify copies it to the order.
WooCommerceKeep the visitor id in a first party cookie named aback_visitor and copy it into the order meta when the order is created, with the snippet below.

The key aback_user in the same place carries your user id instead. A payment without either is still recorded, as unattributed revenue.

A WooCommerce store adds the visitor to its orders with a few lines of PHP. The cookie is one your site sets, so it falls under your own cookie notice.

woocommerce-aback.phpphp
<?php
// In a small plugin or in the functions.php of your theme.
// The tracker keeps its id in local storage, so a script copies it into a
// first party cookie that the checkout request carries to the server.
add_action('wp_footer', function () {
  echo "<script>if (window.aback && aback.identifier) { document.cookie = 'aback_visitor=' + aback.identifier() + '; path=/; max-age=86400; samesite=lax; secure'; }</script>";
});

function aback_copy_visitor($order) {
  if (empty($_COOKIE['aback_visitor'])) return;
  $order->update_meta_data('aback_visitor', sanitize_text_field(wp_unslash($_COOKIE['aback_visitor'])));
  $order->save();
}
// The classic checkout and the block checkout create the order in different hooks.
add_action('woocommerce_checkout_order_created', 'aback_copy_visitor');
add_action('woocommerce_store_api_checkout_order_processed', 'aback_copy_visitor');

Known limits: a Stripe invoice paid outside a checkout or a subscription is not recorded, Lemon Squeezy sends no dispute event so a chargeback there is not seen, and a refund of a Shopify order paid in another currency than the shop's is shown in the currency the customer paid.