Every quote an agent gets from a PairRail seller carries the same signed record, whoever the seller is and whatever bills the money. A buyer tool can parse one format and check any seller's quote with one key set. This page is the spec for version 1.
- Format: compact JWS,
algEdDSA(Ed25519),typpairrail-quote+jws. - Keys:
/.well-known/jwks.json. - JSON Schema for the claims:
/schemas/pairrail-quote-v1.json.
Where the token is
Quotes from quote_request over MCP, POST /v1/quote-requests and GET /v1/quote-requests/{id} include a signature block:
{
"format": "jws-compact",
"alg": "EdDSA",
"kid": "VIiBE87YZismfol7eZmAGFYIMaiczFY39c90ndNbl_4",
"jwksUri": "https://pairrail.com/.well-known/jwks.json",
"token": "eyJhbGciOiJFZERTQSIs…"
}jwksUri on pairrail.com redirects to www.pairrail.com; follow the redirect. token is self-contained: its payload is the signed claim set below. Store the token, not the surrounding JSON, if you want something you can check later. The token is signed when the quote is returned, so a later read of the same quote gives a new token with the status at that time.
Claims
The decoded payload, from an example seller:
{
"iss": "https://pairrail.com",
"sub": "qr_24ab4a08143c4164a645",
"iat": 1793370647,
"exp": 1793975447,
"quote": {
"id": "qr_24ab4a08143c4164a645",
"status": "indicative",
"answerType": "indicative",
"binding": false,
"quoteMode": "indicative",
"disclaimer": "Quotes are indicative and non-binding, approval may be required. …",
"seller": { "organizationId": "org_northstar", "slug": "northstar-compute", "name": "North Star Compute" },
"catalogId": "cat_northstar",
"catalogVersionId": "catv_northstar_1",
"catalogVersion": 1,
"policyVersionId": "pol_northstar_1",
"offerId": "off_inference_usage",
"input": { "units": 4, "commitmentMonths": 1, "requestedDiscountBps": 0 },
"calculation": {
"currency": "USD",
"subtotal": "81.00", "discount": "0.00", "total": "81.00",
"taxTreatment": "exclusive",
"lineItems": [
{ "componentId": "pc_inference_platform", "name": "API access", "type": "flat", "quantity": 1, "subtotal": "49.00" },
{ "componentId": "pc_inference_tokens", "name": "Token usage", "type": "usage", "quantity": 4, "subtotal": "32.00" }
],
"assumptions": ["Prices exclude applicable taxes.", "…"]
},
"buyerReference": "PO-7781",
"createdAt": "2026-10-30T14:30:47.699Z",
"expiresAt": "2026-11-06T14:30:47.699Z"
}
}| Claim | Meaning |
|---|---|
iss | The PairRail origin that signed it: https://pairrail.com. |
sub, quote.id | The quote id. |
iat | When this token was signed (seconds). |
exp, quote.expiresAt | When the quote expires. An expired quote still verifies. |
quote.status | Status when signed: indicative, approval-required, approved, rejected, revoked, expired, superseded, executing or executed. Treat a value you do not know as not approved. |
quote.binding, quote.quoteMode | Always false and indicative in v1. |
quote.seller.slug | The sellerId to use on that seller's MCP and REST URLs. |
catalogVersionId, policyVersionId | The published catalog and the policy the price came from. Versions are immutable once published. |
input | What the agent asked to price, after defaults. |
calculation | Currency, subtotal, discount, total and line items. Amounts are decimal strings with the currency's minor digits. |
buyerReference | Your own reference, such as a PO number, as your agent sent it. |
Verify online
Send the token to POST /v1/public/quote-verify. No key needed.
curl -sS https://www.pairrail.com/v1/public/quote-verify \
-H 'Content-Type: application/json' \
-d '{"token":"eyJhbGciOiJFZERTQSIs…"}'
# { "valid": true, "type": "quote", "kid": "VIiBE87Y…", "quoteId": "qr_24ab4a08143c4164a645",
# "issuedAt": "2026-10-30T14:30:47.000Z", "expired": false, "binding": false, "claims": { … } }When the check fails you get valid: false and a reason: malformed, unexpected_header, unknown_key or bad_signature. Nothing else comes back. See the API reference.
Verify offline
Any JOSE library that supports EdDSA can check the token against the JWKS. The steps:
- Split the token into header, payload and signature, each base64url.
- Check the header:
algisEdDSAandtypispairrail-quote+jws. - Find the key in the JWKS whose
kidmatches the header. If none matches, fetch the JWKS once more before you give up. - Check the Ed25519 signature over
header.payload.
import { createPublicKey, verify } from "node:crypto";
export async function verifyPairRailQuote(token) {
const [h, p, s] = token.split(".");
const header = JSON.parse(Buffer.from(h, "base64url"));
if (header.alg !== "EdDSA" || header.typ !== "pairrail-quote+jws") return null;
const { keys } = await (await fetch("https://www.pairrail.com/.well-known/jwks.json")).json();
const jwk = keys.find((key) => key.kid === header.kid);
if (!jwk) return null;
const ok = verify(null, Buffer.from(h + "." + p), createPublicKey({ key: jwk, format: "jwk" }), Buffer.from(s, "base64url"));
return ok ? JSON.parse(Buffer.from(p, "base64url")) : null;
}kid is the RFC 7638 thumbprint of the key. When the signing key is rotated, the old public key is kept in the JWKS so quotes signed before the rotation still verify. The JWKS is served with Cache-Control: max-age=300.
What valid means
valid: true means PairRail computed these numbers from that seller's published catalog version and policy, and nothing in the payload changed afterwards.
It does not mean:
- The seller is committed. Every v1 quote is
binding: false. The seller's own terms and billing system decide the sale. - The status is current.
statusis the status when the token was signed. Read the quote again with your key for the status now. - Anything was paid. Execute hands an accepted quote to the seller's billing system, and payment happens there.
Evidence bundles
A seller can export an evidence file for any quote from their quote ledger: the quote, the policy decision, approvals, mandate, outcomes and related audit events, in format pairrail-quote-evidence/v1. It is signed with the same key, with typ pairrail-quote-evidence+jws, and POST /v1/public/quote-verify checks it too (type: "evidence"). A seller may share one with you in a dispute or an audit; you cannot download it yourself.
Versioning
typnames the format. This page is v1,pairrail-quote+jws.- Inside v1, changes only add things: new optional claims or new
statusvalues. Ignore claims you do not know. - Removing or renaming a claim, or changing what one means, gets a new
typand a new schema file. It is announced in the changelog before it ships. - In v1,
bindingis alwaysfalse. If PairRail ever signs a quote that commits a seller, it will use a differenttyp, so a v1 reader can never mistake one for the other.