Skip to content

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.

  1. Your client
  2. Address parsing
  3. PII shield
  4. Routing
  5. Model
  6. Restore + sign
  7. Response + x-pulse-proof
What happens to a call. The certificate is stored before the response completes, so the id in the header is always fetchable.

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:

HeaderSent byNotes
Authorization: Bearer sk_live_...OpenAI SDKs, curlThe 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 SDKsTakes precedence when both headers are present.

First call

  1. 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. 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. 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
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

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.

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

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.

Endpoints

POST/v1/chat/completions

OpenAI-compatible chat completion, attested.

API key (Bearer or x-api-key)engine streams (SSE)

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.

Headers

  • x-pulse-conversationstring
    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

  • modelstringdefault pulse-auto
    A model address. See Model addressing. Any other name is accepted, recorded in the certificate, and still routed.
  • messagesobject[]required
    The conversation. Must be a non-empty array containing at least one user message.
  • streambooleandefault false
    Return Server-Sent Events. The proof id is in the response headers from the first byte.
  • temperaturenumber
    Sampling temperature, forwarded to the model.
  • max_tokensinteger
    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.
  • toolsobject[]
    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_formatobject
    Set { "type": "json_object" } for JSON mode. Ignored on agent: addresses.

Request examples

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."}
]
}'

Responses

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].

Example response
{
"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
}
}

Errors

  • 401No key, wrong key, revoked or expired key. Body: { "error": "..." }.
  • 402insufficient_quotaThe workspace allowance is exhausted.
  • 402API_KEY_BUDGET_EXCEEDEDThe key reached its own monthly question ceiling.
  • 403scope_deniedThe key does not carry the engine scope.
  • 429rate_limitedThe 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.

API key (x-api-key or Bearer)engine streams (SSE)

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" } }).

Request body

  • modelstringdefault pulse-auto
    A model address.
  • messagesobject[]required
    Anthropic messages. Content may be a string or an array of content blocks.
  • systemstring | object[]
    System prompt, as a string or text blocks.
  • max_tokensinteger
    Output ceiling, clamped to the Gateway cap.
  • temperaturenumber
    Sampling temperature.
  • toolsobject[]
    Anthropic tool definitions (name, description, input_schema).
  • streambooleandefault false
    Return Server-Sent Events.

Request examples

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."}]
}'

Responses

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

Errors

  • 401Missing, invalid or revoked key.
  • 404not_found_errorThe agent:<slug> address matches no agent.
  • 501invalid_request_errordebate: 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.

API keyengine

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.

Request body

  • messagesobject[]required
    Same shape as POST /v1/messages.
  • systemstring | object[]
    System prompt.
  • toolsobject[]
    Tool definitions, counted as serialised JSON.

Request examples

curl
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

The estimate.

Example response
{ "input_tokens": 6 }

GET/v1/models

List model aliases and your agents as addresses.

API keyengine

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.

Request examples

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

Responses

An OpenAI-style list.

Example response
{
"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)
{
"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.

API keyengine

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

Path parameters

  • idstringrequired
    The value of the x-pulse-proof header, pf_ followed by 32 hex characters.

Request examples

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

Responses

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

GET/v1/proofs/:id/verify

Verify a certificate on the server.

API keyengine

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.

Path parameters

  • idstringrequired
    Certificate id.

Request examples

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

Responses

valid is true only when both the signature and the chain link hold.

Example response
{
"id": "pf_a059713a5f1c4e0b8d7a3c21e9b64f10",
"signature_valid": true,
"chain_valid": true,
"valid": true,
"reason": null
}

GET/v1/gateway/public-key

Public key for offline verification.

None

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.

Request examples

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

Responses

The key in SPKI PEM format.

Example response
{ "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
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'))
)
}

Errors

StatusOpenAI dialectAnthropic dialectMeaning
400invalid_request_errorinvalid_request_errorMalformed body: no messages, no user message.
401{ "error": "..." }{ "error": "..." }Authentication failed. The same plain envelope in both dialects.
402insufficient_quotanot usedWorkspace allowance exhausted, or key budget (API_KEY_BUDGET_EXCEEDED).
403scope_denied, read_only_keysameThe key may not call this route, or may not write.
404model_not_foundnot_found_errorNo such agent: address.
429rate_limitedrate_limit_errorPer-key request limit. In the Anthropic dialect a spent allowance is also a 429.
501address_not_supportedinvalid_request_errorReserved debate: addresses.

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.

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.

Related