Developer Integration Guide
For: Application developers integrating with Axemere Gateway.
This guide covers how to send requests through Axemere Gateway to AI providers (OpenAI, Anthropic, Google Gemini, Azure OpenAI, etc.). The gateway enforces policies, tracks attribution, and records every request for auditability.
Table of Contents
- What Axemere Gateway Does
- Integration Modes
- Additional curl Examples
- Idempotency Keys
- Delegation Tokens
- Response Codes and Errors
- Reporting APIs
- Error Handling and Retries
- Health Check
- SDK and Client Libraries
What Axemere Gateway Does
Axemere Gateway provides:
- Policy enforcement: per-request evaluation using a declarative DSL (allow, deny, downgrade, require_approval)
- Attribution tracking: every request carries customer, account, and project context for chargeback and reporting
- Credential management: BYOK (caller-supplied key passthrough) and key alias (server-side resolution); your application never needs to handle AI provider keys directly in alias mode
- Budget controls: per-request cost caps and per-project daily spend limits
- Execution records: every request is recorded and auditable
- Connector system: routes to the correct AI provider based on policy selection, action type, or target host
Integration Modes
The gateway supports three connection types, assigned automatically based on how the request arrives; callers do not set a connection type field.
| Connection type | Code value | How to use it |
|---|---|---|
| Direct API | direct_api | Your application sends a structured ActionRequest JSON body to POST /v1/actions:execute. Full access to attribution, delegation tokens, and all policy response types. |
| SDK Redirect | sdk_redirect | Point your existing AI SDK at the gateway via a base URL setting (e.g. ANTHROPIC_BASE_URL=http://localhost:7080/proxy/anthropic/). No MVGC-specific code required. Enable with MVGC_PROXY_ENABLED=true. |
| System Proxy | connect_proxy | Route OS-level HTTPS traffic through the gateway via HTTPS_PROXY. The gateway performs TLS interception. |
Explicit Action Request Mode
Send a structured ActionRequest JSON body to POST /v1/actions:execute. This is the
recommended mode; it provides full access to attribution, credential selection, delegation
tokens, and the complete set of policy response types (202 approvals, 429 rate limits).
Request Schema
POST /v1/actions:execute
Content-Type: application/json
{ "schema": "mvgc.action_request.v2", "request_id": "req-001", "org_id": "org-example-001", "caller_id": "caller-001", "workload_id": "wl-prod-app-1", "context": { "purpose": "customer-support-lookup", "labels": { "env": "production" } }, "action": { "type": "ai.infer", "method": "POST", "target": "https://api.openai.com/v1/chat/completions", "target_host": "api.openai.com", "target_path": "/v1/chat/completions", "params": { "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 50 } }, "attribution": { "customer_id": "cust-42", "account_id": "acct-12", "project_id": "proj-123" } }
Required fields:
| Field | Description |
|---|---|
schema | Always "mvgc.action_request.v2" (note: action response schema is "mvgc.action_response.v1") |
org_id | Your organization identifier |
workload_id | The workload this request belongs to |
action.type | Action type, e.g. "ai.infer" |
action.method | HTTP method for the upstream call |
action.target_host | Upstream provider hostname |
action.params | Provider-specific request body |
Attribution Fields
Attribution fields are used for chargeback, reporting, and policy evaluation. Include as many as are meaningful for your use case.
| Field | Description |
|---|---|
attribution.customer_id | End customer identifier |
attribution.account_id | Account or team identifier |
attribution.project_id | Project identifier (required by some budget policies) |
If a workload has default_attribution configured, any fields you omit will be filled in
from the workload defaults.
Optional Fields
| Field | Description |
|---|---|
request_id | Idempotency key; auto-generated as a UUID if omitted |
caller_id | Identifier of the calling service or user |
context.purpose | Caller intent (string); usable in policy DSL conditions via context.purpose |
context.labels | Caller-supplied key-value labels (object); usable in policy DSL via context.labels.<key> |
action.target | Full target URL (e.g. https://api.openai.com/v1/chat/completions); populated during normalization from target_host + target_path if not set explicitly |
credential_hint | Override credential selection: byok -- supply the raw API key; alias -- supply the credential_id |
connector_hint | Override connector selection. Valid values are the connector IDs listed by GET /v1/admin/connectors: built-in values are openai, anthropic, gemini, azure_openai, cohere, generic_http. |
idempotency_key | Client-supplied idempotency key; if a record with this key already exists the cached result is returned |
delegation_id | Reference ID when a delegation token authorizes the request |
delegation_token | Base64-encoded delegation token |
action.target_path | Path on the upstream host; defaults to the provider connector default if omitted |
action.headers | Additional headers to forward to the upstream provider |
Streaming Responses (SSE)
Axemere Gateway supports server-sent events (SSE) streaming for providers that offer it (OpenAI, Anthropic,
Google Gemini, Azure OpenAI, Cohere). To enable streaming, set "stream": true in action.params:
{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "action": { "type": "ai.infer", "method": "POST", "target_host": "api.openai.com", "target_path": "/v1/chat/completions", "params": { "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Tell me a short story."}], "max_tokens": 200, "stream": true } }, "attribution": {"project_id": "proj-123"} }
When stream: true is set, the gateway:
- Forwards the request to the upstream provider with streaming enabled
- Returns the SSE stream directly to your client -- no JSON wrapper
- Computes the SHA-256 body hash incrementally as chunks arrive
- Writes an execution record asynchronously after the stream completes
Supported streaming providers:
| Provider | action.target_host |
|---|---|
| OpenAI | api.openai.com |
| Anthropic | api.anthropic.com |
| Google Gemini | generativelanguage.googleapis.com |
| Azure OpenAI | *.openai.azure.com |
| Cohere | api.cohere.com |
Response format for streaming requests:
The response is a raw SSE stream (Content-Type: text/event-stream), identical to what the upstream provider would return:
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":"Once"},"index":0}]}
data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"delta":{"content":" upon"},"index":0}]}
data: [DONE]
There is NO JSON envelope (ConnectorResultDTO) for streaming responses. Parse the SSE stream directly.
curl example:
curl -s -X POST http://localhost:7080/v1/actions:execute \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "action": { "type": "ai.infer", "method": "POST", "target_host": "api.openai.com", "target_path": "/v1/chat/completions", "params": { "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Say hi."}], "stream": true } }, "attribution": {"project_id": "proj-123"}, "credential_hint": "sk-..." }'
Streaming in transparent proxy mode:
Transparent proxy mode also supports streaming. Pass stream: true in the provider request body as normal; the gateway detects and forwards the SSE stream, adding the same X-MVGC-* headers for attribution.
Limitations:
- For OpenAI, token counts are only available in the stream when
stream_options: {"include_usage": true}is set inaction.params. Without this, the gateway records token counts of 0 for streaming requests, which causes budget tracking to undercount spend. Add it alongside"stream": true:
"params": { "model": "gpt-4o-mini", "messages": [...], "stream": true, "stream_options": {"include_usage": true} }
For Anthropic, usage is always included in streaming responses -- no extra param needed.
require_approvalandrate_limitdecisions (HTTP 202/429) are returned synchronously before streaming begins; the stream only starts on anallowordowngradedecision
Transparent Proxy Mode
In transparent proxy mode your application sends requests directly to the AI provider path
(e.g. POST /v1/chat/completions) and the gateway intercepts them. Identity and attribution
are passed via X-MVGC-* headers instead of a JSON body.
Required headers:
| Header | Description |
|---|---|
X-MVGC-Org-ID | Your organization identifier |
X-MVGC-Workload-ID | The workload this request belongs to |
Optional attribution headers:
| Header | Maps to | Description |
|---|---|---|
X-MVGC-Project-ID | attribution.project_id | Project identifier |
X-MVGC-Customer-ID | attribution.customer_id | End customer identifier |
X-MVGC-Account-ID | attribution.account_id | Account or team identifier |
X-MVGC-Target-Host | action.target_host | Upstream provider hostname; explicit override, required for Azure or custom endpoints |
X-MVGC-Delegation-ID | delegation_id | Reference ID of a delegation token |
Any attribution fields not supplied via headers are filled in from the workload's
default_attribution config, the same as in explicit mode.
Path-prefix routing (recommended)
The preferred way to identify the upstream provider in proxy mode is via the /proxy/{provider}/
path prefix. The gateway strips the prefix and routes to the correct provider without any
additional header:
| Path prefix | Upstream host |
|---|---|
/proxy/openai/ | api.openai.com |
/proxy/anthropic/ | api.anthropic.com |
/proxy/gemini/ | generativelanguage.googleapis.com |
/proxy/cohere/ | api.cohere.com |
/proxy/perplexity/ | api.perplexity.ai |
The prefix only selects the upstream host; everything after it is forwarded verbatim, with no path translation. Two providers worth calling out explicitly:
- Perplexity: its OpenAI-compatible surface is
/chat/completions, no/v1prefix, unlike OpenAI itself and most otheropenai_compatproviders (DeepSeek, Mistral, etc.). SendPOST /proxy/perplexity/chat/completions, not/proxy/perplexity/v1/chat/completions. - Gemini has two separate surfaces with different auth: native
/v1beta/models/{model}:generateContent(auth viax-goog-api-key), and OpenAI-compatible/v1beta/openai/chat/completions(auth viaAuthorization: Bearer, notx-goog-api-key). Picking the wrong path/header combo produces a provider-side 400/401, not a gateway error; see Gemini SDK for both examples.
Example using path-prefix routing:
# Anthropic via path prefix — no X-MVGC-Target-Host needed curl -X POST http://localhost:7080/proxy/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "X-MVGC-Org-ID: org-example-001" \ -H "X-MVGC-Workload-ID: wl-prod-app-1" \ -H "X-MVGC-Project-ID: proj-123" \ -d '{"model": "claude-3-5-sonnet-20241022", "max_tokens": 50, "messages": [{"role": "user", "content": "Hello"}]}'
SDK base URL pattern:
# Set once per provider; SDK sends all requests through the gateway export ANTHROPIC_BASE_URL=http://localhost:7080/proxy/anthropic export ANTHROPIC_API_KEY=unused # gateway injects the stored key; this value is ignored export OPENAI_BASE_URL=http://localhost:7080/proxy/openai export OPENAI_API_KEY=unused
Target host resolution order: (1) X-MVGC-Target-Host header, (2) request Host header,
(3) /proxy/{provider}/ path prefix, (4) 400 error if none resolves.
For Azure OpenAI or any endpoint not in the provider registry, use X-MVGC-Target-Host directly:
curl -X POST http://localhost:7080/v1/chat/completions \ -H "Content-Type: application/json" \ -H "X-MVGC-Org-ID: org-example-001" \ -H "X-MVGC-Workload-ID: wl-prod-app-1" \ -H "X-MVGC-Target-Host: my-resource.openai.azure.com" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 50}'
MVGC_PROXY_ENABLED must be set to true.
Limitations compared to explicit mode:
credential_hintandconnector_hintare not supported; credential and connector selection cannot be overridden per-request.- Delegation token verification is not supported.
X-MVGC-Delegation-IDis recorded but the token payload is not verified by the gateway. - All non-allow policy decisions (deny, require_approval, rate_limit, quarantine) return
HTTP 403. The richer responses (202 withapproval_id, 429 withRetry-After) are only available in explicit mode.
For full control over attribution, credentials, delegation, and approval workflows use explicit action request mode.
Additional curl Examples
Anthropic request with attribution
curl -X POST http://localhost:7080/v1/actions:execute \ -H "Authorization: Bearer $MVGC_WORKLOAD_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "caller_id": "wl-prod-app-1", "workload_id": "wl-prod-app-1", "attribution": {"project_id": "proj-123", "account_id": "acct-12"}, "action": { "type": "ai.infer", "method": "POST", "target_host": "api.anthropic.com", "target_path": "/v1/messages", "params": {"model": "claude-3-5-sonnet-20241022", "max_tokens": 1024} } }'
Expected 200 response:
{ "schema": "mvgc.action_response.v1", "request_id": "req-001", "record_id": "019508a3-...", "record_hash": "a1b2c3d4e5f6...", "decision": "allow", "result": { "status_code": 200, "body": { "id": "msg_...", "type": "message", "content": [...] } } }
Denied request (403) -- missing project_id
When a policy requires attribution fields and they are missing, the gateway returns a
require_attribution denial:
curl -X POST http://localhost:7080/v1/actions:execute \ -H "Authorization: Bearer $MVGC_WORKLOAD_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "action": { "type": "ai.infer", "method": "POST", "target_host": "api.openai.com", "target_path": "/v1/chat/completions", "params": {"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]} } }'
Expected 403 response:
{ "schema": "mvgc.action_response.v1", "decision": "deny", "reason": "require_attribution: project_id required", "reason_codes": ["project_id"], "record_hash": "f0e1d2c3..." }
Downgraded request (200) -- cost cap triggers model downgrade
When a policy downgrade rule matches (e.g. a cost cap), the gateway may substitute a
cheaper model before forwarding the request. The response shows decision: "downgrade"
and the trace includes the mutated action:
curl -X POST http://localhost:7080/v1/actions:execute \ -H "Authorization: Bearer $MVGC_WORKLOAD_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "attribution": {"project_id": "proj-123"}, "action": { "type": "ai.infer", "method": "POST", "target_host": "api.openai.com", "target_path": "/v1/chat/completions", "params": {"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 1024} } }'
Expected 200 response (downgraded):
{ "schema": "mvgc.action_response.v1", "request_id": "req-002", "record_id": "019508b1-...", "record_hash": "b2c3d4e5...", "decision": "downgrade", "decision_trace": { "schema": "mvgc.decision_trace.v1", "decision": "downgrade", "matched_rule_ids": ["cost_cap.downgrade_expensive_models"], "attributes": {"original_model": "gpt-4o", "substituted_model": "gpt-4o-mini"} }, "result": { "status_code": 200, "body": { "id": "chatcmpl-...", "model": "gpt-4o-mini", "choices": [...] } } }
The decision_trace.attributes shows what changed. The upstream provider received
gpt-4o-mini instead of the originally requested gpt-4o.
Transparent proxy mode with X-MVGC-* headers
In transparent proxy mode, configure your application's HTTP client to use the Axemere Gateway
as a proxy. Attribution can be encoded in the base URL path (zero application changes) or passed
via X-MVGC-* headers for per-request control. See the Transparent Proxy Path Attribution guide for the full URL format and SDK examples.
Attribution via X-MVGC-* headers:
export HTTP_PROXY=http://localhost:7080 curl https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "X-MVGC-Org-ID: org-example-001" \ -H "X-MVGC-Workload-ID: wl-prod-app-1" \ -H "X-MVGC-Project-ID: proj-123" \ -H "X-MVGC-Customer-ID: cust-42" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 50}'
In proxy mode the gateway intercepts the request transparently:
- The request URL targets the upstream provider directly (
api.openai.com) X-MVGC-*headers provide org, workload, and attribution context- The gateway evaluates policies, records the execution, and forwards the request
- The response comes directly from the upstream provider -- no JSON wrapper
MVGC_PROXY_ENABLED=truemust be set on the gateway
This mode is useful when you cannot modify the request body (e.g. using an SDK that constructs requests internally). The tradeoff is reduced control: credential/connector hints, delegation tokens, and rich error responses (202, 429) are not available.
Idempotency Keys
Include an idempotency_key in your ActionRequest to deduplicate requests. If the gateway
receives a request with the same (org_id, idempotency_key) pair as an existing execution
record, it replays the cached result without re-executing the connector call.
{ "schema": "mvgc.action_request.v2", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "idempotency_key": "payment-batch-2026-03-12-item-47", "action": { ... }, "attribution": { ... } }
When set, the gateway also forwards the key to upstream providers that support it via the
Idempotency-Key HTTP header (e.g. OpenAI, Anthropic). This provides end-to-end idempotency
from your application through to the AI provider.
Notes:
- Keys are scoped per
org_id-- different orgs can reuse the same key without collision. - A replayed response returns the same
record_id,record_hash, anddecisionas the original execution. - If the original request is still in progress (e.g. streaming), the gateway does not replay; it processes the request normally.
Delegation Tokens
Delegation tokens grant scoped, time-limited authorization. They allow a system to authorize a downstream workload to perform a limited set of actions without sharing full credentials. Tokens are signed JSON documents verified by the gateway before policy evaluation.
The wire format uses schema "mvgc.delegation.v2" and includes org_id and workload_id
scoping fields per wire spec section 4.2:
{ "schema": "mvgc.delegation.v2", "delegation_id": "dt-abc123", "org_id": "org-example-001", "workload_id": "wl-prod-app-1", "principal_id": "user-abc", "issued_at": "2026-03-12T10:00:00Z", "expires_at": "2026-03-12T11:00:00Z", "scope": { "actions_allow": ["ai.infer"], "targets_allow": ["api.openai.com"], "methods_allow": ["POST"], "resource_patterns_allow": ["/v1/chat/*"] }, "budget": { "usd_max": "5.00", "allowed_models": ["gpt-4o-mini"] }, "attribution": { "defaults": { "project_id": "proj-123" }, "allow_overrides": ["customer_id"] }, "sig": { "alg": "ed25519", "kid": "kid_...", "sig": "..." } }
Include a delegation token in your request:
{ "delegation_id": "dt-abc123", "delegation_token": "<base64-encoded-token>", ... }
The gateway verifies the token signature, checks expiry, validates that org_id matches the
request, and populates delegation context for policy evaluation. Invalid or expired tokens
result in a 403 denial.
To create a delegation token from Go:
signer := signing.NewSigner(privKey) tokenBytes, err := delegation.CreateToken(signer, delegation.DelegationClaims{ IssuedBy: "admin-node", OrgID: "org-example-001", // must match request org_id WorkloadID: "wl-prod-app-1", // scoping: only this workload may use the token PrincipalID: "user-abc", // optional: end-user or service identity Audience: "worker-workload", TTL: time.Hour, Scope: delegation.Scope{ ActionsAllow: []string{"ai.infer"}, TargetsAllow: []string{"api.openai.com"}, MethodsAllow: []string{"POST"}, ResourcePatternsAllow: []string{"/v1/chat/*"}, }, Budget: delegation.Budget{ USDMax: "5.00", AllowedModels: []string{"gpt-4o-mini"}, }, Attribution: delegation.TokenAttribution{ Defaults: map[string]string{"project_id": "proj-123"}, AllowOverrides: []string{"customer_id"}, }, })
The verification key must be configured on the gateway via MVGC_DELEGATION_VERIFY_KEY.
Policy DSL fields from delegation tokens:
Once a token is verified, these fields are available in policy rules:
| DSL field | Description |
|---|---|
context.delegation.issued_by | Identity of the token issuer |
context.delegation.principal_id | End-user or service identity from the token |
context.delegation.audience | Intended audience (workload) |
context.delegation.token_id | Unique token identifier |
context.delegation.depth | Delegation chain depth (integer string); max depth is 3 |
context.delegation.policy_bundle_id | Bundle ID pinned by the token (if set) |
context.delegation.max_cost_usd | Per-token spend cap |
context.delegation.actions_allow | Comma-joined list of permitted action types |
context.delegation.targets_allow | Comma-joined list of permitted target hosts |
context.estimates.tokens_out_max | Maximum estimated output tokens (integer string) |
context.attribution.labels.<key> | Attribution label value for the given key |
context.action.headers_present | Comma-separated list of present header names (e.g. "content-type,authorization") |
context.action.header.<name> | "true" if the named header is present (case-insensitive), empty otherwise (e.g. context.action.header.content-type) |
context.candidates.connectors | Comma-separated eligible connector IDs |
context.candidates.credentials | Comma-separated eligible credential IDs |
Use the in operator to check membership in list fields:
rules: - id: delegation.allow.restrict_to_infer conditions: all: - field: context.delegation.actions_allow operator: in value: "ai.infer" effect: allow
Header presence checks:
Use action.header.<name> with the exists operator to require a specific header:
identity: - id: identity.deny.no_auth_header priority: 100 when: field: context.action.header.authorization operator: exists negate: true effect: decision: deny reason: "authorization header required"
Delegation scope enforcement:
The gateway enforces methods_allow and resource_patterns_allow from the delegation token's
scope block. If the request's HTTP method is not in methods_allow, or the request path does
not match any pattern in resource_patterns_allow (glob matching), the request is denied before
policy evaluation. Patterns support * for single-segment wildcards (e.g. /v1/chat/* matches
/v1/chat/completions).
Delegation chains deeper than 3 levels are also denied. The gateway checks
context.delegation.depth and rejects requests where the depth exceeds 3.
Bundle pinning: If a delegation token includes a policy_bundle_id, the gateway verifies
that the active policy bundle matches. Requests are denied if the pinned bundle ID does not
match the currently loaded bundle.
PolicyTrace from connectors:
Connectors can optionally call gateway.PolicyTraceFromContext(ctx) to retrieve the
decision trace from the request context for logging or debugging purposes. This is useful
when building custom connectors that need to inspect the policy decision that led to
their execution.
Decision trace fields:
The decision_trace object in the response includes selected_connector_id and
selected_credential_id, showing which connector and credential were chosen for the request:
{ "decision_trace": { "schema": "mvgc.decision_trace.v1", "decision": "allow", "selected_connector_id": "openai", "selected_credential_id": "cred-openai", "matched_rule_ids": ["identity.allow.all.explicit"] } }
Risk-based policy conditions:
The context.risk.* DSL namespace is fully wired into the execution flow. Risk scores are
computed per-request and available for policy evaluation. See the
Risk Scoring section in the Network Operations Guide for
available fields and configuration.
Policy bundle validation:
Policy bundles must declare schema: "mvgc.policy_bundle.v1". Bundles with a missing or
incorrect schema value are rejected at load time with a parse error. This applies to both
filesystem-loaded bundles and bundles pushed via PUT /v1/admin/policies.
Policy DSL effects for credential and attribution management:
The credentials policy layer supports three effects that control credential selection and attribution enforcement:
select_credential -- Selects a credential for the request. The provider, mode, and
billing_owner attributes filter the candidate credential list rather than being advisory.
Only credentials matching all specified attributes are considered:
credentials: - id: credentials.select.openai when: field: context.action.target_host equals: "api.openai.com" effect: select_credential: credential_id: cred-openai provider: openai # filters: only openai credentials mode: alias # filters: only alias (key alias) credentials billing_owner: customer # filters: only customer-billed credentials decision: allow
enforce_attribution -- Requires specific attribution fields and controls which fields
callers may override. The allow_overrides list now also restricts label keys -- only the
listed keys (including labels.<key> entries) may be overridden by the caller. Labels not
in the allow list are stripped:
credentials: - id: credentials.enforce.require_project effect: enforce_attribution: require: - project_id allow_overrides: ["project_id", "labels.env"] # Only project_id and the "env" label can be overridden; # other label keys supplied by the caller are stripped. decision: allow
default_attribution -- Fills in missing attribution fields before enforce_attribution
checks run. Supports nested labels for setting default label values:
credentials: - id: credentials.defaults.fill effect: default_attribution: project_id: proj-default labels.env: production labels.team: platform decision: allow
Labels use the labels.<key> prefix in the flat map form. Default values are only applied
when the caller has not already supplied a value for that field.
Connector routing by action.type:
The connector manager routes requests using the following precedence (per spec §4.1):
- Policy-selected --
select_connectoreffect in a matching policy rule action.type-- type-based routing viaRegisterActionTypemappingstarget_host-- hostname-based routing to built-in provider connectors- Fallback --
generic_httpconnector
When action.type matches a registered type mapping, the request routes to the mapped
connector without requiring a hostname match. This is useful for custom action types that
don't map to a specific provider hostname:
// In gateway setup code: connectorManager.RegisterActionType("openai_chat", "openai") connectorManager.RegisterActionType("custom.embedding", "embedding_service")
A request with "action.type": "openai_chat" routes to the openai connector regardless
of target_host. Policy-selected connectors always take precedence over type-based routing.
Response Codes and Errors
| HTTP Status | decision value | Meaning |
|---|---|---|
200 OK | allow | Request executed; response body contains provider result |
200 OK | downgrade | Request executed with mutations (e.g. cheaper model substituted) |
202 Accepted | require_approval | Request held for manual approval; approval_id in response body |
403 Forbidden | deny | Request blocked by policy; reason field explains why |
403 Forbidden | quarantine | Request quarantined due to risk signals |
429 Too Many Requests | rate_limit | Request rate-limited; check Retry-After header |
403 Forbidden | require_attribution | Request denied due to missing attribution fields; reason_codes lists the missing fields |
require_attribution vs deny: A require_attribution denial indicates that required
attribution fields (customer_id, account_id, or project_id) are missing from the request.
The reason_codes array in the response body lists the specific missing fields. Add the missing
fields and re-submit. This is distinct from a policy deny, which blocks the request for other
reasons (e.g. target host not in allowlist). Because require_attribution is included in the
bundle stop_on list, it short-circuits evaluation -- no further policy layers are checked once
this decision is reached.
Success response body format (200 OK):
{ "schema": "mvgc.action_response.v1", "request_id": "req-001", "record_id": "019508a3-...", "record_hash": "a1b2c3d4e5f6...", "decision": "allow", "decision_trace": { ... }, "result": { "status_code": 200, "body": { ... } } }
record_hash is the hex-encoded SHA-256 JCS hash of the execution record; use it with
GET /v1/verify/{record_hash} to retrieve an inclusion proof.
Additional fields in the execution record (available via GET /v1/records/{id}):
| Field | Description |
|---|---|
connector_version | Version of the connector that executed the request |
idempotency_key | Client-supplied idempotency key (if provided) |
tokens_in | Input token count (separate from the aggregate model_tokens_used) |
tokens_out | Output token count (separate from the aggregate model_tokens_used) |
When a policy rule sets require_receipt: true, the response also includes a signed receipt:
{ "receipt": { "schema": "mvgc.record_receipt.v1", "record_hash": "a1b2c3d4...", "org_id": "org-example-001", "node_id": "node-001", "ledger_seq": 0, "received_at": "2026-03-11T12:00:00Z", "sig": { "alg": "ed25519", "kid": "kid_...", "sig": "..." } } }
Error response body format:
{ "schema": "mvgc.action_response.v1", "decision": "deny", "reason": "target host not in allowlist", "request_id": "req-001", "record_id": "rec-abc123", "record_hash": "a1b2c3d4..." }
Approval response body format:
{ "schema": "mvgc.action_response.v1", "decision": "require_approval", "approval_id": "a1b2c3d4-...", "request_id": "req-needs-approval", "record_hash": "a1b2c3d4..." }
When your request receives a 202, re-submit the same request body after an operator approves it. The gateway will find the cached approval and execute normally.
Polling for Approval Status
After receiving a 202, your application should re-submit the identical request body periodically until it receives 200 (approved) or 403 (denied/expired). Use a polling interval of 30--60 seconds; avoid hammering the gateway with rapid retries.
The approval_id is not queryable by applications --
only operators can check status via the admin API (see
Approval Workflows in the Network Operations Guide).
The workflow from the application side is: submit, wait, re-submit.
import time def submit_with_approval_wait(client, request_body, poll_interval=30, max_attempts=48): """Poll until approved (up to 24h by default).""" for attempt in range(max_attempts): response = client.execute(request_body) if response["decision"] == "allow": return response if response["decision"] == "require_approval": # First submission: approval request created; subsequent: waiting time.sleep(poll_interval) continue # deny, rate_limit, quarantine: stop polling raise Exception(f"Request not approved: {response}") raise TimeoutError("Approval not granted within timeout")
Reporting APIs
These endpoints are available for project-level usage and spend queries.
GET /v1/reports/usage
Query usage statistics by project and time range.
GET /v1/reports/usage?project_id=proj-123&from=2026-01-01T00:00:00Z&to=2026-03-01T00:00:00Z
GET /v1/reports/spend
Query spend data by project.
GET /v1/reports/spend?project_id=proj-123
GET /v1/records/{recordID}
Retrieve a specific execution record by ID. Requires admin authentication.
GET /v1/records/rec-abc123
Authorization: Bearer <admin-token>
GET /v1/verify/{record_hash}
Verify that an execution record hash is known to the gateway and retrieve its Merkle inclusion proof when available.
GET /v1/verify/a1b2c3d4e5f6...
The record_hash is the hex-encoded SHA-256 hash of the JCS-canonicalized execution record,
available as record_hash in the execution record stored in Postgres.
Response -- verified (200 OK):
When the record has been included in a Merkle batch, the response includes the full inclusion proof:
{ "record_hash": "a1b2c3d4e5f6...", "record_id": "019508a3-...", "status": "verified", "inclusion_proof": { "schema": "mvgc.inclusion_proof.v1", "batch_id": "batch-001", "leaf_count": 64, "path": [{"hash": "...", "side": "left"}, {"hash": "...", "side": "right"}], "root": { "root_hash": "...", "node_id": "node-001", "batch_id": "batch-001" } } }
Response -- pending inclusion (200 OK):
When the record exists but has not yet been included in a Merkle batch:
{ "record_hash": "a1b2c3d4e5f6...", "record_id": "019508a3-...", "status": "pending_inclusion" }
Status values:
status | Meaning |
|---|---|
verified | Record is in a Merkle batch; inclusion_proof is present |
pending_inclusion | Record exists but has not yet been batched |
- Returns
404 Not Foundif the hash is not present in the local database.
Error Handling and Retries
| HTTP Status | Retriable | Notes |
|---|---|---|
200 | No | Success |
202 | Yes (re-submit same body) | Wait for operator approval; see polling guidance above |
400 | No | Malformed request; fix the request body |
403 | No | Policy denial or quarantine; retrying will produce the same result |
429 | Yes | Rate-limited; wait the number of seconds in the Retry-After header (default 60s) |
500, 502, 503 | Yes (with backoff) | Gateway or upstream transient error |
For 429 and 5xx responses, use exponential backoff starting at 1 second and capping at
60 seconds. Add jitter to avoid thundering-herd effects.
import time, random def execute_with_retry(client, request_body, max_retries=5): delay = 1.0 for attempt in range(max_retries): response_status, response_body = client.execute_raw(request_body) if response_status == 429: retry_after = int(response_body.get("Retry-After", delay)) time.sleep(retry_after) elif response_status in (500, 502, 503): time.sleep(delay + random.uniform(0, 0.5)) delay = min(delay * 2, 60) else: return response_status, response_body raise Exception("Max retries exceeded")
Health Check
GET /healthz
Returns HTTP 200 with an enriched JSON payload:
{ "status": "ok", "node_id": "node-001", "connectors": [{"id": "openai", "version": "1.0.0", "healthy": true}], "cp_status": "connected", "bundle_id": "bundle-prod-001", "bundle_version": "2.1.0", "last_hash_submit_at": "2026-03-12T14:30:00Z" }
During the startup grace period (MVGC_HEALTHZ_STARTUP_GRACE, default 10s), the
status field is "starting". After the grace period, status is "ok". Both
states return HTTP 200. Use cp_status ("connected", "offline", or
"unconfigured") to detect control plane connectivity issues in distributed mode.
Use this endpoint for load balancer health checks and readiness probes.
SDK and Client Libraries
Official SDKs are available for Python, TypeScript/Node.js, and Go. Each SDK is a drop-in replacement for your existing AI provider client; swap one import and your code is governed, costed, and recorded without further changes.
| Language | Quick install | Reference |
|---|---|---|
| Python | pip install axemere-gateway-openai | Python SDK → |
| TypeScript / Node.js | npm install @axemere/gateway-openai | TypeScript SDK → |
| Go | go get github.com/Axemere-LLC/axemere-go/gateway | Go SDK → |
Each SDK page includes the full package table, quick-start example, and environment variable reference. The GitHub repositories contain 10–20 runnable examples covering streaming, delegation, governance outcomes, and all supported providers.
See Also
- Delegation Tokens Guide: deep dive on token schema, trust model, chain depth, policy DSL fields, and security considerations
- Merkle Proof Verification Guide: how to independently verify execution records using inclusion proofs
- Admin API Reference: complete endpoint listing with response schemas
- Security Overview: mTLS, bundle signing, and credential encryption
- Operations: Approval Workflows: handling
require_approvalresponses - Glossary: definitions for all fields and concepts used in this guide