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

# Verify Upsolve tokens

> Check the short-lived token Upsolve sends your API for an API connector set to “As the signed-in user”, then act as that user.

An API connector set to **As the signed-in user** sends your API a token on every call: a JSON Web Token, signed by Upsolve with your project's private key, naming the user the call is for. Your API verifies it and then treats the request as that user. This page is the whole contract, written so you, or a coding agent, can follow it from top to bottom.

## Your values

Open the connector in Upsolve (Project settings → API connectors). It shows three values, which you use as constants:

| Value | Example | What it is |
| - | - | - |
| Issuer | `https://api.upsolve.ai/identity/p/9d41c0e2-…` | One per Upsolve project. The token's `iss`. |
| JWKS URL | `<issuer>/jwks.json` | The public keys that check the signature. |
| Audience | `https://api.acme.com` | The token's `aud`. Your API's base URL unless you changed it. |

The issuer also publishes an OpenID configuration at `<issuer>/.well-known/openid-configuration`, which auth providers read to accept tokens from another issuer.

There is no secret. Upsolve keeps the private key; the JWKS URL publishes only public keys.

## The token

It travels in `Authorization: Bearer <token>` unless the connector names another header and prefix.

```json theme={null}
{
  "iss": "https://api.upsolve.ai/identity/p/9d41c0e2-…",
  "aud": "https://api.acme.com",
  "sub": "u_123",
  "org": "acct_4821",
  "act": { "sub": "upsolve", "source": "chat" },
  "iat": 1761200000,
  "exp": 1761200300,
  "jti": "6f0e1f9a-…"
}
```

| Claim | Meaning |
| - | - |
| `sub` | The user's `externalId`, as your backend sends it to [`embed-token`](https://docs.upsolve.ai/api-reference/endpoint/create-embed-token). |
| `org` | The user's organization `externalId`. Left out when the organization has none. |
| `act` | Who is acting and why: always `"sub": "upsolve"`, with `source` one of `chat` (the agent, for a user in a conversation), `rule` (a rule, possibly with nobody signed in), `tile` (a canvas tile refreshing) or `test` (a builder's Check or test token). |
| `iat`, `exp` | Issued now; expires five minutes later. A new token is signed for every call. |
| `jti` | A unique id per token. |

The connector can add up to ten claims of your choosing: a property of the user or their organization (the `properties` you send to `embed-token`), or a fixed text such as `role: "authenticated"`. Upsolve sets `iss aud sub org act iat exp nbf jti` itself.

The header carries `alg: "ES256"`, `typ: "JWT"` and a `kid` naming the key.

## Verification rules

Verify every request that carries an Upsolve token:

1. **Signature**: ES256 only. Find the key by the header's `kid` in the JWKS, which you cache and fetch again when a `kid` is not in it (Upsolve rotates keys; an old key stays published for 24 hours). Reject a token without a `kid`.
2. **`iss`** equals your issuer exactly.
3. **`aud`** equals your audience exactly.
4. **`exp`** is in the future. Allow at most 60 seconds of clock skew.

Then **load the user by `sub`** (and their organization by `org`, when present) and authorize the request exactly as if that user had signed in. Answer `401` for a missing or invalid token, and for a valid token naming nobody you know. Answer `403` for a user who may not do what was asked.

Keep your existing authentication beside it: requests without an Upsolve token work as before. Use `act.source` if you want to limit what Upsolve-made calls may do, for example no writes from `tile`.

## Snippets

Each one verifies all four rules. Replace the values with yours.

```javascript Node (jose) theme={null}
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = "https://api.upsolve.ai/identity/p/<projectId>";
const upsolveKeys = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`));

export async function upsolveUser(req) {
  const raw = req.headers.authorization ?? "";
  if (!raw.startsWith("Bearer ")) return null;
  const { payload } = await jwtVerify(raw.slice(7), upsolveKeys, {
    issuer: ISSUER,
    audience: "https://api.acme.com",
    algorithms: ["ES256"],
  });
  return findUser(payload.sub, payload.org); // then authorize as that user
}
```

```python Python (PyJWT) theme={null}
# pip install "pyjwt[crypto]"
import jwt

ISSUER = "https://api.upsolve.ai/identity/p/<projectId>"
upsolve_keys = jwt.PyJWKClient(f"{ISSUER}/jwks.json")

def upsolve_user(headers):
    raw = headers.get("Authorization", "")
    if not raw.startswith("Bearer "):
        return None
    token = raw[len("Bearer "):]
    key = upsolve_keys.get_signing_key_from_jwt(token)
    payload = jwt.decode(
        token, key.key, algorithms=["ES256"], issuer=ISSUER,
        audience="https://api.acme.com",
        options={"require": ["exp", "iat", "sub"]},
    )
    return find_user(payload["sub"], payload.get("org"))
```

```go Go (golang-jwt + keyfunc) theme={null}
// go get github.com/golang-jwt/jwt/v5 github.com/MicahParks/keyfunc/v3
const issuer = "https://api.upsolve.ai/identity/p/<projectId>"

var upsolveKeys, _ = keyfunc.NewDefault([]string{issuer + "/jwks.json"})

func UpsolveUser(r *http.Request) (string, error) {
	token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
	if !ok {
		return "", jwt.ErrTokenMalformed
	}
	parsed, err := jwt.Parse(token, upsolveKeys.Keyfunc,
		jwt.WithValidMethods([]string{"ES256"}),
		jwt.WithIssuer(issuer),
		jwt.WithAudience("https://api.acme.com"),
		jwt.WithExpirationRequired(),
	)
	if err != nil {
		return "", err
	}
	return parsed.Claims.GetSubject()
}
```

```ruby Ruby (ruby-jwt) theme={null}
# gem "jwt"
ISSUER = "https://api.upsolve.ai/identity/p/<projectId>"

UPSOLVE_KEYS = lambda do |options|
  @upsolve_jwks = nil if options[:kid_not_found]
  @upsolve_jwks ||= JSON.parse(Net::HTTP.get(URI("#{ISSUER}/jwks.json")))
end

def upsolve_user(request)
  raw = request.headers["Authorization"].to_s
  return nil unless raw.start_with?("Bearer ")
  payload, _header = JWT.decode(raw.delete_prefix("Bearer "), nil, true, {
    algorithms: ["ES256"], jwks: UPSOLVE_KEYS,
    iss: ISSUER, verify_iss: true,
    aud: "https://api.acme.com", verify_aud: true,
  })
  find_user(payload["sub"], payload["org"])
end
```

```java Java (Nimbus JOSE+JWT) theme={null}
// com.nimbusds:nimbus-jose-jwt
String issuer = "https://api.upsolve.ai/identity/p/<projectId>";
JWKSource<SecurityContext> keys = JWKSourceBuilder.create(new URL(issuer + "/jwks.json")).build();
DefaultJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();
processor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.ES256, keys));
processor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
    "https://api.acme.com",
    new JWTClaimsSet.Builder().issuer(issuer).build(),
    Set.of("sub", "iat", "exp")));

// For each request, after stripping "Bearer ":
JWTClaimsSet claims = processor.process(token, null);
String userId = claims.getSubject(); // then authorize as that user
```

## Using your auth provider

### Supabase

1. In the Supabase Dashboard, open **Authentication → Sign In / Providers → Third-Party Auth**.
2. Click **Add provider**, choose **Custom OIDC** and enter your issuer. Supabase reads the keys through the OpenID configuration.
3. On the connector, click **Supabase compatible**. It adds the claim `role: "authenticated"`, without which requests run as `anon`.
4. In your RLS policies, read the user as `auth.jwt()->>'sub'`. `auth.uid()` casts `sub` to a UUID, so it only works when the `externalId` you send to Upsolve is the user's Supabase id.
5. If you also use Supabase Auth, add a restrictive policy that checks `auth.jwt()->>'iss'`, so ids from the two issuers cannot collide.

### Auth0

Auth0 issues your users' tokens; Upsolve's tokens are checked by your API next to them. If your API uses `express-oauth2-jwt-bearer`, add a second `auth({ issuerBaseURL: <issuer>, audience: <audience>, tokenSigningAlg: "ES256" })` and accept a request that either one accepts. With any other middleware, add Upsolve as a second trusted issuer the same way, or use a snippet above.

### Clerk

Clerk verifies the sessions it issues, not Upsolve's tokens. Keep Clerk's middleware for your app's own requests, and put a snippet above in front of it for requests that carry an Upsolve token. Look the user up by `sub`, the `externalId` you send to Upsolve (usually the Clerk user id).

## Check it

1. On the connector, click **Copy a test token**. You get a ready `curl` line with a token that lasts five minutes (`act.source` is `test`). Run it against your local server and confirm it answers `200` as that user.
2. Change the audience you expect, or wait five minutes, and confirm the same call answers `401`.
3. Deploy, then run **Check** in Upsolve. It calls the endpoint you pick three times: with a valid token (it expects `2xx`), with a token for another audience, and with an expired token (both properly signed; it expects `401` or `403`). Each failure comes with what to fix. Upsolve never shows your API's response bodies.

## Keys and rotation

Each Upsolve project has one signing key at a time. **Rotate signing key** on the connector makes a new key sign every token from then on; the old key stays in the JWKS for 24 hours, so a token it signed a moment ago still verifies, and is then removed. A verifier that fetches the JWKS again on an unknown `kid` needs no change.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.