Disposition Data CSV guide

Integrate Disposition Data CSV into your applicant tracking system (ATS).

📘

Note:

By using this API and its documentation and building an integration, you agree to the Additional API Terms and Guidelines.

Disposition data integration process

Disposition data is information about an update to an app in your applicant tracking system (ATS), such as change in a recruiter's workflow or a recruiter's or candidate's action that occurs after the candidate applies.

📘

Note:

Examples of disposition data are:

  • A candidate enters the ATS
  • A candidate is contacted, interviewed, given an offer, rejected, or hired

To upload disposition data to Indeed:

1.

To enable your ATS to receive applications from Indeed, set up Indeed Apply.

2.

Request test and production API keys.

Either contact your Indeed Alliances manager or email [email protected].

Indeed emails you the API keys.

📘 Note:

Securely store the keys. Use the keys for the Disposition Data integration and do not use them for other Indeed integrations or APIs.

3.

Review API requirements.

4.

Export data to Indeed:

  1. Export test data to Indeed.

    Use your test API key, and include real date in your test upload, including Indeed Apply IDs.

    Notify your Indeed Alliances manager that the test upload is ready for review.

    Indeed manually reviews the file format and provides their review to you within one week or less.

  2. Export production data to Indeed.

    Use your production API key.

    📘 Note:

    Indeed encourages, but does not require, subsequent test uploads.

5.

Contact the Indeed Alliances team at [email protected] to request a Disposition Data integration.

If you cannot comply with the previous requirements, list the issues in your email.

API requirements

Overview

Requirement Description

Upload format

  • Preferred: CSV file

  • Legacy integrations: XML file

Upload size

Up to 1 GB of data per upload. Split larger files into multiple files.

Upload frequency

Up to once per hour.

Column fields

  • apply_id

    String. Indeed ApplyId. Exactly 64 characters long.

  • disposition_timestamp

    ISO-8601 formatted date and time when a disposition activity occurred. Time zone information is required.

  • status

    Normalized disposition status. For example, the HIRED status.

Typical integration setup

Partners set up a scheduled job that runs periodically to generate a report of applications with status changes. For example, generate a CSV file with these headers: disposition_timestamp, apply_id, and status.

The CSV file requirements are:

  • Each row, or record, represents a change in app state.

  • A single apply ID can have multiple events, or records, per report.

  • Do NOT include data from earlier uploads.

  • Do NOT include repeats of the same fields, such as apply ID or status, if no change in status occurred.

    Example: NEW -> REJECTED is valid, while NEW -> NEW -> REJECTED is not.

Full example: You can update an app multiple times a day, and you can upload it every 24 hours. However, you cannot resubmit the same row and it cannot appear twice.

📘

Note:

The CSV file can only contain applications with a change in status. Download the following correct CSV file and XML file for a series of app status changes.

Correct

Incorrect

Day 1 - File 1
disposition_timestamp apply_id status
2019-01-01T01:00:00z appid001 NEW
Day 1 - File 1
disposition_timestamp apply_id status
2019-01-01T01:00:00z appid001 NEW
Day 2 - File 2
disposition_timestamp apply_id status
2019-01-02T01:00:00z appid002 NEW
Day 2 - File 2
disposition_timestamp apply_id status
2019-01-01T01:00:00z appid001 NEW
2019-01-02T01:00:00z appid002 NEW
Day 3 - File 3
disposition_timestamp apply_id status
2019-01-03T01:00:00z appid001 CONTACTED
2019-01-03T01:00:00z appid003 NEW
Day 3 - File 3
disposition_timestamp apply_id status
2019-01-02T01:00:00z appid002 NEW
2019-01-03T01:00:00z appid001 CONTACTED
2019-01-03T01:00:00z appid003 NEW

This example is correct because it includes data:

  • For new applications only
  • When the status of an apply_id has changed

This example is incorrect for these reasons:

  • (2019-01-01T01:00:00z, appid001, NEW)
    is uploaded twice. It should appear on day 1 only.
  • (2019-01-02T01:00:00z, appid002, NEW)
    is uploaded twice. It should appear on day 2 only.

Field definitions

All fields are required.

Field Description
apply_id

The unique ID for the job app that references the employer, candidate, job, and more.

This is the id field in the Indeed Apply JSON application data.

status

📘 Important:

Normalized application status. Indeed uses the status field to understand what happens to applications after they are in your ATS.

This table describes the statuses. You must map all your ATS's statuses to the Indeed statuses. It is absolutely crucial you accurately map the statuses from your ATS into these categories.

Without an accurate mapping, Indeed cannot reliably determine what happens to an application. If you support custom statuses in your ATS, notify your Indeed Alliances manager.

Indeed status Description
NEW

The application arrived through Indeed Apply.

CONTACTED

The candidate was contacted by phone, email, or another method. Send this status even if the application status on your ATS has not changed. Indeed uses this as a signal to help determine employer responsiveness, which can affect the quality and volume of candidates an employer receives.

INTERVIEWED

The candidate has been interviewed.

OFFERED

An offer of employment has been sent to a candidate.

HIRED

The candidate accepted an offer of employment.

REJECTED

A candidate has been explicitly rejected or all remaining candidates rejected after the role closed.

If you cannot comply with this requirement, contact your Indeed Alliances manager.

disposition_timestamp

ISO 8601-formatted timestamp with time zone information for when the status change occurred.

For example, format November 5, 1994 at 8:15 am Eastern Time as:

1994-11-05T08:15:30-05:00

  • Use the T character to separate the date and time.

  • Time zone information is required.

  • Use the +/-HH:MM format for time zone information, unless it is UTC.

    For UTC, +00:00, Z, and z are valid, but -00:00 is not valid.

  • Use 00:00:00 for hours, minutes, and seconds if you do not have actual time information.

Export data to Indeed

This API enables your organization to programmatically submit disposition information:

1.

To get a signed Amazon S3 URL, make a POST request to this API with the assigned API key.

2.

To upload the file, make a PUT request to the URL.

📘

Note:

To prevent uploading files with the same name, use a naming convention that enables you to distinguish your files from other files in your organization.

Get a signed S3 URL

To get the signed S3 bucket URL, create a POST request to the API with the file names you are uploading. A presigned upload URL is returned for each file.

The requirements for this API call are:

Requirement Description

Request headers
  • Content-Type: application/json

  • Accept: application/json

  • token: <api-key>

    String. 32 characters.

Parameters

  • file_names

    JSON list of file names for which to get an upload URL. Valid extensions are CSV and XML.

    📘 Note:

    Pass the name of the file but do not pass the file itself.

Limitations

  • Files cannot be larger than 1 GB.

  • The generated URL expires in one hour.

  • All files must pass extension validation.

    Files with unsupported extensions fail to upload.

If the request succeeds, the API returns the HTTP 200 status code and a JSON dictionary with the list of file_names and their corresponding presigned S3 URL.

If the request fails, the API returns an HTTP non-200 status code and a JSON dictionary with the Error key, which provides information about the error.

Upload files

After you have the presigned URLs, make a PUT request to upload files to the AWS Bucket.

View the following examples.

Code examples

curl

Get the presigned URLs:

curl \
-H 'Content-Type: application/json' \
-H 'token: XXXXXXXXX' \
-H 'Accept: application/json' \
--request POST \
--data '{"file_names":["test_file_1.csv","test_file_2.csv"]}' \
https://indeed-atsdi.com/api/get_upload_url

The response is:

{
  "test_file_1.csv": "https://blah.s3.amazonaws.com/presigned_url_1",
  "test_file_2.csv": "https://blah.s3.amazonaws.com/presigned_url_2"
}

Upload files one at a time to S3. Put pre-signed URL in quotes.

curl -X PUT -T /path/to/test_file_1.csv \
-L https://blah.s3.amazonaws.com/presigned_url_1

Put pre-signed URL in quotes.

curl -X PUT -T /path/to/test_file_2.csv \
-L https://blah.s3.amazonaws.com/presigned_url_2

Python

import requests
import json

url = 'https://indeed-atsdi.com/api/get_upload_url'
file_paths = ['/path/to/test_file_1.csv', '/path/to/test_file_2.csv']
API_KEY = 'XXXXXXX'

# File tuple [0] = path, [1] = basename.
file_infos = [(fp, ntpath.basename(fp)) for fp in file_paths]


headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'token': API_KEY
}

#  This gets the presigned URLs
r = requests.post(url,
                  headers=headers,
                  data=json.dumps({'file_names': [fi[1] for fi in file_infos]}))
try:
    r.raise_for_status()
except:
    print("Error occurred.")
    print (r.json())

#  Upload file(s) to AWS.
s3_signed_urls = r.json()
for file_info in file_infos:
      with open(file_info[0], 'rb') as data:
          upload_result = requests.put(s3_signed_urls[file_info[1]], data=data)
          try:
              upload_result.raise_for_status()
          except:
              print("Issue uploading file to AWS.")

Ruby

require 'net/http'
require 'net/https'
require 'uri'
require 'json'

Tuple = Struct.new(:_1, :_2)

api_url = URI.parse('https://indeed-atsdi.com/api/get_upload_url')
file_paths = ['/path/to/test_file_1.csv', '/path/to/test_file_2.csv']
api_key = 'XXXXXXX'

names = []
file_info = []
file_paths.each do |path|
 file_info.push(Tuple.new(path, File.basename(path)))
 names.push(File.basename(path))
end

header = {'Accept': 'application/json',
         'Content-Type': 'application/json',
         'token': api_key}
data = {file_names: names}

#  Access indeed api
api_response = nil
Net::HTTP.start(api_url.host, :use_ssl => true) do |http|
 api_response = http.send_request("POST", api_url.request_uri, data.to_json, header)
end

case api_response
when Net::HTTPSuccess
 json = JSON.parse(api_response.body)

 # Upload to S3.
 file_info.each do |upload_file|
   s3_url = URI.parse(json[upload_file._2])
   file = File.open(upload_file._1, "rb")
   file_data = file.read
   file.close
   s3_response = nil
   Net::HTTP.start(s3_url.host, :use_ssl => true) do |http|
       s3_response = http.send_request('PUT', s3_url.request_uri, file_data, {
       # Content type has to be here, even if set to '', or else 403 error.
        "content-type" => '',
       })
     end
   case s3_response
   when Net::HTTPSuccess
     print 'Successfully uploaded ' + upload_file._1 + "\n"
   else
     print 'Error uploading file ' + upload_file._1 + "\n"
     print s3_response.inspect + "\n"
   end
 end
else
 print 'Bad Response from Indeed API.' + "\n"
 print api_response.inspect + "\n"
end

PHP

<? php
class IndeeddispositionClient{
  private static $api_key = 'XXXXXXXXX';
  private static $api_url = 'https://indeed-atsdi.com/api/get_upload_url';

  static function get_s3_url($filename) {
    $url = self::$api_url;
    $data = '{"file_names":["'.$filename.'"]}';
    $headers = array(
      'Content-Type: application/json',
      'token: ' . self::$api_key,
      'Accept: application/json'
    );
    $options = array(
      CURLOPT_POST => 1,
      CURLOPT_POSTFIELDS => $data,
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_RETURNTRANSFER => 1
    );
    $curl = curl_init($url);
    curl_setopt_array($curl, $options);
    $res = curl_exec($curl);
    curl_close($curl);
    return $res;
  }
  static function upload_to_s3($file_path_string, $signed_url){
      $fh = fopen($file_path_string, 'rb');
      $options = array(
        CURLOPT_VERBOSE => 1,
        CURLOPT_POST => 1,
        CURLOPT_RETURNTRANSFER => 1,
        CURLOPT_URL => $signed_url,
        CURLOPT_INFILE => $fh,
        CURLOPT_INFILESIZE => filesize($file_path_string),
        CURLOPT_PUT => 1
      );
      $ch = curl_init();
      curl_setopt_array($ch, $options);
      $result = curl_exec($ch);
      if (curl_errno($ch)) {
          echo 'Error:' . curl_error($ch);
      }
      fclose($fh);
      curl_close($ch);
      return $result;
    }
}

$file_paths = array("test_file_1.csv","test_file_2.csv");
foreach ($file_paths as $file_path){
  if (!file_exists($file_path)) {
    print 'Cannot find file '.$file_path."\n";
    throw new Exception('File not found '.$file_path);
  }
  $file_name = basename($file_path);
  print "Uploading ".$file_name ."\n";
  print "... ".$file_name ."\n";
  $s3_url = json_decode(IndeeddispositionClient::get_s3_url($file_name), true)[$file_name];
  $result = IndeeddispositionClient::upload_to_s3($file_path, $s3_url);

  print $result;

  print "\nFinished with ".$file_name ."\n";
}
s?>

Frequently asked questions

Should I send an empty file for periods that have no data?

No. Do not submit empty files.

Can I send data in smaller intervals than one hour?

No. Aggregate your data before sending it. That said, Indeed is currently working on a solution to support more frequent upload intervals. If this functionality interests you, let your Indeed Alliances manager know.

My ATS supports custom statuses. How can I map them appropriately?

Reach out to an Indeed Alliances manager for assistance.

Why do I get a SignatureDoesNotMatch error during a file upload?

Double-check the headers you are sending. Do NOT send a Content-Type header to S3.

If you continue to have this issue, see the Troubleshoot signed requests for AWS APIs.

Request help

For help, contact our integration support team at [email protected].

Disclaimer

While our integration reference is publicly available, it is intended to be implemented only by ATS partners who have signed a Master Services Agreement with Indeed.

Terms of Service

See also