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
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_…"
}| Field | Meaning |
|---|---|
owner_user_id or owner_service | The accountable human or sponsoring service. If you give neither, you become the owner: every agent has an accountable owner. |
publisher | Who built the agent (for a third-party agent, the vendor). |
operator | Who runs it. Shown to approvers next to the agent name. |
sponsor_user_id | The 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:
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.
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:
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:
Ask for a challenge (valid 600 seconds, single use):
httpPOST /v1/agents/{agent_id}/credentials/challengejson{ "challenge_id": "chk_…", "nonce": "q3V1…", "sign": { "typ": "axiom-pop+jws", "payload": { "agent_id": "agt_…", "nonce": "q3V1…", "aud": "https://api.sectigoaxiom.com" } }, "expires_in": 600 }Sign
sign.payloadas a compact JWS with headertypfromsign.typ, using the private key (axiom agent sign-challengeabove).Bind the public key with the proof:
httpPOST /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").
| Field | Default | Bounds |
|---|---|---|
validity_seconds | 90 days | 300 s to 365 days |
rotation_overlap_seconds | 1 day | 0 to 7 days |
issue_certificate | false | true 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.
Add the issuer to the tenant's Trust profile (
/app/tenants/:tid/trust):httpPATCH /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.
Bind the subject to the agent:
httpPOST /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 with403andreason_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
POST /v1/agents/{agent_id}/status
content-type: application/json
{ "status": "suspended", "reason": "Unexpected refund volume" }suspendedorretiredrevokes every access token the agent holds immediately. New submissions and any resume fail (agent_suspended; a resume of an approved action moves it torevoked).activereactivates a suspended agent. A reason is required and recorded in the audit log.retiredis 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:
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:
| State | Meaning |
|---|---|
observed | Seen by a source but not registered. |
registered | Has a stable ID and an owner, no verified credential yet. |
verified | Has a credential bound with proof of possession or a trusted workload identity. |
enforced | Its 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.