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

# The mint endpoint

> One URL you host, two callers. Your browser mints for a signed-in user; Patchwork mints server-to-server for an asserted subject.

A connection declares **one** `mint_url`. There is no second field, so if both your product and the Patchwork runtime need tokens from you, that single endpoint has to serve both.

They are genuinely different callers with different proofs and — this is the part that catches people — **different response shapes**.

```
browser  ──── your user's session ─────▶  /patchwork/mint     direct
Patchwork ─── Patchwork-Signature ─────▶  /patchwork/mint     relay
```

## Branch on the signature

The one reliable discriminator is whether the request carries a `Patchwork-Signature` header. A browser never has one; Patchwork always does.

```js theme={null}
app.post("/patchwork/mint", (req, res) => {
  if (req.get("Patchwork-Signature")) return mintRelay(req, res);
  return mintDirect(req, res);
});
```

Do not branch on a path, a query parameter, or a body field. Patchwork controls what it sends and it sends the signature.

## Relay: Patchwork asking

Patchwork needs a token for a subject it is already acting for. It proves it is Patchwork with the presenter signature, and names the subject in the body:

```json theme={null}
POST /patchwork/mint
Patchwork-Signature: t=1756558800,v2=9ad30e71…

{ "subject": "user_9f2:ws_acme" }
```

Verify the signature ([Signing and verification](/guides/signing)), then mint for that subject:

```js theme={null}
function mintRelay(req, res) {
  if (!verifySignature(req)) return res.status(401).end();

  const subject = JSON.parse(req.rawBody).subject;
  if (!subject) return res.status(400).json({ error: "missing subject" });

  const token = signSessionToken(subject);
  // Bare object. No envelope.
  res.status(201).json({ token, expires_in: 120 });
}
```

<Warning>
  **The relay response must be a bare `{ token, expires_in }`.** Patchwork reads `token` at the top level. If you wrap it in your API's usual envelope — `{ "status": "success", "data": { "token": … } }` — minting fails with `mint response missing token` and the run dies partway through.

  `session_token` is accepted as an alias for `token`.
</Warning>

### Trust the subject, do not re-authorize it

The subject in the body is one Patchwork was already given — asserted by your own backend in relay, or verified from a token you signed in direct. You are not being asked whether this user exists; you are being asked to issue them a token.

What you *must* do is mint for **exactly** that subject and no other. Never widen it, and never fall back to a default subject when the string is unfamiliar.

## Direct: your browser asking

Your own frontend, minting for the signed-in user. There is no signature — authenticate it the way you authenticate any other endpoint in your product.

```js theme={null}
function mintDirect(req, res) {
  if (!req.user) return res.status(401).end();

  // Compose the subject from session state, never from the request body.
  const subject = `${req.user.id}:${req.session.workspaceId}`;
  const token = signSessionToken(subject);

  // Your own frontend is the consumer here, so use your normal response shape.
  res.status(201).json({ token, expires_in: 120, subject_ref: subject });
}
```

This branch may return whatever shape your frontend expects — Patchwork never reads it. Only the relay branch has a contract.

<Note>
  Patchwork's own integration does exactly this. Its direct branch composes the subject as `user_id:workspace_id`, the same [composite pattern](/guides/subjects#composite-subjects) we recommend, derived from the session rather than from the request.
</Note>

## What `expires_in` actually controls

Patchwork caches each minted token per `(mint_url, subject)` and reuses it, so a burst of tool calls mints once.

| You return | Patchwork caches for |
| - | - |
| `expires_in: 120` | 110 seconds — ten less, so an in-flight call never presents a just-expired token |
| A small value | At least 15 seconds |
| Nothing, or `0` | 60 seconds |

So `expires_in` is not decorative. If you return an hour, Patchwork holds that token for an hour, and revoking early has no effect on calls already in flight. Keep it short.

## Rotating your request secret

Verify the presenter signature against your **current and previous** secret, not just the current one:

```js theme={null}
const secrets = [process.env.REQUEST_SECRET, process.env.REQUEST_SECRET_PREV].filter(Boolean);
const ok = secrets.some(s => timingSafeEqual(hmac(s, payload), provided));
```

Without this, rotating breaks every mint in flight at the moment you cut over. The signature timestamp is also checked against a 300-second window, so reject anything outside it.

## The Playground needs the relay branch

This is the consequence people hit first, and it hits during testing rather than in production.

[Playground](/guides/playground) runs are trusted-internal — there is no browser and no session token on the request — so egress can only take the relay path. A connection using `minted` auth is testable in the Playground **only if** its mint URL handles the relay branch correctly.

The subject Patchwork sends is the persona's subject verbatim, so if your tools resolve a subject, the persona must name one your backend can find — see [The Playground](/guides/playground#use-a-subject-your-backend-knows).

Build only the direct branch and you get a working product with a Playground that fails inside a run:

```
minted egress needs a session token or a connection mint_url
```

Build the relay branch first. It is the one both paths depend on.

## Checklist

<ParamField path="Single endpoint" type="required">
  A connection declares one `mint_url`. Handle both callers there.
</ParamField>

<ParamField path="Branch on Patchwork-Signature" type="required">
  Present means relay. Absent means your own frontend.
</ParamField>

<ParamField path="Bare response on relay" type="required">
  `{ token, expires_in }` at the top level. No envelope.
</ParamField>

<ParamField path="Verify against two secrets" type="required">
  Current and previous, so rotation does not break minting.
</ParamField>

<ParamField path="Mint for the exact subject" type="required">
  Never widen it, never substitute a default.
</ParamField>

<ParamField path="Short expires_in" type="recommended">
  It sets Patchwork's cache lifetime, not just the token's.
</ParamField>


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