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.
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
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
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
SendX-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.
{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_idin 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 a403, 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
403even 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
429withRetry-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.