Public REST
The same public data is available as plain REST:
| Endpoint | Purpose |
|---|---|
GET /v1/public/catalog?sellerId= | Published catalog, access rules and how to request a key. Free and edge-cached. |
POST /v1/public/discover?sellerId= | Body {"need": "…", "region": "US"}. Returns matching public offers, most relevant first. |
POST /v1/public/pricing/calculate?sellerId= | Body as for pricing_calculate. Returns an itemized calculation. |
GET /.well-known/ucp?sellerId= | UCP profile with the seller’s published quote authority. |
curl -sS "https://www.pairrail.com/v1/public/pricing/calculate?sellerId=northstar-compute" \
-H 'Content-Type: application/json' \
-d '{"offerId":"off_northstar-compute_growth","seats":5,"commitmentMonths":12,"region":"US"}'
# → { "answerType": …, "binding": …, "authority": …,
# "data": { "calculation": { "currency": "USD", "subtotal": "10140.00",
# "total": "10140.00", "lineItems": [ … ] } } }Every response carries catalogVersionId, policyVersionId, validFrom and validUntil, so an agent can cite exactly which approved version it priced against.
Agent keys
Keys are issued by the seller, never self-served. An agent asks; a person on the seller’s team approves; the agent picks the key up once.
1. Request access
curl -sS https://www.pairrail.com/v1/access-requests \
-H 'Content-Type: application/json' \
-d '{"sellerId":"northstar-compute","agentName":"Acme procurement agent",
"contactEmail":"buyer@acme.example","clientId":"acme-agent-1",
"note":"Quoting GPU capacity for Q4"}'
# → 201 { "id": "acr_…", "status": "pending",
# "pickupToken": "pickup_…", "pollUrl": ".../v1/access-requests/acr_…" }You can attach identitySignals to describe who the agent is. Sellers see them as claimed; PairRail does not mark them verified. To prove who is asking, sign the request with Web Bot Auth (see Signed requests), which Atlas does check.
2. Poll and claim
curl -sS https://www.pairrail.com/v1/access-requests/acr_… \
-H 'Authorization: Bearer pickup_…'
# pending → { "status": "pending", "pending": true }
# approved → { "status": "approved", "secret": "pat_…", "credential": { "scopes": [ … ] } }The secret is returned once. Later polls return "alreadyDelivered": true and no secret, so store it immediately. Sellers can rotate or revoke a key at any time; a revoked or rotated key gets 401.
Scopes and limits
| Scope | Allows |
|---|---|
catalog:read | Keyed catalog, offers and discover, including offers the seller discloses only to customers or partners. |
pricing:calculate | Keyed pricing with the buyer’s customer-tier prices. |
quote:request | Create quotes. |
quote:read | Read quote status, including approval progress. |
readiness:read | Keyed readiness profile. |
quote:execute | Execute an approved quote. Never granted by default; the seller must add it. |
Scopes decide which calls a key can make. Its access level decides which offers it sees: public, partner or customer. Offers disclosed above that level are left out of every response, and internal-only offers never reach a buyer key.
Per-key limits
Every key carries its own caps. The seller sets them when it approves the key and can change them without republishing the catalog.
| Limit | When exceeded |
|---|---|
| Quotes per hour | 429 VELOCITY_LIMIT |
| Pricing calls per hour (REST and MCP) | 429 VELOCITY_LIMIT |
| Quoted total per day | 429 VELOCITY_LIMIT |
| Executes per hour | 429 VELOCITY_LIMIT |
| Spend per day (executed quotes) | 429 VELOCITY_LIMIT |
| Keyed calls per month (REST, MCP and the protocol rails) | 429 VELOCITY_LIMIT |
| Expiry date | 401 CREDENTIAL_EXPIRED |
VELOCITY_LIMIT details name the window that was hit (hour, day or month), so an agent can back off instead of retrying blindly. They never give the count used, the limit or an amount. Retry-After is the earliest time a retry can succeed; for the monthly cap that is the start of the next month (UTC).
What gets blocked
| Situation | Response |
|---|---|
Call outside the key’s scopes, such as execute without quote:execute | 403 INSUFFICIENT_SCOPE |
| Key used against another seller’s keyed tools | 403 TENANT_DENIED |
| Revoked or rotated key | 401 INVALID_CREDENTIAL |
| Expired key | 401 CREDENTIAL_EXPIRED |
| Seller paused all agent traffic | 503 AGENT_ACCESS_PAUSED |
| Seller turned off this protocol (UCP, ACP, A2A or UAP); MCP and REST stay on | 403 PROTOCOL_DISABLED |
| Offer disclosed above the key’s access level | Not returned; to the agent it does not exist |
| Quote above the seller’s approval threshold | Not blocked: routed to a person (QUOTE_APPROVAL_REQUIRED) |
Pricing & quotes
Keyed REST calls resolve the seller from the key, so no sellerId is needed:
| Endpoint | Scope |
|---|---|
GET /v1/catalog, GET /v1/catalog/offers | catalog:read |
POST /v1/discover | catalog:read |
POST /v1/pricing/calculate | pricing:calculate |
POST /v1/quote-requests | quote:request |
GET /v1/quote-requests/{id} | quote:read |
POST /v1/quote-requests/{id}/execute | quote:execute |
GET /v1/readiness | readiness:read |
curl -sS https://www.pairrail.com/v1/quote-requests \
-H 'Authorization: Bearer pat_…' \
-H 'Idempotency-Key: 7f3c9b2e-quote-1' \
-H 'Content-Type: application/json' \
-d '{"offerId":"off_northstar-compute_growth","seats":25,
"commitmentMonths":12,"region":"US","need":"Team rollout in Q4"}'Idempotency: POST /v1/quote-requests requires an Idempotency-Key header. On MCP quote_request it is optional, since many MCP clients cannot set headers: send the header or an idempotencyKey argument, or every call creates a new quote. Retrying with the same key returns the original quote instead of creating a second one.
Approval: the seller sets how much an agent may commit without a person. Above that threshold the quote comes back with status: "approval-required" and review.submittedAt: null, and nobody at the seller sees it yet. When the buyer wants it at that price, send it to the seller’s deal desk with POST /v1/quote-requests/{id}/submit (MCP quote_submit), then poll GET /v1/quote-requests/{id} (the poll-approval strategy) until a person resolves it. Submitting twice is safe. A quote that is never submitted just expires. Agents never see the threshold, only whether a quote needs approval, and splitting one order into smaller quotes does not avoid it. A quote below the seller’s floor price also waits for a person, whatever its total.
Quote outcomes
Every quote request ends in one of three states. The examples on this site show each one with the same badge.
| Status | Response | What happens |
|---|---|---|
| Within policy | 201, status: "indicative" | Inside the seller’s published rules. The agent can present the quote; it stays non-binding until executed. |
| Needs approval | 201, status: "approval-required" | Above a threshold the seller set. Submit it once the buyer wants it; a person on the seller’s team then approves or rejects it. Poll the quote until they do. |
| Blocked | 422 POLICY_DENIED or 429 VELOCITY_LIMIT | Outside the published terms (region, commitment, discount), or over the key’s spend or rate cap. No quote is created. |
Execute & payment mandates
Handing a quote to the seller’s billing is opt-in. To execute, the key needs quote:execute, the quote must be within policy or approved by a person, and still match the live catalog, and the agent must present a delegated payment mandate its own human gave. PairRail then hands the quote to the seller’s billing (Stripe, Chargebee or any billing system via signed webhook), which creates the invoice or quote; the seller’s processor stays the authority on the payment. See Billing connectors.
curl -sS https://www.pairrail.com/v1/quote-requests/q_…/execute \
-H 'Authorization: Bearer pat_…' \
-H 'Content-Type: application/json' \
-d '{"paymentMandate":"eyJhbGciOiJFZERTQSJ9.eyJleHAiOjE3OTE…"}'A mandate can be sent as paymentMandate or delegatedPaymentToken (a token or compact JWS), as paymentMandateUri (HTTPS URL), or inline as maxAmountMinor plus expiresAt. Atlas refuses a mandate that has expired, is not yet valid, is capped below the quote total, or names a different currency or quote.
Signed mandates
Sellers can register their mandate issuers’ public keys (ES256 on P-256, RS256, or EdDSA). A mandate signed by a registered issuer is verified and must carry:
exp, no more than 24 hours after it was issued;maxAmountMinor, a whole number in minor units (cents);jti, unique per issuer: one mandate cannot pay for two quotes (409 MANDATE_REPLAYED);- and, when present, a matching
iss,currency,quoteIdandnbf.
Any mandate whose aud names a different seller is refused with MANDATE_AUDIENCE_MISMATCH, so a mandate written for one seller cannot be spent at another.
Sellers can require signed mandates for every execute; unsigned or unverifiable mandates are then refused with MANDATE_SIGNATURE_REQUIRED or MANDATE_SIGNATURE_INVALID. In that mode a mandate must also carry aud (the seller’s id or slug) and the quoteId it pays for, which ties it to one seller and one quote: an agent that intercepts it along a delegation chain cannot spend it on anything else.
Receiving what the buyer bought
If the seller delivers credentials (an API key from its vault or provision webhook), they are in data.entitlement of the execute response, and only there. Each buyer gets a different vault key. Atlas keeps a fingerprint so the seller can match a key to a quote, never the key itself. When the seller fulfills later, the next GET /v1/quote-requests/{id} returns it once. Store it when you receive it; if it is lost, ask the seller to issue another.
Binding keys and purchases to your agent
When one agent delegates to others, anything relayed through the chain can be read by every hop. To stop that, give Atlas a public key that only your agent holds (a JWK: EC P-256, X25519, or RSA of 2048 to 8192 bits). Send it as agentPublicKey when you request access, or ask the seller to register it on your key. Atlas then encrypts to it, as a compact JWE (ECDH-ES or RSA-OAEP-256 with A256GCM) that any standard JOSE library opens:
- the bearer key itself, returned as
secretEncryptedwhen you claim it; - what you buy, as
data.entitlement.encrypted.jwe, which later quote reads return again since only you can open it.
A mandate can also name who may spend it. Set azp to your buyer key id (cred_…; other azp values, such as an OAuth client id, are ignored). A mandate signed by an issuer the seller registered can also carry cnf.jkt, the RFC 7638 thumbprint of your agent key, or the key itself in cnf.jwk; cnf in unsigned tokens is ignored. Any other caller is refused with MANDATE_HOLDER_MISMATCH.