twelvebitDocsllms.txtSign in
View as Markdown

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 or 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 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 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. 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"}'
FieldTypeNotes
amountnumberRequired. Major units: 49.99 means 49.99. 0 is a free trial and is not counted.
currencystringRequired. ISO 4217, for example USD, EUR.
transaction_idstringRequired. Your unique id for the payment.
visitor_idstringThe _tb_vid cookie.
session_idstringThe _tb_sid cookie. Gives the exact visit.
emailstringMatches a visitor that identified with this email.
customer_idstringLinks renewals to the first payment.
renewalbooleantrue for a subscription renewal.
refundedbooleantrue for a refund.
planstringThe plan or product name, for the revenue by plan view.
timestampstring or numberISO 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.