&org=&url=
```
When a visitor opens that link, Macropay:
1. **Logs the click** against the affiliate's ``.
2. **Sets an `mp_ref` cookie** scoped to your cookie window.
3. **Redirects** to `` with `?ref=` appended so the code is available client-side.
A sale is attributed when the resulting order carries the referral code in `user_metadata` as **`mp_ref`** (or `referral_code`). At `order.paid`, Macropay matches the code to an active affiliate and records the commission on the **net amount** (subtotal minus discount, excluding tax). Recurring commissions, if enabled, accrue on each subsequent paid invoice until the cap is reached.
### Getting the code onto the order
There are three ways the code reaches the order — pick whichever fits your setup. **No tracking pixel on your site is required for hosted checkout.**
**Automatic, no code.** Because Macropay hosts your checkout, the `mp_ref`
cookie set by the tracking link is read at checkout and folded into the order
for you. Works out of the box with checkout links and the hosted page.
Add our snippet — it captures `?ref` and forwards `mp_ref` into Macropay
checkout links automatically.
Creating sessions yourself? Set `metadata.mp_ref` to the code when you create
the checkout.
**Snippet** — drop this on your site if you link to Macropay checkout from your own pages:
```html theme={null}
```
It reads `?ref`/`?mp_ref` from the landing URL, stores it for your cookie window, and appends `mp_ref` to any Macropay checkout links on the page. It also exposes `window.MacropayAffiliate.getRef()` if you'd rather attach the code yourself.
**API** — pass the code through `metadata.mp_ref` when you create the checkout session. The value flows onto the order as `user_metadata.mp_ref`, which is what attribution matches at `order.paid`:
```bash theme={null}
curl -X POST https://api.macropay.ai/v1/checkouts \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product_id": "",
"metadata": {
"mp_ref": ""
}
}'
```
Explicit `metadata.mp_ref` always wins — automatic injection never overwrites a code you set yourself.
## FAQ
**How do I attribute a sale to an affiliate?**
The order needs the affiliate's referral code in `user_metadata` as `mp_ref` (or `referral_code`). With **Macropay-hosted checkout this is automatic** — the tracking link's `mp_ref` cookie is read at checkout and attached for you. If you run your own site/checkout, add the [`affiliate.js` snippet](https://app.macropay.ai/affiliate.js) or set `metadata.mp_ref` via the API. Attribution is finalized at `order.paid`.
**A percentage commission is a percentage of what amount?**
The **net amount** — the subtotal minus any discount, excluding tax. Because Macropay is the merchant of record, tax is remitted on your behalf and is never part of the commission base.
**Can I set a different rate for one affiliate?**
Yes. Open the affiliate in the **Affiliates** tab and add a per-affiliate override for commission type, rate, fixed amount, or recurring terms. The override applies only to that affiliate and takes precedence over the program defaults.
**Do affiliates earn on subscription renewals?**
Only if you enable **Recurring** on the program (or as a per-affiliate override). When on, commission accrues on each paid invoice up to the optional **recurring cap**; when off, only the first paid sale earns.
**How long after a click can a sale still be attributed?**
For as long as your **cookie window** allows. The `mp_ref` cookie is set when the visitor passes through the tracking link, and a matching order within that window is credited to the affiliate.
## Next steps
How the affiliate program fits together, end to end.
Review accrued commissions and pay your partners.
# Agents
Source: https://docs.macropay.ai/features/agents/introduction
Treat every AI agent as a billable unit — bill by usage, activity, or outcome, and prove its margin and ROI with a certified value receipt.
Macropay treats an AI agent as a first-class billable unit. You register the agent once, then bill it three ways — by **usage** (LLM tokens), by **activity** (actions it takes), or by **outcome** (results it delivers) — and Macropay attributes every cost to it, computes its **agentic margin** (revenue vs. AI cost), and issues a **value receipt** that certifies the ROI behind each charge.
Agent billing builds on the same meter, product, and Merchant-of-Record engine as the rest of Macropay. You get tax, dunning, and dispute handling for free — agents just add the attribution, margin, and ROI layer on top.
## What is an agent
An agent is a named entity in your organization that work and money attach to. You create one with `POST /v1/agents`, giving it a `name`, an optional `external_id` (your own identifier, for idempotent linking), and an optional `description`. Each agent carries a `status`:
| Status | Meaning |
| ---------- | ----------------------------------------------------------- |
| `active` | Billing and usage are live for this agent. |
| `disabled` | Billing and usage are paused — the agent stays on record. |
| `archived` | Retired from active use; history is retained for reporting. |
```bash Create an agent theme={null}
curl https://api.macropay.ai/v1/agents \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Support Copilot",
"external_id": "agent_support_v2",
"description": "Resolves tier-1 support tickets autonomously"
}'
```
Once the agent exists, every token it burns, every action it takes, and every outcome it lands can be tagged with its ID — that attribution is what powers margin and ROI.
## The three billing models
The same agent can be billed on more than one axis. Pick the model that matches how your customer perceives value.
Bill on LLM tokens consumed. Best when cost scales directly with model spend and customers expect metered pricing.
Bill per action the agent takes — a message sent, a tool call, a document processed. Best for predictable per-task pricing.
Bill per result delivered — a resolved ticket, a booked meeting, a closed deal. Best when you sell the outcome, not the effort.
### When to use each
Model You bill on Reach for it when Reported via
Usage
Tokens (input + output)
Your cost tracks the model meter and customers think in tokens or credits.
AI proxy at /ai/v1
Activity
Actions taken
The unit of work is discrete and uniform, and value is roughly per-action.
Activity signals
Outcome
Results delivered
You can verify the result and customers will pay for the win, not the attempt.
Outcome signals
Usage billing is the natural floor (it covers your model cost), and outcome billing is the natural ceiling (it captures the value you create). Many teams run usage as COGS internally while charging the customer on outcomes — and use the [value receipt](/features/agents/value-receipts) to justify the gap.
## Building blocks
Report what an agent did (activity) and achieved (outcome) via `POST /v1/signals`. These feed both activity/outcome billing and ROI.
A certified ROI statement per agent — time saved, cost avoided, revenue generated, risk avoided — split into verified vs. reported value.
Billed revenue minus AI cost (COGS) per agent, with a margin floor that flags agents quietly running you into the red.
Drop-in Python and TypeScript instrumentation — bind an agent once, then emit signals, record tool cost, and read margin in one-liners.
### How costs get attributed
Two cost sources fold into an agent's margin automatically:
* **LLM cost** — when an agent's calls route through the Macropay [AI proxy](/features/llm-inference) at `/ai/v1` using an agent-bound proxy key, every request's token cost is captured and attributed with zero extra code.
* **Non-LLM cost (COGS)** — tool calls, third-party APIs, and human-in-the-loop time are reported with `POST /v1/agents/{id}/costs` (or the SDK's `record_cost`), so margin reflects true cost of delivery.
All of this lands in [Cost Insights](/features/cost-insights/introduction) alongside revenue, so you see real per-agent and per-customer profit — not just top-line usage.
## Get started
Register the agent with `POST /v1/agents`. Keep the returned `id` (or set your own `external_id`) — it's the key everything attributes to.
Route LLM calls through the [AI proxy](/features/llm-inference) with an agent-bound proxy key for automatic token cost, then emit [signals](/features/agents/signals) for activity and outcomes. To bill on activity or outcome, provision a signal-scoped meter and price with `POST /v1/agents/billing-models`.
```bash Report an outcome theme={null}
curl https://api.macropay.ai/v1/signals \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"signals": [{
"event_name": "meeting_booked",
"kind": "outcome",
"agent_id": "",
"external_customer_id": "acct_8842",
"data": { "value_cents": 20000 },
"external_id": "mtg_2026_0603_001"
}]
}'
```
Read agentic margin with `GET /v1/agents/{id}/margin` and the certified ROI with `GET /v1/agents/{id}/value-receipt`, or explore both in the Macropay dashboard.
```bash Read the value receipt theme={null}
curl "https://api.macropay.ai/v1/agents//value-receipt" \
-H "Authorization: Bearer "
```
Signals require a customer (`customer_id` or `external_customer_id`) — billing always rolls up to an account. Outcomes are idempotent on `external_id`, so retries won't double-count.
## FAQ
**When should I bill on outcomes vs. tokens?**
Bill on tokens when your cost scales with model spend and the customer reasons in usage — it's the safest floor because it always covers COGS. Bill on outcomes when you can verify the result (a booked meeting, a resolved ticket, a closed deal) and the customer is buying that result rather than the effort behind it. Outcome pricing captures the value you create instead of just your cost; the [value receipt](/features/agents/value-receipts) is what lets you defend the higher price.
**Do agents require the AI proxy?**
No. The [AI proxy](/features/llm-inference) at `/ai/v1` is the easiest way to capture LLM cost — point an agent-bound proxy key at it and token cost is attributed automatically. But you can run agent billing without it: report non-LLM COGS with `POST /v1/agents/{id}/costs` and bill on activity or outcome [signals](/features/agents/signals) directly. The proxy is recommended, not mandatory.
**How is agentic margin computed?**
Margin is billed revenue minus AI cost (COGS) for an agent over a period, returned by `GET /v1/agents/{id}/margin` as `revenue_cents`, `cost_cents`, `margin_cents`, and `margin_pct`, broken down `by_model`. Revenue comes from the agent's metered charges; cost comes from proxy token spend plus any recorded non-LLM COGS. A `low_margin` flag trips when `margin_pct` falls below the configured `margin_floor_pct`. See [Agentic margin](/features/agents/margin).
**What is a value receipt?**
A value receipt is a certified ROI statement Macropay computes per agent from its signals and cost — covering human-value-equivalent, time saved, cost savings, revenue generated, and risk avoided. You supply the assumptions (e.g. minutes saved per action, an hourly rate) and the engine produces the numbers, splitting outcome value into verified (confirmed by a trusted source) and reported (still self-attested). See [Value receipts](/features/agents/value-receipts).
**Can one agent use more than one billing model?**
Yes. A single agent can bill usage on tokens, activity on actions, and outcomes on results at the same time — each axis is just a different meter and price scoped to the same `agent_id`. Margin and ROI net all of them together.
## Next steps
Resell models through one URL and capture token cost automatically.
See true per-agent and per-customer margin once cost sits next to revenue.
Wire a meter, attach a price, and turn usage into invoices end-to-end.
See how agent products put usage, activity, and outcome billing together.
# Agentic Margin
Source: https://docs.macropay.ai/features/agents/margin
See revenue vs. AI cost (COGS) per agent so you know which agents make money — captured automatically from every proxied model call.
Agentic margin is what each AI agent earns after the cost of running it. Macropay joins the revenue you bill against the model and tool spend it took to deliver, so you can tell a profitable agent from one quietly burning cash — per agent or across your whole organization.
Both sides of the equation are captured for you. When you route model calls through the [AI proxy](/features/llm-inference) (`/ai/v1`), every request writes an `ai.completion` event carrying the billed amount (revenue) and the upstream provider cost (COGS). Macropay rolls those up into a `MarginSummary` — no metering code, no manual joins.
Revenue here is the amount you bill the end customer (the proxy's marked-up
rate). Cost is what the upstream provider charged you. The difference is your
margin, sliced by model and flagged when it drops below a floor.
## Where the numbers come from
Each proxied call records billed amount and upstream model cost on the same
`ai.completion` event. Margin needs no extra reporting from you.
Tool calls, third-party APIs, and human-in-the-loop time can be recorded as
`agent.cost` events that reduce margin — see below.
Every summary includes `by_model[]`, so you can see exactly which model is
eating your spread.
A `low_margin` flag trips the moment an agent's margin falls under your
floor (default 20%), so thin or negative agents surface on their own.
## Read an agent's margin
Two endpoints, same `MarginSummary` shape. Both accept optional `since` and `until` ISO-8601 query params to scope a window.
| Endpoint | Returns |
| ---------------------------- | ----------------------------- |
| `GET /v1/agents/{id}/margin` | Margin for a single agent |
| `GET /v1/agents/margin` | Org rollup across every agent |
```bash Single agent theme={null}
curl https://api.macropay.ai/v1/agents/agt_42/margin \
-H "Authorization: Bearer "
```
```bash Org rollup theme={null}
curl "https://api.macropay.ai/v1/agents/margin?since=2026-05-01T00:00:00Z" \
-H "Authorization: Bearer "
```
### Response: `MarginSummary`
```json theme={null}
{
"agent_id": "agt_42",
"revenue_cents": 124050.0,
"cost_cents": 38200.0,
"margin_cents": 85850.0,
"margin_pct": 69.21,
"low_margin": false,
"margin_floor_pct": 20.0,
"by_model": [
{
"model": "claude-sonnet-4",
"revenue_cents": 98000.0,
"cost_cents": 28000.0,
"margin_cents": 70000.0,
"margin_pct": 71.43,
"count": 1820
},
{
"model": "gpt-4o-mini",
"revenue_cents": 26050.0,
"cost_cents": 10200.0,
"margin_cents": 15850.0,
"margin_pct": 60.84,
"count": 940
}
]
}
```
| Field | Meaning |
| ------------------ | ----------------------------------------------------------------------------- |
| `revenue_cents` | Total billed to customers (your marked-up rate) |
| `cost_cents` | Total COGS — upstream model spend plus any recorded `agent.cost` |
| `margin_cents` | `revenue_cents − cost_cents` |
| `margin_pct` | Margin as a percent of revenue; `0` when there's no revenue |
| `low_margin` | `true` when there is revenue **and** `margin_pct` is below `margin_floor_pct` |
| `margin_floor_pct` | The guardrail threshold (default `20`) |
| `by_model[]` | The same metrics broken out per model, sorted by revenue |
## The low-margin guardrail
`low_margin` is the single signal you alert on. It is `true` only when an agent has earned revenue *and* its `margin_pct` sits under `margin_floor_pct` — so an agent with no billed activity never trips it, and a profitable agent stays quiet. The floor defaults to **20%**.
Wire it into monitoring: poll the org rollup on a schedule, and if `low_margin` is `true` or any entry in `by_model[]` shows a thin spread, you've caught a pricing or model-choice problem before it shows up on a P\&L. A negative `margin_cents` means the agent is losing money on every run — usually a sign the upstream model costs more than you're charging.
A negative margin doesn't stop requests. Margin is reporting, not a budget
cap. To hard-stop spend, set a **budget limit** on the proxy key — see
[LLM inference](/features/llm-inference) — which returns `403` once the ceiling
is hit.
## Record non-LLM costs
LLM spend is captured automatically, but agents also cost money in ways the proxy never sees: a paid search API, a geocoding lookup, a human reviewer. Report those as COGS with `POST /v1/agents/{id}/costs` and they fold straight into the agent's margin as an `agent.cost` event — adding cost without adding revenue.
A `customer_id` or `external_customer_id` is required so the cost attributes to the right account. Pass `external_id` as an idempotency key — it dedupes on `(organization, external_id)`, so retries never double-count.
```bash theme={null}
curl https://api.macropay.ai/v1/agents/agt_42/costs \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"amount_cents": 250,
"currency": "usd",
"description": "serp_api",
"external_customer_id": "acct_8842",
"external_id": "run_91f3-serp"
}'
```
```json theme={null}
{ "status": "recorded", "agent_id": "agt_42" }
```
| Field | Required | Notes |
| -------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| `amount_cents` | Yes | The cost incurred, in cents. Must be greater than 0. |
| `currency` | No | ISO currency code; defaults to `usd`. |
| `description` | No | What the cost was for, e.g. `serp_api`. Surfaces as the "model" in `by_model[]`. |
| `customer_id` / `external_customer_id` | One of | Attributes the cost to a customer. |
| `external_id` | No | Idempotency key — dedupes on `(org, external_id)`. |
Using the Macropay SDK? `cost.record(...)` calls this endpoint for you, and
the tool-cost helpers can capture vendor spend automatically as your agent
runs — no manual POST per tool call.
## FAQ
**How is margin calculated?**
Margin is billed revenue minus AI cost (COGS). Revenue and upstream model cost both come from the `ai.completion` events the proxy records on every call; any `agent.cost` events you report add to the cost side. Macropay sums them into `revenue_cents`, `cost_cents`, and `margin_cents`, with `margin_pct` as margin over revenue. The breakdown per model lives in `by_model[]`.
**How do I record non-LLM costs?**
Call `POST /v1/agents/{id}/costs` with `amount_cents`, a `currency`, an optional `description`, a `customer_id` or `external_customer_id`, and an `external_id` for idempotency. It's stored as an `agent.cost` event that reduces margin without adding revenue. The SDK's `cost.record()` wraps this endpoint.
**What triggers the low-margin flag?**
`low_margin` is `true` when an agent has earned revenue and its `margin_pct` is below `margin_floor_pct` (default 20%). An agent with no billed revenue never trips the flag, even if it has recorded costs.
**Is margin a spending limit?**
No. Margin is reporting only — it never blocks a request. To cap spend, set a budget limit on the proxy key in [LLM inference](/features/llm-inference); the proxy returns `403` once the ceiling is reached.
**How does this relate to Cost Insights?**
Agentic margin is the agent-scoped view of the same revenue-vs-cost ledger. For per-customer profit, LTV, and cost-annotated events beyond agents, see [Cost Insights](/features/cost-insights/introduction).
## Next steps
Send model traffic through `/ai/v1` so revenue and cost are captured for you.
See true profit, margin, and LTV per customer across your whole business.
# Agent SDK
Source: https://docs.macropay.ai/features/agents/sdk
Instrument an AI agent in a few lines of Python or TypeScript — route LLM calls through the proxy, report outcomes, and bill on results.
The Agent SDK is the drop-in instrumentation layer for agent billing: point your existing OpenAI client at the Macropay proxy to capture model cost automatically, then report what the agent did and achieved so margin and ROI compute themselves. It ships for both **Python** (`macropay`) and **TypeScript** (`@macropay/sdk`) with a matching API.
Instrumentation does three things: **captures LLM cost** (via the proxy base URL), **reports signals** (activity + outcome), and **records non-LLM cost** (tools, third-party APIs, human-in-the-loop). Those three feeds power [Agentic Margin](/features/agents/margin) and [value receipts](/features/agents/value-receipts).
## Install
```bash Python theme={null}
pip install macropay
```
```bash TypeScript theme={null}
npm install @macropay/sdk openai
```
## Capture LLM cost with one base URL
The Macropay AI proxy is OpenAI-compatible and mounted at `/ai/v1`. Point any OpenAI client at it, authenticate with an **agent-bound proxy key**, and every call's cost is attributed to that agent — no wrapper code. The SDK exposes a helper so you never hand-write the URL.
```python Python theme={null}
from openai import OpenAI
from macropay.agents import openai_base_url
oai = OpenAI(
api_key="macropay_proxy_...", # agent-bound proxy key
base_url=openai_base_url(), # https://api.macropay.ai/ai/v1
)
```
```ts TypeScript theme={null}
import OpenAI from "openai";
import { openaiBaseUrl } from "@macropay/sdk";
const oai = new OpenAI({
apiKey: "macropay_proxy_...", // agent-bound proxy key
baseURL: openaiBaseUrl(), // https://api.macropay.ai/ai/v1
});
```
`openai_base_url()` / `openaiBaseUrl()` default to production. Pass `sandbox=True` (Python) or `{ sandbox: true }` (TypeScript) to target the sandbox, or pass an explicit base URL to override. See [LLM inference](/features/llm-inference) for the full proxy reference.
## AgentInstrumentation
`AgentInstrumentation` binds one agent (and optionally one customer) once, so every signal and cost call is a one-liner. It wraps the low-level Macropay client.
```python theme={null}
from macropay import Macropay
from macropay.agents import AgentInstrumentation
client = Macropay(api_key="macropay_sk_...")
agent = AgentInstrumentation(
client,
agent_id="agt_sdr",
customer_id="cus_123", # or external_customer_id="acme-co"
)
```
| Method | Purpose |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `activity(event_name, data=None, external_id=None)` | Report an action the agent took (`message_sent`, `tool_call`). |
| `outcome(event_name, value_cents=None, verified=False, data=None, external_id=None)` | Report a result (`meeting_booked`, `deal_won`). `value_cents` drives ROI. |
| `record_cost(amount_cents, description=None, currency="usd", external_id=None)` | Record non-LLM COGS so margin reflects true cost. |
| `margin(**params)` | Fetch this agent's current Agentic Margin. |
| `value_receipt(**assumptions)` | Fetch this agent's ROI value receipt. |
| `track_tool(name, cost_cents=...)` | Async context manager — auto-records cost **on success only**. |
| `track_cost(name, cost_cents=...)` | Decorator — auto-records cost **on success only**. |
All methods are `async`. Pass `external_id` to dedupe — outcomes are idempotent on it.
```ts theme={null}
import { Macropay, AgentInstrumentation } from "@macropay/sdk";
const client = new Macropay({ apiKey: "macropay_sk_..." });
const agent = new AgentInstrumentation(client, "agt_sdr", {
customerId: "cus_123", // or externalCustomerId: "acme-co"
});
```
| Method | Purpose |
| ---------------------------------------------------------------- | ------------------------------------------------------- |
| `activity(eventName, { data, externalId })` | Report an action the agent took. |
| `outcome(eventName, { valueCents, verified, data, externalId })` | Report a result; `valueCents` drives ROI. |
| `recordCost(amountCents, { description, currency, externalId })` | Record non-LLM COGS. |
| `setupBilling({ kind, pricePerUnitCents, ... })` | Provision a meter + product for this agent in one call. |
| `margin(params)` | Fetch this agent's current Agentic Margin. |
| `valueReceipt(params)` | Fetch this agent's ROI value receipt. |
All methods return promises. Pass `externalId` to dedupe — outcomes are idempotent on it.
## Report what the agent did and achieved
[Signals](/features/agents/signals) are the heart of agent billing. **Activity** signals are what the agent *did*; **outcome** signals are what it *achieved* — and an outcome carries the value it produced.
```python Python theme={null}
# Something the agent did
await agent.activity("message_sent", data={"channel": "email"})
# Something the agent achieved, worth $200, confirmed by your CRM
await agent.outcome(
"meeting_booked",
value_cents=20000,
verified=True,
external_id="hubspot-deal-8841", # idempotent dedupe key
)
```
```ts TypeScript theme={null}
// Something the agent did
await agent.activity("message_sent", { data: { channel: "email" } });
// Something the agent achieved, worth $200, confirmed by your CRM
await agent.outcome("meeting_booked", {
valueCents: 20000,
verified: true,
externalId: "hubspot-deal-8841", // idempotent dedupe key
});
```
Set `verified` when an outcome is confirmed by a trusted source — a CRM webhook, a payment, a signed contract — rather than self-reported by the agent. The [value receipt](/features/agents/value-receipts) then splits **verified** from **unverified** value, exposing the trust gap to your customer.
## Record non-LLM cost
LLM cost is captured automatically through the proxy. Everything else — search APIs, enrichment, human review — you report with `record_cost` so [margin](/features/agents/margin) reflects true COGS.
```python Python theme={null}
await agent.record_cost(50, description="serp_api") # 50 cents
```
```ts TypeScript theme={null}
await agent.recordCost(50, { description: "serp_api" }); // 50 cents
```
### Automatic tool-cost capture (Python)
`track_tool` (an async context manager) and `track_cost` (a decorator) record the cost **and** emit a `tool.` activity signal — but **only when the call succeeds**. A tool call that raises was a failure you weren't charged for, so it never touches margin.
```python theme={null}
# As a context manager
async with agent.track_tool("serp_search", cost_cents=2):
results = await serp.search(query)
# As a decorator — cost_cents can be a number or a callable for usage pricing
@agent.track_cost("enrich", cost_cents=lambda rows: len(rows) * 1)
async def enrich(rows: list[dict]) -> list[dict]:
...
```
## End-to-end: route a call, post an outcome
A minimal SDR-style agent: the LLM call goes through the proxy (cost captured), then the booked meeting is reported as a verified outcome worth \$200.
```python Python theme={null}
from openai import OpenAI
from macropay import Macropay
from macropay.agents import openai_base_url, AgentInstrumentation
# 1. LLM client routed through the proxy → cost auto-attributed to the agent
oai = OpenAI(api_key="macropay_proxy_...", base_url=openai_base_url())
# 2. Instrumentation bound to the agent + customer
client = Macropay(api_key="macropay_sk_...")
agent = AgentInstrumentation(client, "agt_sdr", customer_id="cus_123")
# 3. Do the work
reply = oai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Draft a meeting request for Acme."}],
)
await agent.activity("message_sent", data={"channel": "email"})
# 4. Report the result — this is what you bill and prove ROI on
await agent.outcome(
"meeting_booked",
value_cents=20000,
verified=True,
external_id="hubspot-deal-8841",
)
```
```ts TypeScript theme={null}
import OpenAI from "openai";
import { Macropay, openaiBaseUrl, AgentInstrumentation } from "@macropay/sdk";
// 1. LLM client routed through the proxy → cost auto-attributed to the agent
const oai = new OpenAI({ apiKey: "macropay_proxy_...", baseURL: openaiBaseUrl() });
// 2. Instrumentation bound to the agent + customer
const client = new Macropay({ apiKey: "macropay_sk_..." });
const agent = new AgentInstrumentation(client, "agt_sdr", { customerId: "cus_123" });
// 3. Do the work
const reply = await oai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Draft a meeting request for Acme." }],
});
await agent.activity("message_sent", { data: { channel: "email" } });
// 4. Report the result — this is what you bill and prove ROI on
await agent.outcome("meeting_booked", {
valueCents: 20000,
verified: true,
externalId: "hubspot-deal-8841",
});
```
## Close the billing loop in one call
`setup_billing` / `setupBilling` provisions a signal-scoped meter for the agent **and** a product with a `metered_unit` price — so you skip the manual meter to price to product wiring. Bill per activity or per outcome.
```python Python theme={null}
await agent.setup_billing(
kind="outcome",
signal_name="meeting_booked",
price_per_unit_cents=500, # $5 per booked meeting
product_name="SDR — per meeting",
)
```
```ts TypeScript theme={null}
await agent.setupBilling({
kind: "outcome",
signalName: "meeting_booked",
pricePerUnitCents: 500, // $5 per booked meeting
productName: "SDR — per meeting",
});
```
Outcome-based pricing pairs naturally with verified outcomes: charge only for results your CRM confirmed. See [outcome-based billing in practice](/guides/ai-billing) for the full pattern.
## FAQ
**Do I need the SDK?**
No. The SDK is a thin convenience layer over the REST API — you can route LLM calls through the [proxy](/features/llm-inference) and post [signals](/features/agents/signals) with plain HTTP. The SDK just makes cost capture, signal reporting, and billing setup one-liners in Python and TypeScript.
**How do I bill by outcome with the SDK?**
Report results with `outcome(event_name, value_cents=...)`, then create an outcome meter and price. `setup_billing(kind="outcome", signal_name=..., price_per_unit_cents=...)` does both in one call, so each reported outcome bills your customer automatically. Mark outcomes `verified` to bill only on results a trusted source confirmed.
**Which languages are supported?**
Python (the `macropay` package) and TypeScript (`@macropay/sdk`) have full parity for agent instrumentation. Both expose `openai_base_url` / `openaiBaseUrl` and `AgentInstrumentation`. Any other language can use the [REST API](/api-reference) directly.
**Does the proxy add latency?**
The proxy forwards your request to the upstream provider and streams the response back; metering happens on the response, not in the critical path. You also get real-time usage and budget headers on every call — see [LLM inference](/features/llm-inference).
**How is the LLM cost attributed to the right agent?**
Through the proxy key. Mint a proxy key bound to the agent, set it as the OpenAI client's `api_key`, and every call through `openai_base_url()` records its token cost against that agent — feeding [Agentic Margin](/features/agents/margin) with zero extra code.
# Signals API
Source: https://docs.macropay.ai/features/agents/signals
Report what an AI agent did and achieved. Ingest activity and outcome signals to bill on results and prove agent value.
The Signals API is how an AI agent tells Macropay what it did and what it achieved. You `POST /v1/signals` with one or more signals; each becomes an event named `activity.` or `outcome.`, attributed to a specific agent — so you can bill on results and read each agent's value.
## Activity vs. outcome
Every signal has a `kind`. The distinction drives both how you read agent performance and how you bill.
An action the agent took — `message_sent`, `lead_enriched`, `document_processed`. Activities measure effort and throughput. They are not, on their own, billable results.
A billable result the agent produced — `meeting_booked`, `ticket_resolved`, `revenue_attributed`. Outcomes carry a `data.value_cents` and an optional `data.verified` flag, and they feed the value receipt.
A signal of kind `activity` named `message_sent` is recorded as the event `activity.message_sent`. A signal of kind `outcome` named `meeting_booked` is recorded as `outcome.meeting_booked`. Both are attributed to the `agent_id` you pass, so usage-based meters and per-agent reporting can filter on the agent that produced the work.
## Request shape
`POST https://api.macropay.ai/v1/signals` accepts a batch. Send as many signals as you like in one call.
```json theme={null}
{
"organization_id": "org_8af21c",
"signals": [
{
"event_name": "message_sent",
"kind": "activity",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "msg_2024-06-03T14:22:08Z_8f3a",
"data": {
"channel": "email",
"thread_id": "th_91c2"
}
},
{
"event_name": "meeting_booked",
"kind": "outcome",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "mtg_a1b2c3d4",
"data": {
"value_cents": 5000,
"verified": false,
"meeting_at": "2026-06-10T15:00:00Z"
}
}
]
}
```
| Field | Required | Description |
| ------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `event_name` | Yes | The signal name. Mapped to `activity.` or `outcome.`. |
| `kind` | Yes | `"activity"` or `"outcome"`. |
| `agent_id` | Yes | The agent the signal is attributed to. |
| `customer_id` **or** `external_customer_id` | Yes | One of the two is required. Use `external_customer_id` to reference your own customer key. |
| `external_id` | Recommended | Your unique idempotency key for the signal. Required in practice for outcomes — see [dedupe](#dedupe-outcomes-by-external_id). |
| `data` | No | Free-form JSON. For outcomes, set `data.value_cents` and optionally `data.verified`. |
A `customer_id` or `external_customer_id` is required on every signal — Macropay attributes the activity and value to a customer as well as an agent. Send one or the other, not both.
## Post an activity and an outcome
The example below reports one activity (`message_sent`) and one outcome (`meeting_booked`) in a single batch.
```bash cURL theme={null}
curl -X POST https://api.macropay.ai/v1/signals \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"organization_id": "org_8af21c",
"signals": [
{
"event_name": "message_sent",
"kind": "activity",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "msg_2024-06-03T14:22:08Z_8f3a",
"data": { "channel": "email" }
},
{
"event_name": "meeting_booked",
"kind": "outcome",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "mtg_a1b2c3d4",
"data": { "value_cents": 5000, "verified": false }
}
]
}'
```
```python Python theme={null}
import os, requests
requests.post(
"https://api.macropay.ai/v1/signals",
headers={"Authorization": f"Bearer {os.environ['MACROPAY_ACCESS_TOKEN']}"},
json={
"organization_id": "org_8af21c",
"signals": [
{
"event_name": "message_sent",
"kind": "activity",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "msg_2024-06-03T14:22:08Z_8f3a",
"data": {"channel": "email"},
},
{
"event_name": "meeting_booked",
"kind": "outcome",
"agent_id": "agt_sdr_01",
"external_customer_id": "cus_acme_42",
"external_id": "mtg_a1b2c3d4",
"data": {"value_cents": 5000, "verified": False},
},
],
},
)
```
## Dedupe outcomes by external\_id
Outcomes are deduplicated by the pair `(organization, external_id)`. The first signal with a given `external_id` is recorded; later signals carrying the same `external_id` are ignored. This makes the endpoint safe to retry and protects you from double-billing.
Always set a stable, unique `external_id` on every outcome — derive it from the thing that happened (a meeting ID, a resolved ticket ID, an order ID), not from a timestamp or a random value generated per call. If you regenerate the `external_id` on retry, the same outcome will be counted twice.
Activities can share the dedupe behavior too, but the high-value guarantee is for outcomes, because outcomes are what get billed.
## Outcomes drive the value receipt
For an `outcome` signal, two fields in `data` matter most:
* **`data.value_cents`** — the monetary value of the result, in cents. This is the amount attributed to the agent and rolled up into its [value receipt](/features/agents/value-receipts).
* **`data.verified`** — an optional boolean. Set it `true` once the value is confirmed (the meeting actually happened, the revenue actually landed). Verified value is reported separately from reported value, so you can bill on confirmed results and watch the gap between what an agent claimed and what stuck.
Outcome value and the `verified` flag flow straight into the agent's value receipt, where reported value and verified value are split. To turn outcomes into invoices, point a usage-based [meter](/features/usage-based-billing/introduction) at the relevant `outcome.` event and attach a price.
Reporting a signal never bills a customer on its own. Signals are recorded as events; a [meter](/features/usage-based-billing/introduction) aggregates the matching events and a price turns the aggregate into an amount. Macropay then bills as the merchant of record, handling sales tax and VAT for you.
## FAQ
**What is the difference between activity and outcome?**
An activity is an action the agent took (for example `message_sent`), recorded as `activity.`. An outcome is a billable result the agent produced (for example `meeting_booked`), recorded as `outcome.` and carrying `data.value_cents`. Activities measure effort; outcomes measure value and are what you bill on.
**How do I avoid double-billing an outcome?**
Set a stable, unique `external_id` on each outcome, derived from the underlying event (a meeting ID or order ID), not a timestamp or per-call random value. Outcomes dedupe by `(organization, external_id)`, so the first signal wins and any retry with the same `external_id` is ignored — making the endpoint safe to call repeatedly.
**What is verified value?**
Verified value is outcome value you've confirmed actually occurred, marked with `data.verified: true`. Macropay reports verified value separately from reported (unverified) value on the agent's value receipt, so you can bill only on confirmed results and measure the gap between what an agent claimed and what was real.
## Next steps
See how reported and verified outcome value rolls up per agent.
Aggregate `outcome.` events and attach a price to bill on results.
# Value Receipts
Source: https://docs.macropay.ai/features/agents/value-receipts
Generate a certified ROI statement for any agent — human value equivalent, time saved, cost savings, revenue, and risk avoided, with a verified-value split.
A **value receipt** is a certified ROI statement for one agent over a date range. Call `GET /v1/agents/{id}/value-receipt` and Macropay turns the agent's logged [signals](/features/agents/signals) into five dollar figures — Human Value Equivalent, Time Saved, Cost Savings, Revenue Generated, and Risk Avoided — plus a Total Value, and splits that total into verified versus unverified value based on your outcome data.
It is the number you put in front of a customer, a CFO, or a renewal conversation: "this agent returned $X this month, and $Y of it is independently verified."
A value receipt does not move money. It is a read-only computation over your signals — pair it with [margin](/features/agents/margin) to see what the agent *cost* you, and you have both sides of the ROI ledger.
## What the receipt computes
Every figure is derived from two inputs: the count of **activity signals** in the window (`actions`) and a set of **assumptions** (minutes saved per action, a loaded hourly rate, and so on). Revenue is the exception — it comes straight from your **outcome** signals.
| Figure | Formula |
| -------------------------- | ------------------------------------------------------------------------ |
| **Human Value Equivalent** | `actions × (minutes_per_action / 60) × hourly_rate_cents` |
| **Time Saved (hours)** | `actions × minutes_per_action / 60` |
| **Cost Savings** | `actions × cost_avoided_per_action_cents` |
| **Revenue Generated** | sum of `value_cents` across outcome signals |
| **Risk Avoided** | `actions × risk_value_per_action_cents` |
| **Total Value** | Human Value Equivalent + Cost Savings + Revenue Generated + Risk Avoided |
`actions` is the **activity signal count** in the window. Time Saved is reported as hours for readability; it is not added into Total Value (it is the same labor already priced in Human Value Equivalent).
### Verified vs unverified value
Outcome signals carry a `data.verified` flag. When an outcome was confirmed against your system of record — a closed deal, a paid invoice, a resolved ticket — it is **verified**. The receipt sums verified outcomes into `verified_value_cents` and the rest into `unverified_value_cents`, so the reader can trust the hard number and discount the soft one.
Outcome value backed by a `data.verified: true` signal. This is the figure that survives audit.
Outcome value still self-reported by the agent and not yet confirmed. Useful for projections, weaker for proof.
## Assumptions and precedence
The labor, cost, and risk figures depend on assumptions you control. Each one resolves with a clear precedence:
A value passed on the request — `?hourly_rate_cents=7500` — wins for that call. Use it for one-off "what if our rate were higher" scenarios.
If no query param is given, Macropay falls back to the assumptions saved on the agent (`roi_assumptions`). Set these once so every receipt for the agent is consistent.
If neither is set, the documented default applies.
| Assumption | Default | Meaning |
| ------------------------------- | --------------- | ------------------------------------------ |
| `minutes_per_action` | `5` | Minutes a human would spend per action |
| `hourly_rate_cents` | `5000` (\$50/h) | Fully loaded labor rate |
| `cost_avoided_per_action_cents` | `0` | Direct cost the agent removes per action |
| `risk_value_per_action_cents` | `0` | Risk/compliance value protected per action |
Leave `cost_avoided_per_action_cents` and `risk_value_per_action_cents` at `0` until you have a defensible number — Total Value stays conservative and credible. Raise them once finance signs off on the assumption.
## Request a receipt
Pass a date range with `start` and `end`, and override any assumptions inline. Authenticate with a secret key.
```bash curl theme={null}
curl -G "https://api.macropay.ai/v1/agents/agt_4f2c91/value-receipt" \
-H "Authorization: Bearer " \
--data-urlencode "start=2026-05-01" \
--data-urlencode "end=2026-05-31" \
--data-urlencode "minutes_per_action=8" \
--data-urlencode "hourly_rate_cents=7500" \
--data-urlencode "cost_avoided_per_action_cents=120" \
--data-urlencode "risk_value_per_action_cents=300"
```
```python Python theme={null}
import httpx
resp = httpx.get(
"https://api.macropay.ai/v1/agents/agt_4f2c91/value-receipt",
headers={"Authorization": "Bearer "},
params={
"start": "2026-05-01",
"end": "2026-05-31",
"minutes_per_action": 8,
"hourly_rate_cents": 7500,
"cost_avoided_per_action_cents": 120,
"risk_value_per_action_cents": 300,
},
)
print(resp.json())
```
```ts TypeScript theme={null}
const params = new URLSearchParams({
start: '2026-05-01',
end: '2026-05-31',
minutes_per_action: '8',
hourly_rate_cents: '7500',
cost_avoided_per_action_cents: '120',
risk_value_per_action_cents: '300',
})
const resp = await fetch(
`https://api.macropay.ai/v1/agents/agt_4f2c91/value-receipt?${params}`,
{ headers: { Authorization: 'Bearer ' } },
)
const receipt = await resp.json()
```
### Sample response
```json theme={null}
{
"agent_id": "agt_4f2c91",
"period": { "start": "2026-05-01", "end": "2026-05-31" },
"actions": 1240,
"assumptions": {
"minutes_per_action": 8,
"hourly_rate_cents": 7500,
"cost_avoided_per_action_cents": 120,
"risk_value_per_action_cents": 300
},
"human_value_equivalent_cents": 1240000,
"time_saved_hours": 165.33,
"cost_savings_cents": 148800,
"revenue_generated_cents": 920000,
"risk_avoided_cents": 372000,
"total_value_cents": 2680800,
"verified_value_cents": 2280800,
"unverified_value_cents": 400000,
"currency": "usd"
}
```
In the sample, `human_value_equivalent_cents` is `1240 × (8 / 60) × 7500 ≈ 1,240,000`. `total_value_cents` sums human value, cost savings, revenue, and risk avoided — `1,240,000 + 148,800 + 920,000 + 372,000 = 2,680,800`. Of that, `2,280,800` is verified; the remaining `400,000` of revenue is still self-reported.
## FAQ
**Where do the numbers come from?**
From the agent's logged [signals](/features/agents/signals). `actions` is the count of activity signals in the window; Revenue Generated is the summed `value_cents` of outcome signals. The labor, cost, and risk figures multiply that action count by your assumptions. Nothing is estimated by Macropay beyond the formulas above.
**How do I set my own assumptions?**
Three ways, in order of precedence: pass them as query params for a single call (highest), save them on the agent as `roi_assumptions` so every receipt uses them, or omit them and Macropay applies the defaults (`minutes_per_action=5`, `hourly_rate_cents=5000`, the rest `0`). A query param always overrides the stored value, which always overrides the default.
**What makes value verified?**
An outcome signal with `data.verified: true` — meaning the outcome was confirmed against your system of record rather than self-reported by the agent. Verified outcomes roll into `verified_value_cents`; everything else lands in `unverified_value_cents`. The split lets you show a hard, audit-ready number alongside an optimistic projection.
**Does requesting a receipt charge anyone?**
No. The endpoint is a read-only computation over existing signals. To see the cost side of ROI, use [margin](/features/agents/margin).
**Can I change the date range?**
Yes — `start` and `end` define the window, and `actions`, revenue, and every derived figure are recomputed for exactly that period.
# Analytics
Source: https://docs.macropay.ai/features/analytics
A zero-setup dashboard for revenue, conversion, cost, and agent margin — MRR and AI COGS computed for you, no BI tool or SQL required.
Most teams treat analytics as a second project: pipe events somewhere, model them, build charts, keep them in sync. Macropay skips all of that. The moment you take your first payment, your dashboard already knows your revenue, your subscription base, your checkout funnel, and — if you're billing for AI — what each customer costs you to serve.
And because Macropay is the [merchant of record](/merchant-of-record/introduction), the figures are already clean. Sales tax and VAT are collected and remitted on your behalf, disputes and refunds are reconciled for you, so what you see is the business you actually operate — not a gross number you still have to net down by hand.
Looking for a metric we don't expose yet? [Let us know what would help](mailto:support@macropay.ai) and we'll consider adding it.
## Scope every chart with three controls
The whole dashboard responds to the same three filters. Set them once and every chart on the page redraws.
| Control | Effect |
| ------------- | ------------------------------------------------------------------------- |
| **Period** | Chooses the X-axis bucket — hourly, daily, weekly, monthly, or yearly. |
| **Timeframe** | Sets the date range the dashboard reports over. |
| **Product** | Combines all products by default; pick one product or tier to isolate it. |
Mix them to answer specific questions. Want to know whether your *Scale* tier is accelerating without the rest of the catalog drowning it out? Set the period to *weekly*, the timeframe to *last 90 days*, and the product filter to *Scale*.
## Revenue and the funnel behind it
These metrics cover money earned and the checkout flow that produces it.
| Metric | Definition |
| ----------------------------------- | -------------------------------------------------- |
| **Revenue** | Gross revenue earned, before fees. |
| **Orders** | Count of product sales and subscription payments. |
| **Average Order Value (AOV)** | Revenue ÷ orders. |
| **One-Time Products** | Number of one-time products sold. |
| **One-Time Products Revenue** | Revenue from one-time product sales. |
| **New Subscriptions** | Subscriptions started in the period. |
| **New Subscription Revenue** | Revenue from those new subscriptions. |
| **Renewed Subscriptions** | Subscriptions that renewed in the period. |
| **Renewed Subscription Revenue** | Revenue from renewals. |
| **Active Subscriptions** | New + renewed subscriptions currently active. |
| **Monthly Recurring Revenue (MRR)** | Recurring revenue across all active subscriptions. |
| **Checkouts** | Number of checkouts created. |
| **Succeeded Checkouts** | Checkouts that became an order or subscription. |
| **Checkouts Conversion Rate** | Succeeded checkouts ÷ checkouts created. |
Revenue is reported **before** Macropay's fees and gross of sales tax/VAT. As merchant of record we collect and remit that tax for you — it never enters your revenue line, and it never becomes your liability.
## Cost and margin, side by side with revenue
Profitability shouldn't live in a separate spreadsheet. Macropay tracks the cost of serving each product alongside what you charged for it, so margin is something you read off the same page as revenue. For AI and agent products — where the cost of a sale is real model spend that moves with usage — this is where the dashboard earns its keep.
| Metric | Definition |
| -------------------------------- | --------------------------------- |
| **Costs** | Total costs incurred. |
| **Cumulative Costs** | Running total of costs over time. |
| **Cost Per User** | Average cost per active user. |
| **Gross Margin** | Revenue − costs. |
| **Gross Margin Percentage** | Gross margin ÷ revenue. |
| **Net Cashflow** | Revenue − costs. |
| **Monthly Recurring Cost (MRC)** | Costs ÷ active subscriptions. |
| **Return on Investment (ROI)** | (Revenue − costs) ÷ costs. |
### Agentic margin for AI products
Bill with [usage-based pricing](/features/usage-based-billing/introduction) or run [agent billing](/features/usage-based-billing/introduction) and these cost metrics become your **agentic margin**: the revenue you booked measured against the AI cost of goods sold for each agent. You don't have to report any of that spend by hand — Macropay captures it as it happens:
* The [AI proxy](/features/llm-inference) at `/ai/v1` records token cost on every request it forwards, so the model spend behind each call is attributed automatically.
* Cost events and cost traces tie that spend back to the customer or agent that triggered it, feeding **Gross Margin** and **Cost Per User** with real numbers.
The payoff is early warning. When a power user runs a long-context agent loop a hundred times a day on a flat-rate plan, their serving cost climbs while their price doesn't — and you see the margin compress on the dashboard before it quietly erodes the quarter.
Pair the **Product** filter with the cost metrics to compare margin tier by tier. A usage-metered plan and a flat-rate plan can post identical revenue while behaving completely differently once you account for cost.
# Credits Benefit
Source: https://docs.macropay.ai/features/benefits/credits
Grant prepaid usage credits that top up a customer's meter balance automatically
Prepaid credits are the cleanest way to fund metered usage up front — perfect for AI products where every token, request, or agent action draws down a balance. The Credits benefit grants a fixed number of units to a customer's [Usage Meter](/features/usage-based-billing/meters) balance, automatically, every time they buy or renew. No invoices to chase, no surprise overage bills.
## When credits are granted
Attach the Credits benefit to any [product](/features/usage-based-billing/credits) and Macropay tops up the balance for you. The timing depends on the product type:
| Product type | When units are granted |
| ---------------- | ------------------------------------------------------- |
| **Subscription** | At the start of every billing cycle — monthly or yearly |
| **One-time** | Once, at the moment of purchase |
For example, a "Pro" subscription that grants `50,000` inference credits per month gives the customer a fresh 50,000-unit balance on each renewal. A "Starter pack" one-time product that grants `10,000` credits hands them over a single time at checkout.
Want a credits-only product with zero risk of overage? Skip the metered price
entirely. With no price attached to the meter, billing never triggers — the
customer simply spends down what you granted. See
[Credits for usage-based billing](/features/usage-based-billing/credits).
## Rollover unused credits
By default, leftover credits expire when the cycle ends. Enable **Rollover unused credits** to carry the unused balance forward instead: whatever a customer doesn't spend this cycle is added on top of next cycle's grant. Toggle the checkbox when you create or edit the benefit.
Rollover changes apply only to credits issued **after** the change. Credits
already on a customer's balance keep the rollover behavior they were granted
with.
## Reading the balance in your app
To gate features or show a usage widget, read the customer's current meter balance. The simplest path is [Customer State](/integrate/customer-state), which returns the full customer object including the balance of each active meter. For a single meter, query the [Customer Meters API](/api-reference/customer-meters/list) directly.
Macropay tracks the balance but does not block usage when it hits zero —
if there's a metered price, the overage is billed; if there isn't, spending
simply continues. Enforce your own limits wherever you need a hard stop.
## Why prepaid credits fit AI billing
Because Macropay is the [Merchant of Record](/features/usage-based-billing/introduction), every credit purchase is sold by us — we collect and remit sales tax and VAT worldwide, and your tax liability stays capped at one flat fee. That removes a real headache for usage-based AI products selling into dozens of jurisdictions.
Credits also pair naturally with agent and AI workloads: fund a balance with one purchase, meter it down per token or per action, and grant a fresh allotment each cycle. Combined with [usage-based metering](/features/usage-based-billing/meters), it gives you prepaid AI billing without building a balance ledger yourself.
# Custom Benefit
Source: https://docs.macropay.ai/features/benefits/custom
Deliver a private note or build your own entitlement logic for paying customers
When none of the built-in benefits fit, the **Custom** benefit is your escape hatch. It does two jobs: it shows a private note that only paying customers can read, and it acts as a generic entitlement flag you can wire into your own integration logic.
Because Macropay is the merchant of record, the purchase that unlocks this benefit is already tax-compliant and dispute-managed on your behalf — you just decide what the customer receives.
## What it's good for
Reveal a secret link, a support address, or onboarding instructions only after payment clears.
Use it as a named flag your backend reads to grant bespoke access that the standard benefits don't cover.
## Private note
A custom benefit can carry a note that stays hidden until a customer has an active grant. Anything you'd only want a paying customer to see works well here:
* A private scheduling link (for example, a [Cal.com](http://Cal.com) booking URL)
* A dedicated support email or priority inbox
* A coupon code, invite, or early-access instruction
* A short welcome message specific to the product they bought
The note renders for the customer in their [customer portal](/features/customer-portal) once the grant is active, and disappears if the entitlement lapses.
## Custom entitlements for integrations
Each custom benefit you create is distinct, so you can attach several to different products and tell them apart in code. When a customer is granted (or loses) a benefit, Macropay emits webhook events you can act on.
```ts theme={null}
// Listen for benefit grants and unlock your own feature flags.
// See /api-reference/webhooks/benefit_grant.created for the payload shape.
app.post("/webhooks/macropay", (req, res) => {
const event = req.body;
if (event.type === "benefit_grant.created") {
const { customer_id, benefit_id } = event.data;
// Match against the custom benefit you configured, then
// grant whatever bespoke access this product implies.
grantBespokeAccess(customer_id, benefit_id);
}
if (event.type === "benefit_grant.revoked") {
const { customer_id, benefit_id } = event.data;
revokeBespokeAccess(customer_id, benefit_id);
}
res.sendStatus(200);
});
```
Pair this with [usage-based billing](/features/usage-based-billing/introduction) when the bespoke access is metered — for example, an AI agent whose access tier and per-event cost are both governed by the same purchase.
## Steps
In the dashboard, add a new benefit and choose **Custom**. Give it a clear internal name so you can recognize it in webhook payloads.
Add the private message — a link, support address, or instructions — that should appear only after purchase.
Add the benefit to any [product](/features/products) (one-time or subscription). Customers receive it automatically on a successful order.
Subscribe to `benefit_grant.created` and `benefit_grant.revoked` webhooks to drive your own custom integration logic.
Need a more specialized benefit instead? Macropay also ships [license keys](/features/benefits/license-keys), [file downloads](/features/benefits/file-downloads), [Discord access](/features/benefits/discord-access), [GitHub repository access](/features/benefits/github-access), and [prepaid credits](/features/benefits/credits).
# Automate Discord Invites & Roles
Source: https://docs.macropay.ai/features/benefits/discord-access
Grant Discord invites and tier-based roles automatically when customers buy or subscribe
Turn a purchase into a server invite. When a customer checks out, Macropay can drop them straight into your Discord with the right role attached — and pull access the moment a subscription lapses. No bots to babysit, no manual invite links, no spreadsheet of who paid for what.
This is delivered as a **benefit**: an entitlement you attach to any product. Because Macropay is the merchant of record on every sale, the same flow also remits sales tax/VAT worldwide and keeps that liability off your books — you just decide who gets which role.
## What you can do
* **Auto-invite** every buyer or subscriber into your server on payment
* **Map tiers to roles** — `Supporter` for the $5 plan, `Pro` for the $25 plan, `Lifetime` for one-time buyers
* **Connect more than one server** by creating multiple Discord benefits
* **Auto-revoke** when a subscription ends, so access tracks billing state automatically
## Connect your Discord server
Choose **Connect your Discord server**. You'll be sent to Discord to install the Macropay app on the server you want to manage.
Discord will ask you to grant the scopes the app needs. All three are required — see the table below for what each one does.
Back in Macropay, select which role this benefit hands out. That's the role a customer receives the instant they buy.
### Permissions explained
| Permission | Why Macropay needs it |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Manage Roles** | Reads your server's roles so you can choose which one to assign, then applies it to paying customers. |
| **Create Invite** | Generates the invite that adds a buyer to your server when they purchase the product or subscribe to a tier. |
| **Kick Members** | Removes members tied to this benefit when their access should end — for example, a canceled or expired subscription. |
The connected server is fixed once a benefit is created. Need to manage a second community? Create another Discord benefit and authorize the additional server.
## Attach the benefit to a product
Open the product you want gated and scroll to the benefits section of the **Edit Product** form. Toggle on your Discord benefit and save. From then on, anyone who completes checkout for that product is invited and assigned the role you chose.
Tier your community with one benefit per role. Create a `Pro` benefit and a `Founder` benefit, attach each to the matching subscription, and customers automatically land in the right place — upgrades and downgrades follow their billing state.
## How it fits the rest of Macropay
Discord access is one of several entitlements you can bundle with a product. Mix and match it with other [benefits](/features/benefits/introduction) — [license keys](/features/benefits/license-keys), [file downloads](/features/benefits/file-downloads), or [GitHub repository access](/features/benefits/github-access) — on a single subscription. Membership, downloads, and source access all unlock from one checkout, and every order is tax-compliant out of the box because Macropay is the seller of record.
# Automate Customer File Downloads
Source: https://docs.macropay.ai/features/benefits/file-downloads
Attach downloadable files to any product and let Macropay deliver secure, signed URLs to every buyer automatically
Ship an ebook, a font pack, a desktop binary, or a 9GB model checkpoint, and Macropay turns it into a customer entitlement. Attach files to a product as a **File Downloads benefit** and every buyer or active subscriber gets a private, signed URL the moment their order clears — no storage to host, no download gating to build, no S3 to babysit.
And because Macropay is the **merchant of record**, the sale that unlocks the file is already tax-handled: we calculate and remit sales tax / VAT worldwide and sit on the customer's statement as the seller, so you ship bytes instead of chasing compliance.
## What you get
| Capability | Detail |
| ------------------ | ----------------------------------------------------------------------------------- |
| **File size** | Up to 10GB per file |
| **File types** | Anything — ebooks, design assets, ZIPs, installers, full applications |
| **Delivery** | Each customer receives a unique, signed download URL |
| **Integrity** | SHA-256 checksum generated per file, exposable to customers for verification |
| **Access control** | Grant or revoke per benefit; subscription cancellations remove access automatically |
A File Downloads benefit can hold multiple files. Buyers of the product receive every enabled file in the benefit as a single bundle of downloads.
## Create a File Downloads benefit
Go to **Benefits** in the Dashboard sidebar.
Click **+ Add Benefit**.
Choose **File Downloads** as the **Type**.
Drag and drop files onto the dropzone (`Feed me some bytes`), or click it to open a file browser. Uploads stream straight into our storage as you add them.
Attach the benefit to any product — one-time or subscription — and it's live. Every new order grants the files; cancelling a subscription revokes them.
## Manage the files in a benefit
Click the filename to edit it inline. The new name is what customers see on their download.
Drag and drop files into the order you want them presented to customers.
Open the contextual menu (the dots) and choose **Copy SHA-256 Checksum**. Publish it alongside your release so customers can confirm the bytes they downloaded match what you shipped.
Open the contextual menu and choose to disable the file. This is the safe way to retire a file without destroying it — see access rules below.
Open the contextual menu and choose **Delete**.
**Deleting a file is permanent — and it cuts off current customers too.** A delete removes the file from Macropay and our S3 storage entirely (only the metadata is retained). Active subscribers and past buyers lose access immediately. If you only want to stop *new* grants, disable the file instead.
## How access changes propagate
Enabling, disabling, adding, and deleting files each behave differently for people who already own the benefit. Here's the full matrix:
| Action | Existing customers | New customers |
| ----------------------- | ----------------------- | ---------------- |
| **Disable a file** | Keep access to it | Don't receive it |
| **Enable / add a file** | Granted retroactively | Receive it |
| **Delete a file** | Lose access immediately | Never receive it |
**Disabling preserves legacy access on purpose.** Someone who bought before you disabled a file keeps their copy — useful for grandfathering older releases while you steer new buyers to the current version.
**Re-enabling or adding files is retroactive.** The moment you add a new file or re-enable an old one, *every* current customer and subscriber with the benefit is granted access. Treat the benefit as the live source of truth for what buyers can download — shipping a v2 asset is as simple as uploading it.
## Pair downloads with usage or agent billing
File downloads cover the "deliver the artifact" half of a product. For products where value accrues *after* delivery — an agent customers run, an API they call, a model they query — combine the download with [usage-based billing](/features/usage-based-billing/introduction) so the file is the on-ramp and metered usage is the revenue.
A few patterns that work well:
* **SDK or CLI + metered API** — ship the binary as a download, then meter calls against the [AI proxy](/features/llm-inference) or your own ingested events.
* **Agent distribution** — hand over an agent bundle as a file, then bill its real work via the Signals API and attach [value receipts](/features/usage-based-billing/introduction) that certify the outcomes it produced.
* **Templates + credits** — give away a starter file for free, then sell prepaid [credits](/features/benefits/credits) the customer burns down as they use the tooling.
In every case Macropay stays the merchant of record across the bundle: one checkout, one tax-handled order, and the file plus any metered usage settle under the same flat fee.
# Automate Private GitHub Repo(s) Access
Source: https://docs.macropay.ai/features/benefits/github-access
Grant and revoke private GitHub repo access automatically on every purchase, subscription, and cancellation.
Ship code as a product. When a customer buys or subscribes, Macropay invites them as a collaborator on the private repositories you choose. When they cancel, access is pulled — no scripts, no spreadsheets, no manual invites to chase.
Because Macropay is the merchant of record, the same flow also collects and remits sales tax/VAT on the sale and keeps that liability off your books, so you can focus on the repo and not the paperwork.
## What you can sell
GitHub repository access is delivered as a **benefit** (entitlement) attached to any product — one-time or subscription:
* **Subscriptions** → access granted on first payment, revoked the moment the subscription ends.
* **One-time products** → buy once, keep collaborator access for life.
* **Multiple repos** → attach several GitHub benefits to one product to bundle repositories from your organization(s).
Common scenarios:
| Use case | How it works |
| ------------------------------ | ------------------------------------------------------------------ |
| Sponsorware | Gate a repo behind a sponsorship tier until a funding goal is hit. |
| Premium courses & starter kits | Sell the source alongside lessons, templates, and assets. |
| Open-core / self-hosting | Charge for the private enterprise repo on top of your OSS core. |
| Early access | Let paying users see feature branches before they land upstream. |
| Sponsor-only discussions | Keep issues and Discussions private to paying collaborators. |
Selling access to an AI agent or coding tool? Pair this benefit with [usage-based billing](/features/usage-based-billing/introduction) to charge for the repo *and* meter what the agent does once they're inside.
## Create the benefit
In the dashboard sidebar, go to **Benefits** and click **+ New Benefit**.
Set **Type** to **GitHub Repository Access**.
Click **Connect your GitHub Account** and install the dedicated Macropay GitHub App on the repositories you want to automate. After authorizing, you're returned to the benefit form, now connected.
Select the organization repository and the collaborator role to grant. Save, then attach the benefit to a product.
**Why connect GitHub a second time and install a separate app?**
Inviting collaborators requires permission to manage repository access — a sensitive scope. GitHub Apps can't request permissions progressively, so rather than ask *every* user for repo-management rights during normal GitHub login, this feature ships as a standalone app you install only on the repos you actually sell.
## Choose a repository
Pick the organization repository you want to automate invites for.
**Why organization repos only — not personal ones?**
GitHub doesn't offer granular collaborator permissions on personal repositories; every collaborator effectively gets write access, meaning they could push commits and cut releases. To keep your code safe, personal repos are off by default. Need it anyway? Reach out and we can enable it for your account.
## Choose a role
Select the access level granted to each collaborator:
| Role | Recommendation |
| -------- | ----------------------------------------------------------------- |
| **Read** | **Default — use this.** Read-only access covers \~99.9% of cases. |
| Triage | Advanced; manage issues/PRs without code access. |
| Write | Discouraged — grants push access. |
| Maintain | Discouraged — repo management without admin. |
| Admin | Strongly discouraged — full control. |
Anything above **Read** is discouraged unless you have a specific need and fully understand the impact. See the [GitHub role reference](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization#permissions-for-each-role) for the exact permissions behind each role.
Even with Read access, a collaborator can open a pull request ([details](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)).
**Paid GitHub organizations are billed per seat.** GitHub counts each collaborator as a seat and charges your plan accordingly. Either run a free organization plan, or price your product high enough to cover the GitHub seat cost you'll incur for every buyer.
# Automated Benefits
Source: https://docs.macropay.ai/features/benefits/introduction
Provision what a customer paid for the instant they pay — license keys, downloads, repo and Discord access, and prepaid credits — and revoke it the moment they stop.
Collecting a payment is the easy half. The hard half is wiring up everything that has to happen *after* the charge clears — minting a license key, inviting the buyer to a private repo, dropping a Discord role, topping up a usage balance — and then unwinding all of it cleanly when a subscription lapses or a refund lands.
**Benefits** are the entitlement engine that closes that gap. Define an entitlement once, attach it to any product or subscription tier, and Macropay grants access at purchase and revokes it at expiry — automatically. There are no fulfillment cron jobs to babysit and no `customer.subscription.deleted` webhooks to reconcile by hand.
And because Macropay is your [Merchant of Record](/merchant-of-record/introduction), one system does the whole job: it charges the card, remits sales tax and VAT on your behalf, limits your tax liability, *and* drives fulfillment. Billing and entitlement never drift apart, because they're the same flow.
## The benefit types you can attach
A benefit is a reusable resource — configure it once, then link it from as many products as you like.
Top up a customer's usage-meter balance. The backbone of prepaid, metered, and AI/agent billing.
Mint branded keys with your own format, expiry, and activation/validation rules.
Serve downloadable assets of any type — up to 10 GB per file.
Auto-invite paying customers to one or more private repositories.
Issue invites and assign server roles to customers and active subscribers.
Need something off this list? Reach for a **custom benefit**, and watch this space — more built-in types are landing over time.
Building an AI product, API, or autonomous agent? Pair a **Prepaid Credits** benefit with a [usage meter](/features/usage-based-billing/introduction) so each plan grants a balance that draws down as the customer consumes tokens, calls, or agent runs. That pairing is the foundation for [metered AI and agent-outcome billing](/features/usage-based-billing/introduction).
## Define once, reuse everywhere
The thing that makes benefits manageable at scale is that each one is a **standalone resource** — not a blob of config copied into every product. You attach the same benefit to one product or to fifty.
| Benefit as a shared resource | Entitlement copied per product |
| ----------------------------------------- | ------------------------------------- |
| Define once, attach to many products | Re-enter the same config every time |
| Edit in one place — it updates everywhere | Track down and patch every copy |
| One source of truth, no drift | Duplicate data that silently diverges |
| Clean to manage, clean for customers | Cluttered and error-prone |
**Concrete example.** You sell a developer tool through three SKUs: `Indie Monthly`, `Team Annual`, and a `Lifetime` license. A single "Insiders Discord role" benefit attaches to all three. Rename the role or swap the server, and every plan picks up the change at once — no per-product edits.
## Who has access at any moment
Macropay derives entitlements from the customer's *current* standing, so the access list is always correct without sweeping for stale grants.
| Customer state | Access? |
| ------------------------------------------------ | ------------------------------------ |
| Active subscriber on a tier carrying the benefit | Yes |
| Bought a one-time product carrying the benefit | Yes — for life |
| Subscription cancelled, expired, or refunded | No |
| Never purchased | No |
One-time purchases grant **lifetime** access to their benefits. Subscription benefits live and die with the subscription — when it lapses, the associated access (repo invite, Discord role, license key) is pulled automatically.
## Two ways to set one up
Add a benefit directly in the product form as you create or edit an offering — handy when you're standing up a new plan and want everything in one pass.
Manage every benefit in one place under **Benefits**, then attach existing ones to any product or tier. Best when a single entitlement spans several plans.
Once a benefit is attached, fulfillment runs itself for every future purchase, renewal, and cancellation. You stop shipping access by hand — Macropay grants and revokes it for you.
# Automate Customer License Key Management
Source: https://docs.macropay.ai/features/benefits/license-keys
Issue, validate, and revoke software license keys automatically — no key server to build, no sales tax to remit.
Shipping a desktop app, a CLI, a plugin, or a paid API? You need a way to prove a customer is entitled to use it. Macropay turns that into a checkbox: attach a **License Keys** benefit to any product, and every buyer is automatically issued a unique key the moment they pay — backed by a validation endpoint you call at runtime.
Because Macropay is your **merchant of record**, the same flow also handles global sales tax and VAT, absorbs PCI scope, and lists Macropay as the seller on the customer's statement. You sell access; we handle the financial and compliance plumbing behind it.
## What you get out of the box
| Capability | What it does |
| --------------------- | --------------------------------------------------------------------------------- |
| **Branded prefixes** | Ship keys like `ACME_` so they're recognizable in support tickets and logs |
| **Auto-expiration** | Keys can lapse `N` days, months, or years after purchase |
| **Activation limits** | Cap how many devices, seats, or IPs a key can run on |
| **Custom conditions** | Pin a key to a major version, MAC address, or any value you choose |
| **Usage quotas** | Meter consumption per key and increment it on each validation |
| **Auto-revocation** | A cancelled subscription instantly invalidates its key |
No key server to host, no expiry cron to babysit, no admin panel to build for self-service.
## Create the benefit
Select **Benefits** in the dashboard sidebar.
Click **+ New Benefit**.
Set the **Type** to **License Keys**, then configure the options below.
### Branded prefixes
Give keys a recognizable identity. A prefix of `ACME` produces keys such as `ACME_` — useful when a customer pastes one into a ticket and you want to spot it at a glance.
### Automatic expiration
Want access to lapse a fixed period after purchase? Set an expiry window and Macropay stamps `expires_at` on every issued key. Perpetual-with-updates, annual, or short trial licenses all map cleanly onto this.
### Activation limits
Require a key to be **activated** before it validates. This caps usage to a set number of instances — devices, IPs, CI runners, whatever you define — and lets customers self-manage their slots from the customer portal. No bespoke "manage your devices" screen to build on your side.
### Usage quotas
Reselling LLM tokens, render minutes, or API calls? Attach a usage quota to the key and increment it on each validation. The key carries both `usage` and `limit_usage`, so a single call tells you whether the customer still has headroom.
For consumption that varies in real time — per-token AI spend, metered API traffic — pair license keys with Macropay [usage-based billing](/features/usage-based-billing/introduction). Ingest events through meters, bill on what was actually consumed, and let the license key gate access while the meter handles the math. This is also how teams bill **AI agents** by usage, activity, or outcome.
## What the customer sees
The instant a purchase or subscription clears, the buyer is issued a unique key, visible on their purchases page in the customer portal. From there they can:
* View and copy the key
* Check the expiration date, if one is set
* See remaining usage, if a quota applies
* Deactivate activations, if activation limits are enabled
It's a self-serve surface you get for free — no portal to design, host, or maintain.
## Integrate the API
Wiring license keys into your app, library, or API takes two endpoints: an optional **activate** call and a **validate** call you run each session.
### Step 1 — Activate (only with activation limits)
If a benefit caps activation instances, register an activation before the key can validate. Each activation represents one device, seat, or environment counting against the limit.
No activation limit configured? Skip straight to validation.
```bash Terminal theme={null}
curl -X POST https://api.macropay.ai/v1/customer-portal/license-keys/activate \
-H "Content-Type: application/json" \
-d '{
"key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA",
"organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f",
"label": "macbook-pro-ci",
"conditions": { "major_version": 4 },
"meta": { "ip": "84.19.145.194" }
}'
```
The customer's license key, captured from input in your app.
Your organization ID, found in your dashboard settings.
A human-readable label for this activation, e.g. the machine or environment name.
Custom values to re-check on future validations — IP, MAC address, major version, and so on.
Arbitrary metadata to store alongside the activation.
#### Response (200 OK)
```json theme={null}
{
"id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe",
"license_key_id": "508176f7-065a-4b5d-b524-4e9c8a11ed63",
"label": "macbook-pro-ci",
"meta": {
"ip": "84.19.145.194"
},
"created_at": "2024-09-02T13:48:13.251621Z",
"modified_at": null,
"license_key": {
"id": "508176f7-065a-4b5d-b524-4e9c8a11ed63",
"organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f",
"user_id": "d910050c-be66-4ca0-b4cc-34fde514f227",
"benefit_id": "32a8eda4-56cf-4a94-8228-792d324a519e",
"key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA",
"display_key": "****-E304DA",
"status": "granted",
"limit_activations": 3,
"usage": 0,
"limit_usage": 100,
"validations": 0,
"last_validated_at": null,
"expires_at": "2026-08-30T08:40:34.769148Z"
}
}
```
Hold onto the returned activation `id` — you'll pass it as `activation_id` when you validate.
### Step 2 — Validate
Validate the key on each session of your app, library, or API through the
[validate endpoint](/api-reference/customer-portal/license-keys/validate). This is also where you increment usage when a quota applies.
```bash Terminal theme={null}
curl -X POST https://api.macropay.ai/v1/customer-portal/license-keys/validate \
-H "Content-Type: application/json" \
-d '{
"key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA",
"organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f",
"activation_id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe",
"conditions": { "major_version": 4 },
"increment_usage": 15
}'
```
The customer's license key, captured from input in your app.
Your organization ID, found in your dashboard settings.
The activation to validate against. Required when activation limits are enabled and in use (see Step 1).
When validating an activation, pass the same conditions object you registered it with.
Amount to add to the key's usage counter on this validation.
#### Response (200 OK)
```json theme={null}
{
"id": "508176f7-065a-4b5d-b524-4e9c8a11ed63",
"organization_id": "fda84e25-7b55-4d67-916d-60ead04ff61f",
"user_id": "d910050c-be66-4ca0-b4cc-34fde514f227",
"benefit_id": "32a8eda4-56cf-4a94-8228-792d324a519e",
"key": "1C285B2D-6CE6-4BC7-B8BE-ADB6A7E304DA",
"display_key": "****-E304DA",
"status": "granted",
"limit_activations": 3,
"usage": 15,
"limit_usage": 100,
"validations": 5,
"last_validated_at": "2024-09-02T13:57:00.977363Z",
"expires_at": "2026-08-30T08:40:34.769148Z",
"activation": {
"id": "b6724bc8-7ad9-4ca0-b143-7c896fcbb6fe",
"license_key_id": "508176f7-065a-4b5d-b524-4e9c8a11ed63",
"label": "macbook-pro-ci",
"meta": {
"ip": "84.19.145.194"
},
"created_at": "2024-09-02T13:48:13.251621Z",
"modified_at": null
}
}
```
A `granted` status means the key is live; check `expires_at` and the `usage` / `limit_usage` pair to decide whether to unlock your feature.
**Always send `organization_id`.** It scopes validation to your organization so a key issued by one Macropay organization can never be accepted by another. If you ship more than one license-key product, also check the `benefit_id` on the response to confirm the key belongs to the specific product you're gating — Macropay won't disambiguate that for you.
# Basis Theory checkout
Source: https://docs.macropay.ai/features/checkout/basis-theory
Card data is tokenized in a PCI DSS Level 1 vault and never touches your servers or Macropay's
When a customer types their card number into Macropay checkout, those digits
go straight into a sandboxed, cross-origin iframe and are tokenized inside a
**PCI DSS Level 1 compliant** vault. The card number, expiry, and CVC never
reach your origin, your backend, or Macropay's. What you and the acquirer see
is a non-sensitive token — never the raw PAN.
This is the same capture layer whether you ship a hosted
[Checkout Link](/features/checkout/links), drop in the
[embedded checkout](/features/checkout/embed), or build a custom UI on
[Checkout Sessions](/features/checkout/session). One iframe, one PCI
posture, three integration styles.
## Why tokenization in a vault matters
The card form lives in iframes served from a vault domain, not from your page.
Every keystroke is intercepted there, exchanged for a token, and only the token
crosses back to Macropay to authorize the charge. The practical payoff:
| Benefit | What it means for you |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Smallest PCI scope** | Sensitive card data never lands on your infrastructure, so your business qualifies for the lightest self-assessment tier. |
| **Nothing to leak** | No PAN in your logs, error tracker, analytics, or database — there's no card data on your side to spill in the first place. |
| **Merchant of Record** | Macropay charges the card as the seller of record on its own acquirer relationships. You don't bring a processor, pass your own PCI audit, or remit sales tax/VAT — we do, and we cap your tax liability. |
Because Macropay is the Merchant of Record, the PCI burden, tax remittance,
and chargeback handling all sit with us. Tokenization is the mechanism; MoR
is the coverage.
## What gets captured
The form renders three independent fields, each in its own sandboxed iframe:
1. **Card number** — with live brand detection. A row of brand icons on the
right edge lights up the detected network as the customer types.
2. **Expiry** (MM/YY)
3. **CVC**
Splitting the form into separate iframes keeps each field individually
focusable, tabbable, and validatable, while every field stays cross-origin
from your page. Tokenization runs as a single vault call on submit; Macropay
receives the resulting token and nothing else.
## Embedding the form
How much you do depends on which surface you choose:
Using a [Checkout Link](/features/checkout/links) or the
[embedded checkout](/features/checkout/embed)? The vault iframes are
already inside Macropay's hosted checkout. Nothing to wire up.
Building on [Checkout Sessions](/features/checkout/session)? Mount the
` ` React component from `@macropayments/checkout` and the
card form appears, fully orchestrated.
For a custom UI, the component handles iframe orchestration, inline
validation, and tokenization for you:
```tsx theme={null}
import { CheckoutForm } from '@macropayments/checkout/react'
router.push(`/receipt/${order.id}`)}
/>
```
You wire up `onConfirmed` to route the buyer after the charge clears. You
never receive — or have to protect — the raw card data.
## Theming the fields
Vault iframes don't accept style updates after they mount, so a theme switch
re-mounts them in the new color scheme. Macropay watches the
`` toggle through a debounced `MutationObserver` and does
the re-mount for you.
Macropay's hosted checkout (links + embed) renders **light-mode only** by
design. The card fields stay on a crisp light surface for every merchant,
even if you set `data-macropay-checkout-theme="dark"` on a link — the rest
of the page goes dark, the card form stays light. This keeps brand-icon
contrast and field affordances correct across every device and OS theme.
Building your own UI on Checkout Sessions? You can opt the card fields into a
dark theme — just know you own the contrast for the brand-icon row and the
field error states.
Inside the form, the visual states are tuned to sit pixel-aligned next to your
own inputs:
* **Interaction states** — focus, hover, error, and disabled are driven by
stateful classes on the wrapper, not `:focus` selectors. The iframe takes
focus, so the wrapper reflects it.
* **Borders and rings** — border color, ring width, and transitions match
Macropay's `Input` atom, so the card fields line up beside the Cardholder
Name input.
* **Brand row** — non-matching brands fade to 0.25 opacity while the detected
brand pops to full opacity.
## Supported card brands
The on-screen brand row shows live indicators for the four most common
networks: **Visa, Mastercard, American Express, and Discover.** Tokenization
also covers Diners Club, JCB, UnionPay, Maestro, Hipercard, and Elo — buyers
can pay with any of those even though the icon row stays focused on the major
four.
Which cards actually clear is governed by Macropay's acquirer and the buyer's
billing country, not by the iframe.
## Wallets
When the browser supports them, **Apple Pay** and **Google Pay** appear
**above** the card form. The wallet sheet skips the card iframes entirely:
Apple or Google hands back a tokenized payment method that Macropay relays
straight to the acquirer.
Wallets keep working even if the card vault is unreachable, because they
route through the browser rather than the iframe. For embedded checkout,
wallets need a one-time domain validation — see
[enabling wallet payment methods](/features/checkout/embed#enabling-wallet-payment-methods-apple-pay-google-pay-etc).
## FAQ
The visible chrome — border, padding, radius, shadow — is yours to re-skin
with Tailwind classes on the wrapper. The text *inside* the iframe (font
family, size, color) is limited to a small set of style tokens the vault
exposes, which Macropay tunes to match the wrapper. Open a support request
if you need a non-default token.
Split fields let each input be tabbable, focusable, and validated on its
own while keeping every keystroke cross-origin from your page. One field,
one iframe, one isolation boundary.
Yes. Each iframe is labelled and exposes its validation state to assistive
tech. Errors render below a field only after blur, and only when the field
is non-empty and invalid — no shouting while the customer is still typing.
The customer sees a "card capture is unavailable" message and the submit
button stays disabled. Wallets (Apple Pay / Google Pay) still work because
they go through the browser. Macropay monitors vault uptime and posts any
incidents to its status page.
# Embedded Checkout
Source: https://docs.macropay.ai/features/checkout/embed
Sell from inside your own site — no redirect, no PCI scope, tax handled for you
Keep buyers on your page through the entire purchase. The embedded checkout opens our payment form in an overlay on top of your site, so customers never bounce to a hosted page mid-funnel. Card data is captured and tokenized in our PCI DSS Level 1 compliant vault, and because Macropay is the Merchant of Record, sales tax and VAT are calculated, collected, and remitted on the order — you ship the integration, we own the tax and compliance.
There are two ways in:
* **Drop-in snippet** — paste two tags into any HTML page or CMS. Zero build step.
* **JavaScript library** — install the package for SPAs, event hooks, and programmatic control.
## Drop-in snippet
Works anywhere you can paste HTML — a landing page, a Framer site, a Webflow block, a docs page.
Start from a [checkout link](/features/checkout/links). Open the link in your dashboard and click **Copy Embed Code** to grab a ready-to-paste snippet, which looks like this:
```typescript theme={null}
Buy the Pro plan
```
The link renders inline and opens the checkout overlay on click. Style the trigger however you like — any element works as long as it carries the `data-macropay-checkout` attribute. Use `data-macropay-checkout-theme="dark"` to match a dark UI.
## JavaScript library
For a React, Vue, or other bundled app, injecting a raw `