Endpoint for scheduling or cancelling interviews for candidates. This endpoint allows the creation of interview appointments for a specific applicant and job. If the API is called again for the same candidate and job combination, the existing interview details will be updated with the newly provided information, effectively acting as an update operation. It cancels the latest created upcoming interview when the cancel flag is set to true. When cancelling, all other fields in the request body are ignored.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
This endpoint schedules, reschedules or cancels an interview for a job application.
The applicant_id in the path is the job application's id: the id of an entry in the /applicants response. It is not the entry's applicant_id, which identifies the person. The job is the one that application belongs to, so you don't send a job id.
For the full workflow around job applications (finding them, updating their status, messaging applicants and scheduling interviews), see the Applicant & Status Management guide.
Examples
The examples read your API key from an environment variable named APPLOI_API_KEY. Before running them, set APPLOI_API_KEY in your own environment to the Partner API key you received from Apploi. The examples do not set it for you, and the key should never be written into your code.
Schedule an interview
Every request needs date, duration and timezone, plus an interviewer. You can name the interviewer in either of two ways:
interviewerTeamIdandinterviewerEmail: the id of the interviewer's team and their email address, as it appears in Apploi. The examples use this form.interviewerId: the interviewer's Apploi user id, if you have it. Apploi support can give it to you.
location is optional. See Writing the request body for the exact format of each field.
The examples define a save_interview function (saveInterview in TypeScript), which the later examples reuse. It sorts each response into the outcomes described under What the response means.
# APPLOI_API_KEY must hold the Partner API key you received from Apploi.
# These examples do not set it for you: set it yourself, in your own
# environment, before running them. For example, in your shell:
# export APPLOI_API_KEY="<the API key you received from Apploi>"
curl -sS -X PUT https://partners.apploi.com/applicants/265733753/interviews \
-H "X-Api-Key: $APPLOI_API_KEY" \
-H "Content-Type: application/json" \
-w "\nHTTP status: %{http_code}\n" \
-d '{
"date": "2026-11-03T14:00:00Z",
"duration": "45",
"timezone": "America/New_York",
"location": "Virtual",
"interviewerTeamId": "345678",
"interviewerEmail": "[email protected]"
}'import os
import requests
# APPLOI_API_KEY must hold the Partner API key you received from Apploi.
# These examples do not set it for you: set it yourself, in your own
# environment, before running them. For example, in your shell:
# export APPLOI_API_KEY="<the API key you received from Apploi>"
API_KEY = os.environ["APPLOI_API_KEY"]
def save_interview(application_id, body):
response = requests.put(
f"https://partners.apploi.com/applicants/{application_id}/interviews",
headers={"X-Api-Key": API_KEY},
json=body,
timeout=60,
)
if response.status_code == 200:
try:
return "saved", response.json()
except ValueError:
# Some server errors currently arrive as a 200 whose body isn't valid JSON.
# See "A 200 whose body isn't valid JSON" below.
return "unconfirmed", response.text
if response.status_code == 404:
# The application doesn't exist or, when cancelling, it has no upcoming interview.
return "not_found", response.text
# 400: fix the request before sending it again. 403: the application isn't on your
# teams. 5xx: retry with backoff.
raise RuntimeError(f"Request failed with status {response.status_code}: {response.text}")
outcome, result = save_interview(265733753, {
"date": "2026-11-03T14:00:00Z", # 2:00 PM in "timezone", not UTC
"duration": "45", # minutes, as a string
"timezone": "America/New_York",
"location": "Virtual",
"interviewerTeamId": "345678", # a string, not a number
"interviewerEmail": "[email protected]",
})
print(outcome, result)// APPLOI_API_KEY must hold the Partner API key you received from Apploi.
// These examples do not set it for you: set it yourself, in your own
// environment, before running them. For example, in your shell:
// export APPLOI_API_KEY="<the API key you received from Apploi>"
if (!process.env.APPLOI_API_KEY) {
throw new Error("Set APPLOI_API_KEY to the Partner API key you received from Apploi");
}
const API_KEY: string = process.env.APPLOI_API_KEY;
type Outcome = "saved" | "unconfirmed" | "not_found";
async function saveInterview(
applicationId: number,
body: object,
): Promise<{ outcome: Outcome; result: unknown }> {
const response = await fetch(
`https://partners.apploi.com/applicants/${applicationId}/interviews`,
{
method: "PUT",
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
if (response.status === 200) {
const text = await response.text();
try {
return { outcome: "saved", result: JSON.parse(text) };
} catch {
// Some server errors currently arrive as a 200 whose body isn't valid JSON.
// See "A 200 whose body isn't valid JSON" below.
return { outcome: "unconfirmed", result: text };
}
}
if (response.status === 404) {
// The application doesn't exist or, when cancelling, it has no upcoming interview.
return { outcome: "not_found", result: await response.text() };
}
// 400: fix the request before sending it again. 403: the application isn't on your
// teams. 5xx: retry with backoff.
throw new Error(`Request failed with status ${response.status}: ${await response.text()}`);
}
const { outcome, result } = await saveInterview(265733753, {
date: "2026-11-03T14:00:00Z", // 2:00 PM in "timezone", not UTC
duration: "45", // minutes, as a string
timezone: "America/New_York",
location: "Virtual",
interviewerTeamId: "345678", // a string, not a number
interviewerEmail: "[email protected]",
});
console.log(outcome, result);require "json"
require "net/http"
require "uri"
# APPLOI_API_KEY must hold the Partner API key you received from Apploi.
# These examples do not set it for you: set it yourself, in your own
# environment, before running them. For example, in your shell:
# export APPLOI_API_KEY="<the API key you received from Apploi>"
API_KEY = ENV.fetch("APPLOI_API_KEY")
def save_interview(application_id, body)
uri = URI("https://partners.apploi.com/applicants/#{application_id}/interviews")
request = Net::HTTP::Put.new(uri)
request["X-Api-Key"] = API_KEY
request["Content-Type"] = "application/json"
request.body = JSON.generate(body)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60) do |http|
http.request(request)
end
case response.code
when "200"
begin
[:saved, JSON.parse(response.body)]
rescue JSON::ParserError
# Some server errors currently arrive as a 200 whose body isn't valid JSON.
# See "A 200 whose body isn't valid JSON" below.
[:unconfirmed, response.body]
end
# The application doesn't exist or, when cancelling, it has no upcoming interview.
when "404" then [:not_found, response.body]
# 400: fix the request before sending it again. 403: the application isn't on your
# teams. 5xx: retry with backoff.
else raise "Request failed with status #{response.code}: #{response.body}"
end
end
outcome, result = save_interview(
265733753,
date: "2026-11-03T14:00:00Z", # 2:00 PM in "timezone", not UTC
duration: "45", # minutes, as a string
timezone: "America/New_York",
location: "Virtual",
interviewerTeamId: "345678", # a string, not a number
interviewerEmail: "[email protected]"
)
puts outcome, resultReschedule an interview
To change an interview, send the same request for the same application with the new details. Send the whole body again, not only the fields that changed. If the application's latest interview is still upcoming, it is updated, not duplicated.
If the latest interview has already taken place, the request schedules a new interview instead, and the past interview stays in the applicant's history. This is how you book a second-round interview.
These examples reuse save_interview (Python and Ruby) and saveInterview (TypeScript) from the first example. They move the interview to 10:30 AM on 5 November and shorten it to 30 minutes.
curl -sS -X PUT https://partners.apploi.com/applicants/265733753/interviews \
-H "X-Api-Key: $APPLOI_API_KEY" \
-H "Content-Type: application/json" \
-w "\nHTTP status: %{http_code}\n" \
-d '{
"date": "2026-11-05T10:30:00Z",
"duration": "30",
"timezone": "America/New_York",
"location": "Virtual",
"interviewerTeamId": "345678",
"interviewerEmail": "[email protected]"
}'outcome, _result = save_interview(265733753, {
"date": "2026-11-05T10:30:00Z",
"duration": "30",
"timezone": "America/New_York",
"location": "Virtual",
"interviewerTeamId": "345678",
"interviewerEmail": "[email protected]",
})
print(outcome)const { outcome: rescheduled } = await saveInterview(265733753, {
date: "2026-11-05T10:30:00Z",
duration: "30",
timezone: "America/New_York",
location: "Virtual",
interviewerTeamId: "345678",
interviewerEmail: "[email protected]",
});
console.log(rescheduled);outcome, _result = save_interview(
265733753,
date: "2026-11-05T10:30:00Z",
duration: "30",
timezone: "America/New_York",
location: "Virtual",
interviewerTeamId: "345678",
interviewerEmail: "[email protected]"
)
puts outcomeCancel an interview
Set cancel to true to cancel the application's latest upcoming interview. Apploi ignores the other fields' values when cancelling, but the request is still checked before that happens. You must still send date, duration, timezone and an interviewer, in the same formats as when scheduling. A cancel request without them returns 400. Resending the interview's current details is the simplest option.
A cancel returns 404 when the application has no upcoming interview, because none was booked, it has already taken place, or it was already cancelled. That's also what a repeated cancel returns, so a 404 after a retry means the first cancel worked. The examples use this to confirm a cancel whose response they can't rely on (see A 200 whose body isn't valid JSON).
curl -sS -X PUT https://partners.apploi.com/applicants/265733753/interviews \
-H "X-Api-Key: $APPLOI_API_KEY" \
-H "Content-Type: application/json" \
-w "\nHTTP status: %{http_code}\n" \
-d '{
"cancel": true,
"date": "2026-11-05T10:30:00Z",
"duration": "30",
"timezone": "America/New_York",
"interviewerTeamId": "345678",
"interviewerEmail": "[email protected]"
}'cancel_body = {
"cancel": True,
# Ignored when cancelling, but still required
"date": "2026-11-05T10:30:00Z",
"duration": "30",
"timezone": "America/New_York",
"interviewerTeamId": "345678",
"interviewerEmail": "[email protected]",
}
outcome, _result = save_interview(265733753, cancel_body)
if outcome == "unconfirmed":
# Send the cancel again: a 404 now means no upcoming interview is left.
outcome, _result = save_interview(265733753, cancel_body)
if outcome == "not_found":
print("No upcoming interview is left on this application.")const cancelBody = {
cancel: true,
// Ignored when cancelling, but still required
date: "2026-11-05T10:30:00Z",
duration: "30",
timezone: "America/New_York",
interviewerTeamId: "345678",
interviewerEmail: "[email protected]",
};
let { outcome: cancelled } = await saveInterview(265733753, cancelBody);
if (cancelled === "unconfirmed") {
// Send the cancel again: a 404 now means no upcoming interview is left.
({ outcome: cancelled } = await saveInterview(265733753, cancelBody));
}
if (cancelled === "not_found") {
console.log("No upcoming interview is left on this application.");
}cancel_body = {
cancel: true,
# Ignored when cancelling, but still required
date: "2026-11-05T10:30:00Z",
duration: "30",
timezone: "America/New_York",
interviewerTeamId: "345678",
interviewerEmail: "[email protected]"
}
outcome, _result = save_interview(265733753, cancel_body)
# Send the cancel again: a 404 now means no upcoming interview is left.
outcome, _result = save_interview(265733753, cancel_body) if outcome == :unconfirmed
puts "No upcoming interview is left on this application." if outcome == :not_foundWriting the request body
Most rejected requests come from the field formats below. Send every value as a JSON string, even the numbers. Only cancel is a boolean.
| Field | Required | Format | Example |
|---|---|---|---|
date | Yes | ISO 8601 date and time, ending in Z. See the note below the table. | "2026-11-03T14:00:00Z" |
duration | Yes | Length in minutes, as a string | "45" |
timezone | Yes | An IANA time zone name | "America/New_York" |
interviewerTeamId and interviewerEmail | Either these two, or interviewerId | The interviewer's team id, as a string, and their email address in Apploi. The email must belong to a member of that team. | "345678", "[email protected]" |
interviewerId | The interviewer's Apploi user id, as a string | "123456" | |
location | No | Any text, such as an address, "Virtual" or a meeting link, but without double quotes ("), backslashes (\) or line breaks, which make the request fail with 400 | "Virtual" |
cancel | No | true to cancel. See Cancel an interview. | true |
How date and timezone work together. date holds the interview's local time, and timezone says where that time applies. Apploi ignores the offset in date, so "2026-11-03T14:00:00Z" with "America/New_York" means 2:00 PM in New York, not 2:00 PM UTC. Don't convert the time to UTC first: an interview at 2:00 PM in New York is sent as 14:00:00Z, not 19:00:00Z. The offset is still required, so a date with no Z returns 400.
What the response means
Decide what happened from the status code, not from the body. The one exception for now is a 200 whose body isn't valid JSON, described below the table.
| Status | Meaning | What to do |
|---|---|---|
200 | The interview was scheduled, rescheduled or cancelled. Apploi also moves the application to the matching status, such as Interview Scheduled, if the team has one. | Nothing, unless the body isn't valid JSON. |
400 | The request is invalid. The usual causes are a number where a string is expected, a missing required field (cancel requests included), a date with no offset, or an interviewerEmail that doesn't belong to interviewerTeamId's team. | Fix the request before sending it again. An unchanged retry fails the same way. |
403 | The API key is missing or invalid, the application doesn't belong to one of your teams, or your API key isn't allowed to manage interviews. | Check the key and the id in the path. If both are right, contact Apploi support. |
404 | The application doesn't exist. Check that the path has the application's id, not its applicant_id. For a cancel, it can also mean there is no upcoming interview. | Don't retry with the same id. |
5xx | Something failed on Apploi's side, or the request took longer than 29 seconds. | Wait, then retry with exponential backoff. If you send interviewerId and every retry returns 500, the id is probably not an Apploi user. Check it rather than retrying. |
Retrying an interview that is still upcoming is safe. A retry updates the same interview rather than adding a second one, and a repeated cancel returns 404. If the first attempt did go through, the retry counts as a reschedule, so the application's status can read Interview Rescheduled rather than Interview Scheduled. Don't retry a request whose date is in the past: each attempt adds another interview.
A 200 whose body isn't valid JSON
Some server errors, for example during an Apploi deployment, currently reach you as a 200 whose body isn't valid JSON, not as a 5xx. The examples report these as unconfirmed.
- After a schedule or reschedule, a real
200always has a valid JSON body. Treat any other200as a5xx: wait, then retry. - After a cancel, a real
200can also have a body that isn't valid JSON, so the body doesn't tell you whether the cancel worked. Send the cancel again. A404confirms that no upcoming interview is left.
Response bodies
A 400 for a wrong field type or a missing field comes back with no detail:
{
"message": "Invalid request body"
}A request with no interviewer names the problem:
{
"error": "Validation Error",
"message": "Either 'interviewerId' must be provided, or both 'interviewerTeamId' and 'interviewerEmail' must be provided.",
"details": {
"validationErrors": [
"Either 'interviewerId' must be provided, or both 'interviewerTeamId' and 'interviewerEmail' must be provided."
]
}
}Other 400 responses, such as an interviewerEmail that isn't on the team or a date with no offset, carry the reason inside validationErrors:
{
"error": "Bad Request",
"message": "Invalid request body",
"validationErrors": {
"developer_message": "There were one or more field errors",
"errors": {
"message": "No user found with the provided interviewer_email and team_id."
}
}
}After a schedule or reschedule, a 200 response describes the application and an interview. id is the application's id, the one in the path, and applicant_id is the person's id.
{
"data": {
"id": 265733753,
"applicant_id": 71207841,
"applicant_status": "Interview Scheduled",
"applicant_status_id": 8,
"date": "2026-11-03T14:00:00+00:00",
"duration": 45,
"location": "Virtual",
"timezone": "America/New_York",
"interviewer": {
"id": 123456,
"full_name": "Jane Doe"
},
"job_id": 1470975
}
}The response's date ends in +00:00, but like the request it is the local time in timezone.
Don't use the interview fields in the body to confirm what was saved. When the application has more than one interview, for example after a second round, they can describe an earlier interview. After a cancel, don't read the body at all: it describes whatever interview is left, or has empty interview fields and may not be valid JSON. Confirm a cancel as described in A 200 whose body isn't valid JSON.
403Invalid Token
404Not Found
405405 response

