Authentication — mint an access token
Every API call carries a short-lived JWT. You obtain it by signing a fresh payload with your private key and exchanging it at POST /auth-apps/access-token.
Signing scheme
The scheme must match exactly, or verification fails:
- Payload:
{ publicKey, nonce, timestamp } - Canonicalize with the RFC 8785 JSON Canonicalization Scheme (
json-canonicalize) - Sign the UTF-8 bytes with Ed25519 (
nacl.sign.detached) - Encode the signature as base64
import * as nacl from 'tweetnacl';
import * as util from 'tweetnacl-util';
import { canonicalize } from 'json-canonicalize';
import { randomUUID } from 'crypto';
function buildAccessTokenRequest(secretKeyBase64, publicKeyBase64) {
const payload = {
publicKey: publicKeyBase64,
nonce: randomUUID(), // unique per request
timestamp: Math.floor(Date.now() / 1000), // UNIX seconds
};
const payloadCanonical = canonicalize(payload);
const signature = util.encodeBase64(
nacl.sign.detached(util.decodeUTF8(payloadCanonical), util.decodeBase64(secretKeyBase64)),
);
return { signature, payload, payloadCanonical };
}
Request
POST /auth-apps/access-token
{
"signature": "<base64 signature>",
"payload": {
"publicKey": "<base64>",
"nonce": "unique_nonce_12345",
"timestamp": 1625247600
},
"payloadCanonical": "{\"nonce\":\"unique_nonce_12345\",\"publicKey\":\"<base64>\",\"timestamp\":1625247600}"
}
:::tip Send payloadCanonical
Include the exact canonical string you signed. Yumi verifies against it directly, removing any risk of a canonicalization mismatch between your library and ours.
:::
Response
{ "success": true, "data": { "accessToken": "eyJhbGci...", "expires": 1625251200 } }
Rules
| Rule | Failure |
|---|---|
timestamp must be recent | SIGNATURE_EXPIRED |
nonce must be unique per request | SIGNATURE_REUSED |
| Signature must verify against your registered public key | INVALID_APPLICATION_SIGNATURE |
Cache the token until expires, then sign a new request. Send it on every call:
Authorization: Bearer <accessToken>
Next: Sending user data →