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

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 typeCode valueHow to use it
Direct APIdirect_apiYour application sends a structured ActionRequest JSON body to POST /v1/actions:execute. Full access to attribution, delegation tokens, and all policy response types.
SDK Redirectsdk_redirectPoint 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 Proxyconnect_proxyRoute 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:

FieldDescription
schemaAlways "mvgc.action_request.v2" (note: action response schema is "mvgc.action_response.v1")
org_idYour organization identifier
workload_idThe workload this request belongs to
action.typeAction type, e.g. "ai.infer"
action.methodHTTP method for the upstream call
action.target_hostUpstream provider hostname
action.paramsProvider-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.

FieldDescription
attribution.customer_idEnd customer identifier
attribution.account_idAccount or team identifier
attribution.project_idProject 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

FieldDescription
request_idIdempotency key; auto-generated as a UUID if omitted
caller_idIdentifier of the calling service or user
context.purposeCaller intent (string); usable in policy DSL conditions via context.purpose
context.labelsCaller-supplied key-value labels (object); usable in policy DSL via context.labels.<key>
action.targetFull target URL (e.g. https://api.openai.com/v1/chat/completions); populated during normalization from target_host + target_path if not set explicitly
credential_hintOverride credential selection: byok -- supply the raw API key; alias -- supply the credential_id
connector_hintOverride 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_keyClient-supplied idempotency key; if a record with this key already exists the cached result is returned
delegation_idReference ID when a delegation token authorizes the request
delegation_tokenBase64-encoded delegation token
action.target_pathPath on the upstream host; defaults to the provider connector default if omitted
action.headersAdditional 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:

  1. Forwards the request to the upstream provider with streaming enabled
  2. Returns the SSE stream directly to your client -- no JSON wrapper
  3. Computes the SHA-256 body hash incrementally as chunks arrive
  4. Writes an execution record asynchronously after the stream completes

Supported streaming providers:

Provideraction.target_host
OpenAIapi.openai.com
Anthropicapi.anthropic.com
Google Geminigenerativelanguage.googleapis.com
Azure OpenAI*.openai.azure.com
Cohereapi.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 in action.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_approval and rate_limit decisions (HTTP 202/429) are returned synchronously before streaming begins; the stream only starts on an allow or downgrade decision

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:

HeaderDescription
X-MVGC-Org-IDYour organization identifier
X-MVGC-Workload-IDThe workload this request belongs to

Optional attribution headers:

HeaderMaps toDescription
X-MVGC-Project-IDattribution.project_idProject identifier
X-MVGC-Customer-IDattribution.customer_idEnd customer identifier
X-MVGC-Account-IDattribution.account_idAccount or team identifier
X-MVGC-Target-Hostaction.target_hostUpstream provider hostname; explicit override, required for Azure or custom endpoints
X-MVGC-Delegation-IDdelegation_idReference 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.

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 prefixUpstream 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 /v1 prefix, unlike OpenAI itself and most other openai_compat providers (DeepSeek, Mistral, etc.). Send POST /proxy/perplexity/chat/completions, not /proxy/perplexity/v1/chat/completions.
  • Gemini has two separate surfaces with different auth: native /v1beta/models/{model}:generateContent (auth via x-goog-api-key), and OpenAI-compatible /v1beta/openai/chat/completions (auth via Authorization: Bearer, not x-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_hint and connector_hint are not supported; credential and connector selection cannot be overridden per-request.
  • Delegation token verification is not supported. X-MVGC-Delegation-ID is 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 with approval_id, 429 with Retry-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:

  1. The request URL targets the upstream provider directly (api.openai.com)
  2. X-MVGC-* headers provide org, workload, and attribution context
  3. The gateway evaluates policies, records the execution, and forwards the request
  4. The response comes directly from the upstream provider -- no JSON wrapper
  5. MVGC_PROXY_ENABLED=true must 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, and decision as 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 fieldDescription
context.delegation.issued_byIdentity of the token issuer
context.delegation.principal_idEnd-user or service identity from the token
context.delegation.audienceIntended audience (workload)
context.delegation.token_idUnique token identifier
context.delegation.depthDelegation chain depth (integer string); max depth is 3
context.delegation.policy_bundle_idBundle ID pinned by the token (if set)
context.delegation.max_cost_usdPer-token spend cap
context.delegation.actions_allowComma-joined list of permitted action types
context.delegation.targets_allowComma-joined list of permitted target hosts
context.estimates.tokens_out_maxMaximum estimated output tokens (integer string)
context.attribution.labels.<key>Attribution label value for the given key
context.action.headers_presentComma-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.connectorsComma-separated eligible connector IDs
context.candidates.credentialsComma-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):

  1. Policy-selected -- select_connector effect in a matching policy rule
  2. action.type -- type-based routing via RegisterActionType mappings
  3. target_host -- hostname-based routing to built-in provider connectors
  4. Fallback -- generic_http connector

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

ProviderGatewayAppProviderGatewayAppalt[allow][deny][require_approval][rate_limit][quarantine]POST /v1/actions:executeforward requestresponse200 OK403 Forbidden202 Accepted + approval_id429 Too Many Requests + Retry-After403 Forbidden (quarantined)
HTTP Statusdecision valueMeaning
200 OKallowRequest executed; response body contains provider result
200 OKdowngradeRequest executed with mutations (e.g. cheaper model substituted)
202 Acceptedrequire_approvalRequest held for manual approval; approval_id in response body
403 ForbiddendenyRequest blocked by policy; reason field explains why
403 ForbiddenquarantineRequest quarantined due to risk signals
429 Too Many Requestsrate_limitRequest rate-limited; check Retry-After header
403 Forbiddenrequire_attributionRequest 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}):

FieldDescription
connector_versionVersion of the connector that executed the request
idempotency_keyClient-supplied idempotency key (if provided)
tokens_inInput token count (separate from the aggregate model_tokens_used)
tokens_outOutput 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:

statusMeaning
verifiedRecord is in a Merkle batch; inclusion_proof is present
pending_inclusionRecord exists but has not yet been batched
  • Returns 404 Not Found if the hash is not present in the local database.

Error Handling and Retries

HTTP StatusRetriableNotes
200NoSuccess
202Yes (re-submit same body)Wait for operator approval; see polling guidance above
400NoMalformed request; fix the request body
403NoPolicy denial or quarantine; retrying will produce the same result
429YesRate-limited; wait the number of seconds in the Retry-After header (default 60s)
500, 502, 503Yes (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.

LanguageQuick installReference
Pythonpip install axemere-gateway-openaiPython SDK →
TypeScript / Node.jsnpm install @axemere/gateway-openaiTypeScript SDK →
Gogo get github.com/Axemere-LLC/axemere-go/gatewayGo 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