Skip to content

Registering and admitting agents ​

An agent has a stable ID (agt_…) that survives restarts and key rotation, and separately a set of credentials, each with its own lifecycle. Authority (grants) attaches to the agent ID, so rotating a key never orphans pending work. See Agents and credentials for the model.

Console: Agents at /app/tenants/:tid/agents, then the agent's page /app/tenants/:tid/agents/:aid. You need agent.write on the tenant (tenant_admin, org_admin or security_admin).

Register the agent ​

http
POST /v1/tenants/{tenant_id}/agents
content-type: application/json

{
  "name": "Billing agent",
  "description": "Prepares refunds for finance review",
  "owner_user_id": "usr_…",
  "operator": "Acme Corp",
  "publisher": "Example Agents Inc.",
  "sponsor_user_id": "usr_…"
}
FieldMeaning
owner_user_id or owner_serviceThe accountable human or sponsoring service. If you give neither, you become the owner: every agent has an accountable owner.
publisherWho built the agent (for a third-party agent, the vendor).
operatorWho runs it. Shown to approvers next to the agent name.
sponsor_user_idThe person in your tenant who sponsors a third-party agent.

Publisher, operator and sponsor are recorded separately because they often differ for third-party agents. They appear in receipts (agent.operator, agent.publisher, agent.sponsor). The owner and sponsor are excluded from approving the agent's requests when the rule requires separation of duties.

A new agent is active with discovery state registered and no credential. It cannot authenticate until you bind one.

Bind a key with proof of possession ​

Binding a public key always requires proof that the agent holds the private key: Axiom issues a single-use challenge, the key signs it, and only then is the public key bound. The private key never reaches Axiom.

On the agent's page the console offers two ways:

  • Generate in this browser: a P-256 key is created with WebCrypto in your tab, signs the challenge and is bound. Use it for trials; the key lives only in that tab.
  • Bring your own key: get a challenge, sign the payload with the agent's own tooling, paste the compact JWS.

Bind a key you generated elsewhere ​

Generate the key where the agent runs. The private JWK is written with 0600 permissions and the public JWK is printed:

sh
node packages/sdk/bin/axiom.mjs keygen --agent agt_…

Then bind it in one of two ways.

With the CLI, in one step. axiom agent bind fetches the challenge, signs it with the key and submits the public key and proof. The challenge and binding endpoints are console endpoints (an administrator binds the key, not the agent), so the command reuses a signed-in console session: pass the value of your axiom_session cookie in AXIOM_SESSION_COOKIE (preferred, so it stays out of shell history) or --cookie.

sh
export AXIOM_SESSION_COOKIE='…'          # axiom_session cookie of a signed-in admin
node packages/sdk/bin/axiom.mjs agent bind agt_… \
  [--key ~/.config/axiom/keys/KID.jwk] [--issue-certificate] [--validity 7776000] [--json]

--key defaults to the configured key_file. The same flow is available in code as bindAgentKey(await consoleSession({ baseUrl, cookie }), { agentId, signer }) from @sectigo/axiom-sdk (packages/sdk/src/enroll.ts). This is a developer convenience for local and test setups; production enrollment belongs in the console or an automated registration flow.

With the console, signing locally. On the agent's page choose Bring your own key and Get challenge, copy the payload shown, sign it on the machine that holds the key, and paste the proof back:

sh
node packages/sdk/bin/axiom.mjs agent sign-challenge \
  '{"agent_id":"agt_…","nonce":"q3V1…","aud":"https://api.sectigoaxiom.com"}' --agent agt_…

The argument is the payload, the whole challenge response, or a file holding either. --typ defaults to axiom-pop+jws; --agent refuses a challenge issued for another agent. The command prints the compact JWS. signPopChallenge in the SDK does the same.

Over the API directly, the steps are:

  1. Ask for a challenge (valid 600 seconds, single use):

    http
    POST /v1/agents/{agent_id}/credentials/challenge
    json
    {
      "challenge_id": "chk_…",
      "nonce": "q3V1…",
      "sign": {
        "typ": "axiom-pop+jws",
        "payload": { "agent_id": "agt_…", "nonce": "q3V1…", "aud": "https://api.sectigoaxiom.com" }
      },
      "expires_in": 600
    }
  2. Sign sign.payload as a compact JWS with header typ from sign.typ, using the private key (axiom agent sign-challenge above).

  3. Bind the public key with the proof:

    http
    POST /v1/agents/{agent_id}/credentials
    content-type: application/json
    
    {
      "kind": "jwk",
      "public_jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
      "challenge_id": "chk_…",
      "proof": "eyJ…",
      "validity_seconds": 7776000,
      "rotation_overlap_seconds": 86400,
      "issue_certificate": false
    }

Axiom checks that the challenge is unused, unexpired and issued for this agent, that the JWS verifies with public_jwk, and that the payload's nonce, agent_id and aud match. The credential's ID is its RFC 7638 thumbprint (kid). Keys may be P-256 (ES256) or Ed25519 (EdDSA). A key binds to at most one credential: binding a key that is already bound returns 409 conflict ("already bound to this agent; generate a new key to rotate", or "already bound to another agent").

FieldDefaultBounds
validity_seconds90 days300 s to 365 days
rotation_overlap_seconds1 day0 to 7 days
issue_certificatefalsetrue also issues an X.509 client-authentication certificate

With issue_certificate: true the CA port issues a certificate for the key (common name the agent ID, URIs urn:axiom:agent:… and urn:axiom:tenant:…, the organisation name only when the org is Sectigo-validated). The CA may shorten the validity; the credential never outlives its certificate. See Sectigo's CA role.

Mock CA

Until Sectigo Certificate Manager credentials are configured, certificates come from SCM mock mode, backed by a local development CA. They are real X.509 certificates but chain only to the mock anchors published at GET /v1/public/ca/trust-anchors.

After binding, the agent's discovery state becomes verified.

Bind a workload identity instead of a key ​

If the agent runs under a SPIFFE JWT-SVID or an OIDC ID token from an issuer you trust, it can authenticate with that token instead of a key it holds.

  1. Add the issuer to the tenant's Trust profile (/app/tenants/:tid/trust):

    http
    PATCH /v1/tenants/{tenant_id}/trust-profile
    content-type: application/json
    
    {
      "workload_issuers": [
        { "issuer": "https://oidc.example.com", "jwks_uri": "https://oidc.example.com/jwks", "audience": "https://api.sectigoaxiom.com/oauth/token" }
      ]
    }

    The body replaces the whole profile, so send every issuer you want to keep.

  2. Bind the subject to the agent:

    http
    POST /v1/agents/{agent_id}/credentials
    content-type: application/json
    
    { "kind": "spiffe", "issuer": "https://oidc.example.com", "subject": "spiffe://example.com/billing-agent" }

    Use "kind": "oidc" for an OIDC subject. Binding an issuer that is not in the trust profile is refused with 403 and reason_code: issuer_not_trusted.

At the token endpoint the workload token's issuer, audience and signature are checked against the issuer's JWKS, and its sub must be bound to the agent. The token request still needs a DPoP proof.

Rotate a key ​

Bind the new key exactly as above (a new key: re-binding the current one is a 409). Every currently active key credential of the agent, of kind jwk or x509, moves to rotating, its expiry is shortened to the end of the overlap window (rotation_overlap_seconds, at most 7 days), and it records superseded_by the new credential. During the overlap the agent can authenticate with either key. Pending actions stay resumable: a resume rechecks the agent's authority, and a legitimately rotated credential is not a reason to refuse it. Workload identity bindings (spiffe, oidc) are not affected by a key rotation.

Suspend, reactivate or retire ​

http
POST /v1/agents/{agent_id}/status
content-type: application/json

{ "status": "suspended", "reason": "Unexpected refund volume" }
  • suspended or retired revokes every access token the agent holds immediately. New submissions and any resume fail (agent_suspended; a resume of an approved action moves it to revoked).
  • active reactivates a suspended agent. A reason is required and recorded in the audit log.
  • retired is final: a retired agent cannot change status; register a new agent instead.

To stop one key rather than the agent, see Revocation.

Publish an agent card (optional) ​

A tenant admin can publish an Axiom-signed agent card so receiving organisations can check the agent with the free Agent identity check:

http
PUT /v1/agents/{agent_id}/card/publication
content-type: application/json

{ "enabled": true, "domain": "acme.example" }

The card is then served at GET /v1/agents/{agent_id}/card. An unpublished card and an unknown agent both answer 404, so the endpoint cannot be used to enumerate agents.

Discovery states ​

Inventory (/control/orgs/:oid/inventory, GET /v1/orgs/:oid/inventory) labels each agent:

StateMeaning
observedSeen by a source but not registered.
registeredHas a stable ID and an owner, no verified credential yet.
verifiedHas a credential bound with proof of possession or a trusted workload identity.
enforcedIts traffic passes through an enforcing gateway or SDK.

An inventory entry alone never blocks activity on another path; enforcement does.

Admit the agent to a provider's service ​

An agent can only be granted access to a service that has admitted your tenant. The provider sets admitted_tenants on the service (a list of tenant IDs, or "any"); the provider's own tenant is always admitted. If it has not admitted you, creating the grant fails with 403 this service has not admitted your tenant; ask the provider to add it. Then create the grant.

Developer preview. Contract version 2026-11-14.