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.

FieldNormalization
totalInteger. Smallest currency unit (cents for USD/EUR, whole yen for JPY). Must match what you charge.
currencyLowercase ISO 4217 (e.g. <code>usd</code>).
itemsArray sorted by <code>id</code> (missing id → empty string). Fields: id, name, quantity, unit_price.
taxInteger. Omitted → 0 in canonical form.
shippingInteger. Omitted → 0 in canonical form.
feesOptional 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.
metadataOptional. 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.

Canonical JSON (before HMAC)
{
  "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.

Canonical JSON — no merchant fees (fees: [] in HMAC)
{
  "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:

LocationName
Header (preferred)X-Ante-Signature
HeaderX-Ante-Cart-Signature
JSON bodysignature

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

Sign and verify (Node)
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);
Next.js App Router
// 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 seeLikely cause
Invalid cart signatureWrong credential: ante_sign_… required — not ante_sk_… or whsec_…
Invalid cart signatureSecret out of date after dashboard rotation — paste new value and redeploy
Invalid cart signatureCustom signer omitted fees: [] when cart has no fees — use @splitante/sdk ≥ 0.1.7
Invalid cart signatureCart edited after sign; currency not lowercased
Invalid cart signatureItems not sorted like canonicalizeCart (reimplement or use the SDK helper)
Invalid cart signaturetax/shipping/fees present in cart.total but missing or wrong in canonical form
403 secret key from browserSecret key in frontend code

More: Troubleshooting.