Flow
1
Build a JWT assertion
Sign a JWT with the EdDSA private key registered with Zest. Claims are listed below.
2
POST to /v1/oauth2/tokens
Body:
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<JWT>. Response: { accessToken, refreshToken, tokenType, expiresIn, refreshExpiresIn }.3
Send the access token
Add
Authorization: Bearer <accessToken> to every subsequent request.4
Refresh before expiry
Default lifetime is 1 hour. Cache and reuse the access token; mint a new JWT only when the previous access token is expiring.
Required JWT claims
The JWT MUST be signed with EdDSA. Other algorithms are rejected.
Token exchange request
Refresh the access token
Each token exchange returns a long-lived refresh token (refreshToken, default lifetime 30 days). When your access token is close to expiring, redeem the refresh token for a fresh pair without re-doing the JWT assertion dance.
Refresh request
Rotation semantics
The refresh token rotates on every use. As soon as we hand you a new pair, the previous refresh token stops working — store the new one immediately and discard the old one. If you ever try to redeem the old refresh token again (for example, after a crash that lost the new one), the call returns401 and invalidates the entire chain. In that case, mint a fresh JWT assertion and re-do the JWT-bearer flow.
Common pitfalls
401 on a refresh that worked moments ago
401 on a refresh that worked moments ago
The chain has been invalidated — usually because the previous refresh token was redeemed twice. Re-do the JWT-bearer flow to start a new chain.
When to refresh
When to refresh
Refresh proactively — for example, at half of
expiresIn elapsed, or on the first 401 from an API call. Don’t wait until exactly iat + expiresIn; clock skew and request latency can cause edge-of-window failures.Code samples
Python
Node.js
Common pitfalls
invalid_token / 401 — clock skew
invalid_token / 401 — clock skew
Zest tolerates only a few seconds of skew between your clock and ours. If your server clock drifts (common on bare-metal lab machines or some Docker hosts), JWT
iat / exp checks fail. Fix by enabling NTP / chrony.invalid_token / 401 — audience mismatch
invalid_token / 401 — audience mismatch
The
aud claim MUST exactly equal the base URL of the environment you’re calling. Sandbox tokens used against production (or vice versa) are rejected.invalid_token / 401 — algorithm mismatch
invalid_token / 401 — algorithm mismatch
JWT MUST be signed with EdDSA. RSA / HMAC assertions are rejected as a defence-in-depth measure.
invalid_token / 401 — sub != client_id
invalid_token / 401 — sub != client_id
Both claims are required and must match each other. They identify the partner application.