# HTTP API

> Every route under /api of the hosted app, and the proof page's files, by group: who may call it, what it takes and answers, its errors and its rate limit.

The routes of the hosted site and console, and the files of a vault's proof page, with who may call each one, what it takes, what it answers and how often. An agent pays through the SDK on chain, not over HTTP.

Status: Preview · Network: Testnet · Reviewed: 2026-11-12 · Built from: 4ab41ca1436589c22073fa842ee46f970415e3d5

Canonical page: https://probarion.xyz/docs/build/http-api

There is no public agent HTTP API. The routes under `/api` serve the hosted site and its console: sign-in, Payables, the browser extension, top-up, the Canary, the proof page's TEE check, health checks, scheduled jobs, Telegram and the alert links. The last table covers the three files a vault's proof page offers, under `/proof` instead. An agent pays from its own process through the SDK, which sends `spend` to the vault on NEAR with the agent's own key. See [Let an agent pay](https://probarion.xyz/docs/build/let-an-agent-pay.md).

## How to read the tables

Each row is one method of one route. Its anchor is `http-api-`, the method and the path after `/api`, such as [`POST /api/auth/verify`](https://probarion.xyz/docs/build/http-api.md#http-api-post-auth-verify). A proof file's anchor keeps `proof` and writes its dot as a dash, such as [`GET /proof/:vault/payments.csv`](https://probarion.xyz/docs/build/http-api.md#http-api-get-proof-vault-payments-csv).

- **Route**: the method and the path. `:vault` stands for a vault's account id.
- **Who may call**: one of five callers.
  - Public: no credential, or the code or token the request carries is the credential.
  - A signed-in session: the `pb_session` cookie that sign-in sets. The role the row names is the one the vault's membership row must hold, read from the database on every request.
  - An extension token: an `Authorization: Bearer pbx_...` header, never a cookie. These routes send no CORS headers, so no other page can read their answers.
  - `CRON_SECRET`: the bearer secret the scheduler sends. A secret shorter than 32 characters refuses every call.
  - The Telegram secret: the header Telegram sends with each update.
- "From a page at the same address" means the route compares the request's `Origin` with its own host and answers `403 bad_origin` when they differ.
- **Body**: JSON, unless the row says form data or a query.
- **Success**: the status and the answer's fields. Every JSON answer under `/api` is sent with `cache-control: no-store`. The proof files say their own caching below.
- **Errors**: every `{ error }` code the route can answer, as its status and code. Each one is a row on Errors, with what it means and what to do next, anchored as `http-<status>-<code>`, for example [`429 rate_limited`](https://probarion.xyz/docs/build/errors.md#http-429-rate_limited).
- **Rate limit**: the most calls in a window, what the window counts, and the key of the limit in `LIMITS` (`apps/web/src/lib/ratelimit.ts`). The limiter's `429 rate_limited` carries a `Retry-After` header. An IPv6 address counts by its /64. When the limiter's database cannot be reached, the route answers `503` and does nothing.

## The network of a call

A route works on the network of the address it is called at: the server matches the request's host against the origins it serves, as [Testnet and mainnet](https://probarion.xyz/docs/start/testnet-and-mainnet.md#how-the-site-picks-a-network) explains. A session, a sign-in challenge and an extension token each belong to the network they were made on. The routes that need a session answer `409 wrong_network` for a vault registered on the other network, and the faucet answers `404 not_found` instead.

The 1Click top-up routes are built for mainnet only and answer `409 mainnet_only` elsewhere. The faucet serves testnet only. NEAR testnet is live today. On mainnet one small vault runs, `v0.probarion.near`, and no mainnet address is served: see [What works today](https://probarion.xyz/docs/start/what-works-today.md).

## Sign-in

Sign-in takes two calls. `GET /api/auth/challenge` returns a message with a fresh nonce for the wallet to sign as NEP-413, and keeps a copy in the `pb_chal` cookie. `POST /api/auth/verify` takes the wallet's answer. The server uses its own message, nonce and recipient, never the ones a body carries. It burns the nonce before it reads the signature, so a challenge is good once, success or not, and it checks on chain that `publicKey` is a full-access key of `accountId`.

The session is a stateless signed cookie that lasts 12 hours. `POST /api/auth/logout` clears it in the browser, but a copy someone kept still verifies until it expires. Membership is read from the database on every request, so removing an account from a vault takes effect at once.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `GET /api/auth/challenge` | Public | None | `200` with `{ message, recipient, nonce }`, and the `pb_chal` cookie for 5 minutes | `429 rate_limited`, `503 unavailable` | 20 a minute per IP address (`authChallenge`) |
| `POST /api/auth/verify` | Public, from a page at the same address, with the `pb_chal` cookie | JSON `{ accountId, publicKey, signature }`: the wallet's NEP-413 answer, 2048 bytes at most | `200` with `{ accountId }`, and the `pb_session` cookie for 12 hours | `403 bad_origin`, `429 rate_limited`, `401 challenge_missing_or_expired`, `413 payload_too_large`, `503 wrong_chain`, `401 invalid_input`, `401 bad_signature`, `503 unavailable` | 10 a minute per IP address (`authVerify`) |
| `POST /api/auth/logout` | Public, from a page at the same address | None | `200` with `{ ok: true }`. It clears `pb_session` and `pb_chal`. | `403 bad_origin`, `503 unavailable` | None |

## Console

`POST /api/console/:vault/invoices` runs one Payables cycle on an invoice, for the vault's owner or an approver. Payables is Live on testnet. The console finds the mandate whose purpose document the upload carries, then runs the SDK's `process()` on the invoice text. `outcome` is that cycle's `SpendOutcome`, which [Spend outcomes](https://probarion.xyz/docs/build/spend-outcomes.md) explains. When `status` is `unconfirmed`, the payment was sent and its confirmation is pending: do not upload the invoice again.

> **Who signs a hosted payment**
>
> Your wallet signs nothing in this cycle. The console sends `spend` with the agent key the mandate is bound to, and anchors the ledger with the vault's ledger key: the two keys activated on the vault's Keys tab. The vault still runs every one of its checks, so the console can pay only what the mandate allows. See [How a payment is checked](https://probarion.xyz/docs/concepts/payment-checks.md).

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/console/:vault/invoices` | Signed-in session of the vault's owner or an approver, from a page at the same address | `multipart/form-data`: the invoice as `file` (PDF or `.txt`) or as `text`, and an optional `purpose` file. A `Content-Length` header is required: 4,259,840 bytes at most in all, and 4,194,304 bytes at most for each file. | `200` with `{ invoiceId, status, outcome, recorded, amountIssue, requestId }`, and a `notice` when `status` is `unconfirmed` or `awaiting_owner`, or when the refusal of `custom.invoice_number_duplicate` is the outcome's code: then it says whether an upload can run again | `403 bad_origin`, `401 unauthenticated`, `403 forbidden`, `409 wrong_network`, `429 rate_limited`, `411 length_required`, `413 too_large`, `400 invalid_input`, `415 unsupported`, `409 duplicate`, `409 busy`, `409 no_keys`, `409 no_mandate`, `409 key_unavailable`, `503 key_unavailable`, `503 chain_unavailable`, `400 empty`, `400 unreadable`, `502 failed`, `500 internal`, `503 unavailable` | 30 an hour per vault (`invoiceUpload`) |

## Browser extension

These routes let the browser extension pair with an account, read what waits for it, and queue invoices for the owner's Confirm. They are Preview: built and tested. The extension for Chrome that calls them is on the [Chrome Web Store](https://chromewebstore.google.com/detail/probarion/hhadnmnjdlamihjjoejphkimnbcfidmn) ([What works today](https://probarion.xyz/docs/start/what-works-today.md#browser-extension-for-chrome)).

A token is `pbx_` and 43 more characters. The server keeps its hash and, as a hint, its last four characters, which the Browser extension page shows under Connected browsers. The signed-in Browser extension page (`/extension`) connects the extension or shows a pairing code, and `POST /api/ext/pair` exchanges that code for a token. A code is 8 letters and digits, shown as `XXXX-XXXX`, and is good once, for 10 minutes.

`POST /api/ext/invoices` queues one invoice under the mandate the member chose in the extension, for a vault where the token's account is the owner or an approver. The role is read again on each call, and a Canary vault takes no invoices. The server reads the invoice as the console upload does, then runs these checks in order. Each refusal stores nothing:

- the file cannot be read (`422 unreadable`) or has no text (`422 empty`), or the code cannot read one clear total (`422 amount_unclear`);
- the mandate is not on the vault as the chain lists it now (`404 mandate_not_found`), or cannot take an invoice now (`422 mandate_inactive`);
- the mandate's agent key or the vault's ledger key is not active in the console (`409 no_keys`);
- the payee the invoice names contradicts the mandate's, or the vault's free `check_spend` view says it would refuse this payment now: [`422 refused_by_code`](https://probarion.xyz/docs/build/errors.md#http-422-refused_by_code), whose `rule` is `mandate.payee_mismatch` or `contract.check_spend`.

When NEAR cannot be read for the mandates or the view, the answer is `503 chain_unavailable`, and nothing is stored either. An invoice that passes these checks is stored in `awaiting_owner` with three checks: the hold `extension.mandate_chosen`, since no purpose document was matched; `mandate.payee_mismatch` as a pass; and `contract.check_spend` as a pass, or as a hold when the amount is above the mandate's approval limit and the vault would hold the payment for its approvers. The answer is `201`. When the vault already holds the invoice, nothing new is stored, and the answer is `200` with `duplicate: true` and the stored invoice's `status`.

> **Who pays a queued invoice**
>
> The route never pays. It calls no model, opens no agent or ledger key and sends no transaction. A queued invoice is paid only after the owner's Confirm in the console: a NEP-413 signature from the owner's wallet, which the server checks against a full-access key of the vault's on-chain owner before it sends the spend. The vault still runs every one of its checks on that spend, and holds it for the approvers when it is above the mandate's approval limit. The owner can reject the invoice instead, and nothing is paid.

A token can do what these routes do and nothing more: read the inbox, queue an invoice and end itself. It cannot pay, approve, confirm, or change a mandate or a key.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/ext/pair` | Public: the pairing code is the credential | JSON `{ code, label }` with a `Content-Length` header, 1 KiB at most | `200` with `{ token, account, network, expires_at }`. The token is in this answer only: the server keeps its hash and its last four characters. | `429 rate_limited`, `400 invalid_input`, `400 bad_code_format`, `401 bad_code`, `503 unavailable` | 10 in 10 minutes per IP address and network (`extPairPerIp`); 300 in 10 minutes per network (`extPairGlobal`) |
| `GET /api/ext/inbox` | Extension token: `Authorization: Bearer pbx_...` | None | `200` with `{ network, account, counts, vaults }`. Each vault lists its `awaiting` invoices, its `held` spends and its `mandates`. | `401 bad_token`, `429 rate_limited`, `503 unavailable` | 60 a minute per account and network (`extInbox`) |
| `POST /api/ext/invoices` | Extension token: `Authorization: Bearer pbx_...`, of an account that is the vault's owner or an approver. The role is read again on every request, and a Canary vault takes none. | `multipart/form-data`: `vault`, the vault's account id, `mandate`, the id of the mandate the member chose, and the invoice as `file` (PDF or `.txt`). A `Content-Length` header is required: 4,210,688 bytes at most in all, and 4,194,304 bytes at most for the file. | `201` with `{ invoice_id, status, url }`: `status` is `awaiting_owner`, and `url` opens the invoice in the console. Nothing is paid: the invoice waits there for the owner's wallet-signed Confirm. `200` with `{ invoice_id, status, duplicate, url }` when the invoice passes every check and the vault already holds it (the same text once normalised, or the same invoice number for the same payee): `duplicate` is `true`, `status` is the stored invoice's, and nothing new is stored. | `401 bad_token`, `429 rate_limited`, `411 length_required`, `413 too_large`, `400 invalid_input`, `403 not_member`, `409 wrong_network`, `415 unsupported`, `422 unreadable`, `422 empty`, `422 amount_unclear`, `503 chain_unavailable`, `404 mandate_not_found`, `422 mandate_inactive`, `409 no_keys`, `422 refused_by_code`, `503 unavailable` | 20 in 10 minutes per account and network (`extInvoice`) |
| `POST /api/ext/revoke` | Extension token: `Authorization: Bearer pbx_...` | None | `204` with no body. The calling token ends at once. | `401 bad_token`, `503 unavailable` | None |

## Top-up

On mainnet the vault's Top-up tab is built to add Base USDC to the vault's own NEAR Intents account through 1Click. It has not been tried with a live quote yet. `POST /api/topup/quote` asks 1Click for a quote and answers only with one whose signature verifies against 1Click's key and that says what was asked. It builds the deposit transaction from that quote and the network's USDC, never from what the browser sent, and only when the chain names the signed-in account as the vault's owner. `POST /api/topup/submit` tells 1Click which transaction paid the deposit address. It only speeds things up, since 1Click also finds the deposit itself. `GET /api/topup/status` passes on 1Click's status word for the deposit.

> **Owner signs**
>
> The owner signs the deposit transaction that `POST /api/topup/quote` returns, in their own wallet: a `storage_deposit` and an `ft_transfer` of USDC to the 1Click deposit address. The route only builds it.
>
> _The owner signs this transaction. Probarion never signs it._

On testnet, `POST /api/topup/faucet` mints 25 test USDC into the vault, and your wallet signs nothing: the team's mint-only key signs the mint. The faucet is Preview: it is built and tested, and its key is set on probarion.xyz; on 2026-11-09 its button minted 25 test USDC into the testnet demo vault, `demo.vaults.probarion.testnet`. It works once a UTC day per vault, within daily caps per account and for all vaults together. Those day counters start again at midnight UTC, and a `429` from one of them gives the seconds until then as its `Retry-After`. Each press also counts against a press limit per account, before any read of the chain, and that limit's `429` gives the end of its own window instead. A refusal from the checks that run before the day counters does not use up the vault's day.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/topup/quote` | Signed-in session of the vault's owner, from a page at the same address | JSON `{ vault, amountUsdc }`, the amount as a decimal string | `200` with `{ quote, tx }`: a 1Click quote whose signature checked out, and the deposit transaction for the owner to sign | `403 bad_origin`, `400 invalid_input`, `401 unauthenticated`, `403 forbidden`, `409 wrong_network`, `409 mainnet_only`, `503 not_configured`, `429 rate_limited`, `503 unavailable`, `400 invalid_amount`, `503 chain_unavailable`, `409 asset_not_allowed`, `502 quote_rejected`, `502 quote_unavailable` | 10 a minute per account and network (`topupQuote`) |
| `GET /api/topup/status` | Signed-in session of the vault's owner | None. Query: `vault` and `depositAddress` | `200` with `{ status, updatedAt }` as 1Click reports them. `updatedAt` may be missing. | `400 invalid_input`, `401 unauthenticated`, `403 forbidden`, `409 wrong_network`, `409 mainnet_only`, `503 not_configured`, `429 rate_limited`, `503 unavailable`, `502 status_unavailable` | 30 a minute per account and network (`topupStatus`) |
| `POST /api/topup/submit` | Signed-in session of the vault's owner, from a page at the same address | JSON `{ vault, txHash, depositAddress }` | `200` with `{ ok: true }`, even when 1Click refuses or cannot be reached | `403 bad_origin`, `400 invalid_input`, `401 unauthenticated`, `403 forbidden`, `409 wrong_network`, `409 mainnet_only`, `503 not_configured`, `429 rate_limited`, `503 unavailable` | 10 a minute per account and network (`topupSubmit`) |
| `POST /api/topup/faucet` | Signed-in session of the vault's owner, from a page at the same address | JSON `{ vault }` | `200` with `{ status: "minted", txHash, amount: "25" }`. `202` with `{ status: "unknown", txHash, detail }` when the mint was sent and its outcome is not known. | `404 not_found`, `403 bad_origin`, `400 invalid_input`, `401 unauthenticated`, `503 not_configured`, `429 rate_limited`, `409 not_owner`, `409 wrong_token`, `503 unavailable`, `502 mint_failed` | 10 in 10 minutes per account (`faucetTries`); 1 a day per vault (`faucetVault`); 3 a day per account (`faucetAccount`); 50 a day in all (`faucetGlobal`) |

## Canary

`POST /api/canary/attempt` is the public Canary challenge, which is Live on testnet. Mainnet has no Canary vault, so the route answers `503 not_ready` there. A visitor's text goes to a deliberately gullible model, then through the SDK's `process()` as an invoice does: `code` is what the code rules and the vault decided. `raw` says whether a payment the code refused was also sent to the vault unfiltered, which happens only when the vault's free dry run says it would refuse it too. An attempt is stored as hashes and labels, never as the visitor's text, and a visitor is told apart by a keyed hash of the `pb_canary` cookie and of the IP address. `429 budget` carries a `Retry-After` with the seconds until that limit is over.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/canary/attempt` | Public, from a page at the same address | JSON `{ text, via }`: `text` is at most 4000 characters, and `via` is optional | `200` with `status: "done"`, `attemptId`, `techniques`, `proposal`, `code` and `raw` | `403 bad_origin`, `503 not_ready`, `429 rate_limited`, `400 invalid_input`, `400 empty`, `400 too_long`, `429 budget`, `409 busy`, `503 closed`, `502 failed`, `503 unavailable` | 5 in 10 minutes per IP address (`canaryModelVisitor`); 300 an hour in all (`canaryModelGlobal`). Past those, 6 attempts an hour per IP address, 60 an hour and 600 a day in all, and 2,000,000 model tokens a UTC day (`CANARY_LIMITS`), each answered with `429 budget` |

## TEE check

`GET /api/tee/check` is the live TEE check on a vault's proof page. The visitor's browser draws a nonce of 32 random bytes and sends it as `?nonce=`, with no cookie and no `Referer`, and nothing the visitor typed or uploaded. A call that another site's page makes in a browser, which the browser marks in `Sec-Fetch-Site`, answers `403 bad_origin` before the rate limit, so it cannot spend the counter all visitors share. The server asks NEAR AI Cloud's public attestation report, with no API key, for that nonce and `NEARAI_MODEL`, the model Payables is set to use, or, when it is unset, for the SDK's preferred private models in order until one answers. On the report's first four model attestations at most, it checks that `report_data[32..64]` and `request_nonce` are the nonce, that `report_data[0..32]` is the signing address, O1 (Intel DCAP on the TDX quote) and O2 (NVIDIA NRAS on the GPU evidence). `attestations_total` says how many the report holds, and a pass over fewer than that says so. A check that cannot run is `unavailable`, not `pass` or `fail`: NEAR AI does not answer within 12 seconds, Intel or NVIDIA does not answer within the check's 24 seconds, the verifier does not load, or, for O2, the report carries no GPU evidence. The answer carries the few values of the report the browser needs to check both bindings again with its own nonce, and the card shows the worse of the browser's verdict and the server's. The check reads the TEE that serves the model now, not any past decision, and the TEE measurement is not pinned. Its rate limit is one counter for all visitors together, so it stores nothing about a visitor.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `GET /api/tee/check` | Public | None. Query: `nonce`, 64 lowercase hex characters, which the visitor's browser draws | `200` with `{ nonce, model, checked_at, elapsed_ms, checks, attestations, attestations_total }`. Each of the checks `nonce`, `signer`, `o1` and `o2` is `pass`, `fail` or `unavailable`, with a detail. Each attestation holds `signing_address`, `request_nonce`, `report_data`, `tcb_status` and `gpu`, and nothing else of NEAR AI's report. `attestations_total` is how many model attestations the report holds, of which the first four at most are checked. | `403 bad_origin`, `400 invalid_input`, `429 rate_limited`, `503 unavailable` | 30 a minute in all (`teeCheck`) |

## Health

The health routes are for uptime monitors. They answer `ok` and, where they give one, a `detail` the server writes itself, never a third party's own text, and they have no `{ error }` codes. `GET /api/health` calls no other service: it says whether this deployment can read its settings. The RPC and NEAR AI Cloud probes make one call each, of 5 seconds at most.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `GET /api/health` | Public | None | `200` with `{ ok: true, service, network, commit, time }` | None of the `error` codes. `503` with `ok: false` and `detail: "settings"` when the deployment cannot read its settings. | None |
| `GET /api/health/rpc` | Public | None. Query: `provider`, a 0-based index into the network's RPC list, `0` by default | `200` with `{ name, ok, latency_ms, detail, network, provider, providers }` when the provider answers for the right chain with a fresh block | None of the `error` codes. `503` with `ok: false` when the probe fails or the settings cannot be read, and `404` with `ok: false` for an index the list does not have. | None. Each probe's answer is kept 30 seconds per server instance |
| `GET /api/health/nearai` | Public | None | `200` with `{ name, ok, latency_ms, detail, configured }` when NEAR AI Cloud's public model list answers with at least one model. `configured` says whether this deployment holds the model key. | None of the `error` codes. `503` with `ok: false` when the model list does not answer with a model. | None. Each probe's answer is kept 30 seconds per server instance |

## Scheduled jobs

The scheduler calls these routes with `Authorization: Bearer` and the `CRON_SECRET`, and any other call gets `401 unauthorized`. `vercel.json` runs `/api/cron/watch` at 00:00 UTC and `/api/cron/metrics` at 00:10 UTC each day. The watch reads what is new on each vault registered with the app, network by network, pings the heartbeat monitor when every network was healthy, then sends those vaults' alerts. The metrics run computes the usage numbers and prunes the rate limiter's old rows. Both cover every network the deployment serves, mainnet first, and answer with counts only.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `GET /api/cron/watch` | `CRON_SECRET`, sent as `Authorization: Bearer` and the secret | None | `200` with `{ ok: true, networks, heartbeat }` | `401 unauthorized`, `503 unavailable`. A run where a network's watch was not healthy is a `503` with `ok: false` and the same fields. | None |
| `GET /api/cron/metrics` | `CRON_SECRET`, sent as `Authorization: Bearer` and the secret | None | `200` with `{ ok: true, networks, prune }` | `401 unauthorized`, `503 unavailable`. When a network's numbers fail, the `503` also carries `prune`. | None |

## Telegram

Telegram calls `POST /api/telegram/webhook` for each update to the alerts bot. An owner or an approver opens the bot link from the vault's alerts page, Telegram sends `/start <code>`, and the chat is linked to the vault. `/stop` ends every alert of the chat. A wrong secret is a `401` before anything is read. Past that check and the rate limit, the route answers `200`, so Telegram does not resend an update the route has read. A `429`, or a `503` when the key or the limiter's database fails, comes before the update is read: a status Telegram retries, so the update is not dropped.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/telegram/webhook` | Telegram secret: the `x-telegram-bot-api-secret-token` header that `setWebhook` was given | A Telegram update. The route acts on `/start <code>` and `/stop` only. | `200` with `{ ok: true }` for every update past the secret check and the rate limit, even one it cannot act on | `503 unavailable`, `503 not_configured`, `401 unauthorized`, `429 rate_limited` | 120 a minute in all (`telegramWebhook`) |

## Alert links

An alert email's confirmation link opens a page whose button posts the code to `POST /api/alerts/confirm`. The code is good once, for 24 hours. The unsubscribe link, from the email's `List-Unsubscribe` header or the stop page's button, posts to `POST /api/alerts/stop`. Both routes take a POST only, because mail scanners open links and opening one must change nothing. Neither checks the origin: the code or the token is the credential.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `POST /api/alerts/confirm` | Public: the confirmation code from the email is the credential | Form data: `code`, 32 hex characters | `303` to `/console/<vault>/alerts?verified=1`. A code that is malformed, used, expired or unknown goes back to `/alerts/confirm`. | `429 rate_limited`, `503 unavailable` | 30 a minute per IP address (`alertLink`) |
| `POST /api/alerts/stop` | Public: the keyed token in the link is the credential | Query: `s` and `t` from the link. The stop page adds the form field `from=page`. | `200` with `{ ok: true }`, or with `from=page` a `303` back to `/alerts/stop` with `done=1`. The answer is the same whether or not the subscription existed or was on. | `400 invalid_link`, `503 unavailable`. With `from=page`, a failure is a `303` with `failed=1` instead of the `503`. | None: a valid token is all it needs |

## Proof files

A vault's proof page offers three downloads, served under `/proof/<vault>/` rather than `/api`. Each one answers for a vault whose owner made its proof page public, or for a signed-in member of the vault, on the network of the address it is called at. Anyone else gets `404 not_found`, the same answer as for a vault nobody registered, so a file says nothing about who is a customer. A browser does not reuse an answer: a public vault's answer is sent with `cache-control: private, max-age=0, must-revalidate`, a member's with `private, no-store`. The three share the `snapshot` limit, and each counts its own calls.

`snapshot.json` and `ledger.jsonl` are the files [Verify a vault yourself](https://probarion.xyz/docs/trust/verify-a-vault.md) runs the verifier on. `payments.csv` is the same vault for a spreadsheet: one row per decision of the ledger, newest first, built by the code that builds the page's Decisions cards. The download takes its own snapshot, as `snapshot.json` does, so it can be older or newer than the page's: compare its Chain read column, or the time in the file name, with the page's "snapshot taken" line. The testnet demo vault's [`payments-2026-11-09.csv`](https://github.com/probarion/probarion/blob/main/docs/evidence/demo-vault/payments-2026-11-09.csv) is the file for its committed snapshot.

Its columns are Date (UTC), Mandate, Label, Payee, Amount (USDC), Decision, Reason, Spend id, Status now, Chain read (UTC), Spend transaction, Explorer link, Invoice SHA-256 and Ledger seq. The header row is the same in every file. Three of them in more detail:

- **Payee** is the destination of the spend the chain binds to the decision. It is empty when the chain holds no spend for it: nothing was sent. When a rule refused an invoice because it named another account than the mandate's payee, the Reason names both.
- **Status now** is each spend's status on chain when the snapshot read it, and **Chain read (UTC)** is that time, on every row. A spend whose payment entry says held can read paid there: an approver approved it on chain since.
- **Spend transaction** is the tx that sent the spend, as its payment entry records it, and Explorer link opens it. For a spend the contract held and an approver approved later, that tx created the hold and moved no USDC. The ledger does not record the approve tx, so the file does not have it.

A spreadsheet may read a cell that starts with `=`, `+`, `-`, `@`, a tab or a carriage return as a formula. So the file puts an apostrophe before each of them that starts a cell, after any spaces or quotes: at the start of a field, and after a `;` or a line break inside one, since Excel splits each line on `;` in a locale whose list separator is the semicolon.

| Route | Who may call | Body | Success | Errors | Rate limit |
| - | - | - | - | - | - |
| `GET /proof/:vault/snapshot.json` | Public, for a vault whose owner made its proof page public. Otherwise a signed-in member of the vault, read from the `pb_session` cookie; for anyone else the answer is `404 not_found`, as for a vault nobody registered | None | `200` with a snapshot of the vault, its ledger and the chain data read for it, as `application/json; charset=utf-8`, saved as `probarion-<vault>-snapshot.json`: the file `probarion verify --offline` reads | `404 not_found`, `429 rate_limited`, `503 unavailable` | 30 a minute per IP address (`snapshot`) |
| `GET /proof/:vault/ledger.jsonl` | Public, for a vault whose owner made its proof page public. Otherwise a signed-in member of the vault, read from the `pb_session` cookie; for anyone else the answer is `404 not_found`, as for a vault nobody registered | None | `200` with the vault's signed ledger, one entry per line, as `application/x-ndjson; charset=utf-8`, saved as `probarion-<vault>-ledger.jsonl`: the file `--ledger` reads. It reads the database only, never the chain. | `404 not_found`, `429 rate_limited`, `503 unavailable` | 30 a minute per IP address (`snapshot`) |
| `GET /proof/:vault/payments.csv` | Public, for a vault whose owner made its proof page public. Otherwise a signed-in member of the vault, read from the `pb_session` cookie; for anyone else the answer is `404 not_found`, as for a vault nobody registered | None | `200` with one row per decision of the ledger, newest first, joined to its payment entry and to its spend's status on chain when the snapshot was taken, as `text/csv; charset=utf-8` with a BOM and CRLF line ends, saved as `<vault>-payments-<capture time>.csv` | `404 not_found`, `429 rate_limited`, `503 unavailable` | 30 a minute per IP address (`snapshot`) |

## What these tables do not cover

- An agent API or a webhook to your server. An agent pays through the SDK ([SDK reference](https://probarion.xyz/docs/build/sdk-reference.md)), or through the MCP server `@probarion/mcp`, which is Live: it runs beside the agent and speaks MCP over stdio, so it adds no route here. Probarion calls no URL of yours: alerts go out by email and Telegram.
- What the console's screens and the Browser extension page do besides the routes above. That runs as Next.js server actions under `apps/web/src/app/console` and `apps/web/src/app/extension`, not as routes under `/api`.
- The other public files outside `/api`: the docs' own `/docs/<page>.md` twins, `/llms.txt`, `/llms-full.txt` and `/docs/search-index.json`.

## Related pages

- [Errors](https://probarion.xyz/docs/build/errors.md#http-api-errors): what each HTTP error code means and what to do next.
- [Let an agent pay](https://probarion.xyz/docs/build/let-an-agent-pay.md): the payment loop an agent runs, with no HTTP call to Probarion.
- [Spend outcomes](https://probarion.xyz/docs/build/spend-outcomes.md): how to read the `outcome` of a Payables cycle.
- [Security model](https://probarion.xyz/docs/trust/security-model.md): who holds which key, including the agent and ledger keys the console keeps sealed.
- [What works today](https://probarion.xyz/docs/start/what-works-today.md): what is Live, Preview and Planned.
- The sources: [`apps/web/src/app/api`](https://github.com/probarion/probarion/tree/main/apps/web/src/app/api), [`apps/web/src/lib/http.ts`](https://github.com/probarion/probarion/blob/main/apps/web/src/lib/http.ts) and [`apps/web/src/lib/ratelimit.ts`](https://github.com/probarion/probarion/blob/main/apps/web/src/lib/ratelimit.ts).
