Interviews
The lifecycle, from creation to a scored report.

Lifecycle
create ──> Scheduled ──> In Progress ──> Processing ──> Completed
│ │
└──────────> Cancelled report
| Status | Meaning |
|---|---|
Scheduled | Created, nobody has joined |
In Progress | Candidate is in the room now |
Processing | Interview ended, scoring is running |
Completed | Report is ready |
Cancelled | Cancelled, by you or by us |
Creating
You can pass an existing candidate_id, or create the candidate inline:
{
"candidate": { "email": "[email protected]", "name": "Ada Lovelace" },
"round_type": "general_interview",
"difficulty": 3,
"role": "Backend Engineer"
}
difficulty runs 1 to 5. role drives the questions, so make it the real job
title rather than an internal code.
An email that already belongs to an Expert Hire account is rejected with 400.
That is deliberate: adopting it would attach your interview to somebody's
personal prep history.
Candidates
Persist the candidate_id you get back. If you lose it, look the person up by
email rather than creating a duplicate:
curl "https://prep-api.experthire.cloud/v1/[email protected]" \
-H "Authorization: Bearer $EH_SECRET_KEY"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
GET /v1/candidates also pages the whole list, newest first. PATCH corrects a
typo in a name or email.
Deleting is erasure, and it is not reversible:
curl -X DELETE https://prep-api.experthire.cloud/v1/candidates/$CANDIDATE_ID \
-H "Authorization: Bearer $EH_SECRET_KEY"
Still a placeholder: $CANDIDATE_ID, $EH_SECRET_KEY. Add them under Your values above.
The person is de-identified rather than dropped. Name, email, phone and every profile field are overwritten; interviews and reports keep their candidate id, so your own counts and averages still reconcile, but the record can no longer be tied to a person.
PATCH and DELETE only work on a candidate you created through the API. One
who has since signed in to Expert Hire owns their own profile and deletion, and
returns candidate_not_editable. Rewriting their email would otherwise hand
someone else their sign-in.
Listing
curl -G https://prep-api.experthire.cloud/v1/interviews \
-H "Authorization: Bearer $EH_SECRET_KEY" \
--data-urlencode "limit=50"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
{
"object": "list",
"data": [{ "object": "interview", "id": "…", "status": "Completed" }],
"has_more": true,
"next_cursor": "b9ec0ced-…"
}
Newest first. Pass status to filter. Page by setting starting_after to
next_cursor, which is the last id on the page you just read:
curl -G https://prep-api.experthire.cloud/v1/interviews \
-H "Authorization: Bearer $EH_SECRET_KEY" \
--data-urlencode "starting_after=$NEXT_CURSOR"
Still a placeholder: $EH_SECRET_KEY, $NEXT_CURSOR. Add them under Your values above.
The cursor is a position in the list, not a page number. That matters if you poll: with an offset, an interview created between two requests shifts everything down a row and the next page silently repeats or skips one. Paging from the last id you saw cannot.
Listing is not how you find out an interview finished. Poll it and you will
either hammer us or miss the transition. Register a webhook and listen for
interview.completed, then fetch the report.
Scheduling into capacity
Interviews run against a shared pool, so a slot can fill. Ask before you commit a time rather than discovering it when the candidate tries to join:
curl -G https://prep-api.experthire.cloud/v1/interviews/slots \
-H "Authorization: Bearer $EH_SECRET_KEY" \
--data-urlencode "from=$(date +%s)"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
{ "object": "list", "data": [{ "start_time": 1786351800, "available": 500 }] }
One entry per half hour. from and to are unix seconds, the default window is
the next 24 hours, and the maximum is 14 days. Pass candidate_id and slots
that candidate already holds are left out.
Attaching a resume
Resumes make the interview specific rather than generic. Upload, register and
score it first, then pass resume_id when you create the interview. The
interviewer reads the parsed resume and asks about what is in it.
The resume has to belong to the same candidate. One that belongs to another
candidate is rejected with 404 resume_not_found, so a mismatched id fails
rather than quietly interviewing against somebody else's history.
Uploading a file is not enough on its own: the resume has to be registered before anything can use it. The full flow is in resumes.
Getting the candidate in
Three ways, covered in frontend integration:
- Hosted link. We host the room, you email a URL. Least work.
- Embedded. The room runs inside your page. No redirect for the candidate.
- Headless. You build the room. Most control, and you give up the most.
The billable moment
A credit is reserved when you create an interview and committed when the candidate joins. Joining is what dispatches the AI interviewer and starts recording, so it is the point where the interview genuinely costs something.
Cancelling an interview nobody joined returns the credit:
curl -X POST https://prep-api.experthire.cloud/v1/interviews/$ID/cancel \
-H "Authorization: Bearer $EH_SECRET_KEY"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
Cancelling one that already started does not. Creating interviews speculatively is safe; letting candidates join them is what you pay for.
Round types
round_type selects the interview style. The catalogue is per-org and some are
module-gated, so read yours rather than guessing:
curl https://prep-api.experthire.cloud/v1/round-types \
-H "Authorization: Bearer $EH_SECRET_KEY"
Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.
Each entry carries supports_code_editor and supports_whiteboard.
GET /v1/companies, /v1/jobs and /v1/coding-languages are the other lookups
worth reading at boot.
Commonly available: general_interview, coding, hr_round, system_design,
communication, problem_solving_or_aptitude, vc_pitch.
There is no technical round, and no general (it is general_interview).
daily_english is not bookable over the API: it is the prep student app's own
daily drill, so GET /v1/usage may report the module while the round type is
refused. An unknown slug is rejected with 400
invalid_round_type, and the error message lists the slugs your organisation
has, so you never have to guess.
Whether a round offers a code editor or whiteboard comes back on the join grant
as supports_code_editor and supports_whiteboard, so you do not have to
hardcode a mapping that will drift.