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

# Token Exchange

> Exchange a signed Stamped Mandate for a SIS-signed JWT

The token exchange endpoint accepts a signed <Tooltip headline="Stamped Mandate" tip="A signed authorisation a user grants for specific scoped actions — delivered on the wire as a PINT." cta="Glossary →" href="/glossary">Stamped Mandate</Tooltip> and returns a SIS-signed JWT that third-party services can independently verify. This is the core transactional identity flow — it converts a user's cryptographic authorisation into a portable, verifiable credential.

<Snippet file="product-term-disambiguation.mdx" />

A Stamped Mandate is delivered over the API as a `PINT` (header `x-sumvin-pint-token`). The request body, headers, and error codes on this page all use the `PINT` identifier verbatim.

## Endpoint

```
POST /v0/sis/token/pint
```

**Authentication:** API key via `Authorization: Bearer` header. The key must carry the `token_exchange` scope. API keys are currently provisioned on request — [email dan@sumvin.com](mailto:dan@sumvin.com) to get access.

## Request

```bash theme={null}
curl -X POST https://sis.sumvin.com/v0/sis/token/pint \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "pint": {
      "wallet": "0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
      "nonce": 42,
      "statement": "Purchase authorization for partner X",
      "scopes": ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:personalization:read"],
      "resources": ["sr:us:pint:abc123"],
      "max_amount": 0,
      "max_amount_token": "0x0000000000000000000000000000000000000000",
      "expires_at": 1740000000
    },
    "signature": "0x...",
    "audience": "partner-x.example.com"
  }'
```

### Request Body

| Field                    | Type    | Required | Description                                                                                                       |
| ------------------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `pint`                   | object  | Yes      | The [Stamped Mandate payload](/identity/pint) exactly as signed                                                   |
| `signature`              | string  | Yes      | Hex-encoded EIP-712 signature (65 bytes: `0x[0-9a-fA-F]{130}`)                                                    |
| `audience`               | string  | Yes      | Registered identifier of the target third-party service. Must match the caller's registered org.                  |
| `source_chat_message_id` | integer | No       | Links this mandate to an originating chat message. When set, the message must belong to the wallet's owning user. |
| `enforcement_mode`       | string  | No       | One of `strict` (default) or `advisory`. Governs how downstream enforcement treats scope violations.              |

The `pint` object follows the canonical [EIP-712 type structure](/identity/pint). The `scopes` array must contain values from the [scope catalog](/identity/scopes).

<Note>
  The SIS automatically determines the signer type based on whether the signing wallet has a registered agent key. You do not need to specify a signer type in the request — it will appear in the [JWT claims](/identity/jwt) after verification.
</Note>

### Linking a Stamped Mandate to a Chat Message

To associate a Stamped Mandate with a chat session message (for audit and provenance), include `source_chat_message_id`:

```bash theme={null}
curl -X POST https://sis.sumvin.com/v0/sis/token/pint \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "pint": { "wallet": "0x...", "nonce": 42, "...": "..." },
    "signature": "0x...",
    "audience": "partner-x.example.com",
    "source_chat_message_id": 12345,
    "enforcement_mode": "strict"
  }'
```

## Processing Pipeline

When you call the token exchange, the SIS runs the following validation checks before issuing a JWT:

1. **Nonce idempotency check** — if a `PINT` already exists for this `(wallet, nonce)` pair, the exchange short-circuits to the idempotent / multi-audience path (see below).
2. **Audience validation** — confirms the `audience` matches the caller's registered organisation.
3. **Signature verification** — reconstructs the EIP-712 hash and verifies the signature (ECDSA for user keys, EIP-1271 for agent/Safe keys).
4. **Nonce advancement** — advances the wallet's [server-side nonce](/identity/nonces) only after the signature is confirmed, so failed attempts do not burn nonces.
5. **Expiry re-check** — rejects if `expires_at` has passed during processing.
6. **Scope parsing and registry validation** — parses each scope string, validates it against the registry, and checks parameter types and ranges.
7. **KYC status check** — only runs if any requested scope requires KYC. Rejects with `PINT-403-002` if the user's status is not `verified`.
8. **Chat message validation** — if `source_chat_message_id` is set, confirms the message exists and belongs to this user.
9. **Mandate persistence** — stores the `PINT` with status `active`.
10. **JWT generation** — issues an audience-scoped, SIS-signed [JWT](/identity/jwt) and records the issuance.

## Response — 201 Created

On success, the endpoint returns the JWT and Stamped Mandate resource details:

```json theme={null}
{
  "sig": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
  "sri": "sr:us:person:safe:0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
  "id": "sr:us:pint:abc123",
  "audience": "partner-x.example.com",
  "scopes": [
    "sr:us:pint:identity:proof_of_personhood",
    "sr:us:pint:personalization:read"
  ],
  "expires_at": 1740000000,
  "_links": {
    "self": { "href": "/v0/sis/token/pint" },
    "pint": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3Aabc123" },
    "pint_status": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3Aabc123/status" },
    "pint_tokens": { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123/tokens" },
    "revoke": { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123", "method": "DELETE" },
    "jwks": { "href": "/v0/sis/.well-known/jwks.json" }
  }
}
```

The `Location` header points to the new Stamped Mandate resource: `/v0/sis/pint/sr:us:pint:abc123`.

### Response Fields

| Field        | Type             | Description                                                                                                                                                                                |
| ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sig`        | string           | The SIS-signed `PINT` JWT                                                                                                                                                                  |
| `sri`        | string or null   | Sumvin Resource Identifier for the signer (null if the wallet is not bound to a Sumvin user)                                                                                               |
| `id`         | string           | Stamped Mandate SRI (e.g. `sr:us:pint:abc123`)                                                                                                                                             |
| `audience`   | string           | Audience the JWT was issued for                                                                                                                                                            |
| `scopes`     | array of strings | Authorised scopes for this mandate                                                                                                                                                         |
| `expires_at` | integer          | Token expiry as epoch seconds                                                                                                                                                              |
| `_links`     | object           | <Tooltip headline="HAL" tip="Hypertext Application Language — the link format used on every Sumvin response." cta="Glossary →" href="/glossary">HAL</Tooltip> hypermedia links — see below |

### HAL Links

| Link          | Method | Description                                                       |
| ------------- | ------ | ----------------------------------------------------------------- |
| `self`        | POST   | This endpoint — `/v0/sis/token/pint`                              |
| `pint`        | GET    | Stamped Mandate resource details                                  |
| `pint_status` | GET    | Stamped Mandate status (active, revoked, expired)                 |
| `pint_tokens` | GET    | List JWTs issued for this mandate (user-scoped, under `/v0/pint`) |
| `revoke`      | DELETE | Revoke the Stamped Mandate (user-scoped, under `/v0/pint`)        |
| `jwks`        | GET    | Public keys to verify the JWT signature                           |

## Multiple Audiences

A single Stamped Mandate can produce multiple JWTs — one per target service provider. This supports purchase flows that span multiple providers.

To issue a JWT for a second audience, call the exchange again with the same mandate but a different `audience`:

```bash theme={null}
curl -X POST https://sis.sumvin.com/v0/sis/token/pint \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "pint": { "wallet": "0x...", "nonce": 42, "...": "..." },
    "signature": "0x...",
    "audience": "partner-y.example.com"
  }'
```

The SIS recognizes the existing `PINT` (by wallet + nonce), confirms it is still active, and issues a new audience-scoped JWT without re-running the full validation pipeline.

## Idempotency — 208 Already Reported

If you call the exchange with the same mandate **and** the same audience, the endpoint is idempotent. It returns `208 Already Reported` with the previously issued token:

```json theme={null}
{
  "sig": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
  "sri": "sr:us:person:safe:0xE23c9A70BC749EBddd8c78a864fd911D04E9e992",
  "id": "sr:us:pint:abc123",
  "audience": "partner-x.example.com",
  "scopes": [
    "sr:us:pint:identity:proof_of_personhood",
    "sr:us:pint:personalization:read"
  ],
  "expires_at": 1740000000,
  "_links": {
    "self": { "href": "/v0/sis/token/pint" },
    "pint": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3Aabc123" },
    "pint_status": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3Aabc123/status" },
    "pint_tokens": { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123/tokens" },
    "revoke": { "href": "/v0/pint/sr%3Aus%3Apint%3Aabc123", "method": "DELETE" },
    "jwks": { "href": "/v0/sis/.well-known/jwks.json" }
  }
}
```

This means you can safely retry the exchange without worrying about duplicate JWT issuance.

## Error Responses

All errors follow [RFC 7807 Problem Details](/introduction#error-responses):

```json theme={null}
{
  "type": "https://docs.sumvin.com/errors/pint_revoked",
  "title": "PINT Revoked",
  "status": 409,
  "detail": "PINT with nonce 42 has been revoked",
  "instance": "/v0/sis/token/pint",
  "error_code": "PINT-409-002",
  "documentation": "https://docs.sumvin.com/errors/PINT-409-002"
}
```

| Status | Code           | When                                                                                                                                                                                    |
| ------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `PINT-400-002` | Audience is not a registered third-party identifier, or does not match the caller's org                                                                                                 |
| 400    | `PINT-400-003` | Scope parameter is malformed or violates its declared type / range (e.g. `max=abc`, `provider=stripe`). `detail` names the offending scope and param.                                   |
| 400    | `PINT-400-004` | Scope name is not in the registry (unknown MVP scope). `detail` echoes the offending scope string.                                                                                      |
| 400    | `PINT-400-005` | Scope string does not conform to the grammar (wrong segment count, wrong scheme, malformed query). `detail` echoes the offending scope string.                                          |
| 401    | `PINT-401-001` | EIP-712 signature verification failed (ECDSA)                                                                                                                                           |
| 401    | `PINT-401-002` | EIP-1271 verification failed against Safe contract                                                                                                                                      |
| 403    | `PINT-403-002` | User has not completed KYC verification. Only fires if at least one requested scope requires KYC.                                                                                       |
| 409    | `PINT-409-001` | Nonce has already been used for this wallet                                                                                                                                             |
| 409    | `PINT-409-002` | PINT has been revoked                                                                                                                                                                   |
| 410    | `PINT-410-001` | PINT `expires_at` timestamp has passed                                                                                                                                                  |
| 422    | `PINT-422-001` | `source_chat_message_id` is invalid — the message does not exist, belongs to another user, or was supplied without an authenticated user. `detail` identifies the offending message ID. |
| 422    | —              | Malformed request body. Returned by FastAPI schema validation as its default `ValidationError` payload, not as a PINT-coded Problem Details response.                                   |
| 424    | `PINT-424-001` | EIP-1271 on-chain call failed (agent key verification)                                                                                                                                  |
| 429    | `PINT-429-001` | Rate limit exceeded                                                                                                                                                                     |

See the full error reference at [Errors](/merchant/errors).
