Skip to main content
The Zest Public API uses the OAuth 2.0 JWT-Bearer assertion grant (RFC 7523). Each call presents a short-lived bearer token; partners exchange a self-signed JWT to mint that token.

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

Successful response:

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

Successful response — same shape as the JWT-bearer grant:

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 returns 401 and invalidates the entire chain. In that case, mint a fresh JWT assertion and re-do the JWT-bearer flow.

Common pitfalls

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

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.
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.
JWT MUST be signed with EdDSA. RSA / HMAC assertions are rejected as a defence-in-depth measure.
Both claims are required and must match each other. They identify the partner application.

Inspecting your token

Returns metadata about the access token currently presented (client type, client id, application name).