Deuxième couche
L’API, le format, la chaîne
Tout ce dont un reçu est fait, et chaque route qui en produit ou en sert un. Quelqu’un vous a remis un reçu et vous voulez le démonter sans notre aide — c’est à cela que sert cette page, et pouvoir le démonter sans notre aide est toute la conception.
L’accès, honnêtement
Il n’y a pas de clé d’API, pas de compte et pas de formule qui débloque un débit supérieur. Chaque route ci-dessous est soit publique, soit protégée par un jeton qui appartient à une session ou à un reçu et qui est remis à qui l’a lancée.
Les routes qui portent un document ou l’argent refusent une requête dont l’en-tête Origin nomme un hôte qui n’est pas le nôtre. Une requête qui n’envoie aucun en-tête Origin — un serveur qui appelle un serveur — n’est pas refusée, et nous préférons le dire ici plutôt que de laisser quelqu’un le découvrir et supposer que c’était caché. Ce n’est pas une surface d’intégration prise en charge : elle n’a pas de promesse de version, pas de clé à faire tourner et pas de limite de débit réglée pour elle, et elle peut changer à n’importe quel déploiement.
Si vous voulez bâtir là-dessus sérieusement, écrivez-nous. Une surface prise en charge, versionnée et à clés est une chose que nous construirions pour un appelant réel et que nous n’avons pas construite pour un appelant hypothétique.
The API
La table ci-dessous est le protocole lui-même et n’est publiée qu’en anglais.
Sign in to the console with your email, make a key, and send the citations you already have, or a whole document. Every completed check seals a receipt onto the same public chain as everything else here.
curl -X POST "https://exhibitb.autofract.com/api/v1/checks?wait=30" \
-H "Authorization: Bearer xb_live_<id>_<secret>" \
-H "Content-Type: application/json" \
-d '{"level": "says", "claims": [
{"id": "c1", "citation": "https://www.rfc-editor.org/rfc/rfc6962",
"quote": "publicly logging the existence of Transport Layer Security (TLS) certificates"}]}'Prices, per claim
| EXISTS | 1 credit · $0.02 | Is the source there: the registries, a fresh fetch from two vantage points, a hash of what came back. |
|---|---|---|
| SAYS | 3 credits · $0.06 | Are the quoted words in it, and at which character offset. |
| HOLDS | 10 credits · $0.20 | An adversarial pass over one claim. Returned to you while the check's session lives, never sealed, never stored. Not on Free. |
Free is 350 credits a month (200 EXISTS and 50 SAYS, in one balance), resetting on the 1st, UTC. Each claim is charged at the level its receipt records, so a SAYS claim with no quote to read costs what EXISTS costs. A check that fails costs nothing, and a document longer than your balance is read as far as it covers, with the rest counted in the receipt as not checked.
Court citations are checked against a public case registry whose daily allowance this API shares with the site and the nightly benchmark. Past it, the engine falls back to the registry's open search, and then to no verdict rather than a guess.
Webhooks
One endpoint per account, https and public. Events: check.completed, check.failed. Each carries exhibitb-signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t.body)>; reject anything older than five minutes. Nothing a webhook carries is text from your document.
The whole contract is /api/v1/openapi.json (OpenAPI 3.1).
Routes
Chaque réponse est du JSON. Les routes de session sont en no-store et noindex ; un jeton bien formé dont la session s’est terminée répond 404 avec expired, ce qui n’est pas la même chose qu’un jeton qui n’a jamais existé.
La table ci-dessous est le protocole lui-même et n’est publiée qu’en anglais.
| Méthode | Route | Ce qu’elle fait | Répond |
|---|---|---|---|
| POST | /api/check | Start a check. Multipart with a file, or JSON {text, ref}. A ref of word-win, word-mac or word-web over a posted string records that the caller assembled the string out of an open document, and the receipt then names the add-in's extractor rather than the file reader's. — qui: same-origin | 202 {token, expires_at, arm} |
| GET | /api/check/:token | The session as it stands: every claim found, and each verdict as it lands. This is what the page polls while a check runs. — qui: the token | the session view |
| GET | /api/check/:token/status | The completion signal, read off the database row rather than out of memory — so a deploy in the middle of a payment does not tell somebody who has paid that their session is gone. — qui: the token | {receipt_id, sealed, checked_out, paid, closed, session} |
| GET | /api/check/:token/preview.json | The unsigned, unchained receipt-shaped preview of a check that has finished but has not been sealed. — qui: the token | a receipt-shaped object with no signature |
| GET | /api/check/:token/binding.json | The binding bundle: what turns the per-claim fingerprints in a receipt back into the claims they were computed from. — qui: the token | a downloaded file |
| POST | /api/check/:token/holds | Run the adversarial third level over one claim. Off by default, never sealed into a receipt, and it sends the sentence you wrote. — qui: the token | 202 |
| DELETE | /api/check/:token | End the session now and drop the preview. The session expires on its own either way. — qui: the token | {deleted: true} |
| POST | /api/checkout | Turn a finished check into an invoice. {token, email, tier}. — qui: same-origin | a payment URL |
| GET | /api/receipt/:id | The public projection of a sealed receipt: every cited URL removed, every verdict, count and hash kept, signed in its own right. | the stored canonical bytes |
| GET | /api/receipt/:id/proof | The inclusion proof pointing at the day's Merkle root. 202 while that day has not been rooted yet. | an audit path |
| GET | /r/:id/receipt.json | The full sealed receipt, cited URLs and all. This is the creator's file, and the reason the projection exists. — qui: the creator's link | the sealed bytes |
| GET | /r/:id/receipt.pdf | The printable receipt: the page somebody staples to a report and is asked about under oath. — qui: the creator's link | a PDF |
| GET | /r/:id/qr.png | The receipt's QR as a raster, for a printed page. | a PNG |
| GET | /chain/head | Where the chain is now. | {seq, self_hash, issued_at, last_root} |
| GET | /chain/root/:date | One UTC day's signed root file, exactly as it was stored and published. | the stored canonical bytes |
| GET | /chain/verify | Our own replay of the chain, and the first break in it if there is one. Rooted days are re-derived on every call; the answer is memoised for sixty seconds. | {ok, checked, head_seq, first_broken, …} |
| GET | /keys.json | Every public key this install has ever signed with, so a verifier can pin them rather than trust the key a receipt carries about itself. | the key set |
| GET | /healthz | Whether the service is up. | {ok} |
Une route de reçu ne confirme jamais un identifiant qu’elle ne détient pas, et une route d’administration sans son jeton répond octet pour octet comme un chemin derrière lequel il n’y a aucune route — même statut, mêmes en-têtes, même corps. C’est voulu : la différence entre les deux réponses serait sinon une carte de ce qui existe.
Un serveur pour les agents IA
C’est un serveur MCP distant à l’adresse https://exhibitb.autofract.com/api/mcp, en Streamable HTTP, pour les agents IA et tout client MCP capable d’envoyer un en-tête. Il enveloppe l’API ci-dessus et rien de plus — la même clé, les mêmes crédits, les mêmes reçus — sous la forme de cinq outils : check_citations, check_document, get_check, get_receipt et get_usage. Créez une clé dans la console et envoyez-la dans l’en-tête Authorization ; les lignes de configuration ci-dessous relèvent du protocole et sont publiées en anglais. Il n’y a rien à installer.
La table ci-dessous est le protocole lui-même et n’est publiée qu’en anglais.
What every client asks for
URL https://exhibitb.autofract.com/api/mcp
Header Authorization: Bearer xb_live_<id>_<secret>An mcpServers entry, for a client that reads one
{
"mcpServers": {
"exhibitb": {
"type": "http",
"url": "https://exhibitb.autofract.com/api/mcp",
"headers": {
"Authorization": "Bearer xb_live_<id>_<secret>"
}
}
}
}Tools
| check_citations | Citations you already have, with the words quoted from each: EXISTS and SAYS, one receipt. |
|---|---|
| check_document | A whole text: finds its citations, checks them, one receipt. |
| get_check | A check by id: its status, the verdicts, the receipt once sealed. |
| get_receipt | The signed receipt: in full to the account that sealed it, the public projection to anyone else. |
| get_usage | The plan, the allowance and the credits left this month. |
Protocol revisions 2026-07-28 and 2025-03-26 through 2025-11-25, answers in JSON. A check tool waits up to 45 seconds and otherwise answers with the id to read later. A call without a good key comes back as a tool error that says where to get one. A client that connects only through OAuth cannot use this server yet.
La signature
Un reçu est du JSON canonique. Son self_hash est le SHA-256 des octets canoniques du reçu privé de deux membres de premier niveau — self_hash lui-même et signatures — de sorte que l’empreinte vit hors d’elle-même et que les signatures vivent hors du corps haché. C’est cela qui permet de contresigner un reçu plus tard sans que son empreinte bouge.
La signature est Ed25519 sur les 32 octets bruts obtenus en décodant self_hash depuis l’hexadécimal. Pas sur les 64 caractères hexadécimaux, pas sur le JSON canonique, pas sur une empreinte de l’empreinte : Ed25519 hache lui-même son entrée, donc signer le condensé directement est tout le schéma.
Chaque valeur base64 du format est canonique — l’alphabet standard, le remplissage complet, aucun espace, et les bits de poids faible inutilisés du dernier symbole à zéro — de sorte qu’une clé a une seule écriture et que deux clés se comparent comme octets ou comme texte avec la même réponse. Un décodeur qui tolère un remplissage manquant ou l’alphabet compatible URL lirait les mêmes octets depuis plusieurs écritures ; ce format n’en autorise qu’une.
Un reçu porte une seule signature, dont le rôle est issuer. Zéro dit qu’il n’a jamais été émis ; deux ou plus disent qu’il ne peut pas indiquer par qui ; les deux sont des échecs. La signature est vérifiée contre la clé publiée pour son identifiant de clé, jamais contre la clé que le reçu porte à son propre sujet — un reçu ne doit pas pouvoir répondre de lui-même.
La chaîne et les racines
Il y a une seule chaîne. seq est dense et commence à 1, et prev_hash au seq n est le self_hash du seq n−1. Les deux se trouvent dans le corps signé, de sorte que la position d’un reçu est signée avec lui : déplacer un reçu, ou réécrire quoi que ce soit avant lui, change son empreinte.
Les reçus chaînés de chaque jour UTC forment un arbre de Merkle, haché tel que le définit la RFC 6962 — les feuilles préfixées d’un octet nul, les nœuds internes d’un un, pour qu’une feuille ne puisse jamais passer pour un nœud. La racine du jour est signée sous une clé de racine distincte et publiée dans un dépôt public que nous ne pouvons pas modifier en silence. Un jour vide reçoit tout de même un fichier, pour qu’un trou dans la série se voie de l’extérieur.
La preuve d’inclusion d’un reçu est un chemin d’audit vers l’une de ces racines. Un reçu scellé aujourd’hui n’a pas de preuve avant que la racine de cette nuit soit tirée, et la route de preuve dit pending plutôt que d’en inventer une.
Ce qu’un reçu public ne porte pas
Ce qu’un lecteur voit au lien d’un reçu est une projection : chaque URL citée retirée, chaque localisateur réduit à {type, domain}, l’identifiant propre du registre judiciaire pour un dossier retiré avec elle — l’ensemble de ces identifiants serait la table des sources du document. Tout le reste demeure octet pour octet, empreintes et signatures comprises, pour que celui qui détient le reçu complet puisse lier les deux. Une projection est signée à part entière, avec un membre supplémentaire que le reçu scellé n’a jamais, de sorte que toute sa surface visible — verdicts, comptages, états, domaines — est couverte par une signature et pas seulement par l’empreinte du reçu.
Le vérificateur
Le vérificateur est sous licence MIT et répliqué sur un hébergeur de code public. Il ne fait aucune requête de son côté : vérifier, c’est hacher et contrôler des signatures sur des octets que vous détenez déjà, ce pourquoi le compteur à côté de lui sur la page de vérification peut honnêtement afficher zéro.
Il s’exécute ici, dans votre navigateur, comme un module compilé servi depuis ce site. Chargez la page de vérification une fois, déconnectez-vous, et elle vérifie encore — c’est le contrôle à faire avant d’accorder du crédit à tout ceci, et celui à refaire si nous venions à disparaître.
Depuis un terminal
Les mêmes contrôles s’exécutent en ligne de commande contre un reçu sur votre disque. Le paquet n’est pas encore sur le registre public, donc npx exhibitb ne se résout pas aujourd’hui et nous n’allons pas l’imprimer comme s’il le faisait. Ce qui existe aujourd’hui, c’est la version navigateur ci-dessus, qui est le même vérificateur, et les sources sur l’hébergeur de code public, qui compilent vers les deux. Quand le paquet sera publié, il sera nommé dans le journal des modifications le jour même, pas avant.
Ce qu’envoie le complément
Un reçu enregistre quel extracteur a produit le texte qu’il décrit, et ce champ ne vaut rien tant qu’un inconnu détenant le même document ne peut pas reconstituer la même chaîne de caractères et obtenir la même empreinte. Voici cette spécification. C’est le contrat publié derrière l’étiquette exhibitb-word@1, et c’est pourquoi un reçu produit dans un document peut être lié au document.
La spécification ci-dessous est le contrat publié lui-même et n’est imprimée qu’en anglais, pour qu’une reconstitution faite d’après elle soit identique à l’octet près partout.
exhibitb-word@1
exhibitb-word@1 — the string a receipt from the add-in describes
A receipt names the extractor that produced the text it was made from. When the check ran from inside Word, the text was assembled in the open document rather than parsed out of an uploaded file, and this is how. Rebuild the string from your own copy of the document, hash it, and it matches the text_sha256 in the receipt — or it does not, and you have learned something.
L’ordre dans lequel la chaîne est construite
- Take the paragraphs of the document's main story, in the order Word lays them out. Paragraphs inside a table are part of that order: a table is read row by row, and each row cell by cell in reading order, at the point where the table sits.
- Then every footnote body, in footnote-number order.
- Then every endnote body, in endnote-number order.
- A paragraph's text is its own characters. A hyperlink contributes the words the reader sees; the address it points to is not inserted.
- The three groups are concatenated in that order — body, then footnotes, then endnotes — with nothing between them but the same separator that joins paragraphs.
Ce qui les joint
Every paragraph, footnote body and endnote body is joined to the next by two line feeds (U+000A twice), which is one blank line.
Ce qui n’y figure jamais
- the .docx file itself — no part of the container is uploaded
- headers and footers
- comments, and the identity of whoever wrote them
- tracked changes: deleted-but-unaccepted text is not sent, insertions are sent as the paragraph reads with them applied, and no revision author is sent
- styles, fonts, numbering, colour and every other piece of formatting
- images, charts, embedded objects and their captions' pictures (a caption's own words are a paragraph like any other)
- the file name, the folder it sits in, and every piece of document metadata
- fields, in favour of the result they display
Le nettoyage, appliqué en dernier et à rien d’autre
- carriage returns become line feeds: and a lone both become
- control characters are removed (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F, U+007F)
- soft hyphens (U+00AD) are removed
- spaces and tabs immediately before a line feed are removed
- three or more consecutive line feeds collapse to two
- leading and trailing whitespace is removed from the whole string
Contrôler votre propre reconstitution
The cleaning is the same eight lines the uploaded-file path runs, and it is idempotent: running it twice changes nothing, so a rebuild that applies it once is comparable byte for byte. SHA-256 of the resulting UTF-8 bytes is the receipt's document.text_sha256; document.chars is that string's length in code units.
Pourquoi les deux extracteurs ne comptent pas pareil
The two paths do not read the same amount of a document, and the label is how a reader tells them apart. exhibitb-word@1 reads footnotes and endnotes. exhibitb-text@1 over an uploaded .docx reads the main story only, so a footnote-heavy report checked as a file will show fewer citations than the same report checked from inside Word. Neither number is wrong; they are counts of different strings, and the receipt says which one it made.
exhibitb-text@1
L’autre étiquette. Un .docx envoyé est analysé par une bibliothèque qui parcourt les enfants propres du document, c’est-à-dire le récit principal et les tableaux qu’il contient — les notes de bas de page et de fin sont ailleurs dans le conteneur et ne sont pas lues. Tout le reste de la chaîne de caractères est identique, y compris le nettoyage ci-dessous et l’empreinte prise dessus.
Le format en entier
La forme canonique, le schéma du reçu membre par membre, les règles de chaîne, la construction de Merkle, le fichier de racine, la preuve d’inclusion et la règle de projection sont spécifiés dans un seul document, avec des vecteurs de test à côté — le même fichier, octet pour octet, que celui livré dans la copie de travail du vérificateur. Chaque nombre qu’il contient peut être reproduit par un tiers.
Vecteurs de test
- the canonical form of an object, with the worked examples that pin the whitespace and the number formatting
- self-hash vectors, including an object carrying a wrong self-hash and a nonsense signature array hashing to what it hashes to without them
- a signed receipt, its key set and its verification result
- Merkle roots for trees of 0, 1, 2, 3 and 7 leaves, with every audit path
Si quelque chose sur cette page est faux, ou si un contrôle que vous avez écrit contre elle échoue et que vous pensez qu’il ne devrait pas, l’adresse de support en pied de page atteint une personne qui regardera les octets avec vous.