Revenue attribution
twelvebit shows which sources bring revenue, not only visitors. It needs two things:
- 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 or WooCommerce connect the store instead. For other providers, send payments with
POST /api/v1/payments. - The visitor. When you create a checkout on your server, read the
_tb_vidand_tb_sidcookies and put them in the checkout metadata astwelvebit_visitor_idandtwelvebit_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:
- The session in the checkout metadata (
twelvebit_session_id). - The visitor's latest visit that started before the payment (
twelvebit_visitor_id). - A visitor that identified with the same email as the customer.
- 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:
// 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.
// 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:
// 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 the visitor with their email before they pay.
Polar#
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:
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:
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#
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.
- In the Shopify admin, open Settings → Apps and sales channels → Develop apps → Create an app. Name it "twelvebit".
- Open Configuration → Admin API integration → Configure. Select
read_orders,write_script_tagsandread_customers. Click Save, then Install app. - Open API credentials. Copy the Admin API access token (
shpat_…, shown once) and the API secret key. - 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 fromtheme.liquid, or visits count twice. - registers the webhooks
orders/paidandrefunds/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_vidand_tb_sid. Each order carries them innote_attributes, so the order goes to the visit that led to it. - sends the goal
add_to_cartwhen a product goes into the cart (form or AJAX) andinitiate_checkoutwhen 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:
- In twelvebit, open the site's revenue settings, choose WooCommerce, and click Create secret. Keep the page open: the secret is shown once.
- Download twelvebit-woocommerce.zip. In WordPress, open Plugins → Add New Plugin → Upload Plugin, install it and activate it. It needs PHP 7.4 or later.
- 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) andinitiate_checkout(the checkout page). - saves the
_tb_vidand_tb_sidcookies on each new order (classic and block checkout). - sends the order to
https://twelvebit.app/api/webhooks/woocommerce/tb_YOUR_SITE_IDwhen its status becomes Processing or Completed, and each refund. The body is JSON, signed with HMAC-SHA256 (hex) of the body in theX-Twelvebit-Signatureheader. 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.
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.