# Consent record and KYA attestation

The signed, portable record of what a person authorized an agent to do, the record of who the agent is, and the one-click pause that is itself a record.

Status: Live · Updated: 2026-09-14
For: agent
Source: https://enruta.ai/docs/consent-and-kya

Every “consent” field in the industry is a live API attribute, and every KYA framework answers “who is the agent” but not “what may it do”. The consent record is the mandate’s parent: a principal’s one-time or standing authorization of one agent, signed and exportable. The KYA attestation records who the agent is: Enruta reads and seals someone else’s identity claim or a verifiable anchor, and never runs a registry of its own.

## Consent record

| Field | Contents |
|---|---|
| `id` | `cns_01M2F0Q3W7T1K5N9B4D8H2XR` |
| `principal{org_id, principal_id_readable, display_name, verified_by}` | `verified_by`: `oauth` · `passkey` · `email_link` · `sso` |
| `agent{agent_id, kya_attestation_id, operator_org}` |  |
| `scope{max_per_payment, max_cumulative, currency, payees[] or categories[], valid_from, valid_until, recurrence}` | Five things a card-on-file consent lacks: an amount cap, single use, an expiry, a purpose and an offline-verifiable signature |
| `merchant_policies[]{merchant, policy_type, policy_url, policy_hash, consented_at, acceptance_method}` | `policy_type`: `refund` · `cancellation` · `advance_payment` · `guaranteed_reservation` · `estimated_authorization` · `repeated_transaction`, one per Visa Core Rules 4.1.24.6, 4.1.24.8 and 4.1.24.9 clause |
| `acknowledgements[]{text_hash, accepted_at, method}` | The hash of the “I am responsible for what this agent does” text the person accepted |
| `retention{minimum_days, until, export_on_expiry}` | 540 days for card rails, 730 for ACH |
| `revocation{revoked_at, by, effect, institution_notified_at, effective_at, cascades_scheduled, notified_institution_at, cascade}` | `effect: stops_new_mandates_only`; revocation is not a reversal. `effective_at` is when it takes effect: at once, or a later time before which new mandates still work. `cascades_scheduled` says whether what was already scheduled stops too, `cascade` what that did, and `notified_institution_at` when a rail was first asked, through an interface it has, to cancel the voided mandates’ credentials (`null` when none has one). `institution_notified_at` is when a consumer told their institution, which decides whether later debits count as unauthorized. A revocation made before these fields existed reads as effective at `revoked_at`, with no cascade |
| `signature` | The principal’s side (a passkey assertion, or Enruta signing on the strength of an approval event) plus Enruta’s ES256 |
| `state`, `source`, `source_connection_id` | `active` · `revoked` · `expired`; `console` · `oauth` · `api` |
| `record_id`, `revoke_record_id`, `valid_from`, `valid_until` | The sealed linked records of the grant and of the latest revocation (`kind: consent_record`, `chain: consent_for`, org-signed → `customer_attested`) |

A consent stays `active` until `valid_until` passes (the finalize job marks it `expired`) or a revocation takes effect ([revoking a consent](https://enruta.ai/docs/consent-and-kya#revoking-a-consent)). Under a policy with `require_consent`, a request from an agent without an active consent is denied with `consent_revoked`; a policy without `require_consent` records consents without enforcing them. A [recurrence](https://enruta.ai/docs/recurrence) can name the consent that authorizes it.

```json
{
  "id": "cns_01M2F0Q3W7T1K5N9B4D8H2XR", "state": "active", "source": "console",
  "principal": { "org_id": "org_halvern", "principal_id_readable": "halvern/dana.reyes@halvernrobotics.com", "display_name": "Dana Reyes", "verified_by": "passkey" },
  "agent": { "agent_id": "agt_procurement_02", "kya_attestation_id": "kya_01M2F1R6W2T9K4Q7N3B8D5H1XS", "operator_org": "org_halvern" },
  "scope": { "max_per_payment": "200.00", "max_cumulative": "1000.00", "currency": "USD", "categories": ["office_supplies"], "valid_from": "2026-09-13", "valid_until": "2027-03-31" },
  "merchant_policies": [
    { "merchant": "reamandrule.com", "policy_type": "refund", "policy_url": "https://reamandrule.com/policies#returns", "policy_hash": "sha256:…", "consented_at": "2026-09-13T09:00:00Z", "acceptance_method": "console" },
    { "merchant": "reamandrule.com", "policy_type": "cancellation", "policy_url": "https://reamandrule.com/policies#returns", "policy_hash": "sha256:…", "consented_at": "2026-09-13T09:00:00Z", "acceptance_method": "console" }
  ],
  "acknowledgements": [{ "text_hash": "sha256:…", "accepted_at": "2026-09-13T09:00:00Z", "method": "passkey" }],
  "retention": { "minimum_days": 540, "until": "2028-09-22", "export_on_expiry": true },
  "revocation": null,
  "record_id": "rec_01M2F0QF3T8W2K5Q9N4B7D1H6XR",
  "revoke_record_id": null
}
```

Where consents come from today: the console (“Grant consent” on an agent), the OAuth consent page (connecting Claude.ai or ChatGPT to an organization and an agent creates one; existing connections were back-filled), and `POST /v0/consents`.

## Revoking a consent

A developer key calls `POST /v0/consents/{id}/revoke`, or a person uses “Revoke” in the console. A revocation has a time boundary: it takes effect at `effective_at`, at once by default or at a later time. A later one is scheduled: the consent stays `active`, new mandates still work until then, and the scheduled revocation is sealed as its own record (`subject.action: "schedule_revocation"`) chained `consent_for` to the grant. Asking again for a later time does not move it; a revocation effective now brings it forward. From `effective_at` the consent no longer covers the agent, even before the finalize job applies the revocation (its `apply_revocations` step). Taking effect seals the revocation (`subject.action: "revoke"`, with its time boundary and what its cascade did), marks the consent `revoked` and sends `consent.revoked`.

Without `cascades_scheduled` a revocation stops new mandates only: mandates issued before it and not yet expired stay valid. With `cascades_scheduled: true` what is already scheduled stops too, each change sealed on its own: recurrences made under the consent end; the agent’s issued, unused mandates are voided (their payments seal with outcome `voided` and reason `consent_revoked`) and the rail is asked to cancel their single-use credentials; the agent’s payment approvals still pending for requests made while the consent was in force expire, each request ending `expired` with `consent_revoked` among its reasons, so it is not retryable, and its budget reservation released; and when no other active consent covers the agent, its other recurrences pause and the agent is paused. A refund waiting for approval is left alone, and so is a request whose decision named another consent. None of it is a reversal: money already paid stays paid.

```http
POST /v0/consents/cns_01M2F0Q3W7T1K5N9B4D8H2XR/revoke
{ "effective_at": "2026-10-01T00:00:00Z", "cascades_scheduled": true }

200 { "id": "cns_01M2F0Q3W7T1K5N9B4D8H2XR", "state": "active", …,
      "revocation": { "revoked_at": "2026-09-14T10:00:00Z", "by": "key_01M2E7…", "effect": "stops_new_mandates_only", "institution_notified_at": null,
                      "effective_at": "2026-10-01T00:00:00Z", "cascades_scheduled": true, "notified_institution_at": null, "cascade": null },
      "record_id": "rec_01M2F0QF3T8W2K5Q9N4B7D1H6XR", "revoke_record_id": "rec_01M3G1…" }

// on October 1 the finalize job applies it; GET /v0/consents/{id} then answers
"state": "revoked",
"revocation": { …, "cascade": { "applied_at": "2026-10-01T00:05:00Z", "mandates_voided": ["mnd_01M3H0…"], "recurrences_ended": ["rcu_01M2J4…"], "recurrences_paused": [],
                                "approvals_expired": ["apr_01M3H2…"], "agent_paused": true, "pause_record_id": "rec_01M3H3…" } }
```

A revocation effective now answers with the consent already `revoked` and its cascade. `approvals_expired` is on cascades applied from this release. An `effective_at` more than a minute in the past is refused with `400`.

## KYA attestation

| Field | Contents |
|---|---|
| `id`, `agent_id` | `kya_01M2F1R6W2T9K4Q7N3B8D5H1XS` |
| `anchor_type` | `registry` for a registry claim (the tiers below), or one of the six [identity anchors](https://enruta.ai/docs/identity-anchors): `agent_key`, `code_digest`, `workload_identity`, `hardware_attestation`, `legal_entity`, `collateral` |
| `party`, `issuer`, `claims` | For anchors: whose identity (`developer`, `platform`, `user`), who vouched for it, and the subset of claims kept |
| `registry`, `registry_ref`, `tier` | For registry claims; see the tiers below |
| `method`, `status` | `status`: `verified` · `stale` (past the issuer’s validity) · `revoked` · `unverified` |
| `evidence_hash`, `evidence`, `verified_at`, `expires_at` | Enruta stores what it was given and its hash; the bundle carries the evidence so the verifier can re-hash it |
| `attestation_record_id` | The sealed linked record (`kind: kya_attestation`, `chain: attestation_for`) |

| Tier | Registries | What it is |
|---|---|---|
| A, verifiable claim | `cloudflare_web_bot_auth`, `persona_relay`, `vouched_kya` | A signature or credential Enruta can check |
| B, network identity anchor | `visa_agentic_directory`, `mastercard_agent_pay`, `skyfire` | A reference into a card-network or marketplace directory |
| C, vendor asserted | `vendor_attested` with `vendor` and `reference_id` | The operator says so |

The agent’s verified anchors add up to an assurance level from `A0` to `A3`, carried on every payment record as `agent.identity` and in the [risk assessment](https://enruta.ai/docs/risk-assessment) as `agent_identity`, so a merchant sees `unverified` when there is nothing and a policy can step up or deny on it ([identity requirements](https://enruta.ai/docs/identity-anchors)). The [Accept middleware](https://enruta.ai/docs/accept-middleware) can derive a merchant’s bot-management fields from the same object.

## Pausing an agent

An owner pauses an agent in the console, or a developer key calls `POST /v0/agents/{id}/pause` with a reason. From that moment every request by the agent is denied with `agent_paused` before any policy is read, and the pause itself is a linked record (`kind: agent_pause`, `chain: pause_of`, `subject { action: pause | resume, by, reason, at }`, org-signed). `POST /v0/agents/{id}/resume` seals the resume the same way; the webhook is `agent.paused`.

## Interfaces

| Surface | Call |
|---|---|
| REST | `POST /v0/consents`, `GET /v0/consents`, `GET /v0/consents/{id}`, `POST /v0/consents/{id}/revoke` (`effective_at?`, `cascades_scheduled?`, `institution_notified_at?`); `POST /v0/attestations/nonce`, `POST /v0/attestations`, `GET /v0/attestations`, `GET /v0/agents/{id}/identity`; `POST /v0/agents/{id}/pause`, `POST /v0/agents/{id}/resume` |
| SDK | `consents.create/list/get`, `consents.revoke(id, { effective_at, cascades_scheduled })` (Python `consents.revoke(consent_id, effective_at=None, cascades_scheduled=None)`), `attestations.nonce/create/list`, `agents.identity(id)`, `agents.pause(id, reason)`, `agents.resume(id)` |
| CLI | `enruta consents list \| revoke <id>`, `enruta attest workload \| key`, `enruta agents pause \| resume <agt> [--reason]` |
| Console | Identity, anchors, consents and attestations per agent under `/agents`; “Pause” and “Resume” with a reason; “Revoke” on a consent, effective now or at a date (UTC), optionally also ending scheduled payments and unused mandates; all consents under `/settings` |
| Webhooks | `consent.revoked` when a revocation takes effect, with `effective_at`, `cascades_scheduled`, `notified_institution_at` and `cascade`; `approval.completed` for each approval a cascade expired; `agent.paused` |
| Export | `POST /v0/exports` with `consents: true` includes the sealed consent records ([Compliance export](https://enruta.ai/docs/compliance-export)) |
