Assessments
Creating, shaping and reading an assessment.
An assessment is one candidate against one role. A resume_screen, an
ai_interview, a coding_test, a prompt_engineering assessment, a
human_interview and a
phone_screen are all the same resource to you.
Shaping the interview
Three things are yours to set, and none of them needs a round configured up front.
Length. duration_minutes, 5 to 120 for an ai_interview. This is a real lever, not a label: it
decides how long the interviewer stays on and how many questions it gets through.
Omit it and the round type's own default applies.
Shape. round_type picks the kind of conversation, for example
general_interview, hr_round or system_design. GET /v1/round-types lists
what your organisation can use and each one's default length.
Questions. questions replaces the generated set. On a standard interview,
the first becomes the opening question and the list is the pool the interviewer
draws from. Use ask_all_questions: true with hr_round or
general_interview when every supplied question must be asked in order.
The interviewer is handed part of that pool, picked at random, by length: up to 4
questions when the assessment runs 27 minutes or less, up to 6 up to 45 minutes,
and all of them beyond that. Every round type except hr_round, communication
and task adds a 10-minute introduction to that length first, so a
general_interview gets all of your questions from a duration_minutes of 36.
Without ask_all_questions, it paces them against the clock and rephrases them
conversationally, so it may not reach every one. A list of ten on a
fifteen-minute call is a pool, not a script.
For hr_round and general_interview, ask_all_questions: true makes the
supplied list the whole interview, with no separately generated questions.
Follow-ups on answers are still possible. Add strict_question_wording: true
when each primary question must be spoken as written. Live rounds accept at
most min(25, floor(duration_minutes / 2)) supplied questions; the five-minute
sandbox accepts at most four short questions. Create, get and list responses
echo these flags.
Conversations without interview framing
For client intake, candidate introductions or roleplay, create an ai_interview
with round_type: "conversation". This API-only shape is returned by
GET /v1/round-types but is hidden from recruiter creation dropdowns.
{
"type": "ai_interview",
"job_id": "<job-id>",
"candidate_id": "<candidate-id>",
"round_type": "conversation",
"duration_minutes": 15,
"questions": [
"Hi, could you tell me what you need help with?",
"What would a good outcome look like?"
],
"conversation": {
"agent_identity": "a client partner",
"scenario": "Understand the client's project brief",
"tone": "warm and concise",
"mode": "conversational"
}
}
agent_identity, scenario and mode are required for this shape; tone is
optional. Your first question opens the call. After the final answer, the call
ends without a built-in job interview greeting or role/team closing.
Choose mode: "strict" to ask every supplied prompt in order, including one
that an earlier answer may have covered. Add strict_question_wording: true
to a strict call when the primary prompts must be spoken exactly as written.
Choose "conversational" to allow a
short relevant follow-up and skip a later prompt only when the person directly
answered it already. The agent records the answer turn and a supporting quote
for a skip. The conversation settings are echoed on create and retrieve.
For this shape, conversation.mode controls the sequence; do not set a
contradictory ask_all_questions value.
The same supplied-question limits apply to both modes: at most
min(25, floor(duration_minutes / 2)) live, or four short questions in the
five-minute sandbox.
You can attach a CV before the participant joins. The conversation uses its contents when relevant. Without one, it does not assume or invent CV details.
On coding and task rounds, and system_design rounds with a whiteboard, your
questions do not stop generation: generated problems are added after your first
question, and the opening problem is picked from the combined list.
Omit questions and we generate them from the role. That generation runs
after the create returns, because it involves a web search and a model call.
If the candidate joins before generation lands, the interviewer starts without the round's questions. It looks the opening question up again when it reaches it, and uses the generated one if it has landed by then. The rest of the set is not picked up, so the interviewer writes its own follow-ups for that assessment.
The generated set is saved on the round and reused for every later assessment with
the same settings, so this affects only the first ones after a new configuration.
Passing questions removes the wait, because your opening question is there
from the moment the create returns.
Each job gets its own rounds, so two jobs never share a question set, whether it was supplied or generated. Within one job, assessments with the same settings share one. Editing a job does not regenerate a set that already exists.
Inviting the candidate
Set send_invite: true on the create and we email the candidate a link to the hosted
booking page, where they pick a time and then join. Off by default, so an integration
that already emails its own candidates does not start sending twice.
If you set the time yourself with POST /v1/assessments/{id}/schedule instead, the same
flag sends the candidate a booking confirmation with a calendar invite.
The mail goes from your own domain once a sending domain is verified in Settings, otherwise from ours, and carries your logo and brand colour. It is skipped in sandbox and when the candidate has no email address.
The booking page is hosted on your branded domain when you have one configured, so the candidate never sees ours. It is also the answer to "do we have to build a scheduler": usually not.
If you would rather run the whole candidate journey yourself, leave send_invite off and
use POST /v1/assessments/{id}/launch-link. That link skips booking entirely and drops
the candidate straight into the assessment.
Repeat creates are cheap
Behind the scenes each distinct configuration gets one interview round, reused for every candidate you send with the same settings. Changing the length, the shape or the questions makes a new one. You never see this, and you never pay for question generation twice for the same setup.
Giving the interviewer a CV
An ai_interview can take the candidate's CV, and the interviewer asks about what is in
it. It is the same two calls a resume screen uses, on the interview's own id:
# 1. Ask for a URL. PDF, DOC or DOCX.
curl -X POST https://hiring-api.experthire.cloud/v1/assessments/$ID/resume/upload-url \
-H "Authorization: Bearer $EH_KEY" \
-d '{"filename": "jane-doe.pdf"}'
# 2. PUT the file to the returned upload_url with the returned content_type,
# then hand back the key.
curl -X POST https://hiring-api.experthire.cloud/v1/assessments/$ID/resume \
-H "Authorization: Bearer $EH_KEY" \
-d '{"key": "resumes/<assessment-id>/a1b2c3d4e5.pdf"}'
Still a placeholder: $EH_KEY. Add it under Your values above.
The CV is read, not scored, and nothing is charged. Reading it takes tens of seconds,
so poll GET /v1/assessments/{id}/resume until status is ready. A failed result
carries an error_code, and you can post the same key again or upload a different file.
Attach it before the candidate joins. Once the interview has started this returns
assessment_started, because the interviewer keeps the CV it started with. Until then,
posting a new key replaces the CV.
In sandbox the file is read for real but kept out of your candidate pool.
Resume screens are not interviews
A resume_screen has no call, no length and no questions. It scores the
candidate's resume against the role. It comes back as the same assessment
resource with the same statuses, so one code path handles both.
Two things it does need that an interview does not.
The job needs skills. Coverage and the weighted score are computed against
the job's skill list, so a screen on a job without one is refused with
job_skills_required. Send them when you create the job:
{
"title": "Backend Engineer",
"skills": [
{ "name": "Go", "skill_type": "must_have", "weightage": 0.6 },
{ "name": "PostgreSQL", "skill_type": "must_have", "weightage": 0.4 },
{ "name": "Kubernetes", "skill_type": "good_to_have" }
]
}
You have to send us the resume. Creating the assessment does not upload one. It is two calls, because the file goes straight to storage and never passes through this API:
# 1. Ask for a URL. PDF, DOC or DOCX.
curl -X POST https://hiring-api.experthire.cloud/v1/assessments/$ID/resume/upload-url \
-H "Authorization: Bearer $EH_KEY" \
-d '{"filename": "jane-doe.pdf"}'
# 2. PUT the file to the returned upload_url with the returned content_type,
# then hand back the key.
curl -X POST https://hiring-api.experthire.cloud/v1/assessments/$ID/resume \
-H "Authorization: Bearer $EH_KEY" \
-d '{"key": "resumes/<assessment-id>/a1b2c3d4e5.pdf"}'
Still a placeholder: $EH_KEY. Add it under Your values above.
That returns 202 and scoring runs in the background. Poll
GET /v1/assessments/{id}/resume for processing, scored or failed. A
failed result carries an error_code and puts the assessment back to
scheduled, so you can upload a different file.
In sandbox this writes a fixed result straight away without running the scorer, so the report you parse has the same shape a live one does.
Status
A list response wraps the rows and carries an opaque cursor.
{
"object": "list",
"data": [
{
"object": "assessment",
"id": "3b8e1d02-5c77-4c2a-8a41-9b2f7e6d4c10",
"type": "ai_interview",
"status": "completed",
"job_id": "9c1f0b7e-2f4a-4a51-9a2e-6f0d5b3c1a88",
"candidate_id": "7e2a94c1-0f3b-4d88-b6a2-1c5e8f0a9d33",
"score": 68,
"livemode": true,
"created_at": 1767225601,
"updated_at": 1767229200
}
],
"has_more": true,
"next_cursor": "c_b2ZmXzIw",
"total_count": 143
}
| Status | Meaning |
|---|---|
pending | Created, not yet scheduled |
scheduled | Ready for the candidate |
in_progress | Under way |
processing | Finished, being scored |
completed | Scored. The report is available |
cancelled | Ended without a result |
shortlisted and rejected also appear. Those are recruiter decisions made in
the dashboard, not something the assessment does to itself.
Sandbox assessments
A sandbox assessment behaves like a live one right up to scoring. It is never
recorded, so it never reaches completed and never produces a report. It also
never appears in your recruiters' pipeline and never spends a credit.
Build and verify in sandbox. Use live when you need the result.
Next
- Scheduling: set when the candidate is expected
- Reports: read the scored result
Coding tests and prompt engineering
Both pick their content from a catalog rather than taking free-form questions. List what your organisation can use, then pass the id:
curl https://hiring-api.experthire.cloud/v1/coding-tests \
-H "Authorization: Bearer $EH_KEY"
curl -X POST https://hiring-api.experthire.cloud/v1/assessments \
-H "Authorization: Bearer $EH_KEY" \
-d '{"type": "coding_test", "job_id": "...", "candidate_id": "...",
"coding_test_id": "..."}'
Still a placeholder: $EH_KEY. Add it under Your values above.
prompt_engineering works the same way with
GET /v1/prompt-engineering-assessments and
prompt_engineering_assessment_id.
Neither takes round_type, duration_minutes or questions. The catalog entry
decides the shape and the time limit. Neither can be scheduled either, because
there is no call to attend: the candidate takes it whenever they open the link.
Reusing the same catalog id on the same role reuses one configuration. A different id makes a new one.
If the module is not enabled for your organisation, the create returns
module_disabled and nothing is charged.
What the candidate does
Both run in the browser through a launch link, and the room drives them for you. If you are building your own surface instead, the calls are:
| Call | Coding | Prompt engineering |
|---|---|---|
POST /v1/assessments/{id}/start | Problems and progress | Tasks, results and progress |
POST /v1/assessments/{id}/run | Runs against sample cases, costs no attempt | n/a |
POST /v1/assessments/{id}/submissions | Submits a solution, costs an attempt | n/a |
POST /v1/assessments/{id}/attempts | n/a | Submits a prompt, costs an attempt |
POST /v1/assessments/{id}/finalize | Scores and closes | Scores and closes |
These take the candidate's session token, not your secret key.
finalize returns judging_in_progress (409) while work is still being scored.
Retry until it returns 200; the assessment is not finished before that.