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.
- 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. |
First call
- 1Create a key
Open the Subsidia console, Gateway page, and create a key with the
enginescope. It is displayed once; store it in an environment variable, never in source control. - 2Point your client at the Gateway
Set the base URL to
https://api.subsidia.protypa.fr/v1and keep your usual code. Usepulse-autoas the model so Synapse picks the model per call.bashexport SUBSIDIA_API_KEY="sk_live_..." - 3Send a request and keep the proof id
Every response carries an
x-pulse-proofheader. Keep it next to the business record the answer feeds: it is the handle to the certificate.bashcurl -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
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 restoredconsole.log(response.headers.get('x-pulse-proof')) // certificate idUse 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_KEYexport ANTHROPIC_MODEL="pulse-auto"export ANTHROPIC_SMALL_FAST_MODEL="pulse-auto"claudeThe 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.
engineDrop-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-conversationstringContinue an existing agent conversation (agent:addresses only). Omit to start a new one. An unknown id is a 404conversation_not_found.
Request body
modelstringdefaultpulse-autoA model address. See Model addressing. Any other name is accepted, recorded in the certificate, and still routed.messagesobject[]requiredThe conversation. Must be a non-empty array containing at least oneusermessage.streambooleandefaultfalseReturn Server-Sent Events. The proof id is in the response headers from the first byte.temperaturenumberSampling temperature, forwarded to the model.max_tokensintegerOutput ceiling. Values above the Gateway cap (16000 by default) are clamped instead of rejected, so agent loops do not fail.max_completion_tokensis accepted as an alias.toolsobject[]OpenAI function definitions. Their presence marks the call as agentic and routes it to the full model. Ignored onagent:addresses, which run the agent tools server-side.response_formatobjectSet{ "type": "json_object" }for JSON mode. Ignored onagent: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].
{ "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": "..." }. - 402
insufficient_quotaThe workspace allowance is exhausted. - 402
API_KEY_BUDGET_EXCEEDEDThe key reached its own monthly question ceiling. - 403
scope_deniedThe key does not carry theenginescope. - 429
rate_limitedThe key exceeded its requests-per-minute limit. Honourretry-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.
engineDrop-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
modelstringdefaultpulse-autoA model address.messagesobject[]requiredAnthropic messages. Content may be a string or an array of content blocks.systemstring | object[]System prompt, as a string or text blocks.max_tokensintegerOutput ceiling, clamped to the Gateway cap.temperaturenumberSampling temperature.toolsobject[]Anthropic tool definitions (name,description,input_schema).streambooleandefaultfalseReturn 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.
- 404
not_found_errorTheagent:<slug>address matches no agent. - 501
invalid_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.
engineClients 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[]requiredSame shape asPOST /v1/messages.systemstring | object[]System prompt.toolsobject[]Tool definitions, counted as serialised JSON.
Request examples
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.
{ "input_tokens": 6 }GET/v1/models
List model aliases and your agents as addresses.
engineReturns 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.
{ "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.
{ "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.
engineReturns the signed certificate. Certificates are scoped to the workspace of the key: another workspace's proof is indistinguishable from a missing one.
Path parameters
idstringrequiredThe value of thex-pulse-proofheader,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.
engineRecomputes 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
idstringrequiredCertificate id.
Request examples
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.
{ "id": "pf_a059713a5f1c4e0b8d7a3c21e9b64f10", "signature_valid": true, "chain_valid": true, "valid": true, "reason": null}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.
Request examples
curl https://api.subsidia.protypa.fr/v1/gateway/public-keyResponses
The key in SPKI PEM format.
{ "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.
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
| 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. |
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.