Skip to main content

Authentication

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

Expert Hire developer workflow for Authentication
Build against the same evidence trail the product uses.

Every request carries one of four credentials. Which one an endpoint expects is stated on each operation in the API reference.

CredentialWho holds itUsed for
Secret keyYour serverEverything server-side
Session tokenThe candidate's browserJoining and running an interview
Launch tokenThe candidate's emailRedeeming a hosted link, once
NoneAnyoneHealth, 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:

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