Example: send an interview from an ATS
The whole flow an applicant tracking system needs, as real requests: list roles, invite a candidate, poll the interview, and read the decision.
The flow#
A recruiter picks an EasyEnv role for a job. When a candidate reaches that stage, your system invites them. EasyEnv emails the candidate, they take the interview on a real machine, and your team reviews it in EasyEnv. Your system follows along and shows the outcome on the candidate.
The examples use these variables:
API=https://api.easyenv.io/v1
EASYENV_TOKEN=... # see Authentication
EASYENV_ACCOUNT=... # the account's uuid, from GET /v1/accounts/1. List roles#
A role is what a candidate is interviewed on. Offer these to the recruiter:
curl "$API/interviews/?page_size=200&sort=a-z" \
-H "X-Service-Token: $EASYENV_TOKEN" \
-H "Account-ID: $EASYENV_ACCOUNT"{
"count": 2,
"total_pages": 1,
"links": { "next": null, "previous": null },
"results": [
{ "uuid": "r7p2k9xa", "title": "Backend Engineer", "duration": 60, "duration_unit": "minutes", ... },
{ "uuid": "m4t8w1qe", "title": "SRE", "duration": 90, "duration_unit": "minutes", ... }
]
}Archived roles are left out. Keep the uuid of the one the recruiter picks.
2. Invite the candidate#
Create an offline interview on the role. The body is form fields, not JSON:
curl -X POST "$API/interviews/r7p2k9xa/interview-items/" \
-H "X-Service-Token: $EASYENV_TOKEN" \
-H "Account-ID: $EASYENV_ACCOUNT" \
-F type=offline \
-F [email protected] \
-F interviewee_name="Jane Doe" \
-F start_date=2026-09-18 \
-F end_date=2026-09-25 \
-F timezone=Europe/London| Field | Type | Description |
|---|---|---|
| interviewee_emailrequired | string | Where the invitation goes. It cannot be a member of the account. |
| typerequired | string | offline: the candidate takes it on their own, inside the window. Leave it out and you get a live interview instead. |
| start_date / end_date | YYYY-MM-DD | The window the candidate can start in. Set both: an offline interview without an end date never expires. |
| timezone | IANA name | The time zone the window is read in. Defaults to UTC. |
| interviewee_name | string | Shown to reviewers and used in the email. |
| title | string | A name for this interview in the dashboard. |
| candidate_resume | PDF file | Up to 5 MB, for reviewers. |
A 201 means the invitation email is on its way:
{
"uuid": "q9z3v6nd",
"type": "offline",
"candidate_email": "[email protected]",
"interviewee_name": "Jane Doe",
"start_date": "2026-09-18",
"end_date": "2026-09-25",
"timezone": "Europe/London",
...
}Store uuid against the candidate: it is the interview you will poll. Each call creates a new interview and emails the candidate, so if a request times out, check the role's interviews before you retry, or they get two invitations. The plan spends an interview only when the candidate starts.
3. Follow the interview#
curl "$API/interviews/r7p2k9xa/interview-items/q9z3v6nd/" \
-H "X-Service-Token: $EASYENV_TOKEN" \
-H "Account-ID: $EASYENV_ACCOUNT"The answer has two shapes. Until the candidate starts, it describes the invitation. Once they start, it describes their sitting, and it gains an attempt_uuid field; that is how you tell them apart.
// Not started yet
{ "uuid": "q9z3v6nd", "status": "invited", "end_date": "2026-09-25", ... }
// Started
{
"uuid": "q9z3v6nd",
"attempt_uuid": "h2c8j5rt",
"status": "needs_review",
"review_status": null,
"started_at": "2026-09-19T09:02:11Z",
"ended_at": "2026-09-19T10:01:40Z",
"time_spent": 3569.0,
...
}Statuses#
Before the candidate starts (no attempt_uuid):
| Field | Type | Description |
|---|---|---|
| invited | Emailed, not started. | |
| ready | The candidate has an EasyEnv sign-in and can start. Treat it like invited. | |
| started | The candidate has started. This endpoint switches to the started shape at that moment, so you rarely see this value here. | |
| expired | The window closed and the candidate never started. Expiry is applied once a day at 00:00 UTC, so it can show up to a day late. |
After the candidate starts (attempt_uuid present):
| Field | Type | Description |
|---|---|---|
| in_progress | The candidate is taking the interview. | |
| needs_review | Submitted. Waiting for someone on the team to review it. | |
| review_completed | Reviewed. review_status holds the decision. | |
| not_completed | The sitting ended without a submission, for example it ran out of time. |
Most ATSs map these to four stages: invited (invited, ready), started (started, in_progress), submitted (needs_review) and completed (review_completed), with expired and not_completed as cancelled.
4. Read the decision#
When status is review_completed, review_status is accepted or rejected. That is a person's decision on the review page, not an automatic score, and it is the result to show on the candidate. Until someone decides, it is null.
result field, but it is reserved and is null today; do not build on it.Link to the review#
Give recruiters a link from the candidate in your system to the full review in EasyEnv. It opens for anyone signed in to EasyEnv with access to the account:
https://dashboard.easyenv.io/dashboard/{account uuid}/feedback/{role uuid}/{interview uuid}Built something with this, or need a call that is not here? Write to [email protected].
