/applicants/{applicant_id}/message

Retrieves all messages for a specific application. Returns both messages sent to the candidate (outbound) and responses from the candidate (inbound). Messages are ordered by sent date, newest first.

Authentication: Requires valid x-api-key header.

Response Example:

{
  "data": [
    {
      "external_id": "msg7f3a90",
      "message_id": "TWVzc2FnZUlkOjU1NTExMDE=",
      "message_body": "Thank you for applying! We'd like to schedule an interview.",
      "message_direction": "to_candidate",
      "message_type": "text",
      "sent_at": "Thu, 23 Oct 2025 13:12:47 GMT",
      "status": "SENT",
      "subject": null
    },
    {
      "external_id": "msg7f3a9c",
      "message_id": "TWVzc2FnZUlkOjU1NTExMDI=",
      "message_body": "I am available for an interview next week.",
      "message_direction": "from_candidate",
      "message_type": "text",
      "sent_at": "Thu, 23 Oct 2025 13:12:47 GMT",
      "status": "DELIVERED",
      "subject": null
    },
    {
      "external_id": "msg7f3a91",
      "message_id": "TWVzc2FnZUlkOjU1NTExMDU=",
      "message_body": "Hey your interview is scheduled for next week.",
      "message_direction": "to_candidate",
      "message_type": "email",
      "sent_at": "Thu, 23 Oct 2025 13:12:47 GMT",
      "status": "DELIVERED",
      "subject": "Interview Scheduled"
    }
  ]
}
Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…

This endpoint returns every message on one job application. That covers texts and emails in both directions, whether they were recorded through this API or sent by a recruiter in Apploi. To record or send a message, use POST /applicants/{applicant_id}/message.

Before you start:

  • applicant_id is the job application's id, the id field of an entry returned by /applicants. It is not the applicant_id field from that response, which identifies the person.
  • Messaging must be turned on for the team. Ask your Apploi contact to enable it. Until it's on, requests for that team's job applications return the error described under When something goes wrong.

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.

Reading the message history

Replace 98765432 with the id of one of your job applications.

The examples define a get_messages function (getMessages in TypeScript), which the next example reuses. It checks the body as well as the status code, because some errors on this endpoint arrive with a 200 status (see When something goes wrong).

# 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 https://partners.apploi.com/applicants/98765432/message \
  -H "X-Api-Key: $APPLOI_API_KEY" \
  -w "\nHTTP status: %{http_code}\n"
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 get_messages(job_application_id):
    response = requests.get(
        f"https://partners.apploi.com/applicants/{job_application_id}/message",
        headers={"X-Api-Key": API_KEY},
        timeout=30,
    )
    response.raise_for_status()
    try:
        body = response.json()
    except ValueError:
        # A server error can arrive with status 200 and a body that isn't JSON. Retry later.
        raise RuntimeError(f"Unexpected response, retry later: {response.text[:200]}")
    if "data" not in body:
        # Some errors arrive with status 200, for example when messaging isn't enabled.
        raise RuntimeError(f"Request failed: {body.get('developer_message')}")
    return body["data"]  # newest first

for message in get_messages(98765432):
    print(message["sent_at"], message["message_direction"], message["message_type"], message["message_body"])
// 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;

async function getMessages(jobApplicationId: number): Promise<any[]> {
  const response = await fetch(
    `https://partners.apploi.com/applicants/${jobApplicationId}/message`,
    { headers: { "X-Api-Key": API_KEY } },
  );
  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}: ${await response.text()}`);
  }
  const text = await response.text();
  let body: any;
  try {
    body = JSON.parse(text);
  } catch {
    // A server error can arrive with status 200 and a body that isn't JSON. Retry later.
    throw new Error(`Unexpected response, retry later: ${text.slice(0, 200)}`);
  }
  if (!("data" in body)) {
    // Some errors arrive with status 200, for example when messaging isn't enabled.
    throw new Error(`Request failed: ${body.developer_message}`);
  }
  return body.data; // newest first
}

for (const message of await getMessages(98765432)) {
  console.log(message.sent_at, message.message_direction, message.message_type, message.message_body);
}
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 get_messages(job_application_id)
  uri = URI("https://partners.apploi.com/applicants/#{job_application_id}/message")
  request = Net::HTTP::Get.new(uri)
  request["X-Api-Key"] = API_KEY

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 30) do |http|
    http.request(request)
  end
  raise "Request failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

  body = begin
    JSON.parse(response.body)
  rescue JSON::ParserError
    # A server error can arrive with status 200 and a body that isn't JSON. Retry later.
    raise "Unexpected response, retry later: #{response.body[0, 200]}"
  end
  # Some errors arrive with status 200, for example when messaging isn't enabled.
  raise "Request failed: #{body['developer_message']}" unless body.key?("data")

  body["data"] # newest first
end

get_messages(98765432).each do |message|
  puts [message["sent_at"], message["message_direction"], message["message_type"], message["message_body"]].join(" ")
end

The response holds the messages in data, newest first. An application with no messages returns an empty list.

{
  "data": [
    {
      "message_id": "TWVzc2FnZUlkOjU1NTEyMzQ=",
      "external_id": "msg7f3a9c",
      "message_direction": "from_candidate",
      "message_type": "text",
      "message_body": "Yes, Tuesday at 10am works for me.",
      "subject": null,
      "status": "received",
      "sent_at": "Tue, 29 Sep 2026 14:05:12 GMT"
    },
    {
      "message_id": "TWVzc2FnZUlkOjU1NTEyMDE=",
      "external_id": "msg7f3a91",
      "message_direction": "to_candidate",
      "message_type": "email",
      "message_body": "Thanks for applying. Could you come in for an interview on Tuesday at 10am?",
      "subject": "Interview invitation",
      "status": "sent",
      "sent_at": "Tue, 29 Sep 2026 13:40:03 GMT"
    }
  ]
}

What the fields hold:

  • message_direction is to_candidate for messages sent to the applicant and from_candidate for the applicant's replies.
  • message_type is text or email. subject is only set on emails.
  • message_id is Apploi's id for the message. Treat it as an opaque string. It is the reliable way to tell messages apart.
  • external_id is the id the sender supplied. It can be null, for example on messages a recruiter sent from Apploi. If the id you sent contained a hyphen, only the part after the first hyphen is returned, so msg-7f3a-9c comes back as 7f3a-9c. When Apploi delivered a text for you, external_id holds the delivery provider's id rather than yours.
  • status is free text. On messages recorded through this API it is whatever the sender sent, such as SENT or DELIVERED. On emails Apploi sent, it is sent, or failed if sending failed. On texts Apploi sent, it comes from the delivery provider.
  • sent_at is a UTC date in HTTP date format, such as Tue, 29 Sep 2026 14:05:12 GMT, not ISO 8601. Python's email.utils.parsedate_to_datetime, JavaScript's new Date(...) and Ruby's Time.httpdate all parse it.

Picking out new replies

The endpoint has no filter, so each call returns the whole history. To act on new replies from the applicant, keep the message_ids you have already handled and skip them on the next call.

These examples reuse get_messages (Python and Ruby) / getMessages (TypeScript) from the example above.

# 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>"

# Only the applicant's replies (requires jq)
curl -sS https://partners.apploi.com/applicants/98765432/message \
  -H "X-Api-Key: $APPLOI_API_KEY" \
  | jq '.data[] | select(.message_direction == "from_candidate")'
def new_replies(job_application_id, handled_message_ids):
    replies = [
        message
        for message in get_messages(job_application_id)
        if message["message_direction"] == "from_candidate"
        and message["message_id"] not in handled_message_ids
    ]
    return list(reversed(replies))  # oldest first, in the order they were sent

handled_message_ids = set()  # load these from your own storage
for reply in new_replies(98765432, handled_message_ids):
    print(reply["sent_at"], reply["message_body"])
    handled_message_ids.add(reply["message_id"])  # and save it
async function newReplies(jobApplicationId: number, handledMessageIds: Set<string>): Promise<any[]> {
  const messages = await getMessages(jobApplicationId);
  return messages
    .filter(
      (message) =>
        message.message_direction === "from_candidate" && !handledMessageIds.has(message.message_id),
    )
    .reverse(); // oldest first, in the order they were sent
}

const handledMessageIds = new Set<string>(); // load these from your own storage
for (const reply of await newReplies(98765432, handledMessageIds)) {
  console.log(reply.sent_at, reply.message_body);
  handledMessageIds.add(reply.message_id); // and save it
}
require "set"

def new_replies(job_application_id, handled_message_ids)
  get_messages(job_application_id)
    .select { |message| message["message_direction"] == "from_candidate" }
    .reject { |message| handled_message_ids.include?(message["message_id"]) }
    .reverse # oldest first, in the order they were sent
end

handled_message_ids = Set.new # load these from your own storage
new_replies(98765432, handled_message_ids).each do |reply|
  puts [reply["sent_at"], reply["message_body"]].join(" ")
  handled_message_ids.add(reply["message_id"]) # and save it
end

When something goes wrong

Some errors on this endpoint arrive with a 200 status, so check for data as well as the status code. They include the errors in the table below, whose body has a developer_message and no data. They also include server errors other than a plain 500, such as a 502 or 503 while Apploi is deploying, whose body may not be JSON at all.

StatusBodyCauseWhat to do
200{"developer_message": "422 Unprocessable Entity: Feature is not enabled for this team", "extra": null}Messaging isn't turned on for the team that owns the job.Ask your Apploi contact to enable it. Don't retry.
200{"developer_message": "422 Unprocessable Entity: Invalid permission for API!!!", "extra": null}The job application belongs to a team your API key can't reach.Check the id. Use one returned by /applicants for a team your key can read.
200Any other body without data, or a body that isn't JSONA temporary error on Apploi's side.Wait, then retry with backoff.
404{"developer_message": "404 Not Found: Application not found or candidate not found", "extra": null}No job application has that id.Check that you sent the job application's id, not its applicant_id. Don't retry.
403{"message": "Forbidden"}The X-Api-Key header is missing or invalid.See Errors.
5xxVariesSomething failed on Apploi's side.Wait, then retry with backoff. This endpoint only reads, so retrying is safe.
Path Params
string
required

The application form ID corresponding to a job application in the Apploi platform. The application form ID is a unique identifier for the job application in the Apploi platform. The application form ID can be found in the 'id' field of the each of the items in the response body of a call to the /applicants endpoint in the Apploi Partner API.

Examples: 265733753, 265733754, etc.

Responses

403

The X-Api-Key header is missing or invalid.

404

No job application has this id. Send the job application's id, not its applicant_id.

405

405 response

Language
Credentials
Header
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json