Cart signing
Sessions created with a publishable key (ante_pk_*) must include an HMAC proving your server saw the same cart the client sends. Without it, a browser could change prices or line items after you thought you had approved the total.
Copy this page as a setup prompt for your coding assistant.
Signing secret ≠ secret API key
Cart signing uses ante_sign_* (ANTE_SIGNING_SECRET), not the secret API key (ante_sk_*). The secret key is for headless REST and is never used in browser checkout. See Credentials.
Generate or rotate under Developers → Signing. Store it in server env as ANTE_SIGNING_SECRET. Do not ship it to the client, a mobile app binary, or a public repo.
Algorithm
HMAC-SHA256. Key: signing secret as UTF-8. Message: canonical JSON string of the cart (next section). Output: lowercase hex, 64 characters. Use createCartSignature from @splitante/sdk/signingso you stay byte-compatible with Ante's verifier.
Canonical cart
Two carts that look the same to a human can produce different JSON. Ante verifies against a normalized object. Implementation: canonicalizeCart() in @splitante/sdk/signing.
| Field | Normalization |
|---|---|
| total | Integer. Smallest currency unit (cents for USD/EUR, whole yen for JPY). Must match what you charge. |
| currency | Lowercase ISO 4217 (e.g. <code>usd</code>). |
| items | Array sorted by <code>id</code> (missing id → empty string). Fields: id, name, quantity, unit_price. |
| tax | Integer. Omitted → 0 in canonical form. |
| shipping | Integer. Omitted → 0 in canonical form. |
| fees | Optional on the request body. Canonical form always includes the array (empty → <code>fees: []</code>). Sorted by <code>id</code>. Fields: id, label, amount (cents). Included in <code>total</code> and HMAC. |
| metadata | Optional. Keys sorted A–Z. String values only. Use <code>order_ref</code> for your order id. |
Payout currency
currency is the checkout currency for the whole group — and the Stripe Connect transfer currency. Merchants can set a preferred payout currency in Business settings; when it differs from checkout, Ante withholds an FX fee on settlement (the transfer still posts in checkout currency). See Currencies & payouts.
{
"total": 12500,
"currency": "usd",
"items": [
{ "id": "sku_1", "name": "Ticket", "quantity": 2, "unit_price": 5000 }
],
"tax": 1000,
"shipping": 1000,
"fees": [
{ "id": "cleaning", "label": "Cleaning fee", "amount": 500 }
],
"metadata": { "order_ref": "ORD-1042" }
}Empty fees still canonicalize as fees: []
When the cart has no custom fees, you may omit fees on the JSON you send to POST /api/v1/sessions. Ante's verifier always includes "fees": [] in the HMAC payload. Custom signers that omit the fees key entirely fail with Invalid cart signature even when ANTE_SIGNING_SECRET is correct.
Use createCartSignature from @splitante/sdk/signing (package version ≥ 0.1.7) instead of hand-rolling JSON.stringify. See the example below for a cart with no merchant fees.
{
"total": 2444,
"currency": "usd",
"items": [
{ "id": "mug", "name": "Ceramic Mug", "quantity": 1, "unit_price": 1800 }
],
"tax": 144,
"shipping": 500,
"fees": [],
"metadata": { "order_ref": "ORD-ABC" }
}Sign the exact string canonicalizeCart returns. If the shopper changes quantity in the UI after you signed, the signature will not match. Sign again at click time.
Sending the signature
On POST /api/v1/sessions, attach the digest using one of:
| Location | Name |
|---|---|
| Header (preferred) | X-Ante-Signature |
| Header | X-Ante-Cart-Signature |
| JSON body | signature |
The browser SDK calls your getSignature hook and sets the header for you. You can also pass signature on createButton if you signed server-side earlier in the page load.
Server examples
import { createCartSignature, verifyCartSignature } from "@splitante/sdk/signing";
const signature = createCartSignature(cart, process.env.ANTE_SIGNING_SECRET!);
// Optional sanity check before returning to the client:
verifyCartSignature(cart, process.env.ANTE_SIGNING_SECRET!, signature);// app/api/cart/sign/route.ts
import { createCartSignature } from "@splitante/sdk/signing";
export async function POST(req: Request) {
const { cart } = await req.json();
if (!cart?.total || !cart?.currency || !Array.isArray(cart.items) || cart.items.length === 0) {
return Response.json({ error: "Invalid cart" }, { status: 400 });
}
const signature = createCartSignature(cart, process.env.ANTE_SIGNING_SECRET!);
return Response.json({ signature });
}Secret keys (headless)
Server requests with ante_sk_* may skip cart signing when headless_api_enabledis true on your merchant after Ante's technical review. Ante still validates cart totals (items + tax + shipping + fees) on every POST /api/v1/sessions request — unsigned headless calls cannot invent line items or fees. Browsers sending an Origin header with a secret key get 403. See REST API.
Signature failures
The API returns Invalid cart signature (X-Ante-Signature) with a details array listing common causes. That message does not always mean the signing secret is wrong — canonical JSON mismatch is equally common.
| What you see | Likely cause |
|---|---|
| Invalid cart signature | Wrong credential: ante_sign_… required — not ante_sk_… or whsec_… |
| Invalid cart signature | Secret out of date after dashboard rotation — paste new value and redeploy |
| Invalid cart signature | Custom signer omitted fees: [] when cart has no fees — use @splitante/sdk ≥ 0.1.7 |
| Invalid cart signature | Cart edited after sign; currency not lowercased |
| Invalid cart signature | Items not sorted like canonicalizeCart (reimplement or use the SDK helper) |
| Invalid cart signature | tax/shipping/fees present in cart.total but missing or wrong in canonical form |
| 403 secret key from browser | Secret key in frontend code |
More: Troubleshooting.