ExactHire Support

ExactHire Integration API Reference

This page describes the ExactHire API that approved integrations, such as the ExactHire app for Zapier, can call. Access is deny-by-default: an OAuth access token can reach only the endpoints listed here, within the scopes the administrator approves on the consent screen. Every other API route returns HTTP 403 to an integration token.

Base URL and request format

  • Base URL: https://api.exacthire.com/api

  • Authentication: Authorization: Bearer <access token>. The token acts as the ExactHire user who approved the connection, and it is tied to one organization, so no organization id appears in any URL.

  • Request and response bodies are JSON unless noted. The token endpoint uses application/x-www-form-urlencoded.

Response envelope

Almost every endpoint returns HTTP 200 with this envelope. Check code on every call.

JSON
{
  "code": 200,
  "message": null,
  "content": { ... }
}
  • Success is code: 200. An application-level failure is code: 500 with the reason in message (occasionally in content).

  • Form validation failures return code: 500 with content.children.<field>.errors.

  • Real HTTP statuses appear only for 403 (missing scope or permission), unhandled errors, and the OAuth and discovery endpoints, which return standard OAuth JSON.

  • Entity objects use snake_case keys. Webhook payloads and subscription wrappers use camelCase.

Authentication: OAuth 2.0 with PKCE

ExactHire is an OAuth 2.1 authorization server. The integration sends the administrator to the consent page on their organization's own admin address, then exchanges the code for tokens.

Item

Value

Authorization (consent) page

https://{organization-subdomain}.admin.exacthire.com/oauth/authorize

Token endpoint

POST https://api.exacthire.com/api/public/oauth/token

Metadata

https://api.exacthire.com/.well-known/oauth-authorization-server

Response type / grants

code; authorization_code and refresh_token

PKCE

Required. Method S256 only; plain is rejected.

Client authentication

Confidential clients send the client secret with HTTP Basic or in the form body.

Authorization code

Single use, valid 5 minutes.

Access token

Bearer JWT, valid 24 hours (expires_in: 86400).

Refresh token

Rotates on every use; valid 30 days from issue or last rotation. Reusing an already-rotated refresh token revokes the whole connection.

Authorize request parameters: response_type=code, client_id, redirect_uri (exact match against the registered redirect URI), scope (space separated), state, code_challenge and code_challenge_method=S256. The organization must have the integration enabled in its Integrations marketplace.

Token request (application/x-www-form-urlencoded):

grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...

grant_type=refresh_token&refresh_token=...

Successful token response:

JSON
{
  "access_token": "<JWT>",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "<opaque>",
  "scope": "jobs:read applications:read ..."
}

Errors use standard OAuth JSON, {"error": "...", "error_description": "..."}: invalid_client returns 401, slow_down returns 429, and invalid_request, invalid_grant, unsupported_grant_type and invalid_scope return 400.

Disconnecting. When the connection is revoked in ExactHire, or the approving user is removed or disabled, already-issued access tokens stop working immediately and webhook subscriptions made with that connection are retired. Re-authorizing keeps existing subscriptions active.

Scopes

Scope

Allows

jobs:read

Read jobs and job lookup lists (statuses, job types, locations). Required for job webhooks.

applications:read

Search and read applications. Required for application webhooks.

applications:status

Change an application's stage; read the stage list.

applications:notes

Add a note to an application.

applications:archive

Archive and unarchive applications.

applications:copy

Copy an application to another job ad.

applications:create

Add an application to a job; send the applicant thank-you email; read application sources.

webhooks:manage

Create and delete webhook subscriptions; read sample payloads.

  • Scopes match exactly; one scope never implies another.

  • The approving user must hold the matching ExactHire permission for each scope. Scopes they cannot grant are withheld on the consent screen.

  • A missing scope returns HTTP 403: The access token lacks the required "<scope>" scope.

Limits and safe retries

  • Write rate limit. Add note, change stage, archive/unarchive, copy and add application are limited to 60 requests per 60 seconds per connection by default. Over the limit returns code: 500 and Too many requests: at most 60 per 60 seconds. Please retry shortly.

  • Thank-you email. Limited to 30 requests per 60 seconds.

  • One application per request. Change stage and archive/unarchive accept a single application id per request when called with an integration token.

  • Idempotency. POST /applications/manual and POST /applications/send_thank_you_email accept an optional Idempotency-Key header (up to 255 characters, remembered for one day, scoped to the connection and endpoint). A repeated completed request returns the stored response with Idempotent-Replay: true. The same key with a different body, or while the first request is still running, is refused. A refused or failed request releases the key.

Webhook subscriptions (REST hooks)

Subscribe an HTTPS endpoint to one or more event types. Scope: webhooks:manage plus the data scope of each event type.

POST /webhooks/subscriptions

Field

Type

Required

Notes

targetUrl

string

yes

Valid http or https URL, up to 2048 characters. The host must resolve only to public addresses; private, loopback and link-local addresses are rejected.

eventTypes

string[]

yes

Non-empty, up to 100, each a supported event type (below).

label

string

no

Up to 255 characters.

isActive

boolean

no

Default true.

JSON
{
  "code": 200,
  "message": null,
  "content": {
    "subscription": {
      "id": 17,
      "label": "Zapier",
      "target_url": "https://...",
      "event_types": [
        "APPLICATION_SUBMITTED"
      ],
      "is_active": true,
      "created_at": "..."
    },
    "signingSecret": "<64 hex characters>"
  }
}

The signingSecret is returned only when the subscription is created. A subscription can be deleted only by the connection that created it.

DELETE /webhooks/subscriptions/{id}

Deactivates the subscription. Success: {"code": 200, "message": null, "content": null}. An unknown id, or one owned by another connection, returns code: 500 and Webhook subscription not found.

GET /webhooks/event-types/{eventType}/samples

Returns one synthetic payload in the delivery shape, for building and testing. Contains no customer data. {"code":200,"content":{"eventType":"...","samples":[ ... ]}}. An event type that is unavailable to integrations, or whose data scope is missing, returns HTTP 403.

Event types

Event

Fires when

Needs scope

APPLICATION_SUBMITTED

An application is submitted

applications:read

APPLICATION_STATUS_UPDATED

An application's stage changes

applications:read

APPLICATION_ARCHIVED

An application is archived

applications:read

APPLICATION_UNARCHIVED

An application is unarchived

applications:read

APPLICATION_DISQUALIFIED

An application is disqualified

applications:read

APPLICATION_REINSTATED

A disqualified application is reinstated

applications:read

APPLICATION_NOTE_CREATED

A note is added (notes that are admin-only or restricted to named users do not fire)

applications:read

POSITION_CREATED

A job is created, including drafts

jobs:read

POSITION_STATUS_UPDATED

A job's status changes

jobs:read

Events caused by ExactHire platform staff acting inside an organization are not delivered.

Delivery payload

JSON
{
  "event": "APPLICATION_SUBMITTED",
  "eventId": 1001,
  "occurredAt": "2026-01-01T12:00:00+00:00",
  "organizationId": 7,
  "data": {
    "resourceType": "APPLICATION",
    "resourceId": 2002,
    "description": "Application submitted"
  },
  "application": {
    "id": 2002,
    "url": "https://example.admin.exacthire.com/applications/detail/application/2002",
    "submittedAt": "2026-01-01T12:00:00+00:00",
    "stage": "Applied",
    "applicantName": "Jane Sample",
    "job": {
      "id": 3003,
      "title": "Warehouse Associate",
      "location": "Indianapolis"
    }
  },
  "job": {
    "id": null,
    "url": null,
    "title": null,
    "location": null,
    "status": null,
    "statusIdentifier": null
  }
}
  • Application events fill application and leave job null. Job events fill job (id, url, title, location, status, statusIdentifier) and leave application empty.

  • data.resourceType is APPLICATION or POSITION. data.description is a fixed sentence for the event type; it never contains note text or a disqualification reason.

  • The application block carries the applicant's name only: no email, phone, resume or answers. url opens the record in ExactHire and requires the viewer to have access.

Headers and signature

Header

Value

Content-Type

application/json

User-Agent

ExactHire-Webhooks/1.0

X-ExactHire-Event

The event type

X-ExactHire-Delivery

Unique delivery id. Delivery is at-least-once, so de-duplicate on it.

X-ExactHire-Timestamp

Unix seconds

X-ExactHire-Signature

sha256=<hex> where the hex is HMAC-SHA256 of "<timestamp>.<raw body>" keyed with the signing secret string, used as-is.

Reject deliveries whose timestamp is stale. Any 2xx response counts as success.

Delivery timing and retries

  • Deliveries are sent in batches about every five minutes, so a first attempt can take up to roughly five minutes after the event.

  • Up to six attempts, with exponential backoff (1, 2, 4, 8 and 16 minutes plus batch timing), roughly 30 minutes in total, then the delivery is marked failed. Connect timeout 10 seconds; total timeout 15 seconds.

  • A delivery is dropped, not retried, when the subscription is gone, the connection was revoked, the approving user was removed or lost permission, or the record is no longer visible to that user.

  • An integration receives only events for records its approving user could read through the API.

Applications

GET /applications

Scope applications:read. Query-string parameters; most structured ones are JSON-encoded.

Parameter

Notes

page

Default 1.

limit

Page size, default 100.

positions

JSON array of job ids.

applications

JSON array of application ids to restrict to.

criteria

JSON object. Defaults to submitted, not archived, not disqualified. Keys include archive_status ([1] not archived, [0] archived, [2] all), disqualified ([1] only disqualified), stages, locations, application_sources and date filters such as submitted_at.

search

{"type":"basic","slug":"<text>","sections":["name","email","phone"]} keyword search.

JSON
{
  "code": 200,
  "content": {
    "applications": [
      {
        "id": 2002,
        "person_id": 41,
        "position": {
          "id": 3003,
          "title": "..."
        },
        "applicant_full_name": "...",
        "applicant_email": "...",
        "submitted_at": "...",
        "archived_at": null,
        "stage": {
          "id": 110
        },
        "is_disqualified": false
      }
    ],
    "total": 1
  }
}

Results are capped at 1,000 matches per query; use the default newest-first order with a small limit when polling. Stage is the option id; translate it with the stage list below. Rows contain the applicant's name and email.

GET /applications/{applicationId}

Scope applications:read. Returns content.application (id, submitted_at, archived_at, stage, position, applicant name and email, is_disqualified and related fields). Answers to questions flagged sensitive are withheld from integrations. A missing or out-of-scope id returns code: 500 and Application not found.

POST /applications/manual

Scope applications:create. Adds an application to a job ad. Supports Idempotency-Key.

Field

Type

Required

Notes

positionAd

integer

yes

A job ad id (not the job id). Find ids with GET /positions?includeAds=1.

firstName

string

yes


lastName

string

yes


email

string

usually

Valid address. Required unless the organization allows applicants without email.

phone

string

conditional

At least 5 characters. Required when there is no email and the organization allows that.

middleName

string

no


source

integer

no

Application source option id. Defaults to Career Site.

jobType

string

no

internal or external (default).

createNew

boolean

no

Default false. When false and the applicant already has a submitted application on the ad, that application is returned with already_applied: true and nothing is written.

JSON
{
  "code": 200,
  "content": {
    "application": {
      "id": 2002,
      "applicant_name": "...",
      "stage": "Applied",
      "position_ad": {
        "id": 2027,
        "title": "..."
      }
    },
    "applications": [
      {
        "id": 2002
      }
    ],
    "user": 41,
    "applicant_created": true,
    "already_applied": false
  }
}

An existing applicant is reused as-is; nothing on them is edited. The application is created, saved and submitted in one step, so a failure leaves nothing behind. This fires APPLICATION_SUBMITTED and the organization's new-application notifications. The thank-you email is not sent by this call; use the thank-you endpoint with the returned user and application ids. Closed jobs are accepted. The response contains no email or phone.

POST /applications/{id}/clone

Scope applications:copy. Copies application {id} to other job ads. The source is unchanged.

Field

Type

Required

Notes

positions

array of {"id": <job ad id>}

yes

Job ad ids.

sourceId

integer

no

Application source option id; defaults to Career Site.

copyOptions

object

no

Booleans: notes, messages, events, assignedUsers.

Success: {"code":200,"content":{"applications":[ ...new applications... ]}}. Each copy starts at stage Applied. If a target ad belongs to the same job as one already processed (including the source's own job), no new application is created for it. Errors: Application not found, Choose the job ad to copy the application to., Job ad not found. (the whole request is refused and nothing is written).

POST /data_point_answers/statuses/applications (change stage)

Scope applications:status.

Field

Type

Required

Notes

applications

integer[]

yes

One application id per request for integrations.

dataPointOptions

integer[]

yes

The target stage option id first. Ids come from GET /data_point_options/applications/statuses.

Success returns the updated application and fires APPLICATION_STATUS_UPDATED. Only submitted, non-disqualified applications can be moved. Errors: Change the status of one application per request., A status is required., Applications required, Invalid application(s), Invalid data point options.

PUT /applications/archived_status (archive and unarchive)

Scope applications:archive.

Field

Type

Required

Notes

applications

integer[]

yes

One application id per request for integrations.

status

integer

yes

1 archives, 2 unarchives. Send a JSON integer; a string is treated as unarchive.

Fires APPLICATION_ARCHIVED or APPLICATION_UNARCHIVED. Errors: Archive one application per request., No applications found, Status is not valid.

POST /application_notes (add a note)

Scope applications:notes.

Field

Type

Required

Notes

application

integer

yes

Application id in the organization.

note

string

yes

At least 2 characters.

adminOnly

boolean

no

Default false.

category

string

no


users

integer[]

no

Restrict visibility to these users.

userMentions

integer[]

no


Success: {"code":200,"content":{"application_note":{ "id": ..., "note": "...", "application_id": ..., "created_at": "..." }}}. Only notes that are not admin-only and have no restricted users fire APPLICATION_NOTE_CREATED.

POST /applications/send_thank_you_email

Scope applications:create. Sends the applicant the organization's thank-you email (or the disqualification email, for a disqualified application). Supports Idempotency-Key.

Field

Type

Required

Notes

user

integer

yes

The user id of the applicant (the user returned by /applications/manual).

applications

array of {"id": <application id>}

yes


sendThankYouEmail

boolean

no

An explicit false suppresses the send. true cannot override an organization that has turned the setting off.

resubmit

boolean

no

Selects the resubmit template.

The call returns code: 200 even when nothing is sent because the organization's settings suppress it. The email may be sent as a text message when the applicant prefers text and has consented.

Jobs

GET /positions

Scope jobs:read. Lists jobs, not job ads or templates. Parameters: page, limit (default 100), positions (JSON id array), criteria (JSON object; keys include position_status, organization_departments, locations, internal_ids, slug title search, and archive_status) and includeAds=1, which adds position_ads: [{id, title, location, internal_id, career_sites}] to each job.

JSON
{
  "code": 200,
  "content": {
    "positions": [
      {
        "id": 3003,
        "title": "...",
        "full_location": "...",
        "employment_type": "...",
        "created_at": "..."
      }
    ],
    "positionStatuses": {
      "3003": {
        "id": 12,
        "identifier": "POSITION_STATUS_ACTIVE",
        "label": "Active"
      }
    },
    "total": 1
  }
}

A job's status is not on the job object. It is in positionStatuses, keyed by job id. Compare on identifier, because option ids differ between environments.

GET /positions/{positionId}

Scope jobs:read. Returns content.position, including position_ads. A missing job returns code: 500 with Job not found. in content.

Lookup lists

All return {"code":200,"content":{"data_point_options":[ {id, identifier, label, ...} ], "total": N}}, with optional slug (name search), page and limit.

Endpoint

Scope

Notes

GET /data_point_options/applications/statuses

applications:status

Application stages. stage_status: 1 active (default), 0 archived, 2 all.

GET /data_point_options/applications/application_sources

applications:create

Application sources.

GET /data_point_options/positions/statuses

jobs:read

Job statuses.

GET /data_point_options/positions/departments

jobs:read

Job types of the caller's organization.

GET /locations/organizations

jobs:read

Locations in the caller's scope. Returns content.locations.

Errors

Where

Shape

Missing scope, permission or revoked connection

HTTP 403

Application-level failure

HTTP 200 {"code": 500, "message": "<reason>"}

Validation failure

HTTP 200 {"code": 500, "message": null, "content": {"children": {"<field>": {"errors": ["..."]}}}}

OAuth endpoints

{"error": "...", "error_description": "..."} with a real HTTP status

Questions or access to a test organization: contact ExactHire support.