> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner Applications

> Submit qualified applicants to Alex on behalf of a customer and receive their interview link via callback

## Introduction

The Partner Applications API lets an application platform submit a qualified applicant to Alex on behalf of a shared customer. Alex syncs the applicant from the customer's ATS, creates the application, and runs the customer's configured workflow. The applicant's interview link is delivered **asynchronously via callback** — the API response is an acknowledgment, not the link itself.

<Note>
  Unlike the rest of this reference, this endpoint lives under the `/v2` base:

  ```http theme={null}
  POST https://api.alex.com/v2/api/partner-applications
  ```
</Note>

## Authentication

Requests are authenticated with the **customer's** API key — the same key used for the rest of the Alex API. A partner integrating on behalf of multiple customers holds one key per customer, and the key determines which customer's ATS and jobs the request applies to.

```http theme={null}
"X-API-Key": <CUSTOMER-API-KEY>
```

## Prerequisites

The target job's workflow must have a **partner application trigger** configured — in the workflow builder it appears under the partner platform's name. Requests for jobs without one are rejected with a `409` so misconfiguration surfaces to the caller rather than silently doing nothing.

## Creating a partner application

**Request fields:**

| Field            | Description                                                      |
| ---------------- | ---------------------------------------------------------------- |
| `atsApplicantId` | The ATS applicant entity ID (see the requirements matrix below). |
| `atsJobId`       | The ATS job ID the applicant applied to.                         |
| `atsCandidateId` | The ATS candidate/person ID.                                     |

**Requirements by ATS:**

| ATS      | `atsApplicantId`                                              | `atsJobId`                                               | `atsCandidateId` |
| -------- | ------------------------------------------------------------- | -------------------------------------------------------- | ---------------- |
| Bullhorn | **Required** — `JobSubmission` ID (int)                       | Recommended                                              | Optional         |
| Tracker  | **Required** — `OpportunityResource` ID (int)                 | Recommended                                              | Optional         |
| Loxo     | **Required** — `Candidate` ID (int), the person-on-job record | **Required** — the record can only be fetched job-scoped | Optional         |

For Bullhorn and Tracker the applicant record carries its own job and candidate references, so the applicant ID alone is sufficient — the job is derived from the record when `atsJobId` is omitted. For Loxo, the person ID is always derived from the record, so `atsCandidateId` is never needed.

<Note>
  **What "Recommended" and "Optional" buy you:** when `atsJobId` is provided, the
  job and its workflow configuration are validated *before* any ATS call — you
  get faster, clearer `404`/`409` errors — and the ID is cross-checked against
  the applicant record. `atsCandidateId` is purely a cross-check. A mismatch on
  either returns `422`.
</Note>

**Example requests:**

Bullhorn or Tracker — applicant ID alone is sufficient:

```json theme={null}
{
  "atsApplicantId": "9001"
}
```

Loxo — the job ID is required:

```json theme={null}
{
  "atsApplicantId": "555001",
  "atsJobId": "98765"
}
```

**Response — `202 Accepted`:**

```json theme={null}
{
  "success": true,
  "applicationId": "4c5f8811-4c3d-4e2d-b25a-c6d2b001ce72",
  "created": true
}
```

Store the `applicationId` — every subsequent callback for this applicant carries it as the correlation key.

Retrying the same request is safe: the application is created once, and retries return the same `applicationId` with `created: false`. A repeat still gets its own terminal callback — see below.

## Response codes

| Code  | Meaning                                                                                        | Retry?                |
| ----- | ---------------------------------------------------------------------------------------------- | --------------------- |
| `202` | Accepted — a terminal callback will follow.                                                    | —                     |
| `400` | `atsApplicantId` is missing, or `atsJobId` is missing for an ATS that requires it.             | No — fix the request. |
| `401` | Invalid or missing API key.                                                                    | No — fix the key.     |
| `404` | No Alex job matches `atsJobId`, or the applicant/candidate does not exist in the ATS.          | No.                   |
| `409` | The job is closed, or its workflow has no partner application trigger configured.              | After reconfiguring.  |
| `422` | The IDs don't belong together, or the customer's ATS isn't supported for partner applications. | No.                   |
| `429` | Rate limited.                                                                                  | Yes, with backoff.    |
| `5xx` | Transient failure (including a momentary ATS or infrastructure outage).                        | **Yes.**              |

<Warning>
  A `404` means the applicant genuinely does not exist in the ATS. Transient ATS
  outages are surfaced as `5xx`, never `404` — so a `404` is safe to treat as
  final, and a `5xx` should always be retried.
</Warning>

## Callbacks

Every `202` is followed by **exactly one terminal callback** to your callback endpoint, delivered at-least-once — including a repeat submission of an applicant Alex has already processed. Handle callbacks idempotently using `applicationId` + `status`.

Response codes other than `202` are never followed by a callback: the response itself is the final answer.

| Status         | Meaning                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------- |
| `invited`      | The applicant was invited — the payload includes their interview link. Redirect them to it. |
| `not_eligible` | The customer's workflow screened the applicant out. Do not redirect.                        |
| `error`        | The application could not be processed. Do not redirect; the customer's team is alerted.    |

The `invited` callback is sent by the customer's workflow and typically arrives within seconds:

```json theme={null}
{
  "applicationId": "4c5f8811-4c3d-4e2d-b25a-c6d2b001ce72",
  "status": "invited",
  "interviewLink": "https://meet.alex.com/..."
}
```

The `not_eligible` and `error` callbacks are sent either by the customer's workflow (with customer-authored messaging) or by the platform's fallback guarantee:

```json theme={null}
{
  "applicationId": "4c5f8811-4c3d-4e2d-b25a-c6d2b001ce72",
  "status": "not_eligible",
  "reason": "workflow_completed_without_callback"
}
```

Platform-sent callbacks include an `x-alex-callback-secret` header carrying the shared secret agreed during onboarding — validate it before trusting the payload.

### Repeat submissions

Submitting an applicant Alex has already processed does not re-run the customer's workflow — the candidate is not contacted again, and no second interview is created. The submission is still answered with a callback that replays the application's settled outcome, carrying `"reason": "repeat_submission"` and, for `invited`, the same `interviewLink` as the original:

```json theme={null}
{
  "applicationId": "4c5f8811-4c3d-4e2d-b25a-c6d2b001ce72",
  "status": "invited",
  "reason": "repeat_submission",
  "interviewLink": "https://meet.alex.com/..."
}
```

The one exception is a repeat that arrives while the first submission is still being processed: both are answered by that in-flight run's single callback.

<Note>
  Because the terminal callback is guaranteed, your timeout handling is crash
  insurance rather than a normal code path. Hold the applicant on a brief
  waiting page and redirect when the `invited` callback lands; treat a timeout
  as not-invited.
</Note>
