Portal
Documentation: all sections

Verifiable receipts

Every chat completion and embedding comes with a signed receipt: a small, metadata-only attestation that Sable served the request, on which machine, in which region, under which tier, signed with secp256k1 so anyone can verify it without trusting us. The receipt proves that inference happened and how; it never carries the prompt or completion.

What you get back

Non-streaming POST /v1/chat/completions and POST /v1/embeddings responses add these headers:

The x-sable-receipt* headers are CORS-exposed, so a browser client can read them from a cross-origin response. You don't need a server-side proxy just to collect receipts.

curl -i https://api.buildsable.com/v1/chat/completions \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "model": "sable-llama-3.3-70b",
  "messages": [{"role":"user","content":"hi"}]
}'

# ... response headers ...
# x-sable-node: gateway
# x-sable-region: unspecified
# x-sable-receipt: eyJ2IjoxLCJyZXF1ZXN0X2lkIjoi...
# x-sable-receipt-sig: 0x4f8c...1b
# x-sable-receipt-signer: 0xA1b2...9F

The receipt payload

Base64url-decode x-sable-receipt and you get canonical JSON. It is metadata only: note content_fingerprint, a SHA-256 over the sealed payload, not the content itself.

{
  "v": 1,
  "request_id": "req_8f3c...",
  "content_fingerprint": "9b1c4e...",
  "model": "sable-llama-3.3-70b",
  "privacy_tier": "standard",
  "node": "gateway",
  "region": "unspecified",
  "prompt_tokens": 12,
  "completion_tokens": 48,
  "total_tokens": 60,
  "cost_micro_usd": 540,
  "logging": "metadata-only",
  "issued_at": "2026-05-30T12:00:00Z"
}

The fingerprint lets you bind a receipt to a specific request you made (hash your own sealed envelope and compare) without ever exposing the plaintext to the receipt itself. cost_micro_usd is the metered cost in millionths of a dollar. See Cost & metering.

The logging claim

Every receipt carries logging: "metadata-only". It is a signed, per-request claim that for this specific call Sable persisted no prompt, no completion, and no submitted code, only the metadata that appears on the receipt itself. Where most services state their retention posture in a policy page, this is the privacy contract restated as a verifiable artifact: a claim you can pin to one request and check the signature on, not a promise you have to take on trust.

Read it for what it is. This is Sable's signed claim about its own logging, not a hardware proof that the host could not have retained the plaintext. The confidential tier is the rung where that guarantee is hardware-backed; on every other tier logging: "metadata-only" is the gateway attesting, under its own signature, to the contract it operates under.

engine: what actually served

Some catalog ids are names for an engine rather than the engine itself. The flagship lane (sable, sable-fast, sable-max) most of all. Whenever the upstream engine that served the request differs from the public model id, the receipt carries an engine field naming it. On flagship receipts it is always present, because a flagship id always resolves to a disclosed engine, and because the flagship ids carry an ordered failover chain, the receipt is how you see which link in the chain ran:

{
  "model": "sable",
  "engine": "anthropic/claude-opus-5"
}

Billing prices the engine that ran, so engine is also the field that explains the metered cost when a failover served the request. This is the honesty mechanism that keeps flagship routing from being a black box: the signed receipt names the engine, every time.

scrubbed: the outbound scrub ran

A request that opted into outbound scrubbing with sable_scrub: true gets "scrubbed": true on its receipt. The field is present only when the scrub ran; it means the redaction filter executed on the outbound prompt before it left for the vendor. Nothing more. It does not change what is stored (nothing is, scrubbed or not: §3 is unchanged; only the redaction count is logged) and it is not a claim that the content was anonymized. See the scrubbing page for exactly what the filter catches and what it doesn't.

What node and region mean

node names the machine that served the request, and it only ever names something that exists. One gateway process serves inference today, so an inference receipt reads "node": "gateway". A sandbox run served by an enrolled fleet node is recorded against that node's real id; one served by the deployment's configured backend is recorded against the backend that executed the code: sandbox:http-runner or sandbox:local-docker.

GET /v1/nodes lists the same set from the same source: the gateway itself, one tee:<name> entry per configured confidential backend carrying its live attestation state, and every enrolled fleet node with a status computed from its heartbeat, never stored optimism. Each entry has a synthetic flag: true means the entry is derived from this deployment's configuration rather than from a separately enrolled machine; enrolled nodes carry synthetic: false.

Receipts issued before August 2026 name one of four seeded ids (node-fra-01, node-nyc-02, node-sin-01, node-lhr-03) describing SGX/SEV/NVIDIA-CC hardware in four cities. That hardware never existed. Those rows are retired and excluded from GET /v1/nodes, but the ids stay resolvable, because old receipts name them and a receipt is a permanent claim. Per-machine node ids are real now for fleet-served sandbox runs; inference still serves from the gateway process.

region is the region this deployment declares it runs in (one of eu-central, eu-west, us-east, us-west, ap-southeast), or the literal string unspecified when the operator has declared none. It is not your sable_region pin echoed back: a pin is honored only when it resolves to the region the gateway actually runs in, and refused with 400 otherwise. See Region pinning.

A confidential-tier receipt still reads "node": "gateway". That is the machine you called. The enclave that ran the model is identified in the attestation block below, which is the part that carries a hardware guarantee.

Confidential-tier attestation

A request on the confidential tier (today, the sable-confidential-* models) adds an attestation block (also metadata only) proving it ran inside an attested Intel TDX enclave. Standard-tier receipts omit it entirely.

{
  "attestation": {
    "platform": "tdx",
    "measurement": "3b6dd335…",
    "mrtd": "f06dfda6…",
    "tcb_status": "UpToDate",
    "verification": "tee-attested",
    "response_bound": true,
    "signer": "0x79a5061e…"
  }
}

The gateway verifies the enclave's TDX quote against a pinned measurement before routing, and verifies a per-response signature from the key bound into that quote after, so verification: "tee-attested" with response_bound: true means this specific response came from a correctly-measured enclave the host couldn't see into. The public GET /v1/attestation returns the live verified status, and the SDK's verifyAttestation() checks it as a pre-flight before you send anything.

Streaming

A streamed chat completion can't set trailing headers, so the receipt arrives as the final SSE event after the stream ends:

event: sable.receipt
data: {"receipt":"eyJ2Ijox...","signature":"0x4f8c...","signer":"0xA1b2...9F"}

Read it the same way you'd read the headers: base64url-decode receipt for the payload, verify signature against signer.

Re-fetching a receipt

Receipts are stored (metadata only, like everything else), so losing the response headers no longer means losing the proof. GET /v1/receipts/:id (session-authed, Bearer sess_…) returns the stored receipt and its signature. The :id is either the receipt's own request_id or the usage-event id shown in the dashboard history. The prompt and completion are not part of the receipt and were never stored, so a re-fetched receipt carries exactly what the original headers did.

Verifying a receipt

Because receipts use standard secp256k1 + EIP-191, any Ethereum signature library verifies them. Recover the signer over the decoded payload string (atob(receipt)) and check it equals x-sable-receipt-signer.

import { verifyMessage } from "viem";

// receipt = x-sable-receipt, signature = x-sable-receipt-sig,
// signer  = x-sable-receipt-signer
async function verifyReceipt(
receipt: string,
signature: `0x${string}`,
signer: `0x${string}`,
): Promise<boolean> {
const payload = atob(receipt); // base64url -> canonical JSON string
return verifyMessage({ address: signer, message: payload, signature });
}

// ethers works too:
//   ethers.verifyMessage(payload, signature) === signer

Verify endpoints

Two public endpoints (no auth) help you verify without wiring up crypto yourself:

The signing key is derived from the deployment's master key, so the signer address is stable per deployment: fetch it once from /v1/receipts/pubkey, pin it, and reject any receipt that recovers to a different address.

operator: who served it (receipt v:3)

When a run executes on an enrolled fleet node that counter-signs its work, the sandbox receipt carries an operator block:

"operator": { "node_id": "node-…", "output_fp": "…", "verified": true }

The serving node holds its own ed25519 key and signs a fingerprint of (input, output) for every run. The gateway verifies that signature against the key it has on record for that node (recorded on first sight, then pinned; a silent key change is refused, not attributed) and checks that the signed input fingerprint is its own hash of the code you submitted. verified: true means both held.

Read this honestly: operator attribution proves which machine served the run: the honest label is operator-attributed, not a proof that the work itself was performed correctly (that is the confidential/TEE tier). It is the accountability rung: it says who to hold responsible. A run on a node that doesn't sign, or whose signature doesn't verify, simply omits the block, and the receipt stays the earlier v:2 shape.

Verifiable context

An agent doing retrieval can declare the context it was given, and have the receipt prove those inputs. Pass sable_context on a chat or /v1/messages request: an array of strings, or objects { text, label? } where the label is a citation.

curl https://api.buildsable.com/v1/chat/completions \
-H "authorization: Bearer $SABLE_KEY" \
-H 'content-type: application/json' \
-d '{
  "model": "sable",
  "messages": [{"role":"user","content":"Summarize the policy."}],
  "sable_context": [
    {"text":"Refunds are issued within 30 days.","label":"policy.pdf#p3"},
    {"text":"Enterprise plans renew annually."}
  ]
}'

The gateway fingerprints each item in-frame and stamps a context block on the receipt. Nothing of the text is stored (§3): only sha256 fingerprints, byte lengths, and any labels.

"context": {
  "count": 2,
  "root": "9f2c…",
  "items": [
    { "label": "policy.pdf#p3", "fp": "3b1e…", "len": 33 },
    { "fp": "a07d…", "len": 27 }
  ]
}

root is sha256( sha256(item_0) ++ sha256(item_1) ++ … ) over the items in order, so it is sensitive to both content and ordering. Anyone later holding the same documents recomputes each item's sha256 (its prefix must match fp) and the root (it must match root), and so proves those exact inputs, in that order, fed this exact signed call. It is the input-side twin of the output fingerprint: proof of the context, not just the completion.

Read it honestly: this proves what context was declared, not that the model attended to it and not that the documents are true. It closes the "did the agent get the context it claims" gap, which is the one a third party can check.

Action attestation

When a chat completion emits tool calls, the signed receipt gains an actions block recording the calls the model asked for. Like everything else on the receipt it is metadata only: tool names, argument-hash prefixes, and byte lengths, never the argument text itself.

"actions": {
  "count": 2,
  "root": "7c4a…",
  "calls": [
    { "tool": "search_docs", "args_fp": "3b1e…", "args_len": 48 },
    { "tool": "send_email",  "args_fp": "a07d…", "args_len": 132 }
  ]
}

Each call's fingerprint is sha256("name|arguments"). The root is the sha256 of the full per-call hashes concatenated in emission order, so it is sensitive to both the calls and their ordering. Anyone later holding the same argument payloads recomputes each call's sha256 (its prefix must match args_fp) and the root (it must match root), and so proves the model emitted exactly those tool calls, in that order, on this exact signed call.

It works on buffered and streamed responses identically. The block is omitted entirely when a response carries no tool calls, so tool-free receipts are byte-for-byte unchanged. Past 64 calls the block is omitted rather than truncated, so a partial list can never be mistaken for a complete one. Only tool names, hash prefixes, and byte lengths reach the receipt (§3): the argument text is never stored or logged.

Read it honestly: this proves what tool calls the model emitted through Sable, not that the caller executed them and not what they returned. Together with context (the inputs) and content_fingerprint (the output), the receipt now covers inputs, output, and requested actions.

Agent runs: chain a whole session

Tag any billable request with sable_run_id (1 to 64 chars of [A-Za-z0-9._:-], accepted on chat, embeddings, /v1/messages, and sandboxes) and its signed receipt joins a per-run hash chain:

chain(n) = sha256( chain(n-1) ‖ sha256(receipt(n)) )

One head hash then proves the entire session: every model call and sandbox execution, in order, across kinds. Rewriting or omitting any step breaks every later link.

curl https://api.buildsable.com/v1/chat/completions \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
  "model": "sable",
  "sable_run_id": "my-agent-42",
  "messages": [{"role": "user", "content": "…"}]
}'

Session-authed: GET /v1/runs lists your runs; GET /v1/runs/{run_id} returns the chained receipts (each independently signed and verifiable); POST /v1/runs/{run_id}/proof mints a run proof (a signed statement carrying the head hash, receipt count, total cost, and time span) which verifies through the same public POST /v1/receipts/verify as everything else. Run heads are batch-anchored to Solana (memo format sable-runs:v1:<root>) when the deployment's anchor account is funded; until a batch finalizes, a run honestly reports no anchor. The proof's embedded trust_model states exactly what is proven: attested by the gateway over its hash chain, publicly anchored when an anchor exists, not zero-knowledge.

Sharing a receipt

Receipts are metadata-only by construction, so an owner can safely publish one: POST /v1/receipts/{id}/share (session-authed) makes it fetchable at GET /v1/receipts/shared/{request_id} and viewable at /r/{request_id} on the site: model, cost, fingerprint, and chain position, never content. DELETE /v1/receipts/{id}/share unshares it. Nothing is public unless you shared it, and unsharing takes effect immediately.