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

# Session tokens

> The short-lived JWT your backend signs so a browser can talk to Patchwork directly.

In [direct](/guides/loom-direct) integration the browser holds no workspace secret. It holds a token your backend signed for one user, valid for a couple of minutes.

## Claims

<ParamField path="iss" type="string" required>
  Your API key **public id** (`key_…`), not the secret. Routes the token to your workspace.
</ParamField>

<ParamField path="sub" type="string" required>
  The [subject](/guides/subjects) this token acts as. Opaque to us — compose it server-side from ids that never change.
</ParamField>

<ParamField path="aud" type="string[]" required>
  Must include `patchwork`. Include your own API audience too, so the same token passes your tool middleware.
</ParamField>

<ParamField path="exp" type="integer" required>
  Keep it short — around 120 seconds. The browser remints as needed.
</ParamField>

<ParamField path="conn" type="uuid">
  The [connection](/guides/connections) this token may use. A UUID — not a name. Required when the agent has unpinned customer tools.
</ParamField>

<ParamField path="kid" type="string" required>
  JWT header, not a claim. Names the signing key so we can match it in your JWKS.
</ParamField>

## Algorithms

`RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`.

Symmetric algorithms (`HS*`) are rejected. A shared secret would mean we could mint tokens as you, which is exactly the property the design refuses.

## Publishing your key

Serve a JWKS document at a stable URL and set it on the workspace. We fetch and cache it, matching tokens to keys by `kid`. Rotation is adding a new key alongside the old one and switching `kid` when you are ready — never removing the old key first.

If you cannot host a JWKS URL, paste the public key in the dashboard instead.

## Minting

The endpoint that issues these tokens serves **two** callers — your browser, and the Patchwork runtime minting server-to-server. A connection declares only one mint URL, so both live at one path. [The mint endpoint](/guides/mint-endpoint) covers the branch, the relay response contract, and why the Playground depends on it.

The direct half looks like this:

```js theme={null}
app.post("/patchwork/mint", requireUser, (req, res) => {
  const token = jwt.sign(
    {
      iss: PATCHWORK_API_KEY_ID,
      aud: ["patchwork", "your-api"],
      sub: `${req.user.id}:${req.session.workspaceId}`,
      conn: CONNECTION_ID,
    },
    PRIVATE_KEY,
    { algorithm: "RS256", expiresIn: 120, keyid: "your-app-2026-01" },
  );
  res.json({ token, expires_in: 120 });
});
```

<Warning>
  Derive `sub` from your server session, never from a request parameter. We do not know what your subject string means, so we cannot tell that a user asked for a tenant they do not belong to. See [Subjects](/guides/subjects#composite-subjects).
</Warning>

## Verification and precedence

A JWT that matches no keyed workspace, or fails signature or claim checks, is **rejected** — never downgraded to relay. When a session token is present, its `sub` wins and any `subject_ref` in the body is ignored.


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