Authentication
Secret keys, browser sessions, and how to rotate without downtime.

Every request carries one of four credentials. Which one an endpoint expects is stated on each operation in the API reference.
| Credential | Who holds it | Used for |
|---|---|---|
| Secret key | Your server | Everything server-side |
| Session token | The candidate's browser | Joining and running an interview |
| Launch token | The candidate's email | Redeeming a hosted link, once |
| None | Anyone | Health, JWKS, the spec |
Secret keys
Send the key as a bearer token. X-API-KEY also works if that suits your HTTP
client better.
curl https://prep-api.experthire.cloud/v1/usage \
-H "Authorization: Bearer ehp_sk_live_..."
The prefix tells you what you are holding:
| Prefix | Meaning |
|---|---|
ehp_sk_live_ | Secret, live. Spends real credits. |
ehp_sk_test_ | Secret, sandbox. |
Keys are stored hashed. We cannot show you a key again after it is issued, and we cannot recover one you have lost. Store it in your secret manager, not your repo.
A secret key must never reach a browser, a mobile app, or anything else you do not control. It can read every report in your organisation. If one leaks, ask us to rotate it immediately.
Browser sessions
Browsers get a session token instead: short lived, scoped to one interview, and bound to one origin.
curl -X POST https://prep-api.experthire.cloud/v1/sessions \
-H "Authorization: Bearer $EH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": "https://app.yourcompany.com",
"interview_id": "1c9d6b3a-77aa-4e21-8b0e-6b2f9c4d1a55"
}'
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
{
"object": "session",
"session_token": "eyJhbGciOiJFZERTQSIsImtpZCI6...",
"expires_at": 1770000900,
"refresh_after": 1770000720
}
Three things about this are load-bearing:
The origin must be allowlisted. We match it exactly, on scheme, host and
port. There are no wildcards, so https://app.example.com and
https://app.example.com:8443 are separate entries. Manage the list yourself:
curl -X POST https://prep-api.experthire.cloud/v1/domains \
-H "Authorization: Bearer $EH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"origin":"https://app.example.com"}'
curl https://prep-api.experthire.cloud/v1/domains \
-H "Authorization: Bearer $EH_SECRET_KEY"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
https is required except on localhost. Repeating a call returns the existing
entry rather than failing, so setup scripts are safe to re-run, and every entry
records which key added it.
The origin is re-checked on every request, not only when the token is minted. A token lifted from one page cannot be replayed from another site.
Always pass interview_id. It binds the session to one interview, so a token
that escapes cannot be used to reach a different candidate's session.
Refreshing
Sessions last 15 minutes. Interviews last longer. Refresh at refresh_after,
not at expires_at:
curl -X POST https://prep-api.experthire.cloud/v1/sessions/refresh \
-H "Authorization: Bearer $SESSION_TOKEN"
Still a placeholder: $SESSION_TOKEN. Add it under Your values above.
A session that is never refreshed expires mid-interview, and the candidate is
dropped from a call they cannot rejoin. This is the most common integration bug
we see. If you use @experthireai/room, refreshing is handled
for you.
Verifying our tokens
Session tokens are Ed25519 JWTs. If you want to verify one yourself, the public
keys are at GET /v1/.well-known/jwks.json. Every key that can still appear on a
live token is listed, including one just rotated out, so cache the set and
re-fetch on an unknown kid rather than pinning a single key.
Rotation
Ask us to rotate and we issue the new key immediately, with the old one staying valid for a grace period you choose, up to seven days. Deploy the new key during that window and nothing drops. A grace of zero revokes on the spot, which is what you want if a key has leaked.