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

# JWT authentication

> Authenticating consumer requests with a partner-signed JWT.

When a user opens the webview (or hits any `/me/*` endpoint directly), they
present a JWT signed by **your** app. We verify it using the public key you
publish via `jwksUrl` or supply as a static `jwtPublicKey`.

## Who provides what

The Voucher Platform **verifies** your JWTs — there is no verification endpoint
for you to call. To make that possible, **you (the partner) provide the
verification inputs** and we store them on your partner record:

| Setting                       | Provided by                                | Required?        | Purpose                                                                                       |
| ----------------------------- | ------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------- |
| `jwtIssuer`                   | **You (partner)** — you choose it, tell us | Yes              | The exact `iss` value your tokens carry. We match it to route to your verifier.               |
| `jwksUrl`                     | **You (partner)** — you host the endpoint  | One of these two | We fetch your public keys from it. **Recommended** — lets you rotate keys without telling us. |
| `jwtPublicKey`                | **You (partner)** — static PEM you hand us | …or this         | Alternative when you can't host a JWKS endpoint.                                              |
| `jwtAudience`                 | **You (partner)** — optional               | No               | If set, your tokens must include a matching `aud`.                                            |
| JWT signing key (**private**) | **You (partner)** — never shared           | —                | Stays in your backend; you sign each user's token with it. We never receive it.               |
| Token **verification**        | **Voucher Platform**                       | —                | We verify the signature + `iss` / `aud` / `exp` on every `/me/*` call.                        |

<Note>
  You must supply `jwtIssuer` **and** one of (`jwksUrl` | `jwtPublicKey`)
  **before** your partner record can be created. So decide your issuer and
  stand up your JWKS endpoint first — the rest of onboarding follows. See
  [Onboarding & credentials](/concepts/credentials) for the full list of who
  provides which value.
</Note>

## Required claims

| Claim | Type   | Description                                                                                                                         |
| ----- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `iss` | string | Must match `jwtIssuer` on your partner record. This is how we route the request to your verifier.                                   |
| `sub` | string | Your external user ID. **Identical** to what you sent to `POST /integrations/users`. This links the JWT to the right internal user. |
| `aud` | string | Required only if you set `jwtAudience` on your partner record.                                                                      |
| `exp` | number | Unix seconds. Recommend ≤ 1 hour.                                                                                                   |
| `iat` | number | Unix seconds.                                                                                                                       |

## Optional claims (JIT user creation)

If you skip the `POST /integrations/users` step entirely, we'll create the
user the **first** time a JWT for an unknown `sub` arrives ("just-in-time"
provisioning). These optional claims populate the new row:

| Claim                        | Maps to       | Notes                               |
| ---------------------------- | ------------- | ----------------------------------- |
| `email`                      | `email`       |                                     |
| `displayName` *or* `name`    | `displayName` | `displayName` wins if both present. |
| `phone`                      | `phone`       |                                     |
| `countryCode` *or* `country` | `countryCode` | First two chars, uppercased.        |
| `locale`                     | `locale`      |                                     |

<Tip>
  JIT works, but the **recommended pattern** is to upsert the user from your
  backend *before* opening the webview. That way you can correct stale data
  (e.g. user changed their email) and you don't depend on JWT claims for PII
  integrity.
</Tip>

## Algorithm

We require **RS256** with a 2048+ bit key. We do not currently accept HS256,
ES256, or unsigned tokens.

## Example

<CodeGroup>
  ```ts node (jose) theme={null}
  import { SignJWT, importPKCS8 } from 'jose';

  const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, 'RS256');

  const jwt = await new SignJWT({
    email: user.email,
    displayName: user.name,
    countryCode: user.country,
  })
    .setProtectedHeader({ alg: 'RS256' })
    .setIssuer('https://acme.example.com')   // == partner.jwtIssuer
    .setAudience('voucher-platform')          // optional
    .setSubject(user.id)                      // == externalUserId
    .setIssuedAt()
    .setExpirationTime('1h')
    .sign(privateKey);
  ```

  ```python python (PyJWT) theme={null}
  import jwt as pyjwt
  from datetime import datetime, timezone, timedelta

  with open('jwt-private.pem') as f:
      private_key = f.read()

  token = pyjwt.encode(
      {
          'iss': 'https://acme.example.com',
          'aud': 'voucher-platform',
          'sub': user.id,
          'email': user.email,
          'displayName': user.name,
          'iat': datetime.now(tz=timezone.utc),
          'exp': datetime.now(tz=timezone.utc) + timedelta(hours=1),
      },
      private_key,
      algorithm='RS256',
  )
  ```
</CodeGroup>

## Key rotation

If you set `jwksUrl`, we fetch the JWK set on each unique `kid` we haven't
seen, with internal caching. To rotate:

1. Publish the new key alongside the old one at your JWKS URL.
2. Start signing new JWTs with the new `kid`.
3. Wait for any in-flight tokens signed with the old key to expire.
4. Remove the old key from your JWKS.

We don't pin keys, so no admin action is needed on our side.

If you supply a static `jwtPublicKey` instead, rotation requires an admin
update to your partner record. JWKS is preferred for production.
