Gateway data-plane API
Every endpoint agents call on the MAQPNA gateway - MCP, A2A, model calls, egress proxy and probes - with headers, bodies, status codes, reason values and curl examples.
This page is the reference for the HTTP API that agents and their SDKs call on the MAQPNA gateway: Model Context Protocol (MCP) tool calls on /mcp, agent-to-agent (A2A) calls on /a2a, model calls on /llm, the browser egress proxy, and the health, readiness, version and metrics endpoints. The admin API (/v1/*) has its own page: Admin API. Session tokens come from the identity broker: Identity broker API.
Use this page when you write an agent without an SDK, debug what an SDK sends, or build a client in a language MAQPNA has no SDK for. For a task-first introduction, read Connect to the gateway and Tool calls over MCP first.
Source: cmd/maqpna-gateway (route table in server.go and the register*Routes functions).
Endpoints at a glance#
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/mcp/{server} |
Session token, scope tools:{server} |
Governed MCP JSON-RPC (single message or batch) |
GET |
/mcp/{server} |
Session token, scope tools:{server} |
MCP server-to-client SSE stream (streamable HTTP) |
DELETE |
/mcp/{server} |
Session token, scope tools:{server} |
End the MCP session (Mcp-Session-Id) |
GET |
/.well-known/oauth-protected-resource/mcp/{server} |
None | RFC 9728 metadata for external MCP clients (only when identity.trustedIssuers is set) |
POST |
/a2a/{ns}/{agent} |
Session token, scope agents:{agent} |
A2A v1.0 JSON-RPC binding |
| any | /a2a/{ns}/{agent}/{rest...} |
Session token, scope agents:{agent} |
A2A v1.0 HTTP+JSON binding |
GET |
/a2a/{ns}/{agent}/.well-known/agent-card.json |
Session token (none with a2a.publicCards) |
Agent card |
GET |
/a2a/jwks.json |
None | Public keys that sign served agent cards |
POST |
/llm/{route}/v1/chat/completions |
Session token, scope models:{route} |
OpenAI Chat Completions |
POST |
/llm/{route}/v1/completions |
same | OpenAI Completions |
POST |
/llm/{route}/v1/embeddings |
same | OpenAI Embeddings |
GET |
/llm/{route}/v1/models |
same | OpenAI model list |
POST |
/llm/{route}/v1/responses |
same | OpenAI Responses API |
POST |
/llm/{route}/anthropic/v1/messages |
same (Authorization or x-api-key) |
Anthropic Messages |
POST |
/llm/{route}/anthropic/v1/messages/count_tokens |
same | Anthropic token counting |
CONNECT, absolute-form |
any host | Proxy-Authorization, scope tools:maqpna-egress |
Browser egress proxy (only with egress.enabled) |
POST |
/v1/sessions/self/result |
Session token | Report the session's result (see Admin API) |
GET |
/healthz |
None | Liveness |
GET |
/readyz |
None | Readiness |
GET |
/version |
None (hidden on the public listener with adminListen) |
Build information |
GET |
/metrics |
None (hidden on the public listener with adminListen) |
Prometheus metrics |
The session-scoped endpoints that sandbox tools use (/v1/sessions/{id}/exec, /files, /evidence, /tools:authorize, /infra:*, /liveview) accept a session token too. They are listed with the rest of /v1/sessions/* in the Admin API.
Auth model#
Every data-plane call carries a session token: a short-lived, Ed25519-signed token that the identity broker issued for one session. The gateway verifies it against the broker's public keys (JWKS), checks the scope for the target, and forwards the call with its own credential for the MCP server or model endpoint. The agent's Authorization header is never forwarded.
sequenceDiagram
autonumber
participant OP as Operator
participant IB as Identity broker
participant AG as Agent (sandbox)
participant GW as Gateway
participant UP as MCP server / model
OP->>IB: POST /v1/token (namespace, agent, session, user, scopes)
IB-->>OP: session token (Ed25519 JWS)
OP->>AG: token file (MAQPNA_TOKEN_FILE)
GW->>IB: GET /.well-known/jwks.json (every 300 s, and on an unknown kid)
AG->>GW: POST /mcp/github<br/>Authorization: Bearer <session token>
GW->>GW: verify signature, aud, exp, revocation,<br/>scope tools:github, DLP, policy, budget
GW->>UP: tools/call with the gateway's credential<br/>+ X-Maqpna-Namespace, -Agent, -Session, -User
UP-->>GW: result
GW->>GW: response DLP, taint, audit record
GW-->>AG: JSON-RPC result
Token presentation#
| Header | When |
|---|---|
Authorization: Bearer <session token> |
Default for every endpoint |
Authorization: DPoP <session token> and DPoP: <proof> |
The token is DPoP-bound (claim cnf.jkt). See DPoP |
x-api-key: <session token> |
Only on /llm/{route}/anthropic/v1/*, when Authorization is absent (Anthropic SDKs send the key there) |
Proxy-Authorization: Bearer <session token> (or Basic, token as password) |
Only on the egress proxy |
The gateway also accepts access tokens from external identity providers listed in identity.trustedIssuers (external MCP clients). A missing token on /mcp returns 401 with a WWW-Authenticate header that points at the RFC 9728 metadata document.
Scopes#
| Scope | Grants |
|---|---|
tools:<server> |
/mcp/<server>, and the built-in servers maqpna-web, memory, maqpna-exec, maqpna-client, maqpna-infra, maqpna-egress, maqpna-k8s |
models:<route> |
/llm/<route>/... |
agents:<name> / agents:<ns>/<name> |
A2A calls to an agent in the caller's namespace / in <ns>. tools:* never grants it |
A granted scope may be a glob (tools:*, models:coder-*). See Identity and delegation.
Session-token claims the gateway reads#
| Claim | Use |
|---|---|
sub |
SPIFFE-style ID spiffe://<trust-domain>/ns/<ns>/agent/<agent>/session/<session> |
maqpna_ns, maqpna_agent, maqpna_session, maqpna_tier |
The session identity; policy, budgets and audit use them |
act |
The user the agent acts for ({"sub":"user:[email protected]"}), nested for A2A delegation |
scope |
Space-separated scopes |
aud, iss, exp, nbf |
Verified; default audience maqpna-gateway, leeway 30 s |
cnf.jkt |
DPoP binding |
The full claim table is on the Identity broker API page.
Common error envelope#
Every error the gateway generates carries the same fields, wherever the protocol puts them. Clients match on the reason token (the text before the first :), never on message or detail.
| Field | Type | Meaning |
|---|---|---|
domain |
string | Always maqpna.com for gateway errors. Errors from a remote A2A agent keep their own domain |
reason |
string | <token>[:<qualifier>], for example budget_exceeded:agent/day, dlp:github_token, missing_scope:tools:github |
detail |
string | Free text when the raw reason was not a token, for example a rule's own reason text |
policy, rule |
string | The deciding policy and rule, when one decided (dlp and the profile for DLP; budget and <scope>-<window> for budgets) |
approvalId, expiresAt |
string | On approval_required (code -32002) |
connectURL |
string | On consent_required: where the user connects the provider |
Where the envelope lives:
| Surface | Shape |
|---|---|
| MCP and A2A JSON-RPC | error.data of the JSON-RPC error |
| A2A HTTP+JSON | google.rpc.Status with one ErrorInfo; reason is the upper-case token (BUDGET_EXCEEDED), the rest is in metadata |
/llm (OpenAI wire) |
{"error": {"message", "type", "code", "reason", "domain"}} |
/llm/{route}/anthropic |
{"type": "error", "error": {"type", "message"}}; the MAQPNA type and code are appended to message |
| Egress proxy | Plain text maqpna egress: <reason> with header X-Maqpna-Egress: denied |
A denied MCP call:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32001,
"message": "denied by policy default-agent-policy rule destructive-needs-human: approval_denied by [email protected] (apr-3f9c)",
"data": {
"domain": "maqpna.com",
"reason": "approval_denied",
"detail": "approval_denied by [email protected] (apr-3f9c)",
"policy": "default-agent-policy",
"rule": "destructive-needs-human"
}
}
}
The values are illustrative; the field names, the code and the message pattern (denied by policy <policy> rule <rule>: <reason>) come from the gateway. The complete list of reason tokens, their groups and how a client should react to each is in Error envelope and reason codes and Error and reason codes.
JSON-RPC codes#
| Code | Default reason | Meaning |
|---|---|---|
-32001 |
policy_denied |
Denied: policy, kill switch, DLP, budget, tool pin, denied or expired approval |
-32002 |
approval_required |
Held for approval (async mode, see below) |
-32003 |
unauthorized |
Missing or invalid token, missing scope, unknown server |
-32020 |
header_mismatch |
Mcp-Method, Mcp-Name or MCP-Protocol-Version disagrees with the body |
-32021 |
missing_client_capability |
The request needs a client capability the client did not declare |
-32022 |
unsupported_protocol_version |
The gateway or the MCP server does not govern that revision; data.supported lists alternatives |
-32700, -32600, -32601, -32602, -32603 |
parse_error, invalid_request, method_not_found, invalid_params, internal_error |
Standard JSON-RPC |
MCP: /mcp/{server}#
The gateway speaks MCP over streamable HTTP. {server} is the name of an MCP server visible to the caller's namespace (an MCPServer resource rendered into the gateway's registry), or a built-in server (maqpna-web, memory, maqpna-k8s). Supported revisions: 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28.
POST /mcp/{server}#
Send one JSON-RPC message or a batch (a JSON array).
Request headers
| Header | Required | Meaning |
|---|---|---|
Authorization |
Yes | Bearer <session token> (or DPoP) |
Content-Type |
Yes | application/json |
Accept |
Recommended | application/json, text/event-stream (the MCP server may answer with an SSE stream) |
Mcp-Session-Id |
After initialize |
Passed through to the MCP server |
MCP-Protocol-Version |
2025-06-18 and later | Must match the revision in use |
Mcp-Method, Mcp-Name |
2026-07-28 | Must match the body's method and tool or resource name; a mismatch or a missing required header is 400 with -32020 |
X-Maqpna-Async |
No | 1: do not wait for a held approval; answer -32002 with an approvalId at once |
X-Maqpna-Approval-Id |
No | Retry a held call with the ID of an approved approval |
traceparent |
No | W3C trace context; the gateway continues the trace |
The X-Maqpna-* request headers are removed before the call is forwarded.
Methods and how the gateway handles them
| Method | Gateway behaviour |
|---|---|
initialize |
Forwarded; the result's capabilities are narrowed to what the gateway proxies (tools, resources, prompts, completions, logging; tasks is removed) |
tools/list |
Forwarded; the result is filtered per caller: tools the policy would always deny and drifted or unpinned tools (pin onChange: hide) are removed. Every tool definition is hashed for tool pinning |
tools/call |
Fully governed: kill switch, scope, request DLP, injection guards, tool pin, policy, budgets, approvals, response DLP, taint, audit record |
resources/read, prompts/get, completion/complete |
Forwarded; client payloads pass request DLP; the content they return taints the session when the server is untrusted |
server/discover (2026-07-28) |
Forwarded; supportedVersions and capabilities narrowed to what the gateway governs |
ping, notifications, responses to server requests (sampling, elicitation, roots) |
Forwarded; they still need the tools:{server} scope; client answers pass request DLP |
tasks/*, tools/call with params.task |
Refused: -32601 / -32602. Task-augmented execution is not proxied |
Request IDs must be a string or an integer. Other IDs (null, fractions, booleans, objects) get -32600. In a batch, request IDs must be unique.
Example: call a tool
curl -sS "$MAQPNA_GATEWAY_URL/mcp/echo" \
-H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello"}}}'
An allowed call returns the MCP server's result unchanged, except for DLP redactions:
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"hello"}]}}
Approvals. When a rule says require_approval, the gateway holds the call:
- Synchronous (default). The HTTP request stays open until an approver decides or the approval expires (
approvalTimeoutSeconds, default 300). Approved: the call runs once and you get its result. Denied or expired:-32001with reasonapproval_deniedorapproval_expired. - Asynchronous (
X-Maqpna-Async: 1). The gateway answers at once:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "approval required (destructive); retry with header X-Maqpna-Approval-Id once approved",
"data": {
"domain": "maqpna.com",
"reason": "approval_required",
"detail": "destructive",
"approvalId": "apr-3f9c",
"expiresAt": "2026-10-18T14:10:00Z",
"policy": "default-agent-policy",
"rule": "destructive-needs-human"
}
}
}
Retry the same call (same tool, same arguments) with X-Maqpna-Approval-Id: apr-3f9c. While the approval is pending you get -32002 again; once approved the call runs and the approval is consumed (single use). An approval ID for another call gets -32001 with reason approval_invalid.
- MCP 2026-07-28 clients get an
InputRequiredResultinstead of-32002, whoserequestState(maqpna-approval:<id>) the client echoes on retry.
A session can hold at most 20 pending approvals; the next held call is denied with approval_quota_exceeded. An identical pending call reuses its existing approval.
Tool pinning. The gateway hashes every tool definition it sees in tools/list and compares it with the pins of the caller's policies (ToolPolicy.spec.toolPins) or of the MCPServer:
| Pin mode | Effect |
|---|---|
off |
Nothing recorded |
learn |
First-seen hashes recorded and audited; nothing blocked |
enforce + onChange: hide |
A changed or unpinned tool is removed from tools/list; tools/call is denied with -32001, reason tool_definition_changed or tool_unpinned |
enforce + onChange: approve |
A call to a changed tool requires approval |
enforce + onChange: alert |
Allowed and audited |
A tools/call for a tool the gateway has not seen listed triggers one upstream tools/list (at most every pinCacheSeconds, default 60 s). See MCP servers and tool pinning.
Status codes
| HTTP | JSON-RPC | When |
|---|---|---|
200 |
result or in-band error | Every governed decision (allowed, denied -32001, held -32002), including missing scope on a request |
400 |
-32700, -32600, -32020, -32022 |
Invalid JSON, invalid batch, bad request ID, duplicate member names, header mismatch, unsupported revision |
401 |
-32003 |
Missing or invalid token (WWW-Authenticate set) |
403 |
— / -32001 |
A notification or response that was denied (no ID to answer), or a revoked caller on the stream endpoints |
404 |
-32003 |
Unknown MCP server (unknown_server, ambiguous_server, cross_tenant_server in data.reason) |
413 |
-32600 |
Body larger than maxBodyBytes (default 4 MiB) |
502, 503 |
-32603 |
MCP server unreachable (upstream_unavailable), audit ledger unavailable in auditFailurePolicy: closed (audit_unavailable) |
GET /mcp/{server} and DELETE /mcp/{server}#
GET opens the server-to-client SSE stream of the streamable HTTP transport; DELETE ends the MCP session named by Mcp-Session-Id. Both need the session token and the tools:{server} scope, are checked against the kill switch, and are proxied with the gateway's credential. Server-initiated messages on the stream pass DLP.
curl -N "$MAQPNA_GATEWAY_URL/mcp/echo" \
-H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
-H "Accept: text/event-stream" \
-H "Mcp-Session-Id: <session id from initialize>"
| HTTP | When |
|---|---|
200 |
Stream open / session ended (the MCP server's answer) |
401 |
Missing or invalid token |
403 |
Token lacks tools:{server}, or the caller is revoked |
404 |
Unknown MCP server |
Built-in server memory#
POST /mcp/memory serves governed long-term memory with the tools memory_put {key, value, tags?}, memory_get {key}, memory_search {query, limit?} and memory_delete {key}. It needs scope tools:memory and a memory store bound to the caller's agent. Values are never written to the audit ledger; the record holds the store and the value's SHA-256. Without a store you get memory_backend_unavailable.
GET /.well-known/oauth-protected-resource/mcp/{server}#
RFC 9728 protected-resource metadata for external MCP clients that authenticate with your identity provider. It is served only when identity.trustedIssuers is configured and an issuer allows the server; otherwise 404.
{
"resource": "https://gateway.example.eu/mcp/github",
"authorization_servers": ["https://sso.example.eu/realms/acme"],
"scopes_supported": ["tools:github"],
"bearer_methods_supported": ["header"],
"resource_name": "MAQPNA governed MCP server github"
}
A2A: /a2a/{ns}/{agent}#
Agents call other agents through the gateway. {ns}/{agent} is a hosted agent with spec.a2a.expose or an A2APeer (a remote agent). The gateway governs A2A v1.0 and accepts A2A 0.3 method names, which it governs as their v1.0 equivalents. The upstream always receives JSON-RPC; the HTTP+JSON binding is translated.
Every call needs scope agents:<agent> (agents:<ns>/<agent> across namespaces), must come from a caller in the target's allowedCallers, and is governed like a tool call: policy with server a2a/<agent> (a2a/<ns>/<agent> across namespaces) and tool SendMessage, skill:<id> or the method name; approvals, DLP, taint, budgets, the kill switch and the audit ledger apply. Both hops are audited.
Request headers
| Header | Meaning |
|---|---|
Authorization |
Bearer <session token> |
Content-Type |
application/json (or application/a2a+json) |
A2A-Version |
1.0; absent or 0.3.x selects the 0.3 protocol |
A2A-Extensions |
Passed through |
X-Maqpna-Async, X-Maqpna-Approval-Id |
As for MCP |
POST /a2a/{ns}/{agent} (JSON-RPC binding)#
| Method (v1.0) | 0.3 name |
|---|---|
SendMessage |
message/send |
SendStreamingMessage (SSE) |
message/stream |
GetTask |
tasks/get |
ListTasks |
tasks/list |
CancelTask |
tasks/cancel |
SubscribeToTask (SSE) |
tasks/resubscribe |
CreateTaskPushNotificationConfig |
tasks/pushNotificationConfig/set |
GetTaskPushNotificationConfig |
tasks/pushNotificationConfig/get |
ListTaskPushNotificationConfigs |
tasks/pushNotificationConfig/list |
DeleteTaskPushNotificationConfig |
tasks/pushNotificationConfig/delete |
GetExtendedAgentCard |
agent/getAuthenticatedExtendedCard |
curl -sS "$MAQPNA_GATEWAY_URL/a2a/team-a/reviewer" \
-H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"role":"ROLE_USER","messageId":"m-1","parts":[{"text":"Review PR 4821"}]}}}'
The answer is the callee's JSON-RPC response. Errors use the envelope in error.data.
/a2a/{ns}/{agent}/{rest...} (HTTP+JSON binding)#
| HTTP | Path below the agent | Equivalent method |
|---|---|---|
POST |
message:send |
SendMessage |
POST |
message:stream |
SendStreamingMessage |
GET |
extendedAgentCard |
GetExtendedAgentCard |
GET |
tasks |
ListTasks (query parameters become params; historyLength, pageSize are numbers) |
GET |
tasks/{id} |
GetTask |
POST |
tasks/{id}:cancel |
CancelTask |
POST, GET |
tasks/{id}:subscribe |
SubscribeToTask |
POST |
tasks/{id}/pushNotificationConfigs |
CreateTaskPushNotificationConfig |
GET |
tasks/{id}/pushNotificationConfigs |
ListTaskPushNotificationConfigs |
GET |
tasks/{id}/pushNotificationConfigs/{configId} |
GetTaskPushNotificationConfig |
DELETE |
tasks/{id}/pushNotificationConfigs/{configId} |
DeleteTaskPushNotificationConfig |
A unary answer is the result object with 200; streams are SSE streams of the bare response objects. Errors are google.rpc.Status:
{"error":{"code":403,"status":"PERMISSION_DENIED","message":"denied by policy budget rule agent-day: budget_exceeded:agent/day","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"BUDGET_EXCEEDED","domain":"maqpna.com","metadata":{"jsonrpcCode":"-32001","policy":"budget","reason":"budget_exceeded:agent/day","rule":"agent-day"}}]}}
A pending approval on this binding is 409 with status ABORTED.
Agent card#
GET /a2a/{ns}/{agent}/.well-known/agent-card.json returns the card of a hosted agent, or a gateway-issued proxy card of a verified remote peer. It needs a session token with the agents: scope for that agent, unless a2a.publicCards is true. The card lists two interfaces, both at <publicURL>/a2a/{ns}/{agent}: JSONRPC and HTTP+JSON, protocol version 1.0, and a Bearer security scheme. With a2a.cardSigningKey set, the card is signed (JWS over the RFC 8785 canonical form) and the keys are published at GET /a2a/jwks.json (Cache-Control: public, max-age=300; 404 when signing is off).
curl -sS "$MAQPNA_GATEWAY_URL/a2a/team-a/reviewer/.well-known/agent-card.json" \
-H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")"
| HTTP | When |
|---|---|
200 |
Card |
401, 403 |
Missing token, or no agents: scope for the agent |
404 |
Unknown agent |
502 |
Remote peer card unavailable or not verified |
Push notifications#
A push-notification config names a webhook the callee posts task updates to, bypassing the gateway. The gateway therefore refuses every webhook URL (in CreateTaskPushNotificationConfig and in a send's configuration) that is not https to a host matching a2a.pushNotificationHosts. With no hosts configured, push notifications are refused. The reason is push_url_not_allowed.
A2A reasons#
unknown_agent, caller_not_allowed, unknown_skill, unknown_method, delegation_depth (act chain longer than a2a.maxDelegationDepth, default 5), delegation_loop, callee_revoked, peer_card_unverified, push_url_not_allowed. See Agent-to-agent calls.
Model calls: /llm/{route}#
{route} is a model route rendered from the agent's spec.model (or a static models entry). Every model call needs scope models:<route>; the route may also restrict namespaces and agents. The gateway meters tokens and cost, runs DLP and injection guards on prompts and completions, enforces budgets and tokensPerMinute, and fails over to the route's fallbacks. With modelPolicy: policy it also evaluates your policies (server llm/<route>, tool = the operation, for example chat.completions); a require_approval result is a denial, because model calls cannot wait for approvals.
Wire formats#
The governance pipeline always runs on the client's wire format. Just before the upstream call, the endpoint's provider adapter turns the request into the provider's API, and the response back:
| Client wire format | Paths |
|---|---|
| OpenAI | /llm/{route}/v1/chat/completions, /completions, /embeddings, /models (GET) |
| OpenAI Responses | /llm/{route}/v1/responses |
| Anthropic Messages | /llm/{route}/anthropic/v1/messages, /messages/count_tokens |
| Provider (route endpoint) | Modes | Upstream auth (default) |
|---|---|---|
openai (OpenAI-compatible, default) |
— | apiKeyFile as Bearer |
azure-openai |
v1 (default), deployments |
Entra bearer token, or apiKeyFile as api-key |
bedrock |
converse (default), openai |
SigV4, or a Bedrock API key as Bearer |
vertex |
native (default), openai |
Google OAuth bearer token |
gemini |
— | apiKeyFile as x-goog-api-key |
anthropic |
direct (default), vertex, bedrock |
x-api-key, Google bearer, SigV4 |
oci-genai |
— | apiKeyFile as Bearer |
llamay |
OpenAI-compatible | per-session context binding; see Llamay models |
Point an SDK at the route: OpenAI SDKs use base URL $MAQPNA_GATEWAY_URL/llm/<route>/v1; Anthropic SDKs use ANTHROPIC_BASE_URL=$MAQPNA_GATEWAY_URL/llm/<route>/anthropic. The session token is the API key.
curl -sS "$MAQPNA_GATEWAY_URL/llm/team-a.coder/v1/chat/completions" \
-H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Say hello"}]}'
import os
from openai import OpenAI
client = OpenAI(
base_url=os.environ["MAQPNA_GATEWAY_URL"] + "/llm/team-a.coder/v1",
api_key=open(os.environ["MAQPNA_TOKEN_FILE"]).read().strip(),
)
r = client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "Say hello"}])
print(r.choices[0].message.content)
Anthropic wire format:
curl -sS "$MAQPNA_GATEWAY_URL/llm/claude/anthropic/v1/messages" \
-H "x-api-key: $(cat "$MAQPNA_TOKEN_FILE")" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-5","max_tokens":256,"messages":[{"role":"user","content":"Say hello"}]}'
The response is the provider's answer in the client's wire format. For streaming chat completions the gateway may add stream_options.include_usage to meter the stream; it then drops the usage-only final chunk if the client did not ask for it.
Provider-hosted tools. A request that asks the provider to call MCP servers itself (Anthropic mcp_servers / mcp_toolset, OpenAI Responses mcp tools) is denied with provider_hosted_mcp. A provider-hosted server tool (web search or fetch, code execution, file search, computer use, image generation, Gemini grounding, Bedrock system tools) is denied with provider_hosted_tool:<name> unless the route's providerTools lists it.
Errors#
{"error":{"message":"agent budget (day) exhausted for team-a/coder: spent 10.000412 of 10.000000 USD","type":"maqpna_budget_exceeded","code":"budget_exceeded","reason":"budget_exceeded","domain":"maqpna.com"}}
| HTTP | type |
Typical code / reason |
|---|---|---|
400 |
invalid_request_error |
invalid_body, invalid_json, unsupported_by_provider |
401 |
maqpna_unauthorized |
missing_token, invalid_token, token_expired |
402 |
maqpna_budget_exceeded |
budget_exceeded |
403 |
maqpna_unauthorized |
missing_scope, namespace_not_allowed, agent_not_allowed |
403 |
maqpna_policy_denied |
policy_denied, provider_hosted_mcp, provider_hosted_tool, model_not_allowed_for_data_class |
403 |
maqpna_approval_required |
approval_required (approval_required_unsupported_for_models) |
403 |
maqpna_revoked |
revoked |
403 |
maqpna_dlp_blocked |
dlp:<detector> |
403 |
maqpna_egress_denied |
egress_denied |
404 |
maqpna_not_found |
route_not_found |
429 |
maqpna_rate_limited |
tokens_per_minute (reason rate_limited); honour Retry-After |
502 |
maqpna_upstream_error |
upstream_error, upstream_unavailable |
503 |
maqpna_audit_unavailable |
audit_unavailable |
code keeps its pre-v0.1.0 value for OpenAI-SDK compatibility; reason is the published token. See Model calls and LLM call governance.
Egress proxy#
With egress.enabled, the gateway's data listener is also an HTTP forward proxy for browser-profile sandboxes. CONNECT requests (HTTPS tunnels) and absolute-form requests (plain HTTP) are governed here instead of being routed to the API. The browser sidecar's relay adds the session token.
Each request is:
- Authenticated:
Proxy-Authorization: Bearer <session token>(orBasicwith the token as password), scopetools:maqpna-egress, and the kill switch. - Checked against the URL rules and the domain policy (the agent's
spec.browser.allowedDomains). - Evaluated by your policies with server
maqpna-egressand toolconnectorhttp(argumentshost,port,method,url).require_approvalis enforced as a denial. - Dialled through the guarded transport (SSRF block list, rebinding-safe DNS, residency check) and audited.
curl -sS -x "$MAQPNA_GATEWAY_URL" \
--proxy-header "Proxy-Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
https://docs.example.eu/
| HTTP | When |
|---|---|
200 |
Tunnel established / response relayed |
400 |
Bad target |
403 |
Denied (X-Maqpna-Egress: denied; body maqpna egress: <reason>), including revoked and missing_scope:tools:maqpna-egress |
407 |
Missing or invalid token (Proxy-Authenticate: Bearer realm="maqpna-egress") |
DPoP-bound tokens cannot be used at the proxy (dpop_proxy): a proxy request cannot carry a proof.
Health, readiness, version and metrics#
GET /healthz#
Liveness. Always 200 while the process serves.
curl -sS "$MAQPNA_GATEWAY_URL/healthz"
{"status":"ok"}
GET /readyz#
Readiness. 200 with {"status":"ready"} when policies are loaded, the gateway has identity verification keys, and the revocation list, state backend and audit ledger are healthy. Otherwise 503 with the list of problems. A draining replica answers 503 with shutting down.
{"status":"not ready","problems":["policies not loaded","no identity verification keys"]}
Other checks (audit ledger, governance stores) add their own problem texts. The common ones:
| Problem text | Cause |
|---|---|
policies not loaded |
policyFile missing or invalid at start |
no identity verification keys |
JWKS not reachable and no identity.publicKeyFiles |
revocation list unavailable (failing closed): <error> |
Revocation list unreadable |
state backend unreachable: <error> |
PostgreSQL state backend down |
shutting down |
The replica is draining |
GET /version#
Build information. Hidden (404) on the public listener when adminListen is set.
{"component":"gateway","version":"2.1.0","commit":"<commit>","buildDate":"<date>","goVersion":"<go version>","platform":"linux/amd64"}
The fields are component, version, commit, buildDate, goVersion, platform and tags; the values above are placeholders.
GET /metrics#
Prometheus text format (text/plain; version=0.0.4). Hidden (404) on the public listener when adminListen is set; then scrape the admin listener. The metric families are listed in Metrics.
curl -sS "$MAQPNA_GATEWAY_URL/metrics" | grep maqpna_gateway_requests_total
Request limits#
| Limit | Default | Setting |
|---|---|---|
| Request body | 4 MiB | maxBodyBytes |
| Time to send the body | 60 s | bodyReadTimeoutSeconds |
| Upstream timeout | 60 s | upstreamTimeoutSeconds |
| Approval wait | 300 s | approvalTimeoutSeconds |
| Pending approvals per session | 20 | fixed |
Every response carries X-Content-Type-Options: nosniff. Browser origins are allowed only when listed in the gateway's CORS settings; preflight allows GET, POST, DELETE, OPTIONS with Authorization, Content-Type.
Next steps#
- Admin API: approvals, audit, sessions, kill switch and every other
/v1/*route - Identity broker API: how session tokens are minted, exchanged and verified
- Handle decisions: what your agent should do with each reason
- Gateway configuration: every
gateway.jsonkey named on this page - Gateway request pipeline: the order of checks behind each call