Guias
Sign identity tokens from your backend
Copy the mint endpoint for Node, PHP, Python or Go, build the identical ES256 claim set, publish your JWKS, and prove it with the conformance checker.
Nesta página
Busymate AI needs exactly one endpoint on your API to recognize a signed-in customer: one that requires your own session and returns a freshly signed proof. Four ready-to-copy samples build the identical claim set in Node, PHP, Python and Go, so the language you already run is never a reason to hand-roll a JWT.
1. Pick your language, copy one file
Node (zero dependencies): https://busymate.ai/sdk/v1/kit/backend/node/mint.mjs. PHP (zero dependencies): https://busymate.ai/sdk/v1/kit/backend/php/mint.php. Python (cryptography): https://busymate.ai/sdk/v1/kit/backend/python/mint.py. Go (standard library): https://busymate.ai/sdk/v1/kit/backend/go/mint.go. Each builds the same response from the same claims; only the syntax differs.
2. The one shape every language builds
The Node sample shows the reference shape — the others build the identical claims in their own syntax:
import { SignJWT, importJWK } from "jose";
// POST https://YOUR-PRODUCT-DOMAIN/api/bmai/identity — requires YOUR OWN logged-in product session.
app.post("/api/bmai/identity", requireSession, async (req, res) => {
// Fresh on EVERY mint. Never persist the launch token/nonce in localStorage,
// sessionStorage, cookies, React state, or module state: every assistant
// launch consumes this pair exactly once.
const nonce = typeof req.body?.nonce === "string" ? req.body.nonce : "";
if (!/^[A-Za-z0-9_-]{32,200}$/.test(nonce)) return res.status(400).json({ error: "invalid_nonce" });
const key = await importJWK(JSON.parse(process.env.AI_LAUNCH_PRIVATE_JWK), "ES256");
const token = await new SignJWT({
tenant_id: process.env.BMAI_TENANT_ID, // the tenant id shown in Console → Identity
nonce, // equals the sibling field below
name: req.user.displayName, // optional low-sensitivity display claim
})
.setProtectedHeader({ alg: "ES256", kid: process.env.AI_LAUNCH_KEY_ID })
.setIssuer(process.env.BMAI_ISSUER) // the Issuer you registered in Console → Identity
.setAudience("busymate-ai")
.setSubject(req.user.id) // IMMUTABLE internal account id; never email/phone/session id
.setJti(crypto.randomUUID()) // one-time (replay-protected)
.setIssuedAt()
.setExpirationTime("120s") // <= registered max age (120s)
.sign(key);
res.set("Cache-Control", "no-store");
res.status(201).json({ token, nonce, expiresIn: 120 });
});See Recognize signed-in customers for the full claim table, the never-put-in-a-token list, and the key-rotation order. The short version: iss, aud, your workspace claim and sub (your unchanging internal id, never an email or session id) are required on every mint; nonce is echoed back verbatim; jti is one-time; exp - iat is at most 120 seconds.
3. The one thing a hand-rolled ES256 JWT gets wrong
All four samples do the ES256 DER → raw r‖s signature conversion explicitly. Skip it and a library that expects the raw form refuses every token your endpoint mints, and the refusal reads exactly like a wrong key rather than a wrong encoding — which is why every sample spells the conversion out instead of trusting a library default.
4. Publish the key, never the endpoint's private half
Publish the public key at /.well-known/jwks.json with a kid and a short cache lifetime. If the object about to be served carries a d field, stop: that is the private scalar, and none of the four samples will print it for you. Rotate by publishing the new key beside the old one, waiting past your cache lifetime, switching which kid signs, waiting one more token lifetime, then removing the old key — never the reverse order.
5. Prove the endpoint, not just the file
A copied file with a wrong claim, a missing Cache-Control: no-store, or a clock five minutes off from real time all look identical from the outside: a customer stuck as a guest. Run the CLI (node v2/scripts/identity-conformance.mjs --host <your-host> --json) against the live endpoint before wiring any platform to it — it checks the response your language actually produces, not the sample you started from.
Verify
POSTyour endpoint your own session cookie and a fresh nonce; it returns201with{ token, nonce, expiresIn }andCache-Control: no-store.- The same request signed out, or with no session, is refused — never a token for nobody.
- Two calls in a row return two different
jtivalues and two different signatures, never a cached pair. - Run the CLI against the endpoint; every claim, TTL, nonce-echo and clock-skew cell passes before any platform guide above is wired to it.
Next
- Recognize signed-in customers — the full claim contract and the key-rotation order.
- Fix a signed-in customer who shows as a guest — symptom-first fixes when a cell fails.
- In-app AI support for iOS and Android — the platforms that call this endpoint from a native bridge.
Perguntas
Why does the reference sample show Node when my API is in a different language?
The claim set and response shape are identical across all four; Node is shown once as the shape, and the PHP, Python and Go files build the same result in their own syntax.
What happens if I skip the DER-to-raw conversion?
Most ES256 verifiers, including the one this platform runs, refuse the token outright. It reads as a rejected key, not as an encoding mistake, which is why the samples handle it for you.
Can I sign with more than one algorithm?
Yes, if you register more than one in your provider setup; every mint must use an algorithm in that registered list, checked against your published JWKS.
Do I need a separate signing key per platform I integrate?
No. One key pair, one JWKS entry, and every platform's guide above calls the same mint endpoint your key signs.