> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization mandates: scoped, revocable agent authority

> Issue scoped, revocable authorization mandates that let AI agents act on a principal's behalf and delegate narrowed authority down a sub-agent chain.

# Authorization mandates

An authorization mandate is a scoped, action-capped, revocable grant that lets an AI agent perform **non-payment** actions on a principal's behalf — read a profile, book or reschedule an appointment, send a message on-behalf, invoke a specific tool, or access a data category. A principal (an account holder, contact, or end-user) issues the mandate once; an agent — one of your own, or an external agent arriving over A2A or MCP — then acts against it, and either party can revoke it.

It is the general-purpose sibling of the payment-scoped commerce mandate, which only ever authorizes a spend. A mandate here decides *whether an action is allowed*; the action itself still runs through your own rails.

## How it works

Every mandate carries a **consent digest** — a SHA-256 commitment over its immutable scope (the principal, the agent, the allowlists, the invocation cap, the expiry, and — for a delegated mandate — its parent pointer). Any later tampering with the scope changes the digest, so the runtime gate (and any auditor) can prove the mandate presented at act-time is the exact one the principal consented to.

A mandate is a **serializable snapshot**. You own storage: the API returns the mandate object, you persist it, and you round-trip it verbatim in each subsequent request body. Handlers never trust a client-supplied mandate blindly — every action re-verifies the consent digest before authorizing.

The scope fields are fixed at issue time and bound by the digest. Only `status`, `invocationCount`, `updatedAt`, and `revokedAt` advance over a mandate's life.

### The mandate object

| Field                     | Type            | Description                                                                                         |
| ------------------------- | --------------- | --------------------------------------------------------------------------------------------------- |
| `id`                      | string          | Opaque, caller-supplied id, stable across actions.                                                  |
| `agentId`                 | string          | The agent authorized to act (an Orbit agent id or an external A2A/MCP identity).                    |
| `principalId`             | string          | The principal who granted consent.                                                                  |
| `allowedActions`          | string\[]       | Action verbs the agent may perform (e.g. `profile:read`, `booking:create`). Empty means any action. |
| `allowedTools`            | string\[]       | Tool ids the agent may invoke. Empty means any tool.                                                |
| `allowedDataCategories`   | string\[]       | Data categories the agent may access (e.g. `contact_profile`). Empty means any category.            |
| `maxInvocations`          | integer         | Cumulative ceiling on actions authorized under this mandate.                                        |
| `invocationCount`         | integer         | Actions authorized so far (starts at 0).                                                            |
| `status`                  | enum            | `active`, `exhausted`, `revoked`, or `expired`. The last three are terminal.                        |
| `consentDigest`           | string          | SHA-256 hex commitment over the immutable scope.                                                    |
| `expiresAt`               | integer \| null | Epoch-ms expiry, or null for no expiry.                                                             |
| `createdAt` / `updatedAt` | integer         | Epoch-ms issue time / last change.                                                                  |
| `revokedAt`               | integer \| null | Epoch-ms of revocation, or null.                                                                    |
| `parentMandateId`         | string \| null  | Id of the mandate this one was delegated from; null for a root mandate.                             |
| `parentDigest`            | string \| null  | The parent's consent digest at delegation time; null for a root mandate.                            |

An empty allowlist means "any" — omit `allowedActions` to authorize any action, or pass a list to restrict to exactly those verbs. Allowlists are stored trimmed, sorted, and de-duped.

### Roles and scopes

Issuing, committing, delegating, and revoking are write operations — they require the `agents:write` scope and an owner, admin, or developer role, and they are audit-logged. The dry-run checks (`/authorize`, `/verify`, `/authorize-chain`, `/audit-chain`) never mutate anything and are available to any authenticated caller with agent read access.

## Endpoints

| Method | Path                                                         | Purpose                                                        |
| ------ | ------------------------------------------------------------ | -------------------------------------------------------------- |
| `POST` | `/api/v1/agents/agent-authorization-mandate`                 | Issue a fresh scoped mandate.                                  |
| `POST` | `/api/v1/agents/agent-authorization-mandate/authorize`       | Dry-run the runtime gate for an action (no state change).      |
| `POST` | `/api/v1/agents/agent-authorization-mandate/act`             | Authorize **and** commit an action.                            |
| `POST` | `/api/v1/agents/agent-authorization-mandate/revoke`          | Withdraw consent (terminal).                                   |
| `POST` | `/api/v1/agents/agent-authorization-mandate/verify`          | Recompute the consent digest (tamper-evidence).                |
| `POST` | `/api/v1/agents/agent-authorization-mandate/delegate`        | Mint an attenuated sub-agent mandate from a live parent.       |
| `POST` | `/api/v1/agents/agent-authorization-mandate/authorize-chain` | Dry-run the gate for a delegated action across its full chain. |
| `POST` | `/api/v1/agents/agent-authorization-mandate/act-chain`       | Authorize the chain **and** commit the leaf action.            |
| `POST` | `/api/v1/agents/agent-authorization-mandate/audit-chain`     | Reconstruct and verify a full delegation chain (read-only).    |

See the full request/response schemas in the [API reference](/api-reference/agents).

## Issue a mandate

Grant an agent a capped set of actions on a principal's behalf.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "id": "authzmandate_9f2a",
    "agentId": "agent_support_bot",
    "principalId": "contact_42",
    "allowedActions": ["booking:create", "profile:read"],
    "maxInvocations": 25,
    "expiresAt": 1767225600000
  }'
```

The response `data` is the full mandate snapshot, including its `consentDigest`. Store it — you will send it back on every subsequent call. `maxInvocations` must be a positive integer, and `expiresAt` (when set) must be a future epoch-ms timestamp; otherwise the call returns `422 VALIDATION_ERROR`.

## Authorize an action (dry-run) vs. commit it

`/authorize` evaluates an action against a mandate and returns the decision **without** advancing the invocation count — call it from your dispatch layer before performing an action. `/act` authorizes **and** commits, returning the advanced mandate (its `invocationCount` incremented, flipped to `exhausted` once the cap is reached).

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/act \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "mandate": { "...": "the stored mandate snapshot" },
    "action": {
      "action": "booking:create",
      "tool": "calendar",
      "reference": "task_8b21"
    }
  }'
```

The `action.reference` is your correlation id — the A2A task id, MCP call id, or tool-invocation id this authorization covers. It is echoed back on the decision for the audit trail.

An action passes only when the mandate is intact and active, the action verb is non-empty and within `allowedActions`, any `tool` / `dataCategory` is within its allowlist, and the invocation cap has headroom. When you call `/authorize` on an action that fails, you get `200` with `authorized: false` and a deny `reason`. When you call `/act` on an action that fails, the mandate is left untouched and the call returns `422 VALIDATION_ERROR` carrying the deny reason — you can never act past consent by ignoring a decision.

### Deny reasons

`authorized` is true only when `reason` is `ok`. Otherwise:

| Reason                              | Meaning                                                                       |
| ----------------------------------- | ----------------------------------------------------------------------------- |
| `integrity_failed`                  | The mandate's scope was altered after consent was recorded.                   |
| `revoked` / `expired` / `exhausted` | The mandate is in a terminal state (revoked, past its expiry, or at its cap). |
| `invalid_action`                    | The action named no verb.                                                     |
| `action_not_allowed`                | The verb is outside `allowedActions`.                                         |
| `tool_not_allowed`                  | The tool is outside `allowedTools`.                                           |
| `data_category_not_allowed`         | The data category is outside `allowedDataCategories`.                         |
| `exceeds_invocation_cap`            | No invocations remain under the cap.                                          |

The chain endpoints add `empty_chain`, `broken_ancestry_link`, `scope_not_attenuated`, and `ancestor_revoked` / `ancestor_expired` / `ancestor_exhausted` — see [Delegation](#delegation-sub-agent-chains).

## Verify a mandate

Recompute the consent digest and report whether the scope is intact — tamper-evidence for an auditor, without committing anything.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/verify \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "mandate": { "...": "the stored mandate snapshot" } }'
```

`data.intact` is `false` when any scoped field was changed after issue.

## Revoke a mandate

Withdraw consent. The mandate moves to the terminal `revoked` status and authorizes nothing further — and, because revocation cascades down a delegation chain, neither does anything delegated from it. Revoking an already-revoked mandate returns `422 VALIDATION_ERROR`.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/revoke \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "mandate": { "...": "the stored mandate snapshot" } }'
```

## Delegation: sub-agent chains

When an orchestrator agent hands work to another agent — an "agent-as-tool" call, or a hand-off over A2A — the sub-agent needs authority of its own. Delegation mints a **child** mandate from a live parent whose scope is always **attenuated**: its allowlists are a subset of the parent's, and its invocation cap and expiry are each no broader than the parent's. A child can never end up with more authority than it was given.

The child carries a `parentMandateId` and a `parentDigest` — a pointer to the exact parent scope it descends from, baked into the child's own consent digest so the ancestry link cannot be forged or repointed.

### Delegate a sub-mandate

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/delegate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "parent": { "...": "the live parent mandate snapshot" },
    "id": "authzmandate_child_1c7e",
    "agentId": "agent_scheduler",
    "allowedActions": ["booking:create"],
    "maxInvocations": 5
  }'
```

Omit `allowedActions` / `allowedTools` / `allowedDataCategories` / `maxInvocations` / `expiresAt` to inherit the parent's unchanged. When you do pass them, they can only narrow: the effective invocation cap is the minimum of your request and the parent's, and the effective expiry is the earlier of the two. Requesting an allowlist that shares nothing with a restricted parent — which would broaden the child — returns `422 VALIDATION_ERROR`. Delegation is also refused from a parent that is revoked, expired, exhausted, or fails its own integrity check.

### Authorize and act across a chain

To authorize a delegated action, present the full chain, ordered root-first and leaf-last (`[root, ..., leaf]`). The gate walks the entire ancestry back to the principal's root mandate — re-verifying every hop's integrity, terminal status, expiry, ancestry link, and attenuation — and only then evaluates the leaf's own scope. A revoked, expired, or exhausted mandate *anywhere* in the lineage denies the action.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/act-chain \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "chain": [
      { "...": "root mandate snapshot" },
      { "...": "child mandate snapshot (the leaf)" }
    ],
    "action": { "action": "booking:create", "reference": "task_9c02" }
  }'
```

`/authorize-chain` is the dry-run — it returns `200` with the decision and never advances the leaf. `/act-chain` commits the leaf action, returning the advanced leaf mandate; when the chain doesn't hold it returns `422 VALIDATION_ERROR` with the deny reason. A well-formed chain that simply isn't authorized comes back from `/authorize-chain` as `200` with `authorized: false` and an `ancestor_*`, `broken_ancestry_link`, or `scope_not_attenuated` reason.

### Audit a chain

Reconstruct and verify the full principal → orchestrator → sub-agent → … lineage for review. Read-only.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent-authorization-mandate/audit-chain \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "chain": [
      { "...": "root mandate snapshot" },
      { "...": "child mandate snapshot" }
    ]
  }'
```

The response reports the end-to-end verdict (`valid`, `reason`, `failedAt`, `rootPrincipalId`) plus a per-hop breakdown: each hop's `depth`, identity, `parentMandateId`, `status`, whether its integrity is `integrityIntact`, its allowlists, and its invocation state. This is how an auditor proves a sub-agent's action traces back to authority the principal actually granted.

## Error handling

All four dry-run endpoints (`/authorize`, `/verify`, `/authorize-chain`, `/audit-chain`) return `200` for a well-formed request even when the verdict is a denial — inspect `authorized` / `intact` / `valid` in the body. The committing and mutating endpoints (`/`, `/act`, `/revoke`, `/delegate`, `/act-chain`) return `422 VALIDATION_ERROR` when the action or mandate is not authorized, and the error message carries the deny reason. A malformed request body is always `422 VALIDATION_ERROR` on every endpoint.

## See also

* [Agents API](/api-reference/agents) — full request/response schemas for every mandate endpoint.
* [A2A federation](/agents/a2a-federation) — task remote agents across peers; delegation is how you carry a principal's consent into an A2A hop.
* [Human-in-the-loop oversight](/agents/human-in-the-loop-oversight) — approval gates for high-risk agent actions.
