# Gateway

> OpenAI and Anthropic compatible endpoints: change one base URL and every call is masked, routed, metered and signed with a proof certificate.

The Gateway is the fastest way to put Subsidia in front of existing software. It speaks the two wire protocols the AI ecosystem already uses, OpenAI `chat/completions` and Anthropic `messages`, so any client that lets you set a base URL (the official SDKs, Claude Code, Cursor, n8n, LangChain) works unchanged.

What you get in exchange for that one line: personal data is masked before it leaves your perimeter, each call is routed to a model that fits the task, usage is metered against your workspace, and every completion comes back with a **signed, hash-chained proof certificate** that you can hand to an auditor.

**What happens to a call. The certificate is stored before the response completes, so the id in the header is always fetchable.**

Flow: Your client -> Address parsing -> PII shield -> Routing -> Model -> Restore + sign -> Response + x-pulse-proof

## Base URL and authentication

Use `https://api.subsidia.protypa.fr` as the host. OpenAI-style clients append `/v1` themselves or expect it in the base URL; Anthropic-style clients expect the host only and add `/v1/messages`.

Authenticate with a developer API key (`sk_live_...`, created in the Subsidia console). Both header styles are accepted, so each official SDK works with its native convention:

| Header | Sent by | Notes |
| --- | --- | --- |
| `Authorization: Bearer sk_live_...` | OpenAI SDKs, curl | The key must start with `sk_live_` or `sk_test_`; any other Bearer value is ignored and the call is a 401. |
| `x-api-key: sk_live_...` | Anthropic SDKs | Takes precedence when both headers are present. |

> **WARNING: Scopes**
> A key restricted to capability scopes can only reach the route families it names. The Gateway routes (`/v1/chat/completions`, `/v1/messages`, `/v1/models`, `/v1/proofs`, `/v1/gateway/*`) need the `engine` scope. A missing scope is a `403 scope_denied`. See [Authentication](https://dev.subsidia.protypa.fr/docs/authentication.md).

## First call

1. **Create a key**

   Open the Subsidia console, Gateway page, and create a key with the `engine` scope. It is displayed once; store it in an environment variable, never in source control.

2. **Point your client at the Gateway**

   Set the base URL to `https://api.subsidia.protypa.fr/v1` and keep your usual code. Use `pulse-auto` as the model so Synapse picks the model per call.

   ```bash
   export SUBSIDIA_API_KEY="sk_live_..."
   ```

3. **Send a request and keep the proof id**

   Every response carries an `x-pulse-proof` header. Keep it next to the business record the answer feeds: it is the handle to the certificate.

   ```bash
   curl -i https://api.subsidia.protypa.fr/v1/chat/completions \
     -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"model":"pulse-auto","messages":[{"role":"user","content":"Hello"}]}'
   ```

## Use the official SDKs

**Same SDK, one changed line**

_OpenAI (TypeScript)_

```typescript
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: 'https://api.subsidia.protypa.fr/v1', // the one changed line
  apiKey: process.env.SUBSIDIA_API_KEY, // sk_live_...
})

const { data, response } = await client.chat.completions
  .create({
    model: 'pulse-auto',
    messages: [{ role: 'user', content: 'Summarise this contract clause.' }],
  })
  .withResponse()

console.log(data.choices[0].message.content) // answer, personal data restored
console.log(response.headers.get('x-pulse-proof')) // certificate id
```

_OpenAI (Python)_

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.subsidia.protypa.fr/v1",
    api_key=os.environ["SUBSIDIA_API_KEY"],
)

raw = client.chat.completions.with_raw_response.create(
    model="pulse-auto",
    messages=[{"role": "user", "content": "Summarise this contract clause."}],
)
completion = raw.parse()
proof_id = raw.headers.get("x-pulse-proof")
```

_Anthropic (TypeScript)_

```typescript
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic({
  baseURL: 'https://api.subsidia.protypa.fr', // host only: the SDK adds /v1/messages
  apiKey: process.env.SUBSIDIA_API_KEY,
})

const { data, response } = await client.messages
  .create({
    model: 'pulse-auto',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Summarise this contract clause.' }],
  })
  .withResponse()

console.log(data.content) // Anthropic content blocks
console.log(response.headers.get('x-pulse-proof'))
```

## Use it with Claude Code

Claude Code speaks the Anthropic protocol, so it can run entirely through the Gateway: every turn of the agent loop is masked, routed and attested. The variables below are session-scoped; closing the terminal returns Claude Code to its normal login.

_bash_

```bash
export ANTHROPIC_BASE_URL="https://api.subsidia.protypa.fr"
export ANTHROPIC_AUTH_TOKEN="sk_live_..."   # AUTH_TOKEN, not API_KEY
export ANTHROPIC_MODEL="pulse-auto"
export ANTHROPIC_SMALL_FAST_MODEL="pulse-auto"
claude
```

_PowerShell_

```powershell
$env:ANTHROPIC_BASE_URL = "https://api.subsidia.protypa.fr"
$env:ANTHROPIC_AUTH_TOKEN = "sk_live_..."   # AUTH_TOKEN, not API_KEY
$env:ANTHROPIC_MODEL = "pulse-auto"
$env:ANTHROPIC_SMALL_FAST_MODEL = "pulse-auto"
claude
```

> **TIP: ANTHROPIC_AUTH_TOKEN, not ANTHROPIC_API_KEY**
> Claude Code only accepts Anthropic-issued key formats (`sk-ant-...`) in `ANTHROPIC_API_KEY` and reports "Not logged in" otherwise. `ANTHROPIC_AUTH_TOKEN` sends the key as a Bearer header, which the Gateway accepts.

## The model field is an address

Third-party tools expose one setting you can always change: the model name. The Gateway reads it as an **address** that selects which Subsidia capability serves the call, so your agents and your knowledge base are reachable from tools that know nothing about them.

With no Subsidia prefix or flag the Gateway is a faithful passthrough: capabilities are opt-in through the address, never imposed. The full grammar is in [Model addressing](https://dev.subsidia.protypa.fr/docs/model-addressing.md).

- **pulse-auto**: Synapse picks the model per call: small for simple asks, full for complex work, local for detected-sensitive content.
- **[agent:<slug>](https://dev.subsidia.protypa.fr/docs/agents.md)**: Served by one of your workspace agents, with its prompt, memory, documents and tools.
- **[+cortex](https://dev.subsidia.protypa.fr/docs/knowledge-query.md)**: Grounds the call in your knowledge base; the retrieved passages are hashed into the certificate.
- **[+sensitive](https://dev.subsidia.protypa.fr/docs/synapse.md)**: Forces local processing on an installation that has a local model; on a hosted cloud without one it only guarantees masking. The certificate shows which provider answered.

## Endpoints

### POST /v1/chat/completions

OpenAI-compatible chat completion, attested.

Drop-in for the OpenAI chat-completions endpoint. The request passes through the Synapse pipeline (PII shield, routing, metering) and a certificate is signed for the exchange. Unknown fields are accepted and ignored, so SDK upgrades do not break.

The answer is returned with the personal data restored, so your application sees what it would have seen without the Gateway.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `engine`
- **Streaming:** yes, Server-Sent Events when `stream: true`

#### Headers

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `x-pulse-conversation` | `string` | no |  | Continue an existing agent conversation (`agent:` addresses only). Omit to start a new one. An unknown id is a 404 `conversation_not_found`. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | `string` | no | `pulse-auto` | A model address. See [Model addressing](https://dev.subsidia.protypa.fr/docs/model-addressing.md). Any other name is accepted, recorded in the certificate, and still routed. |
| `messages` | `object[]` | yes |  | The conversation. Must be a non-empty array containing at least one `user` message. |
| `messages.role` | `string` | yes |  | Author of the message. One of: `system`, `user`, `assistant`, `tool`. |
| `messages.content` | `string | object[] | null` | yes |  | Text, or an array of `{ type: "text", text }` parts. `null` is allowed on assistant messages that carry `tool_calls`. |
| `messages.tool_calls` | `object[]` | no |  | Tool calls made by an assistant message, in the OpenAI shape. |
| `messages.tool_call_id` | `string` | no |  | On `tool` messages: the call this message answers. |
| `stream` | `boolean` | no | `false` | Return Server-Sent Events. The proof id is in the response headers from the first byte. |
| `temperature` | `number` | no |  | Sampling temperature, forwarded to the model. |
| `max_tokens` | `integer` | no |  | Output ceiling. Values above the Gateway cap (16000 by default) are clamped instead of rejected, so agent loops do not fail. `max_completion_tokens` is accepted as an alias. |
| `tools` | `object[]` | no |  | OpenAI function definitions. Their presence marks the call as agentic and routes it to the full model. Ignored on `agent:` addresses, which run the agent tools server-side. |
| `response_format` | `object` | no |  | Set `{ "type": "json_object" }` for JSON mode. Ignored on `agent:` addresses. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/chat/completions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "pulse-auto",
    "messages": [
      {"role": "system", "content": "You are a careful legal assistant."},
      {"role": "user", "content": "Summarise the termination clause for Jean Dupont."}
    ]
  }'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'pulse-auto',
    messages: [
      { role: 'system', content: 'You are a careful legal assistant.' },
      { role: 'user', content: 'Summarise the termination clause for Jean Dupont.' },
    ],
  }),
})

const proofId = res.headers.get('x-pulse-proof')
const completion = await res.json()
console.log(completion.choices[0].message.content, proofId)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/chat/completions",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={
        "model": "pulse-auto",
        "messages": [
            {"role": "system", "content": "You are a careful legal assistant."},
            {"role": "user", "content": "Summarise the termination clause for Jean Dupont."},
        ],
    },
)
proof_id = res.headers["x-pulse-proof"]
print(res.json()["choices"][0]["message"]["content"], proof_id)
```

#### Responses

**200**: A `chat.completion` object plus a `pulse` block. Headers: `x-pulse-proof` (certificate id), `x-pulse-provider` (`ollama`, `openai`...), `x-pulse-pii-masked` (number of values masked), and `x-pulse-conversation` for `agent:` calls. With `stream: true` the body is `text/event-stream` of `chat.completion.chunk` events ending with `data: [DONE]`; only `x-pulse-proof` is sent as a header, and the final chunk carries `usage` plus a `pulse` block (`proof`, `conversation`) for `agent:` calls only. The certificate is persisted before `[DONE]`.

```json
{
  "id": "chatcmpl-pf_a059713a5f1c4e0b8d7a3c21e9b64f10",
  "object": "chat.completion",
  "created": 1790000000,
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Either party may terminate with 30 days written notice..." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 53, "completion_tokens": 38, "total_tokens": 91 },
  "pulse": {
    "proof": "pf_a059713a5f1c4e0b8d7a3c21e9b64f10",
    "provider": "openai",
    "task": "chat",
    "pii_masked": 1
  }
}
```

**400**: Missing or empty `messages`, or no `user` message.

```json
{ "error": { "message": "'messages' is a required property and must be a non-empty array", "type": "invalid_request_error", "code": null, "param": null } }
```

**402**: The workspace has no questions left. Returned before any model is called, so nothing is consumed.

```json
{ "error": { "message": "Insufficient balance", "type": "insufficient_quota", "code": "insufficient_quota", "param": null } }
```

**404**: The `agent:<slug>` address matches no agent in this workspace (`model_not_found`), or `x-pulse-conversation` is unknown (`conversation_not_found`).

**501**: `debate:<slug>` addresses are reserved and not served here (`address_not_supported`). Use the conversations API.

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 401 |  | No key, wrong key, revoked or expired key. Body: `{ "error": "..." }`. |
| 402 | `insufficient_quota` | The workspace allowance is exhausted. |
| 402 | `API_KEY_BUDGET_EXCEEDED` | The key reached its own monthly question ceiling. |
| 403 | `scope_denied` | The key does not carry the `engine` scope. |
| 429 | `rate_limited` | The key exceeded its requests-per-minute limit. Honour `retry-after`. |

#### Notes

Tool-call requests cannot stream token by token: with `stream: true` and `tools` present, the full answer is produced first and emitted as a single chunk. Calls to `agent:` addresses run the agent tools on the server and always stream plain text.

### POST /v1/messages

Anthropic-compatible messages endpoint, attested.

Drop-in for the Anthropic Messages API, with the same pipeline, address grammar and certificate registry as the OpenAI dialect. Errors use the Anthropic envelope (`{ "type": "error", "error": { "type", "message" } }`).

- **Authentication:** API key (x-api-key or Bearer)
- **Scopes:** `engine`
- **Streaming:** yes, Server-Sent Events when `stream: true`

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `model` | `string` | no | `pulse-auto` | A model address. |
| `messages` | `object[]` | yes |  | Anthropic messages. Content may be a string or an array of content blocks. |
| `system` | `string | object[]` | no |  | System prompt, as a string or text blocks. |
| `max_tokens` | `integer` | no |  | Output ceiling, clamped to the Gateway cap. |
| `temperature` | `number` | no |  | Sampling temperature. |
| `tools` | `object[]` | no |  | Anthropic tool definitions (`name`, `description`, `input_schema`). |
| `stream` | `boolean` | no | `false` | Return Server-Sent Events. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/messages \
  -H "x-api-key: $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "pulse-auto",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Summarise the termination clause."}]
  }'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/messages', {
  method: 'POST',
  headers: { 'x-api-key': process.env.SUBSIDIA_API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    model: 'pulse-auto',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Summarise the termination clause.' }],
  }),
})
const message = await res.json()
const proofId = res.headers.get('x-pulse-proof')
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/messages",
    headers={"x-api-key": os.environ["SUBSIDIA_API_KEY"]},
    json={
        "model": "pulse-auto",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": "Summarise the termination clause."}],
    },
)
message = res.json()
proof_id = res.headers["x-pulse-proof"]
```

#### Responses

**200**: A `message` object with Anthropic content blocks (`text`, `tool_use`). The certificate id is in the `x-pulse-proof` header.

**400**: `messages` missing or empty (`invalid_request_error`).

**429**: Workspace allowance exhausted (`rate_limit_error`), or the key rate limit. In the Anthropic dialect the quota error is a 429, not a 402.

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 401 |  | Missing, invalid or revoked key. |
| 404 | `not_found_error` | The `agent:<slug>` address matches no agent. |
| 501 | `invalid_request_error` | `debate:` addresses are not served here. |

#### Notes

Differences from the OpenAI dialect: the certificate address records only `kind`, `agent` and `cortex` (not `+nocortex` / `+nomemory`); the response has no `pulse` body block, so read `x-pulse-provider` and `x-pulse-pii-masked` from the headers; `stop_reason` is `end_turn` or `tool_use`.

### POST /v1/messages/count_tokens

Estimate the input size of a Messages request.

Clients such as Claude Code call this to check that a prompt fits the context window. The result is a characters-divided-by-four estimate, the same heuristic Synapse uses for routing, not a tokenizer count.

- **Authentication:** API key
- **Scopes:** `engine`

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `messages` | `object[]` | yes |  | Same shape as `POST /v1/messages`. |
| `system` | `string | object[]` | no |  | System prompt. |
| `tools` | `object[]` | no |  | Tool definitions, counted as serialised JSON. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/messages/count_tokens \
  -H "x-api-key: $SUBSIDIA_API_KEY" -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"How long is this?"}]}'
```

#### Responses

**200**: The estimate.

```json
{ "input_tokens": 6 }
```

### GET /v1/models

List model aliases and your agents as addresses.

Returns the Gateway aliases, the currently configured full model, and one `agent:<slug>` entry per agent of the workspace. Because model pickers are populated from this route, your own agents appear as selectable models in tools such as Cursor, Continue or n8n without any integration work.

- **Authentication:** API key
- **Scopes:** `engine`

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/models -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

models = requests.get(
    "https://api.subsidia.protypa.fr/v1/models",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
).json()["data"]
print([m["id"] for m in models])
```

#### Responses

**200**: An OpenAI-style list.

```json
{
  "object": "list",
  "data": [
    { "id": "pulse-auto", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "pulse-sensitive", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "pulse-agent", "object": "model", "created": 1790000000, "owned_by": "pulse-gateway" },
    { "id": "agent:demo-cfo", "object": "model", "created": 1790000000, "owned_by": "pulse-agents" }
  ]
}
```

## Proof certificates

Every completion returns an `x-pulse-proof` header. The certificate commits, by SHA-256, to your request, to what actually left toward the model provider (after masking), and to the response. It never contains the content itself, so it can be shared with a client or a regulator as is.

Each certificate embeds the hash of the previous one (`chainIndex`, `prevHash`): removing one leaves a visible hole for every verifier. Calls served by an agent or carrying `+cortex` add a second level, a `grounding` array that commits to the exact knowledge passages the answer drew on.

**Certificate payload (abridged)**

```json
{
  "id": "pf_a059713a5f1c4e0b8d7a3c21e9b64f10",
  "chain_index": 12,
  "prev_hash": "90a8ecd956...",
  "payload": {
    "request": { "model": "pulse-auto+cortex", "sha256": "b2205bba..." },
    "address": { "kind": "auto", "cortex": true },
    "grounding": [{ "source": "etat-des-lieux.md", "chunkId": "ck_...", "page": null, "sha256": "11d10642..." }],
    "egress": { "provider": "ollama", "model": "llama3.1:8b", "sha256": "876be067...", "pii": { "masked": 2, "categories": ["EMAIL", "NAME"] } },
    "response": { "sha256": "ae792e49...", "finishReason": "stop" }
  },
  "payload_hash": "sha256(canonical-json(payload))",
  "signature": "base64 Ed25519 over payload_hash",
  "algorithm": "Ed25519"
}
```

### GET /v1/proofs/:id

Fetch a proof certificate.

Returns the signed certificate. Certificates are scoped to the workspace of the key: another workspace's proof is indistinguishable from a missing one.

- **Authentication:** API key
- **Scopes:** `engine`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The value of the `x-pulse-proof` header, `pf_` followed by 32 hex characters. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/proofs/pf_a059713a5f1c4e0b8d7a3c21e9b64f10 \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const cert = await fetch('https://api.subsidia.protypa.fr/v1/proofs/' + proofId, {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
}).then((r) => r.json())
```

#### Responses

**200**: The certificate: `id`, `chain_index`, `prev_hash`, `payload`, `payload_hash`, `signature`, `algorithm`, `hash`, `created_at`.

**404**: Unknown id, or the proof belongs to another workspace (`proof_not_found`).

### GET /v1/proofs/:id/verify

Verify a certificate on the server.

Recomputes the payload hash, checks the Ed25519 signature, and checks that the previous record in the chain still matches `prev_hash`. Use the public key route below if you would rather not trust the server for this.

- **Authentication:** API key
- **Scopes:** `engine`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | Certificate id. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/proofs/$PROOF_ID/verify -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

#### Responses

**200**: `valid` is true only when both the signature and the chain link hold.

```json
{
  "id": "pf_a059713a5f1c4e0b8d7a3c21e9b64f10",
  "signature_valid": true,
  "chain_valid": true,
  "valid": true,
  "reason": null
}
```

**404**: Unknown id (`proof_not_found`).

### GET /v1/gateway/public-key

Public key for offline verification.

Open route, no key required, on purpose: an auditor holding only a certificate and this key can validate it without any access to your account.

- **Authentication:** None

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/gateway/public-key
```

#### Responses

**200**: The key in SPKI PEM format.

```json
{ "algorithm": "Ed25519", "format": "spki-pem", "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" }
```

### Verify offline

The signature covers `payload_hash`, which is the SHA-256 of the canonical JSON of `payload` (keys sorted, `undefined` dropped). Verification takes about fifteen lines in any language with an Ed25519 implementation.

_Node.js_

```typescript
import crypto from 'node:crypto'

function canonicalJson(v: unknown): string {
  if (v === null || typeof v !== 'object') return JSON.stringify(v)
  if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
  const entries = Object.entries(v as Record<string, unknown>)
    .filter(([, x]) => x !== undefined)
    .sort(([a], [b]) => (a < b ? -1 : 1))
  return '{' + entries.map(([k, x]) => JSON.stringify(k) + ':' + canonicalJson(x)).join(',') + '}'
}

export function isAuthentic(cert: any, publicKeyPem: string): boolean {
  const recomputed = crypto.createHash('sha256').update(canonicalJson(cert.payload), 'utf8').digest('hex')
  return (
    recomputed === cert.payload_hash &&
    crypto.verify(null, Buffer.from(cert.payload_hash, 'utf8'), crypto.createPublicKey(publicKeyPem), Buffer.from(cert.signature, 'base64'))
  )
}
```

> **INFO: Two proof levels**
> Level 1 (every call) attests the pipeline: request, masking, model, response. Level 2 (agent and `+cortex` calls) also commits to the sources the answer was grounded in. Details in [Proof certificates](https://dev.subsidia.protypa.fr/docs/proofs.md).

## Errors

| Status | OpenAI dialect | Anthropic dialect | Meaning |
| --- | --- | --- | --- |
| 400 | `invalid_request_error` | `invalid_request_error` | Malformed body: no `messages`, no user message. |
| 401 | `{ "error": "..." }` | `{ "error": "..." }` | Authentication failed. The same plain envelope in both dialects. |
| 402 | `insufficient_quota` | not used | Workspace allowance exhausted, or key budget (`API_KEY_BUDGET_EXCEEDED`). |
| 403 | `scope_denied`, `read_only_key` | same | The key may not call this route, or may not write. |
| 404 | `model_not_found` | `not_found_error` | No such `agent:` address. |
| 429 | `rate_limited` | `rate_limit_error` | Per-key request limit. In the Anthropic dialect a spent allowance is also a 429. |
| 501 | `address_not_supported` | `invalid_request_error` | Reserved `debate:` addresses. |

> **DANGER: Streaming errors arrive in-band**
> Once an SSE stream has started, the HTTP status is already 200. A failure mid-stream is delivered as a final `data: { "error": { ... } }` event followed by `[DONE]`. Always check for an `error` object in the events, not only the status code.

## Frequently asked

**Does the Gateway change my prompts?**

Not unless you opt in. Without a Subsidia prefix or flag the Gateway is a passthrough with masking, routing and a certificate. `+cortex` adds knowledge passages to the prompt, and `agent:` hands the call to an agent; both are recorded in the certificate.

**Is my data sent to a third party?**

Only to the provider that serves the call. Masking applies to **external** providers; with `pulse-sensitive` or `+sensitive` the call stays on a local model on installations that have one (local and on-premise), and nothing is redacted because nothing leaves. On a hosted cloud without a local model, masking is what protects the data. The certificate records the provider that answered.

**How are calls billed?**

Against the workspace allowance of questions, charged on the model that actually answered. A larger model counts for more. See [Rate limits and quotas](https://dev.subsidia.protypa.fr/docs/rate-limits.md).

**Can I verify a certificate without calling Subsidia?**

Yes. Fetch the certificate once, fetch `GET /v1/gateway/public-key` once, and run the offline check above. The public key route needs no credentials.
