# trustpdf API

Forensic facts about a PDF, cited to byte offsets.

This service reports what is **in** a file: its revision history, what its signatures
actually cover and whether the signed bytes still match, and what it says about its own
origin. Every observation carries the evidence it came from and both readings of that
evidence — the ordinary explanation and the adversarial one, side by side and unweighted.

It returns **no score, no risk level, and no judgement of authenticity.** That decision
belongs to whoever bears the consequences of getting it wrong. We supply the evidence.

- **Base URL** — `https://trustpdf.io`
- **Report contract** — `https://trustpdf.io/v1/schema`
- **Ceiling** — 4 MB per document, on every route

---

## Choosing a route

| | pay | best for |
|---|---|---|
| **[On demand](#1-on-demand)** | per document, at the moment you send it | occasional use, or a caller that holds no balance |
| **[Member](#2-member)** | once, up front, then spend it down | anything repeated. Cheaper per document, and faster |
| **[Test](#3-test)** | nothing | proving an integration before you commit to it |

All three return the identical report. What differs is how it is paid for and how much
you may send.

---

## 1. On demand

Pay for one document at the moment you send it. No key, no balance, nothing held between
calls.

### The exchange

```
1. POST /v1/analyze  ──▶  402, with the price list
2. sign the option you want
3. POST /v1/analyze  ──▶  200, the report, with your receipt
      + PAYMENT-SIGNATURE
```

**Step 1 — ask.** Send the document's `Content-Length` and no payment. The answer is
**402** carrying a `PAYMENT-REQUIRED` header: base64 JSON listing every price, the token,
the network, and the EIP-712 domain to sign against. The price is quoted from the declared
length, so a document that would be refused is never uploaded.

**Step 2 — sign.** Pick the option that covers your document and sign an EIP-3009
`transferWithAuthorization` for it. Your wallet needs no gas; the authorisation is
broadcast for you.

**Step 3 — send.** Repeat the request with a `PAYMENT-SIGNATURE` header — base64 JSON
carrying the signature and the authorisation — and the document as the body. The report
comes back with `PAYMENT-RESPONSE`, your on-chain receipt.

Payment is [x402](https://x402.org), an HTTP-native protocol. Network
`eip155:84532`, settled in USDC. There is no account anywhere in it.

### Price

| document | price | units |
|---|---|---|
| up to 4 MB | $0.05 | 1 |

**$0.05 per document, whatever it weighs**, up to the ceiling.
Nothing about a file changes what it costs, so a quote needs only its size to be refused
or accepted — and you can hold the price in your head instead of computing it.

The right-hand column is the same document expressed in member units, so the two routes
price identically and cannot drift apart. Compare it against the member rate below before
choosing: **on demand is five to ten times the member price**, because a settlement costs
us the same whether it buys one document or ten thousand.


---

## 2. Member

Buy units once, spend them down, and skip the payment round trip on every call after that.

### Buying

```
1. POST /v1/bundle  ──▶  402, with the package list
2. choose a package and sign it
3. POST /v1/bundle  ──▶  200, your key
      + PAYMENT-SIGNATURE
```

Identical in shape to the on-demand exchange, and paid the same way. Which package you
bought is read back from the amount you signed, so there is nothing to declare and nothing
to get wrong.

| units | price | per unit |
|---|---|---|
| 100 | $1.00 | $0.0100 |
| 1,000 | $7.50 | $0.0075 |
| 10,000 | $50.00 | $0.0050 |

**Topping up is the same call.** Send a key you already hold in `Authorization` and the
purchase credits that key instead of issuing a new one.

The key is shown **once** and cannot be recovered. It is a bearer instrument: we store its
digest and a balance, and nothing that identifies you. Anyone holding it can spend it.

**A key bought here works over MCP, and the other way round.** The ledger holds a digest, a
balance and a timestamp — it has never known which surface sold a key or where it is spent.

### Using

```
POST https://trustpdf.io/v1/analyze
Authorization: Bearer tp_...
Content-Type: application/pdf

<the pdf bytes>
```

```bash
curl -X POST https://trustpdf.io/v1/analyze \
  -H "Authorization: Bearer $TRUSTPDF_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @document.pdf
```

One call. The report comes back with `CREDITS-REMAINING` — the units left on the key
after this document — so a balance never has to be asked for separately.

JSON works too, for callers whose input must be JSON:

```
Content-Type: application/json

{"document": "<base64 of the pdf>"}
```

Base64 declares about a third more length than the document holds, and the price is quoted
from what you declare. Send raw bytes where you can.

### Price

| document | units |
|---|---|
| up to 4 MB | 1 |

The same scale as on demand, so a document costs the same whichever route it arrives on.
At the best package rate that is $0.0050 a document, against $0.05
paying per document.

### Sending the same document twice

If you send bytes you have already been given a report for, under the same engine, you get
that report back at **no charge**, and marked. `CACHED: 1` and `ANALYSED-AT`, the unix second
you first received it. Nothing is deducted and `CREDITS-REMAINING` does not move.

It is the same artefact, not a new one: the engine is deterministic, so the same bytes under
the same ruleset produce the same observations, byte for byte. You paid for the analysis
once. A ruleset change makes it a new document again, because the answer could differ.

```bash
curl -X POST https://trustpdf.io/v1/analyze?force=1 \
  -H "Authorization: Bearer $TRUSTPDF_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @document.pdf
```

`force` runs the engine anyway and costs the same as any other check. Over MCP it is
`force: true`. **Paying per document never returns a stored report at all** — an on-demand
payment always runs the engine.

### What we will not tell you

We will never tell you whether *anyone else* has sent a document. Not with a flag, not by
answering faster, not by leaving your allowance untouched. `CACHED` says "you have had
this", never "this has been seen".

That distinction is the reason a test check is paced rather than instant: a reply arriving
faster than the work it represents would say a stranger had sent those exact bytes, and for
a forensics service that is a fact about the stranger — whether a document is circulating,
whether a counterparty has been checking it. Uploading a copy is not a way to find out.

### Your history

```bash
curl https://trustpdf.io/v1/history?limit=20 -H "Authorization: Bearer $TRUSTPDF_KEY"
```

One row per call: when, your document's sha256, its size, how long it took, what it cost,
what was left afterwards, and the User-Agent your caller sent. It answers only for the key
that asks — there is no parameter that could widen it — and it spends nothing.

---

## 3. Test

Prove the integration before you pay for it. Send the same request with **no**
`Authorization` header and **no** payment.

```bash
curl -X POST https://trustpdf.io/v1/analyze \
  -H "Content-Type: application/pdf" \
  --data-binary @small.pdf
```

The report is the real one — same engine, same contract, nothing withheld.

### Limits

| | |
|---|---|
| size | up to 4 MB |
| everyone | one document every 15 minutes |
| you | one document every 3 hours |

The first limit is shared. If anyone tested in the last 15 minutes,
the next one is not available yet — no matter whose turn it was.

A refusal carries `TEST-NEXT-AT`, a unix timestamp for when to come back, and the full
price list — so a caller who would rather not wait can pay and continue in the same exchange.

**This is a test bench, not a tier.** It is bounded so that it cannot be built on, and
those bounds are the reason it can stay open at all.

---

## What comes back

A report validating against the published contract at `https://trustpdf.io/v1/schema`.

```json
{
  "schema": 1,
  "engine": { "version": "1.1.0-beta", "ruleset": "2026.07.28.2-beta" },
  "input": { "sha256": "26c3d285…", "bytes": 1255 },
  "observations": [
    {
      "id": "SIG.BYTERANGE_GAP",
      "statement": "Signature in object 2 covers bytes 0–179, 1205–1222. Bytes 1222–1255 are not covered by the signature.",
      "evidence": { "object": 2, "covered": [[0, 179], [1205, 1222]], "uncovered": [[1222, 1255]] },
      "causes": {
        "benign": "A viewer appended annotation or form data after signing.",
        "adversarial": "Content inserted after signing (incremental update / shadow attack)."
      }
    }
  ],
  "undetermined": [
    {
      "id": "SIG.CHAIN_UNRESOLVED",
      "statement": "The certificate chain for signature object 2 was not validated: this ruleset pins no trust list, so no chain was built.",
      "evidence": { "object": 2 }
    }
  ]
}
```

**Read both arrays.** `observations` is what was found. `undetermined` is what could not
be checked and why, which matters just as much — an absent finding and an unrunnable check
look identical if you only read one of them.

`schema` is the envelope version. Refuse a report whose version you do not recognise
rather than guessing at it.

## Response headers

| header | when |
|---|---|
| `CREDITS-REMAINING` | a key was spent — units left on it |
| `CACHED` | `1` — this is a report **you** were already given, returned at no charge |
| `ANALYSED-AT` | with `CACHED` — unix seconds, when you first received it |
| `TEST-NEXT-AT` | a test check — unix seconds until the next one |
| `PAYMENT-REQUIRED` | 402 — the price list, base64 JSON |
| `PAYMENT-RESPONSE` | a payment settled — the receipt |

## Refusals

Every refusal is `{ "error": "...", "code": "...", "hint": "..." }`. Branch on `code`;
it is stable. `error` is a sentence for a person, and `hint` says what to do next.

| code | status | meaning |
|---|---|---|
| `key_required` | 401 | no key, and no test check available |
| `key_unknown` | 401 | that key is not one of ours, or was revoked |
| `credit_exhausted` | 402 | the key has fewer units than this document costs |
| `credit_required` | 402 | no test check available, and this route takes no single payment |
| `quote` | 400/411/413 | missing `Content-Length`, empty body, or over 4 MB |
| `intake` | 400/413 | the body was not a readable document of the declared size |
| `engine` | 502 | the analysis engine did not answer |

## Endpoints

| | |
|---|---|
| `POST /v1/analyze` | a report — paid per document, or against a key, or as a test |
| `POST /v1/bundle` | buy units, or top up the key you hold |
| `GET /v1/history` | what your key has been spent on. Needs the key; costs nothing |
| `GET /v1/schema` | the report contract, JSON Schema |
| `GET /v1/stats` | how many documents have been analysed |
| `GET /v1/docs` | this document |
| `POST /mcp` | the same service over MCP |

## MCP

```json
{ "mcpServers": { "trustpdf": {
    "type": "http",
    "url": "https://trustpdf.io/mcp",
    "headers": { "Authorization": "Bearer tp_..." } } } }
```

The same capabilities as tools — `analyze_pdf`, `purchase_bundle`, `usage_history` —
plus resources a REST caller has no way to ask for: the report contract, every observation
identifier this engine can emit with what each one means, and this manual.

An agent can read all of that before spending anything, which is the reason to prefer MCP
where you have the choice.

## Limits and handling

- Documents up to 4 MB on every route.
- `Content-Length` is required. The price is quoted before the body is read, so a
  document that will be refused is never uploaded.
- **Your document is never stored.** It exists as bytes for the length of one request.
  Nothing kept afterwards can reconstruct it or read a word of its content.
