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

# Proof of Resolution

> The ES256 JWT BotShield issues when a user confirms or denies an action — its claims, local JWKS verification, and the Q webhook events.

# Proof of Resolution

A **Proof of Resolution** is the artifact Q produces when a user resolves a Q card with a biometric. It is a **JSON Web Token signed with ES256** (EC P-256, asymmetric) — a portable, action-scoped, independently verifiable attestation of the user's verdict.

Because it is signed with a private key whose **public** counterpart is published as a JWKS, an agent or your backend can verify a Proof of Resolution **locally, with no round-trip to BotShield**.

<Info>
  The token carries a verdict of `approve` or `denied` — both are signed. An expired or cancelled card produces **no** token at all. See [Resolution Flow](/queue/resolution-flow#q-card-states).
</Info>

## Claims

The token's header carries `alg: ES256` and `kid: v2`. The payload claims:

```json theme={null}
{
  "iss": "https://api.botshield.ai",
  "sub": "b5e1…",
  "aud": "a9f2…",
  "iat": 1750075200,
  "exp": 1750161600,
  "jti": "3f1b2c4d-5e6a-4b7c-8d9e-0f1a2b3c4d5e",
  "verdict": "approve",
  "action": {
    "category": "travel.book",
    "description": "Book Uber to SFO",
    "trusted_account_id": "7a8b9c0d-…"
  },
  "kid": "v2",
  "ceremony_id": "e4d3c2b1-…"
}
```

| Claim         | Meaning                                                                                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss`         | Always `https://api.botshield.ai`.                                                                                                                               |
| `sub`         | The BotShield user who confirmed (an opaque internal id — **not** an email or identity).                                                                         |
| `aud`         | The Trusted Agent the token is issued to. Verify this matches your agent.                                                                                        |
| `iat` / `exp` | Issued-at and expiry (Unix seconds). Tokens live **24 hours** — redeem within that window.                                                                       |
| `jti`         | The `request_id` you supplied to `inquire`. Your join key.                                                                                                       |
| `verdict`     | `approve` or `denied` — the user's decision.                                                                                                                     |
| `action`      | The sanitized action context the user saw — `category`, `description`, optional `trusted_account_id`, `total`, `total_currency`. **Not** the full Adaptive Card. |
| `kid`         | Key id, currently `v2`. Selects the JWKS key for verification.                                                                                                   |
| `ceremony_id` | Present when several cards were confirmed in one biometric ceremony. Cards sharing a `ceremony_id` (and an identical `iat`) were authorized in a single act.     |

<Warning>
  The Proof of Resolution carries **no identity**. `sub` is an opaque BotShield user id; there is no email, no device identifier, and no Adaptive Card body. It attests the *verdict* on a *specific action*, nothing more.
</Warning>

## Verify locally with the JWKS

BotShield publishes the Resolution public key as a JWK Set at:

```
https://api.botshield.ai/.well-known/jwks.json
```

The `kid` in the JWKS matches the JWT header, so verification is the standard JWKS flow. This is the recommended path — no network call to BotShield per verification.

### Node

```typescript theme={null}
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://api.botshield.ai/.well-known/jwks.json")
);

async function verifyProof(token: string, myAgentId: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: "https://api.botshield.ai",
    audience: myAgentId,            // must match your registered agent
    algorithms: ["ES256"],
  });

  // jwtVerify already enforced signature, iss, aud, and exp.
  if (payload.verdict !== "approve" && payload.verdict !== "denied") {
    throw new Error(`unexpected verdict: ${payload.verdict}`);
  }

  return payload; // { jti: request_id, verdict, action, ceremony_id, … }
}
```

Always check `verdict === "approve"` before acting — a `denied` token is a valid, signed *refusal*, not an authorization.

## Verify via the SDK (coming)

A round-trip verifier — **`sdk/verify-resolution`**, for callers who prefer not to pin a JWKS client — is **planned but not yet available**. It will perform the same ES256 verification server-side (rejecting bad signatures, wrong issuer, expired tokens, or an invalid verdict) and return the decoded claims.

<Note>
  Until it ships, **local JWKS verification (above) is the supported path** — and it is sufficient: it enforces signature, issuer, audience, expiry, and algorithm entirely client-side with no network round-trip.
</Note>

## Key rotation

The `kid` header tags the key version (today `v2`). When BotShield rotates keys, the new key is appended to the JWKS under a new `kid`; the old key is retained until all tokens signed with it expire (24-hour TTL). A correct JWKS verifier selects the key by `kid` automatically — no action required on rotation. Pin `algorithms: ["ES256"]` to reject any token that claims a different algorithm.

## Webhook events

In addition to (or instead of) polling, Q delivers events to your endpoint via **[Svix](https://svix.com)**. Each delivery is signed with your endpoint's `whsec_` secret and carries the `svix-id` / `svix-timestamp` / `svix-signature` headers — verify exactly as described in [Webhooks](/concepts/webhook-payloads#verify-the-signature-server-side).

<Warning>
  **Not yet firing in the current build.** The `q.card.proposed` and `q.resolution.*` events are defined as below, but they **do not yet fire for agent-proposed cards** — partner organization association for the Q flow is still being wired up. Until that lands, use **polling** ([`check-status`](/queue/api-reference#check-status)) as the reliable way to learn an outcome. The schemas here are stable; treat the webhook path as forthcoming.
</Warning>

| Event (`type`)           | Fires when                                                                     |
| ------------------------ | ------------------------------------------------------------------------------ |
| `q.card.proposed`        | An agent proposed an action; a Q card was created and pushed to the user.      |
| `q.resolution.confirmed` | The user confirmed the action (Proof of Resolution issued, verdict `approve`). |
| `q.resolution.denied`    | The user denied the action (Proof of Resolution issued, verdict `denied`).     |
| `q.resolution.expired`   | The card's TTL lapsed before the user acted — no Proof of Resolution.          |

### `q.card.proposed`

```json theme={null}
{
  "type": "q.card.proposed",
  "request_id": "3f1b2c4d-…",
  "card_id": "c1d2e3f4-…",
  "agent_id": "a9f2…",
  "agent_name": "ticketz-agent",
  "category": "travel.book",
  "summary_title": "Book Uber to SFO",
  "ttl_at": "2026-06-16T12:10:00Z"
}
```

### `q.resolution.confirmed` / `q.resolution.denied`

```json theme={null}
{
  "type": "q.resolution.confirmed",
  "request_id": "3f1b2c4d-…",
  "resolution_id": "r0a1b2c3-…",
  "outcome": "confirmed",
  "proof_token": "eyJhbGciOiJFUzI1Ni␣…",
  "metadata": {
    "card_id": "c1d2e3f4-…",
    "ceremony_id": "e4d3c2b1-…",
    "agent_id": "a9f2…",
    "agent_name": "ticketz-agent"
  }
}
```

`outcome` is `confirmed` for `q.resolution.confirmed` and `denied` for `q.resolution.denied`. The `proof_token` field carries the Proof of Resolution JWT — verify it as above before acting. When several cards were confirmed in one ceremony, each fires its own message, all sharing one `ceremony_id` in `metadata`.

### `q.resolution.expired`

```json theme={null}
{
  "type": "q.resolution.expired",
  "request_id": "3f1b2c4d-…",
  "card_id": "c1d2e3f4-…",
  "outcome": "expired",
  "metadata": {
    "agent_id": "a9f2…",
    "agent_name": "ticketz-agent",
    "summary_title": "Book Uber to SFO"
  }
}
```

No `proof_token` — there is no signed verdict on an expiry. Treat it as "stand down."

<Info>
  Authenticity comes from the **Svix envelope signature**, verified with your `whsec_` endpoint secret — not from a token inside the webhook. For confirmed/denied events you then independently verify the `proof_token` JWT. Dedupe on the Svix message id for idempotency; BotShield may retry.
</Info>

## Next steps

<CardGroup cols={2}>
  <Card title="Q API Reference" icon="code" href="/queue/api-reference">
    Every `agentlink/*` operation with request and response shapes.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/concepts/webhook-payloads">
    The shared Svix signature-verification pattern, with per-language snippets.
  </Card>
</CardGroup>
