> ## 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.

# Authentication

> How to authenticate with the Sumvin Platform API and SIS API

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

The Sumvin platform exposes two API surfaces with different authentication mechanisms.

## Platform API — JWT Authentication

All Platform API requests require a valid JWT passed in a custom header. The API supports multiple authentication providers — your platform administrator configures which provider to use.

### Making Authenticated Requests

Pass the JWT from your auth provider in the `x-juno-jwt` header:

```bash theme={null}
curl https://api.sumvin.com/v0/user/me \
  -H "x-juno-jwt: <your-jwt-token>"
```

If your Platform API credentials belong to more than one Sumvin organisation, scope the request with the `x-juno-orgid` header:

```bash theme={null}
curl https://api.sumvin.com/v0/user/me \
  -H "x-juno-jwt: <your-jwt-token>" \
  -H "x-juno-orgid: org-3b8b85b2a0fc4564b813fa93c8ef89cf:env-fd617c5800134505a1c205e5a3123589"
```

<Warning>
  The Platform API uses `x-juno-jwt`, not the standard `Authorization: Bearer` header.
</Warning>

### The `x-juno-orgid` Header

Multi-tenant integrations identify both the organisation and the environment a request belongs to using a single header value formatted as `<org-id>:<env-id>`.

| Half       | Source                                                                            |
| ---------- | --------------------------------------------------------------------------------- |
| `<org-id>` | The Organisation ID under [**Organisation → Settings**](/dashboard/organisations) |
| `<env-id>` | The Environment ID under [**Environment → Settings**](/dashboard/environments)    |

Both halves are required, even if your organisation only has a single environment — Sumvin always resolves auth configuration per environment, so the env half pins the request to the correct provider credentials.

A complete header looks like this:

```http theme={null}
x-juno-orgid: org-3b8b85b2a0fc4564b813fa93c8ef89cf:env-fd617c5800134505a1c205e5a3123589
```

If the value is present but not formatted as two non-empty halves separated by a single colon, the API responds with `401 Unauthorized` and error code `USR-401-002`:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/usr-401-002",
  "title": "Invalid Org ID Format",
  "status": 401,
  "detail": "x-juno-orgid must be formatted as '<org-id>:<env-id>'.",
  "instance": "/v0/user/me",
  "error_code": "USR-401-002"
}
```

Single-tenant integrations can omit the header entirely; the API resolves the JWT against the default provider in that case.

### Supported Auth Providers

| Provider         | Description                                                          |
| ---------------- | -------------------------------------------------------------------- |
| **Dynamic Labs** | Web3-native auth with wallet and email login                         |
| **Privy**        | Embedded wallets and email/SMS/social login, with MPC key management |
| **SIWE**         | Sign-In With Ethereum (EIP-4361) — direct wallet-based auth          |

Which providers are active is configured per environment in the [SIS Dashboard](/dashboard/auth-providers). Your platform is configured with one or more providers — refer to your provider's SDK documentation for how to obtain JWTs.

### Token Requirements

Sumvin verifies the JWT signature against the configured provider's JWKS endpoint (or pinned verification key) and reads the following standard claims from the payload:

| Claim | Required    | Description                                                                                      |
| ----- | ----------- | ------------------------------------------------------------------------------------------------ |
| `sub` | Yes         | User's unique identifier from the auth provider                                                  |
| `exp` | Yes         | Token expiration time (Unix timestamp, seconds)                                                  |
| `iss` | Yes         | Issuer URL — used to detect which provider signed the token (e.g. `privy.io`, `dynamicauth.com`) |
| `aud` | Recommended | Audience — for Privy, this should be your App ID                                                 |

A Privy identity token decoded payload looks roughly like this:

```json theme={null}
{
  "sub": "did:privy:cm5n6f0h300ab12abcdef1234",
  "iss": "privy.io",
  "aud": "cm5h7abcdefghijklmnopqrst",
  "exp": 1735689600,
  "iat": 1735603200,
  "linked_accounts": [
    { "type": "email", "address": "user@example.com" }
  ]
}
```

Expired or tampered tokens are rejected with `401 Unauthorized` and error code `USR-401-001`.

#### Privy: Identity Tokens vs Access Tokens

When your environment is configured in [App ID only mode](/dashboard/auth-providers), Sumvin verifies Privy **identity tokens**. Identity tokens carry user claims (linked accounts, wallets, email) directly in the JWT — Privy access tokens do not, and will fail verification.

<Note>
  **Privy identity token ≠ Sigil.** The Privy identity token authenticates the end-user's linked accounts (wallets, email, OAuth) to Privy's system and is the credential Sumvin uses to establish a session. It tells the API *who* the user is, but carries no KYC or personhood verification. **Sigil** is Sumvin's portable Proof of Personhood — a separate product layer provisioned after KYC passes and onboarding completes. A valid Privy identity token is a prerequisite for reaching the Sumvin API, but it does not constitute a Sigil. The identity token is a standard JWT issued by Privy's client SDK and carries the user's linked-account claims directly; refer to [Privy's documentation](https://docs.privy.io) for the token-issuance mechanics.
</Note>

Privy exposes identity tokens through the `useIdentityToken` React hook:

```tsx theme={null}
import { useIdentityToken } from "@privy-io/react-auth";

export function useSumvinAuthHeaders() {
  const { identityToken } = useIdentityToken();

  if (!identityToken) {
    return null;
  }

  return {
    "x-juno-jwt": identityToken,
    "x-juno-orgid": "org-...:env-...",
  };
}
```

`getIdentityToken()` from the headless SDK returns the same value if you're not using React. See the [Privy documentation](https://docs.privy.io/) for setup details — in the Privy console, identity tokens must be enabled under **User management → Authentication → Advanced → Return user data in an identity token**.

### Token Freshness

Privy identity tokens follow the JWT lifetime configured in the Privy console (typically one hour for identity tokens; access tokens are shorter-lived). The token Sumvin verifies must be fresh on every request.

* `useIdentityToken` re-fetches when the underlying session refreshes — read `identityToken` per request rather than caching the value.
* For long-lived sessions (dashboards, background tasks), refresh proactively before `exp` to avoid silent `401`s. Privy's React SDK refreshes in the background as long as `usePrivy` is mounted.
* Once `exp` has passed, Sumvin returns `401` with `USR-401-001`. Treat this as a signal to refresh and retry once.

### User Identity

The API resolves user identity from the JWT — you never pass `user_id` in URLs or request bodies. Endpoints scoped to "the current user" derive identity from the token rather than a path parameter. For example:

[`GET /v0/user/me`](/api-reference/user/get-user-account) — Fetch the current user.

[`POST /v0/user/me/onboarding/steps`](/api-reference/user/submit-onboarding-step-route) — Submit onboarding for the current user.

On first request, if no user exists for the JWT's `sub` claim, you must create one:

```bash theme={null}
curl -X POST https://api.sumvin.com/v0/user/ \
  -H "x-juno-jwt: <your-jwt-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "primary_eoa_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD78",
    "chain_id": 1329
  }'
```

If the JWT is missing or invalid, this call returns `401`:

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/usr-401-001",
  "title": "User Authentication Failed",
  "status": 401,
  "detail": "Missing or invalid authentication token",
  "instance": "/v0/user/",
  "error_code": "USR-401-001"
}
```

Subsequent requests with the same JWT will resolve to this user.

## SIS API — API Key Authentication

This surface is how you query <Tooltip headline="Sigil" tip="Sumvin's portable, KYC-verified Proof of Personhood. On the wire it is the SRI identity system — identity tokens, scopes, and verification." cta="Glossary →" href="/glossary">Sigil</Tooltip> — Sumvin's portable, KYC-verified Proof of Personhood. On the wire, Sigil is the SRI identity system: the `SRI` identifiers, identity tokens, scopes, and KYC verification status you read through the endpoints below.

The Sumvin Identity Service (SIS) uses API key authentication via the `Authorization: Bearer` header.

```bash theme={null}
curl https://sis.sumvin.com/v0/users/sr:us:person:safe:0x... \
  -H "Authorization: Bearer <your-api-key>"
```

API keys are scoped to your registered external party identity and determine which data fields you can access when looking up users by <Tooltip headline="SRI" tip="Sumvin Resource Identifier — URI-style identifier for users and capabilities." cta="Glossary →" href="/glossary">SRI</Tooltip>.

### Scopes & partitions

SIS keys are scoped at mint time. Each key carries an explicit set of **scopes** drawn from a dotted-namespace catalogue. A key's **partition** — public-safe vs server-only — is derived from the namespace prefix of its scopes.

#### Scope catalogue

| Scope                | Allows                               | Namespace      |
| -------------------- | ------------------------------------ | -------------- |
| `sis.lookup`         | `GET /v0/users/{sri}` (basic lookup) | `sis` (server) |
| `sis.get_details`    | Full identity record fields          | `sis` (server) |
| `sis.get_kyc`        | KYC verification status              | `sis` (server) |
| `sis.token_exchange` | Token exchange flows                 | `sis` (server) |
| `sis.get_pints`      | PINTs balances                       | `sis` (server) |
| `rpc.invoke`         | Calling the public RPC endpoint      | `rpc` (public) |

#### Partitions

The `sis` and `rpc` namespaces sit in two distinct partitions. A key cannot span both.

| Partition  | Namespaces | Where to use                                             | Public-safe?                                                                  |
| ---------- | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **Server** | `sis.*`    | Server-side only — your backend calling `sis.sumvin.com` | **No** — never embed in browsers, mobile bundles, or anything user-controlled |
| **Public** | `rpc.*`    | Client-side — browsers, mobile apps, server-side proxies | **Yes** — designed to be exposed in client bundles                            |

**Partition is fixed post-mint.** A key's partition is determined by the namespace of its scopes at mint time and cannot be changed. To move a key between partitions, revoke and mint a new one. Within a partition, scopes are mutable (a future PATCH endpoint will let you grant additional same-partition scopes — not yet shipped).

The mint response surfaces `partition` (`"public"` or `"server"`) and the verbatim `scopes` array, so dashboards and ops tooling can render badges without a second round-trip.

### Minting a key

The request body takes a `scopes` array. The partition is derived from the namespaces of the scopes you request — mixing `sis.*` and `rpc.*` in the same request is rejected with `422 SIS-422-006`.

Minting keys is a dashboard-admin operation on the SIS host (`sis.sumvin.com`), authenticated with your dashboard session token via the `Authorization: Bearer` header. The organisation is taken from the `<org_id>` path segment.

```bash theme={null}
curl -X POST https://sis.sumvin.com/v0/organisation/<org_id>/keys \
  -H "Authorization: Bearer <dashboard-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-public-rpc-key",
    "env_id": "<env-id>",
    "scopes": ["rpc.invoke"]
  }'
```

**Response (server-partition key, full SIS scopes):** `201 Created`

```json theme={null}
{
  "id": "key_abc123",
  "name": "my-server-key",
  "key": "sk_live_xxx...",
  "partition": "server",
  "scopes": [
    "sis.lookup",
    "sis.get_details",
    "sis.get_kyc",
    "sis.token_exchange",
    "sis.get_pints"
  ],
  "meta": {
    "org_id": "org_abc",
    "env_id": "env_def"
  },
  "_links": {
    "self":         { "href": "/v0/organisation/<org_id>/keys/key_abc123" },
    "organisation": { "href": "/v0/organisation/<org_id>" },
    "environment":  { "href": "/v0/organisation/<org_id>/environments/<env_id>" },
    "revoke":       { "href": "/v0/organisation/<org_id>/keys/key_abc123", "method": "DELETE" },
    "list":         { "href": "/v0/organisation/<org_id>/keys" }
  }
}
```

**Response (public-partition RPC key):** `201 Created` — same shape plus an `rpc_docs` HAL link:

```json theme={null}
{
  "id": "key_abc123",
  "name": "my-public-rpc-key",
  "key": "sk_live_xxx...",
  "partition": "public",
  "scopes": ["rpc.invoke"],
  "meta": {
    "org_id": "org_abc",
    "env_id": "env_def"
  },
  "_links": {
    "self":         { "href": "/v0/organisation/<org_id>/keys/key_abc123" },
    "organisation": { "href": "/v0/organisation/<org_id>" },
    "environment":  { "href": "/v0/organisation/<org_id>/environments/<env_id>" },
    "revoke":       { "href": "/v0/organisation/<org_id>/keys/key_abc123", "method": "DELETE" },
    "list":         { "href": "/v0/organisation/<org_id>/keys" },
    "rpc_endpoint": { "href": "https://rpc.sumvin.com" },
    "rpc_docs":     { "href": "https://docs.sumvin.com/rpc" }
  }
}
```

The plaintext `key` is returned **once** — store it immediately. Subsequent reads do not return the plaintext.

Public-partition keys carry both `rpc_endpoint` (the RPC base URL the key authenticates against) and `rpc_docs` (the RPC method reference) HAL links. Server-partition keys carry neither.

#### Validation errors

| Status | `error_code`                     | When                                                                                                |
| ------ | -------------------------------- | --------------------------------------------------------------------------------------------------- |
| `422`  | `SIS-422-006` (`Invalid Scopes`) | The `scopes` array is empty, contains an unknown scope, or mixes `sis.*` and `rpc.*` in one request |
| `422`  | (FastAPI default)                | `scopes` field missing entirely from the body, or `env_id` missing                                  |

<Warning>
  Pre-existing partner keys are migrated to the dotted scope catalogue automatically. Their partition is preserved (a key with the legacy `lookup` scope ends up in the server partition with `sis.lookup`); they cannot be elevated across partitions via mutation — the only path between partitions is re-mint.
</Warning>

For more on the SIS API and identity concepts, see [Identity & PINTs](/identity/overview).

## Error Responses

Authentication failures return [RFC 7807 Problem Details](/introduction#error-responses):

```json theme={null}
{
  "type": "https://api.sumvin.com/errors/usr-401-001",
  "title": "User Authentication Failed",
  "status": 401,
  "detail": "Missing or invalid authentication token",
  "instance": "/v0/user/me",
  "error_code": "USR-401-001"
}
```

| Status           | Error Code    | Meaning                                                                                                            |
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| 401 Unauthorized | `USR-401-001` | Missing, expired, or invalid JWT / API key                                                                         |
| 401 Unauthorized | `USR-401-002` | `x-juno-orgid` header is malformed (not `<org-id>:<env-id>`)                                                       |
| 403 Forbidden    | —             | Token is valid but the user lacks required permissions                                                             |
| 404 Not Found    | `USR-404-001` | Token is valid but no user account exists (call [`POST /v0/user/`](/api-reference/user/create-user-account) first) |

## Request Headers Summary

| Header                  | API Surface  | Required           | Description                                                                                   |
| ----------------------- | ------------ | ------------------ | --------------------------------------------------------------------------------------------- |
| `x-juno-jwt`            | Platform API | Yes                | JWT from your configured auth provider                                                        |
| `x-juno-orgid`          | Platform API | Multi-tenant only  | Composite `<org-id>:<env-id>` scoping the request to a specific organisation and environment. |
| `Authorization: Bearer` | SIS API      | Yes                | API key issued during partner onboarding                                                      |
| `Content-Type`          | Both         | For POST/PUT/PATCH | `application/json`                                                                            |
| `X-Timestamp-Format`    | Platform API | No                 | Set to `iso8601` for ISO 8601 timestamps                                                      |
