Protect my service
The sample shows the loop with synthetic data. Protecting your own service means supplying three things the sample faked: real authentication for the upstream, the mapping from your requests to business operations, and downstream idempotency for the write.
There are three enforcement paths. A service picks one with enforcement_mode.
| Path | What you implement | Choose it when |
|---|---|---|
Hosted proxy (proxy) | Register the upstream, map routes or MCP tools to operations, and make the upstream accept protected routes only with a valid Axiom-Authorization. | You want a fast evaluation, or to protect several routes or tools without editing each handler. |
Embedded SDK (sdk) | Call POST /v1/authorize at the business operation with authoritative parameters, commit atomically with the operation key, report a signed outcome. | You need fine-grained checks inside the service, atomic deduplication in your own database and authoritative outcome receipts. |
Existing gateway adapter (adapter) | Map the gateway's authenticated request context into the same decision interface and forward the result; the application verifies trusted context at its boundary. | You already run Kong, Apigee or AWS API Gateway in front of the service. |
A single transaction can cross both a proxy and an SDK. They share the action ID, the operation key and one execution record. The proxy can attest that it forwarded a request or observed a response; only your application can attest that the refund was posted.
Common setup
Everything below is in the console under the project (Services and operations, API keys), or through the API with a signed-in session.
1. Create the service
POST /v1/projects/{project_id}/services{
"name": "Billing API",
"kind": "both",
"upstream_url": "https://billing.example.com",
"upstream_mcp_url": "https://billing.example.com/mcp",
"upstream_auth": { "type": "bearer", "secret_ref": "env:BILLING_UPSTREAM_TOKEN" },
"enforcement_mode": "proxy",
"admitted_tenants": ["ten_…"],
"outcome_keys": [{ "kty": "EC", "crv": "P-256", "x": "…", "y": "…", "kid": "billing-outcome-1", "alg": "ES256" }]
}| Field | Meaning |
|---|---|
kind | http, mcp or both. |
upstream_url | Base URL for HTTP dispatch and for the reconciliation lookup GET /.well-known/axiom/operations/{operation_key}. |
upstream_mcp_url | MCP endpoint the gateway's MCP proxy talks to. |
upstream_auth | Credentials the gateway adds when calling the upstream: none, bearer or header (with header name). The secret is a reference, never inline. |
enforcement_mode | proxy (default), sdk or adapter. The hosted proxy only fronts proxy services. |
admitted_tenants | Customer tenants whose admins may grant their agents access: a list of tenant IDs or "any". The project's own tenant is always admitted. |
outcome_keys | Public JWKs of your outcome signing keys. Without them, outcomes are recorded as observed by the gateway only. |
The plan limits how many services a project can protect (one on Free); see Plans and limits.
2. Declare each business operation
POST /v1/services/{service_id}/operations{
"name": "billing.refund.create",
"description": "Issue a refund against an invoice",
"sensitivity": "sensitive_write",
"resource_type": "invoice",
"material_fields": [
{ "name": "amount", "type": "decimal", "required": true },
{ "name": "currency", "type": "currency", "required": true },
{ "name": "beneficiary", "type": "string", "required": true }
],
"summary_template": "{agent_name} requests {params.currency} {params.amount} against invoice {resource.id} for customer {tenant_name}",
"http_route": {
"method": "POST",
"path": "/v1/invoices/{resource_id}/refunds",
"resource_id_from": "path:resource_id",
"params_from": "body"
},
"mcp_tool": {
"name": "issue_refund",
"resource_id_arg": "invoice_id",
"params_args": { "amount": "amount", "currency": "currency", "beneficiary": "beneficiary" }
},
"idempotency_support": "atomic"
}- Material fields are what the approver sees and what the digest binds. Types are
string,decimal(send as a string, normalised:"25000.00"becomes"25000"),integer,booleanandcurrency(ISO 4217, upper-cased). An undeclared field inparamsis a validation error.approver_visible: falsehides a field from the approver view while still binding it;sensitive: truemarks a low-entropy field. - Summary template renders the approver-facing summary. Placeholders are
{agent_name},{tenant_name},{operation},{resource.id},{resource.type}and{params.<field>}. http_routemaps one method and path template.{resource_id}captures one path segment;resource_id_fromcan instead bebody:<field>,query:<name>orheader:<name>;params_fromisbody(a JSON object) orquery.mcp_toolmaps a tool name to the operation: which argument is the resource ID, and which arguments carry which material fields.sensitivityisread,writeorsensitive_write. Through the proxy, writes require anIdempotency-Key; reads get a fresh key and become their own allowed action.
3. Let a customer admit an agent
The customer's tenant admin registers an agent, binds its credential and creates a grant for your service. See Registering and admitting agents and Grants and approval thresholds. Then test the expected decision with the policy simulator (POST /v1/projects/{project_id}/simulate) before any real traffic.
Hosted proxy
Agents call your real routes through the gateway instead of your origin:
| Your route | Through the gateway |
|---|---|
POST https://billing.example.com/v1/invoices/417/refunds | POST https://gw.sectigoaxiom.com/p/{project_id}/http/v1/invoices/417/refunds |
MCP at https://billing.example.com/mcp | POST https://gw.sectigoaxiom.com/p/{project_id}/mcp |
The agent sends its DPoP-bound token with a proof for the gateway URL, plus Idempotency-Key for a write. The gateway:
- refuses any method and path that is not mapped to an operation (
403,operation_not_permitted). There is no pass-through; - allowed within the standing grant: claims execution and forwards the original body and headers, minus the agent's
Authorization,DPoP, cookies and hop-by-hop headers, plus yourupstream_authcredential and Axiom's signed context. The upstream response comes back withAxiom-Action-Id; - needs approval: answers
202with the action status document and calls nothing upstream; - denied:
403problem withreason_code.
To resume through the proxy, the original agent re-sends the same request with Axiom-Action-Id: act_…. A changed material field is refused and nothing is forwarded.
Restrict your upstream. The gateway is only an enforcement point if the upstream refuses protected requests that did not come through it. Verify Axiom-Authorization on every protected route:
import { AxiomProvider, ProviderError } from "@sectigo/axiom-sdk/provider";
const axiom = new AxiomProvider({
issuer: "https://api.sectigoaxiom.com",
audience: "https://billing.example.com", // the origin Axiom dispatches to
outcomeSigner, // your outcome key (see the embedded SDK guide)
});
app.post("/v1/invoices/:id/refunds", async (c) => {
const ctx = await axiom.verifyDispatch(c.req.raw.headers); // signature, issuer, audience, lifetime
axiom.assertMatches(ctx, { resource: { type: "invoice", id: c.req.param("id") }, params: await c.req.json() });
// ...commit with executeOnce and return the attestation in the axiom-outcome header
});Axiom-Authorization is a compact JWS (typ: axiom-dispatch+jws) signed by Axiom's grant key. It states the action, operation key, tenant, agent, resource and the full approved canonical request, and it expires 120 seconds after issue. Return your signed outcome in an axiom-outcome response header (HTTP) or _meta["com.sectigoaxiom/outcome"] (MCP) so the receipt can say the destination confirmed the result. The Embedded SDK provider guide covers executeOnce and attestations, which apply to proxied requests too.
Customer-hosted proxy
The brief's customer-hosted gateway runs the same enforcement inside your environment. Control can register such a gateway, sign time-bounded policy bundles for it and hand it the project's single executor flag (Fleet, gateways and cutover). The customer-hosted gateway runtime itself is not part of this repository yet.
Embedded SDK
The agent calls your application directly, with its own DPoP token and a proof bound to your URL. Your handler forwards both to Axiom with a project API key (scope provider:authorize):
const result = await axiom.authorizeRequest(req, {
operation_key: req.headers.get("idempotency-key")!,
operation: "billing.refund.create",
service: "svc_…",
tenant: tenantFromYourSession, // authoritative: from your records, not the agent's body
resource: { type: "invoice", id: invoiceId },
params: { amount, currency, beneficiary },
});
switch (result.decision) {
case "execute": // this call holds the single execution claim; result.context is verified
case "completed": // already done: return the recorded result
case "pending": // 202 with result.status; nothing has started
case "in_progress":
case "stop": // denied, expired, revoked or cancelled
}Use it when the authorization check belongs inside the business operation, next to the tenant, invoice-status and balance checks your application already does. Read the Embedded SDK provider guide before going to production.
In this mode the agent resumes by repeating its original request to your application with the same operation key. POST /v1/actions/{id}/resume on an sdk service returns 409 invalid_state telling the agent to do exactly that.
Existing gateway adapter
An adapter maps the request context your API gateway has already authenticated into the same decision interface (POST /v1/authorize) and forwards the result. Your application still verifies the trusted context at its boundary.
Preview status
Kong, Apigee and AWS API Gateway are candidates. A gateway can be registered with mode: "adapter" and an adapter name, and services can be set to enforcement_mode: "adapter", but no adapter is certified or shipped in this repository. Before support is claimed, each adapter must pass the common tests: authenticated tenant mapping, a pending result with no upstream side effect, original-agent resume and parity with the embedded SDK.
Checklist before production
- The upstream refuses protected requests without a valid
Axiom-Authorization(proxy) or without anexecutedecision (SDK). - The tenant, resource and amount you commit come from your own records or the verified context, never only from the agent's request body.
- The business write and the operation-key record commit in one transaction, or the target offers equivalent idempotency and result lookup. A shared key alone cannot guarantee one side effect.
- You serve
GET /.well-known/axiom/operations/{operation_key}so unknown outcomes reconcile. - Your outcome public key is registered in the service's
outcome_keys. - Accumulated limits (daily refund budgets, balances) are reserved or debited atomically by the system of record across parallel actions.