# Revenue attribution

twelvebit shows which sources bring revenue, not only visitors. It needs two things:

1. **The payments.** Connect your provider in the site's revenue settings in the dashboard (Stripe, Polar, Lemon Squeezy, Paddle or Dodo Payments). twelvebit then receives each payment, renewal and refund. Stores on [Shopify](#shopify) or [WooCommerce](#woocommerce) connect the store instead. For other providers, send payments with `POST /api/v1/payments`.
2. **The visitor.** When you create a checkout on your server, read the `_tb_vid` and `_tb_sid` cookies and put them in the checkout metadata as `twelvebit_visitor_id` and `twelvebit_session_id`. The provider sends the metadata back with the payment.

## How the credit is given

The model is **last touch: the visit before the payment**. For each payment, twelvebit picks the credited visit in this order:

1. The session in the checkout metadata (`twelvebit_session_id`).
2. The visitor's latest visit that started before the payment (`twelvebit_visitor_id`).
3. A visitor that [identified](/docs/identify) with the same email as the customer.
4. For renewals and refunds: the customer's earlier attributed payment.

The visit's channel, referrer, campaign, landing page and country get the revenue. Refunds count against the same visit.

## Read the cookies

The cookies are first-party, so your server receives them with each request from your site:

~~~ts
// lib/twelvebit.ts
import { cookies } from "next/headers";

export async function twelvebitIds() {
  const c = await cookies(); // cookies() is async in Next.js 15 and later
  const visitor = c.get("_tb_vid")?.value;
  const session = c.get("_tb_sid")?.value;
  return {
    ...(visitor && { twelvebit_visitor_id: visitor }),
    ...(session && { twelvebit_session_id: session }),
  };
}
~~~

## Stripe

Put the ids on the Checkout Session. For subscriptions, also put them on the subscription, so renewals carry them. For one-time payments, put them on the PaymentIntent.

~~~ts
// app/api/checkout/route.ts
import Stripe from "stripe";
import { twelvebitIds } from "@/lib/twelvebit";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { priceId } = await req.json();
  const ids = await twelvebitIds();
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: "https://example.com/welcome",
    cancel_url: "https://example.com/pricing",
    metadata: { ...ids, plan: "pro" },
    subscription_data: { metadata: { ...ids, plan: "pro" } },
    // mode: "payment" → use payment_intent_data: { metadata: ids } instead of subscription_data
  });
  return Response.json({ url: session.url });
}
~~~

Express:

~~~js
// server.js (with cookie-parser)
app.post("/checkout", async (req, res) => {
  const ids = {
    twelvebit_visitor_id: req.cookies._tb_vid,
    twelvebit_session_id: req.cookies._tb_sid,
  };
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: [{ price: req.body.priceId, quantity: 1 }],
    success_url: "https://example.com/thanks",
    metadata: ids,
    payment_intent_data: { metadata: ids },
  });
  res.json({ url: session.url });
});
~~~

Payment Links cannot carry server metadata. Use a Checkout Session, or [identify](/docs/identify) the visitor with their email before they pay.

## Polar

~~~ts
import { Polar } from "@polar-sh/sdk";
import { twelvebitIds } from "@/lib/twelvebit";

const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! });

export async function POST() {
  const checkout = await polar.checkouts.create({
    products: [process.env.POLAR_PRODUCT_ID!],
    successUrl: "https://example.com/welcome",
    metadata: await twelvebitIds(),
  });
  return Response.json({ url: checkout.url });
}
~~~

## Lemon Squeezy

Use `checkout_data.custom`:

~~~ts
import { createCheckout } from "@lemonsqueezy/lemonsqueezy.js";
import { twelvebitIds } from "@/lib/twelvebit";

export async function POST() {
  const { data } = await createCheckout(process.env.LS_STORE_ID!, process.env.LS_VARIANT_ID!, {
    checkoutData: { custom: await twelvebitIds() }, // sent as checkout_data.custom
  });
  return Response.json({ url: data?.data.attributes.url });
}
~~~

With plain HTTP, send `"checkout_data": { "custom": { "twelvebit_visitor_id": "…", "twelvebit_session_id": "…" } }` in the checkout attributes.

## Paddle

Paddle Checkout often opens in the browser. Read the cookies there and pass them as `customData`:

~~~js
const cookie = (k) => document.cookie.match(new RegExp("(?:^|; )" + k + "=([^;]*)"))?.[1];

Paddle.Checkout.open({
  items: [{ priceId: "pri_123", quantity: 1 }],
  customData: {
    twelvebit_visitor_id: cookie("_tb_vid"),
    twelvebit_session_id: cookie("_tb_sid"),
  },
});
~~~

For transactions that your server creates, set `custom_data` with the same keys.

## Dodo Payments

~~~ts
import DodoPayments from "dodopayments";
import { twelvebitIds } from "@/lib/twelvebit";

const dodo = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY! });

export async function POST() {
  const session = await dodo.checkoutSessions.create({
    product_cart: [{ product_id: process.env.DODO_PRODUCT_ID!, quantity: 1 }],
    return_url: "https://example.com/welcome",
    metadata: await twelvebitIds(),
  });
  return Response.json({ url: session.checkout_url });
}
~~~

## Shopify

twelvebit connects to your store with a custom app. You do not need to edit the theme or the checkout.

1. In the Shopify admin, open **Settings → Apps and sales channels → Develop apps → Create an app**. Name it "twelvebit".
2. Open **Configuration → Admin API integration → Configure**. Select `read_orders`, `write_script_tags` and `read_customers`. Click **Save**, then **Install app**.
3. Open **API credentials**. Copy the **Admin API access token** (`shpat_…`, shown once) and the **API secret key**.
4. In twelvebit, open the site's revenue settings, choose **Shopify**, and paste the shop domain (`acme.myshopify.com`), the token and the secret.

Since January 2026, Shopify creates new custom apps in the **Dev Dashboard**. If your admin has no "Develop apps" button, create the app there with the same scopes, install it on the store, and paste its **client ID** in place of the token and its **client secret** as the secret.

When you connect, twelvebit:

- adds the script to the online store with a ScriptTag (`https://twelvebit.app/js/script.js?id=tb_YOUR_SITE_ID&shopify=1`). Remove any twelvebit tag from `theme.liquid`, or visits count twice.
- registers the webhooks `orders/paid` and `refunds/create`. Shopify signs them with the API secret key.

In Shopify mode the script also:

- writes the visitor and session ids into the cart as the attributes `_tb_vid` and `_tb_sid`. Each order carries them in `note_attributes`, so the order goes to the visit that led to it.
- sends the goal `add_to_cart` when a product goes into the cart (form or AJAX) and `initiate_checkout` when the visitor clicks the checkout button.

Each paid order becomes a payment (the order total in the store currency, the first product as the plan) and a `payment` goal. Refunds subtract. Disconnect in twelvebit removes the ScriptTag and the webhooks.

## WooCommerce

Install the twelvebit plugin for WordPress:

1. In twelvebit, open the site's revenue settings, choose **WooCommerce**, and click **Create secret**. Keep the page open: the secret is shown once.
2. Download [twelvebit-woocommerce.zip](/plugins/twelvebit-woocommerce.zip). In WordPress, open **Plugins → Add New Plugin → Upload Plugin**, install it and activate it. It needs PHP 7.4 or later.
3. Open **Settings → twelvebit** and paste the **Site ID** (`tb_YOUR_SITE_ID`), the **Domain** (`example.com`), the **App URL** (`https://twelvebit.app`) and the **Webhook secret**. Save.

The plugin:

- adds the script to every page. Remove any other twelvebit tag, or visits count twice.
- sends the goals `add_to_cart` (AJAX buttons, the product page form and the block cart) and `initiate_checkout` (the checkout page).
- saves the `_tb_vid` and `_tb_sid` cookies on each new order (classic and block checkout).
- sends the order to `https://twelvebit.app/api/webhooks/woocommerce/tb_YOUR_SITE_ID` when its status becomes **Processing** or **Completed**, and each refund. The body is JSON, signed with HMAC-SHA256 (hex) of the body in the `X-Twelvebit-Signature` header. The request is non-blocking, so checkout never waits.

twelvebit counts each order once, also when it is sent for Processing and again for Completed.

## Other providers: the payments API

For PayPal, Gumroad, your own billing or a Zapier flow, send each payment from your server. The call is safe to repeat: `transaction_id` makes it idempotent.

~~~sh
curl -sS -X POST https://twelvebit.app/api/v1/payments -H "Authorization: Bearer $TWELVEBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 49, "currency": "USD", "transaction_id": "order_1042", "visitor_id": "VISITOR_ID", "email": "ada@example.com", "plan": "pro"}'
~~~

| Field | Type | Notes |
|---|---|---|
| `amount` | number | Required. Major units: `49.99` means 49.99. `0` is a free trial and is not counted. |
| `currency` | string | Required. ISO 4217, for example `USD`, `EUR`. |
| `transaction_id` | string | Required. Your unique id for the payment. |
| `visitor_id` | string | The `_tb_vid` cookie. |
| `session_id` | string | The `_tb_sid` cookie. Gives the exact visit. |
| `email` | string | Matches a visitor that identified with this email. |
| `customer_id` | string | Links renewals to the first payment. |
| `renewal` | boolean | `true` for a subscription renewal. |
| `refunded` | boolean | `true` for a refund. |
| `plan` | string | The plan or product name, for the revenue by plan view. |
| `timestamp` | string or number | ISO 8601 or Unix time. Default: now. |

With an account token, add `"site_id"` to the body.

## Check it

Make a test payment in the provider's test mode. Then open the Revenue view, or ask the MCP server: "What revenue came from each channel in the last 7 days?" The `get_revenue` tool lists recent payments with their credited channel.

---

Source: https://twelvebit.app/docs/revenue · All docs: https://twelvebit.app/llms.txt
