Skip to main content
One developer key, two calls to log in, one header to act as a client.

Log in

Two calls, two signatures, no browser. POST /dapi/v1/auth/challenge proves you hold the key. Sign the proof string hevn-developer-key-login:{email}:{nonce}:{requestExpiry} with your developer key and send it with the public key you registered. nonce is any integer you have not used before; requestExpiry is a millisecond timestamp at most five minutes in the future.
The answer is a payload HEVN built for you:
POST /dapi/v1/auth/token exchanges your signature over that payload for a session. Sign the decoded bytes of payload, exactly as at payment time — Signing is the same mechanism.
Response
The challenge is single-use and dies with its expiresAt: two /auth/token calls on one challengeId answer 409 challenge_consumed. userId is your own account’s id, and you can use it wherever a client id is accepted for your own account.

Two tokens, two jobs

Both carry a developer_key_id claim naming the key registration that established the session, and the two audiences do not cross. /dapi/v1 refuses anything that is not a platform access token carrying that claim: no bearer at all is 401 unauthenticated, and an ordinary app token, a refresh token on a business route or a token with no registration behind it is 403 forbidden with details.reason = "developerSessionRequired". The ordinary /api/v1 refuses a platform token symmetrically, which is why the routes that register a developer key are not on this API — see Developer key. Re-mint with the refresh token as the bearer and an empty body:
Response
This is the one call that does not go through the client: it carries the refresh token, not the access token. The reference client wraps it in a Session so the request layer never thinks about expiry. Log in once per process, keep the refresh token in your secret store, and re-mint the access token about a minute before it expires. A refresh token is not an API credential: sent to any other route it is refused.

Acting as a client

Send X-Hevn-Account: cl_… alongside your own access token, and the call reads and writes that client. On most routes you may omit it, and the same call acts on your integrator account; on the singular /client* routes the header is required, so reading your own account there means sending your own cl_…. One token covers every client you control, so there is nothing to cache per client.
No path names a client any more — there is no {clientId} parameter left in the API — so the header is the only thing that selects an account, and each route treats it one of four ways: Sending the header where it is refused is an error rather than a silent no-op, so you find out in development:
POST /dapi/v1/auth/refresh also accepts {"userId": "cl_…"} and mints an access token scoped to that one client. It still works, and it is no longer the documented path: it costs one token mint per client per hour and buys nothing the header does not give you.

What a session is bound to

  • An environment. Sandbox tokens are refused in production and the reverse.
  • The key registration behind it, on every single request. The developer_key_id in the token is re-read before the request is served: the registration must still be active, and the call must come from an address inside its immutable allowlist. Pausing a registration therefore does cut the tokens already minted from it — the next call is a 403, and so is the refresh that would have replaced it. See Developer key.
  • A set of source addresses. A deploy from a new egress IP fails every call with 403 even though the key, the signature and the token are all correct.
  • A budget. Reads and writes are limited per credential, and login has its own per-IP and per-email windows — over them is 429 with Retry-After. The numbers are in Limits. Logging in on a loop is the fastest way to lock yourself out; log in once and re-mint.

Next: Conventions

The seven rules that hold for every operation: money, ids, casing, idempotency, pagination, status codes, errors.