# Cardvera API Reference > Cardvera is pre-gateway card-testing prevention. Your server signs a short-lived > HS256 JWT describing the checkout, the browser loads the SDK with it and calls > `Cardvera.assess()`, your server verifies the returned verdict JWT before charging > the card, and then reports the PSP outcome to `POST /api/outcome`. This is the complete developer reference as a single Markdown file, for LLMs and coding agents. It is generated from the HTML docs (index.html); the JSON Schemas for every token and request body are appended at the end and are also served individually under `schemas/` next to this file. Base URL: `https://cardvera.dev` ## Introduction **Stop card testing before it reaches your gateway.** Cardvera scores every checkout at the edge — browser, transport, velocity, and merchant-attested context — and hands your server a signed **allow** or **block** before you authorize the card. This reference is everything you need to integrate it. - Base URL **https://cardvera.dev** - Auth **JWT · HS256** - No cardholder data - One SDK tag + two server calls ## How it works A Cardvera integration is one embedded SDK tag and two server-side calls. Everything is authenticated with a **JWT you sign using your Cardvera secret** (HS256) — one mechanism, any JWT library. Your secret never leaves your backend, and Cardvera never sees a card number. 1. **Mint a signed SDK token.** On each checkout render, your server signs a short-lived JWT whose claims describe the attempt — order id, flow, whether the shopper is signed in, amount. No API call; signing is local. 2. **Embed the SDK.** The browser loads ` ``` ## Authentication Everything is a **JWT signed with your Cardvera secret using HS256**, so you can use any JWT library in your language. Signing is local — there's no API call to obtain a token. ### Your keys You get two keys, a primary and a secondary, so you can rotate without downtime. Both are accepted for inbound validation; you sign with the primary. Keys carry a readable prefix for breach triage: | Prefix | Slot | |---|---| | `cv_sk_p_…` | Primary | | `cv_sk_s_…` | Secondary | > **Use the whole key as the secret.** Pass the key string **exactly as issued** — including the `cv_sk_p_` prefix — as the HS256 secret. (Rotation: stage the new key in the secondary slot, cut your signer over, then ask us to promote it.) ### The three tokens Three JWTs appear across the API. All are HS256, signed with your key, short-lived via `iat`/`exp`. | Token | Who signs | Key claims | |---|---|---| | Session (SDK URL) | You | `iss`=aid, `sid`, `txn`, `flow`, `ident`, `sub?`, `amt?`, `cur?` | | Verdict | Cardvera | `iss`=`cardvera`, `action`, `sid`, `txn` | | Outcome (bearer) | You | `iss`=aid, `jti` (fresh nonce) | > **Always pin the algorithm to HS256.** When you verify the verdict, pass `algorithms: ["HS256"]` (or your library's equivalent). A verifier that accepts any algorithm can be tricked by an `alg: none` or algorithm-confusion token. Every library supports pinning in one line — this is the one JWT rule you must not skip. **sign** — a session token cURL / shell: ```bash # A JWT is base64url(header).base64url(payload).base64url(HMAC). # Any JWT tool works (jwt.io, jwt-cli); with openssl by hand: b64() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } KEY="cv_sk_p_test_0000111122223333" H=$(printf '{"alg":"HS256","typ":"JWT"}' | b64) P=$(printf '{"iss":"acct_7f2a","sid":"9c1d…","txn":"ORD-1","flow":"full_checkout","ident":"guest","iat":%s,"exp":%s}' \ "$(date +%s)" "$(( $(date +%s) + 1800 ))" | b64) S=$(printf '%s.%s' "$H" "$P" | openssl dgst -sha256 -hmac "$KEY" -binary | b64) echo "$H.$P.$S" ``` Node.js: ```javascript import jwt from "jsonwebtoken"; // npm i jsonwebtoken const KEY = process.env.CARDVERA_KEY; // cv_sk_p_… (whole string) const token = jwt.sign( { iss: "acct_7f2a", sid, txn, flow: "full_checkout", ident: "guest" }, KEY, { algorithm: "HS256", expiresIn: "30m" } ); ``` Python: ```python import jwt, time # pip install pyjwt KEY = os.environ["CARDVERA_KEY"] # cv_sk_p_… (whole string) token = jwt.encode( {"iss": "acct_7f2a", "sid": sid, "txn": txn, "flow": "full_checkout", "ident": "guest", "iat": int(time.time()), "exp": int(time.time()) + 1800}, KEY, algorithm="HS256") ``` PHP: ```php 'acct_7f2a', 'sid' => $sid, 'txn' => $txn, 'flow' => 'full_checkout', 'ident' => 'guest', 'iat' => $now, 'exp' => $now + 1800, ], $key, 'HS256'); ``` Go: ```go import "github.com/golang-jwt/jwt/v5" // go get github.com/golang-jwt/jwt/v5 key := []byte(os.Getenv("CARDVERA_KEY")) // cv_sk_p_… (whole string) tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "iss": "acct_7f2a", "sid": sid, "txn": txn, "flow": "full_checkout", "ident": "guest", "iat": time.Now().Unix(), "exp": time.Now().Add(30 * time.Minute).Unix(), }) token, _ := tok.SignedString(key) ``` C#: ```csharp using JWT.Builder; // dotnet add package JWT using JWT.Algorithms; var key = Environment.GetEnvironmentVariable("CARDVERA_KEY"); // whole string var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var token = JwtBuilder.Create() .WithAlgorithm(new HMACSHA256Algorithm()) .WithSecret(key) .AddClaim("iss", "acct_7f2a").AddClaim("sid", sid).AddClaim("txn", txn) .AddClaim("flow", "full_checkout").AddClaim("ident", "guest") .AddClaim("iat", now).AddClaim("exp", now + 1800) .Encode(); ``` ## Context claims The business context of a payment travels as **claims in the session JWT** — your backend's signed statement of which transaction, which flow, and whether the shopper is signed in. Because they're inside the signed token, the browser can read them but can never change them. Cardvera stores the verified claims at SDK fetch and reads only that copy for every decision. The session token is **required**. A fetch without a valid one is rejected — there is no context-less fallback. ### Claims | Claim | Req | Value | |---|---|---| | `iss` | yes | Your account id (`acct_…`). | | `sid` | yes | Fresh session nonce per checkout — the session id. 32 hex chars recommended. | | `txn` | yes | Your transaction / order id (≤200 chars). Echoed in the verdict for binding. | | `flow` | yes | `full_checkout` · `express_checkout` · `minimal_payment` · `donation` | | `ident` | yes | `guest` · `authenticated_new` · `authenticated_established` | | `sub` | no | Merchant-scoped pseudonymous subject id (≤128 chars). Omit for guests. See below. | | `amt` | no | **Integer** in the currency's smallest unit — e.g. `31104` = $311.04 (USD/EUR, 2 digits), `1000` = ¥1000 (JPY, 0 digits), `5000` = 5.000 KWD (3 digits). Never a decimal. Requires `cur`. | | `cur` | with amt | ISO-4217, upper-case, e.g. `USD`. | | `iat`, `exp` | yes | Issued-at and expiry (Unix seconds). Keep the window tight — e.g. 30 min. | Your JWT library encodes and signs these; you never touch base64url or the signature yourself. Unknown `flow`/`ident`, a missing required claim, or an expired token are rejected at SDK fetch — an unknown flow never falls through to the most permissive policy. > **Claims are readable, not secret.** A JWT payload is signed, not encrypted — anyone with the URL can decode the claims. That's fine for what's here (a pseudonymous subject, a flow, an amount) and is the same as any signed token. Never put anything confidential in a claim. ### Deriving `sub` For a signed-in customer, derive the subject from your immutable internal customer id with a **dedicated** key — not your Cardvera signing key: `sub = HMAC(subjectKey, "subject:v1:" + internalCustomerId)`. It is stable for that customer at your store, different at every other merchant, and not reversible by Cardvera. Never send a raw id or an unsalted hash of an email. What is **never** a claim: card number, CVV, cardholder name, raw email or phone, addresses, raw customer ids. ### Choosing a `flow` Send the profile that matches the page the shopper is on. The single most common mistake is defaulting everything to `full_checkout` — a bare donation form scored as if it were a full checkout is the classic false positive. | flow | Send it when | |---|---| | `full_checkout` | A standard multi-field cart — name, address, card. Meaningful dwell and interaction. The default for normal e-commerce. | | `express_checkout` | A shortened, returning-customer path — wallet, saved card, one-tap (Apple Pay, Shop Pay). Less on-page interaction is expected and normal. | | `minimal_payment` | A small form with little behavioural surface — a "pay this invoice" link, an amount and a card field, not much else. | | `donation` | Often guest-accessible, a fixed or chosen amount, low navigation depth. Land, pick an amount, give. | **What it does in the system.** - **Sets the behavioural baseline.** A `minimal_payment` or `donation` page legitimately produces little mouse/keyboard evidence; declaring the flow stops that thinness from counting against a real shopper the way it would on a full checkout. - **Isolates detection cohorts.** Decline-rate and volume anomalies are measured *per flow*, so a card-testing spike on your donation page is caught on its own baseline instead of being diluted by — or falsely implicating — checkout traffic. - **Is validated fail-closed.** An unrecognised flow is rejected at SDK fetch; it never silently falls back to the most permissive profile. ### Choosing an `ident` State what is *true* about the shopper's session with you. `ident` selects which population the session is judged against — it is not a trust dial. | ident | Send it when | |---|---| | `guest` | No account, or not signed in for this purchase. There is no `sub`. The verdict leans on browser, transport, velocity and behaviour. | | `authenticated_new` | Signed in, but without earned history — a fresh account, or one that doesn't yet meet your "established" bar. Send a `sub`. | | `authenticated_established` | A signed-in account that meets criteria *you* define — e.g. N prior settled orders, or an account age threshold. Send a `sub`. | **What it does in the system.** - **Selects the population & isolates cohorts.** Guest sessions are judged and aggregated separately from authenticated ones, so guest card-testing never pollutes — or borrows credibility from — your signed-in customers. - **Gates account-level protection.** The `sub`-keyed defense that survives IP and device rotation only applies to `authenticated_*` sessions. A guest has no account to key on. - **Is a label, not a bypass.** `authenticated_established` does **not**, by itself, relax any control — it never overrides a block, and it earns no automatic pass today. Send it because it's accurate, not to get looser treatment. **session token** — decoded claims ```json { "iss": "acct_7f2a", "sid": "9c1d4e77b0a3f5628d10c4ba9e73f001", "txn": "ORD-20260921-abc123", "flow": "full_checkout", "ident": "authenticated_new", "sub": "3f1c9a0b7d2e4f6a…", "amt": 31104, "cur": "USD", "iat": 1787000000, "exp": 1787001800 } // HS256, signed with your key -> the "t" URL parameter ``` ## Quickstart A complete server-side integration is three functions: **mint** a session token, **verify** the verdict, and **report** the outcome. All three are one JWT-library call plus a little glue. The rail has a self-contained module in your language — set your `aid` and key, wire the three call sites. ### 1 · Render the checkout Call `mintSdkUrl(orderId, …)`, drop the returned ` ``` ## Verify the verdict Your page posts the verdict JWT to your server with the checkout submission. **Verify it before you authorize the card.** It's a local `jwt.verify` — no Cardvera call. The [Quickstart](index.html#quickstart) module's `verifyVerdict` does all of this. ### What to check, in order 1. **A token is present.** Empty → decline. 2. **Signature verifies with your key, pinned to HS256.** This alone defeats a forged or patched verdict. 3. **Not expired** — the library checks `exp`; allow small clock leeway. 4. **You have a recorded `txn` for this `sid`**, and the token's `txn` claim equals it. No record → decline. 5. **`action == "allow"`** before charging. > **Why txn is a claim you re-check.** Binding the verdict to a `txn` you hold — never one the browser sends — stops a valid `allow` from a cheap order being replayed onto an expensive one. A verdict minted for order A only passes when you check it against order A's id. **verdict** — decoded claims ```json { "iss": "cardvera", "action": "allow", // or "block" "sid": "9c1d4e77b0a3f5628d10c4ba9e73f001", "txn": "ORD-20260921-abc123", "iat": 1787000019, "exp": 1787000319, "agt": { // only when a verified AI agent drove the session "op": "openai", "tier": "MajorOperator", "tag": "web-bot-auth", "mode": "enforce" } } // verify(token, key, { algorithms: ["HS256"] }) // then: claims.action == "allow" AND claims.txn == your stored order id ``` ## Report outcomes POST `/api/outcome` — tell Cardvera what your PSP decided. This is the feedback loop: confirmed declines and chargebacks train the blocks that stop the next attempt. Call it server-side after each PSP response, and again when a chargeback or refund lands later. Fire-and-forget — don't block checkout on it. Authenticated with a **bearer JWT** (`Authorization: Bearer `), claims `{ iss, jti, iat, exp }` signed HS256 with your key. The `jti` is a fresh nonce per call — one session reports many outcomes over its life, each with its own bearer. The body is pure data; you never resend context (Cardvera joins by `sessionId`). ### Body | Field | Req | | |---|---|---| | `sessionId` | yes | The `sid` from the session token. | | `outcomeType` | yes | `auth` · `chargeback` · `refund` | | `outcome` | yes | Closed vocabulary — see below. | | `occurredAt` | yes | ISO-8601, when the PSP event happened. | | `outcomeId` | no | Idempotency key (e.g. PSP txn id). Dedupes on `(sessionId, outcomeType, outcomeId)`. Without it, Cardvera dedupes on `(sessionId, outcomeType, outcome, occurredAt)`. | | `pspName` | no | Audit only, e.g. `stripe`. | | `pspRawCode` | no | Audit only: the PSP's own decline code, e.g. `card_declined`. | ### Outcome vocabulary | outcomeType | outcome values | |---|---| | `auth` | `approved`, `declined_fraud`, `declined_insufficient_funds`, `declined_do_not_honor`, `declined_card_invalid`, `declined_3ds_failed`, `declined_other`, `declined_technical` | | `chargeback` | `chargeback_fraud`, `chargeback_other` | | `refund` | `refund_customer`, `refund_merchant` | > **Map gateway faults to declined_technical.** A timeout or issuer-unavailable is operational, not fraud evidence — map it to `declined_technical` (weight zero). Mapping outages to `declined_other` poisons your own signal and can friction real shoppers during an incident. Response 202 Accepted — recorded, move on: ``` (empty body) ``` Response 400 invalid_outcome: ``` { "error": "invalid_outcome" } // also: missing_session_id, invalid_outcome_type, invalid_occurred_at ``` Response 401 invalid_token: ``` { "error": "invalid_token" } // bad/expired bearer, or wrong alg { "error": "missing_token" } // no Authorization: Bearer header ``` Response 503 storage_unavailable: ``` { "error": "storage_unavailable" } // safe to retry ``` **POST** /api/outcome ```bash curl -X POST https://cardvera.dev/api/outcome \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3Mi…" \ -H 'Content-Type: application/json' \ -d '{ "sessionId": "9c1d4e77b0a3f5628d10c4ba9e73f001", "outcomeType":"auth", "outcome": "declined_fraud", "occurredAt": "2026-09-21T14:32:00Z", "outcomeId": "stripe_pi_3OABCxyz", "pspName": "stripe" }' 202 Accepted -> recorded, move on 400 { "error": "invalid_outcome" } -> or missing_session_id, invalid_outcome_type, invalid_occurred_at 401 { "error": "invalid_token" } -> bad/expired bearer, wrong alg 503 { "error": "storage_unavailable" } -> safe to retry ``` ## Verified AI agents AI agents that shop on a customer's behalf, like ChatGPT Agent or agents enrolled in Visa's Trusted Agent Protocol, drive a real browser through your iframe checkout. Many sign every request with [Web Bot Auth](https://datatracker.ietf.org/doc/draft-ietf-webbotauth-httpsig-protocol/) (RFC 9421 HTTP message signatures). That includes the requests the browser makes to Cardvera. **You don't need to integrate anything.** Cardvera verifies the signature on the SDK fetch and on `assess()`. ### What a verified agent gets An agent isn't a human, so Cardvera stops judging it on the "is this a human browser?" layer (mouse, typing and automation markers). That's the only change. Proof of work, blocklists, velocity, decline history and card-testing detection still apply, and an agent can still be stepped up or blocked. - **Only allowlisted operators count.** Cardvera trusts a signature only from a key directory on its reviewed list (payment networks and major AI operators). A valid signature from any other operator gets no special treatment. - **Forged, replayed or expired signatures are always blocked.** No real client produces one. - **Rollout is per merchant.** Accounts start in `shadow`: Cardvera verifies agents and records what it would have decided, but judges them like any other client. Ask us to switch your account to `enforce`. ### The `agt` claim When a verified agent drove the session, the verdict carries `agt` with `op` (operator), `tier`, `tag` and `mode`. It's signed with the rest of the verdict, so it's as trustworthy as `action`. You can ignore it, log it, or apply your own policy, for example requiring a signed-in account for agent orders. Its absence means no verified agent. **agent request** — what the agent's browser sends ```http GET /v1/cv.js?t=eyJ... HTTP/2 Host: edge.cardvera.dev Signature-Agent: "https://chatgpt.com" Signature-Input: sig1=("@authority" "@method" "@path"); created=1787000000;expires=1787000060; keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U"; tag="web-bot-auth" Signature: sig1=:jdq0SqOwHdyHr9+r5jw3iYZH6aNGKijYp/EstF4RQTQ=: // Cardvera: fetch the operator's key directory (cached), // rebuild the signature base, verify, reject replays. ``` ## Errors Token failures are deliberately terse — they don't say whether the signature, algorithm, or expiry was the problem, so they can't be used as an oracle. Validation failures return `400` with a specific `error` you can act on. ### Status codes | Code | Meaning | |---|---| | 200 | Allow / step-up (SDK & assess). | | 202 | Outcome accepted. | | 400 | Validation error — see `error`. | | 401 / 403 | Invalid token, or a `block` verdict. | | 503 | Storage unavailable — safe to retry. | | 500 | `internal_error` — safe to retry. | ### Common `error` values | error | Fix | |---|---| | `missing_token` | SDK URL has no `t` param (400), or no bearer on `/api/outcome` (401). | | `invalid_token` | Signature failed, wrong algorithm, or expired. Check you signed with the whole key and set `alg` HS256. | | `invalid_claims` | Unknown `flow`/`ident`, or a missing required claim (e.g. `txn`). | | `missing_session_id` | `/api/outcome` body has no `sessionId`. | | `invalid_outcome_type` | `outcomeType` is not `auth`, `chargeback` or `refund`. | | `invalid_outcome` | `outcome` is not in the vocabulary for its `outcomeType`. | | `invalid_occurred_at` | `occurredAt` is missing or not an ISO-8601 timestamp. | | `agent_auth_invalid` | The request carried a Web Bot Auth signature that failed verification (forged, replayed or expired). Not something your integration causes; the client is blocked. | **400** — validation error shape ```json { "error": "invalid_outcome" } // Token failures carry no detail by design: // 401 { "error": "invalid_token" } (POST /api/outcome) // 403 { "error": "invalid_token" } (GET /v1/cv.js) ``` ## Production checklist - **Secrets server-side only.** Your `cv_sk_p_` key and subject key live in your secret store, never in client code or the repo. - **Use a JWT library** and sign HS256. Don't hand-roll the encoding. - **Pin HS256 on every verify** — `algorithms: ["HS256"]`. Never accept `alg: none` or an unpinned algorithm. - **Use the key whole** — the full `cv_sk_p_…` string is the HS256 secret. - **Verify every verdict.** No path authorizes a card on the client `action` without verifying the JWT and matching your stored `txn`. - **Fail closed.** If `assess()` throws, the token is empty, or verification fails, decline — don't fall through to a charge. - **Fresh `sid` per checkout render**, mapped to your order id server-side. Keep token lifetimes short. - **Clock on NTP.** Expired-token errors on live traffic are almost always server clock drift. - **Wire `/api/outcome`.** Report every auth result, and chargebacks/refunds when they land. Map faults to `declined_technical`, not `declined_other`. - **Send `sub` for signed-in customers**, derived with a dedicated key — it enables account-level protection that survives IP and device rotation. - **Rotate with the secondary slot** — stage the new key in `cv_sk_s_`, cut your signer over, then promote. > **Need credentials?.** Request an account at [cardvera.io](https://cardvera.io/#register). You'll receive an `aid` and a primary/secondary key pair to drop into the Quickstart module. ## Appendix: JSON Schemas JSON Schema (draft 2020-12) for each token payload and request body. Validate what you sign or send against these; each is also served at `schemas/`. ### error.schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Cardvera error response", "description": "Error body returned by GET /v1/cv.js and POST /api/outcome. Token errors are deliberately terse and never say which check failed.", "type": "object", "required": [ "error" ], "properties": { "error": { "type": "string", "examples": [ "missing_token", "invalid_token", "invalid_claims", "agent_auth_invalid", "missing_session_id", "invalid_outcome_type", "invalid_outcome", "invalid_occurred_at", "storage_unavailable", "internal_error" ] } } } ``` ### outcome-bearer.schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Cardvera outcome bearer token claims", "description": "Payload of the JWT you sign (HS256, your whole key) and send as `Authorization: Bearer ` on POST /api/outcome. Sign a new one per request.", "type": "object", "required": [ "iss", "jti", "iat", "exp" ], "additionalProperties": false, "properties": { "iss": { "type": "string", "pattern": "^acct_", "description": "Your account id (aid)." }, "jti": { "type": "string", "minLength": 1, "description": "Fresh random nonce per request (e.g. 32 hex chars). NOT the session id." }, "iat": { "type": "integer", "minimum": 0 }, "exp": { "type": "integer", "minimum": 0, "description": "About 5 minutes after iat; at most 30 minutes." }, "nbf": { "type": "integer", "minimum": 0 } } } ``` ### outcome-request.schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Cardvera POST /api/outcome request body", "description": "JSON body reporting a PSP disposition for a session. Responses: 202 (empty body) accepted; 400 {error: missing_session_id | invalid_outcome_type | invalid_outcome | invalid_occurred_at}; 401 {error: missing_token | invalid_token}; 503 {error: storage_unavailable} and 500 {error: internal_error} are safe to retry. Map gateway timeouts / issuer-unavailable to declined_technical, not declined_other.", "type": "object", "required": [ "sessionId", "outcomeType", "outcome", "occurredAt" ], "additionalProperties": false, "properties": { "sessionId": { "type": "string", "minLength": 1, "description": "The sid from the session token." }, "outcomeType": { "enum": [ "auth", "chargeback", "refund" ] }, "outcome": { "enum": [ "approved", "declined_fraud", "declined_insufficient_funds", "declined_do_not_honor", "declined_card_invalid", "declined_3ds_failed", "declined_other", "declined_technical", "chargeback_fraud", "chargeback_other", "refund_customer", "refund_merchant" ], "description": "Closed vocabulary; must belong to outcomeType's set." }, "occurredAt": { "type": "string", "format": "date-time", "description": "ISO-8601 time of the PSP event (not the time you report it)." }, "outcomeId": { "type": "string", "description": "Idempotency key, e.g. the PSP transaction id. Dedupes on (sessionId, outcomeType, outcomeId); without it, on (sessionId, outcomeType, outcome, occurredAt)." }, "pspName": { "type": "string", "description": "Audit only, e.g. stripe." }, "pspRawCode": { "type": "string", "description": "Audit only: the PSP's own decline code." } }, "allOf": [ { "if": { "properties": { "outcomeType": { "const": "auth" } }, "required": [ "outcomeType" ] }, "then": { "properties": { "outcome": { "enum": [ "approved", "declined_fraud", "declined_insufficient_funds", "declined_do_not_honor", "declined_card_invalid", "declined_3ds_failed", "declined_other", "declined_technical" ] } } } }, { "if": { "properties": { "outcomeType": { "const": "chargeback" } }, "required": [ "outcomeType" ] }, "then": { "properties": { "outcome": { "enum": [ "chargeback_fraud", "chargeback_other" ] } } } }, { "if": { "properties": { "outcomeType": { "const": "refund" } }, "required": [ "outcomeType" ] }, "then": { "properties": { "outcome": { "enum": [ "refund_customer", "refund_merchant" ] } } } } ] } ``` ### session-token.schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Cardvera session token claims", "description": "Payload of the JWT your server signs (HS256, your whole cv_sk_p_… key as the secret) and puts on the SDK URL as ?t=. Validated at GET /v1/cv.js; any failure returns 400 invalid_claims or 403 invalid_token.", "type": "object", "required": [ "iss", "sid", "txn", "flow", "ident", "iat", "exp" ], "additionalProperties": false, "properties": { "iss": { "type": "string", "pattern": "^acct_", "description": "Your account id (aid)." }, "sid": { "type": "string", "minLength": 1, "description": "Fresh session nonce per checkout render; the session id. 32 lowercase hex chars recommended. Never reuse." }, "txn": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "\\S", "description": "Your transaction / order id. Echoed in the verdict; re-check it there against the id you stored for this sid." }, "flow": { "enum": [ "full_checkout", "express_checkout", "minimal_payment", "donation" ], "description": "Payment-flow profile of the page the shopper is on." }, "ident": { "enum": [ "guest", "authenticated_new", "authenticated_established" ], "description": "Which shopper population the session belongs to. Send sub with the authenticated_* values." }, "sub": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Merchant-scoped pseudonymous subject: HMAC(subjectKey, \"subject:v1:\" + internalCustomerId), using a key that is NOT your Cardvera key. Omit for guests." }, "amt": { "type": "integer", "minimum": 0, "description": "Amount in the currency's smallest unit (31104 = $311.04). Never a decimal. Requires cur." }, "cur": { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO-4217 currency code, upper-case." }, "iat": { "type": "integer", "minimum": 0, "description": "Issued-at, Unix seconds." }, "exp": { "type": "integer", "minimum": 0, "description": "Expiry, Unix seconds. Required. Must be no more than 30 minutes after the time Cardvera receives the token (60 s skew allowed)." }, "nbf": { "type": "integer", "minimum": 0, "description": "Optional not-before, Unix seconds." } }, "dependentRequired": { "amt": [ "cur" ] } } ``` ### verdict-token.schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Cardvera verdict token claims", "description": "Payload of the verdict JWT returned by Cardvera.assess() as `token`. Cardvera signs it HS256 with your primary key. Verify the signature with algorithms pinned to [\"HS256\"], check exp (allow small leeway), then require action == \"allow\" AND txn == the order id YOU stored for sid before charging. New optional claims may be added; ignore ones you don't know.", "type": "object", "required": [ "iss", "action", "sid", "txn", "iat", "exp" ], "additionalProperties": true, "properties": { "iss": { "const": "cardvera" }, "action": { "enum": [ "allow", "block" ], "description": "Terminal decision. Step-ups are resolved inside the SDK and never reach you." }, "sid": { "type": "string", "description": "The sid from your session token." }, "txn": { "type": "string", "description": "The txn from your session token. Always set on allow; may be empty on block." }, "iat": { "type": "integer", "minimum": 0 }, "exp": { "type": "integer", "minimum": 0, "description": "About 5 minutes after iat." }, "agt": { "type": "object", "description": "Present only when a verified AI agent (Web Bot Auth) drove the session. Absence means no verified agent.", "required": [ "op", "tier", "tag", "mode" ], "additionalProperties": true, "properties": { "op": { "type": "string", "description": "Allowlisted operator name, e.g. openai, visa-tap." }, "tier": { "type": "string", "examples": [ "PaymentNetwork", "MajorOperator", "Registry" ], "description": "How much vetting stands behind the operator's keys." }, "tag": { "type": "string", "examples": [ "web-bot-auth", "agent-browser-auth", "agent-payer-auth" ], "description": "The signature tag the agent used." }, "mode": { "enum": [ "off", "shadow", "enforce" ], "description": "Your account's agent rollout mode for this session." } } } } } ```