Skip to main content
The token exchange endpoint accepts a signed Purchase Intent 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.

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 to get access.

Request

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

FieldTypeRequiredDescription
pintobjectYesThe PINT payload exactly as signed
signaturestringYesHex-encoded EIP-712 signature (65 bytes: 0x[0-9a-fA-F]{130})
audiencestringYesRegistered identifier of the target third-party service. Must match the caller’s registered org.
source_chat_message_idintegerNoLinks this PINT to an originating chat message. When set, the message must belong to the wallet’s owning user.
enforcement_modestringNoOne of strict (default) or advisory. Governs how downstream enforcement treats scope violations.
The pint object follows the canonical EIP-712 type structure. The scopes array must contain values from the scope catalog.
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 after verification.

Linking a PINT to a Chat Message

To associate a PINT with a chat session message (for audit and provenance), include source_chat_message_id:
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 pipeline 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. Signer determination — detects whether the wallet has a registered agent key. If yes, the SIS generates the signature through the agent signing path; otherwise the caller-supplied signature is verified.
  4. Signature verification — reconstructs the EIP-712 hash and verifies the signature (ECDSA for user keys, EIP-1271 for agent/Safe keys).
  5. Nonce advancement — advances the wallet’s server-side nonce only after the signature is confirmed, so failed attempts do not burn nonces.
  6. Expiry re-check — rejects if expires_at has passed during processing.
  7. Scope parsing and registry validation — parses each scope string, validates it against the registry, and checks parameter types and ranges.
  8. KYC status check — only runs if any requested scope requires KYC. Rejects with PINT-403-002 if the user’s status is not verified.
  9. Chat message validation — if source_chat_message_id is set, confirms the message exists and belongs to this user.
  10. PINT persistence — stores the PINT with status active.
  11. JWT generation — issues an audience-scoped, SIS-signed JWT and records the issuance.

Response — 201 Created

On success, the endpoint returns the JWT and PINT resource details:
{
  "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 PINT resource: /v0/sis/pint/sr:us:pint:abc123.

Response Fields

FieldTypeDescription
sigstringThe SIS-signed PINT JWT
sristring or nullSumvin Resource Identifier for the signer (null if the wallet is not bound to a Sumvin user)
idstringPINT SRI (e.g. sr:us:pint:abc123)
audiencestringAudience the JWT was issued for
scopesarray of stringsAuthorised scopes for this PINT
expires_atintegerToken expiry as epoch seconds
_linksobject hypermedia links — see below
LinkMethodDescription
selfPOSTThis endpoint — /v0/sis/token/pint
pintGETPINT resource details
pint_statusGETPINT status (active, revoked, expired)
pint_tokensGETList JWTs issued for this PINT (user-scoped, under /v0/pint)
revokeDELETERevoke the PINT (user-scoped, under /v0/pint)
jwksGETPublic keys to verify the JWT signature

Multiple Audiences

A single PINT 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 PINT but a different audience:
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 PINT and the same audience, the endpoint is idempotent. It returns 208 Already Reported with the previously issued token:
{
  "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:
{
  "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"
}
StatusCodeWhen
400PINT-400-002Audience is not a registered third-party identifier, or does not match the caller’s org
400PINT-400-003Scope parameter is malformed or violates its declared type / range (e.g. max=abc, provider=stripe). detail names the offending scope and param.
400PINT-400-004Scope name is not in the registry (unknown MVP scope). detail echoes the offending scope string.
400PINT-400-005Scope string does not conform to the grammar (wrong segment count, wrong scheme, malformed query). detail echoes the offending scope string.
401PINT-401-001EIP-712 signature verification failed (ECDSA)
401PINT-401-002EIP-1271 verification failed against Safe contract
403PINT-403-002User has not completed KYC verification. Only fires if at least one requested scope requires KYC.
409PINT-409-001Nonce has already been used for this wallet
409PINT-409-002PINT has been revoked
410PINT-410-001PINT expires_at timestamp has passed
422PINT-422-001source_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.
422Malformed request body. Returned by FastAPI schema validation as its default ValidationError payload, not as a PINT-coded Problem Details response.
424PINT-424-001EIP-1271 on-chain call failed (agent key verification)
429PINT-429-001Rate limit exceeded
See the full error reference at Errors.