Applications
An Application represents a candidate linked to a job in the pipeline — a row in candidate_jobs. The Applications API gives you direct control over creating, reading, updating, and deleting these relationships.
You can also apply candidates inline when creating or updating them via the Candidates API using the nested application object — both paths are fully equivalent and idempotent.
Idempotency
POST /applications is idempotent: if a candidate_id + job_id pair already exists for your organization, the response returns 200 with the existing application (and patches any provided fields). A genuinely new application returns 201. This makes it safe to replay ATS webhooks without creating duplicate records.
Side effects
- Outreach email is sent once, on the first successful apply only. Retries do not trigger a second email.
- Screening is not started automatically on apply. Use the Screenings API or the Dashboard to initiate it.
- Pipeline timestamps — updating status triggers side effects:
screening_started_atis set when status becomesscreening;screening_completed_atwhen it becomesscreened;closed_atfor hired / rejected / Candidate joined. - DELETE — removes the candidate from the job pipeline.
candidate_job_status_eventsandmarketing_campaign_recipientsare cascade-deleted. Screening sessions and video interviews are retained. shortlisted_by_user_idisnullwhen applied via API key (no associated user identity).
Pipeline statuses
Status strings are case-sensitive and must match your organization's configured pipeline statuses exactly (e.g. Applied, Shortlisted, screening). Sending an unrecognized status returns invalid_pipeline_status (400) with an allowed list. See Errors for full error codes.
Talent pool
Applying a talent-pool candidate whose profile has not yet been revealed returns talent_pool_reveal_required (403). Reveal the profile in the Dashboard first. In list and detail responses, candidate_email is null for masked profiles.
List query parameters (GET)
| Field | Type | Required | Description |
|---|---|---|---|
| candidate_id | string (uuid) | No | Filter by candidate |
| job_id | string (uuid) | No | Filter by job |
| status | string | No | Exact pipeline status match |
| search | string | No | ILIKE on candidate name, email, job title |
| limit | integer | No | Page size (1–100, default 50) |
| offset | integer | No | Skip N records (default 0) |
Request body — Create (POST)
| Field | Type | Required | Description |
|---|---|---|---|
| candidate_id | string (uuid) | Yes | Candidate to apply (must belong to your org) |
| job_id | string (uuid) | Yes | Job to apply to (must belong to your org) |
| status | string | No | Pipeline status (default: Applied; must match org settings) |
| notes | string | null | No | Recruiter notes |
| match_score | number | null | No | Match score 0–100 |
| metadata | object | No | Custom JSON (e.g. ATS application ID) |
Request body — Update (PUT)
At least one field is required. Omitted fields are left unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | No | New pipeline status. Triggers timestamp side effects. |
| notes | string | null | No | Updated recruiter notes |
| match_score | number | null | No | Updated match score 0–100 |
| metadata | object | No | Shallow-merged with existing metadata |
Request body — Bulk create (POST)
| Field | Type | Required | Description |
|---|---|---|---|
| job_id | string (uuid) | Yes | Job to apply all candidates to |
| candidate_ids | string[] (uuid, max 100) | Yes | Candidate UUIDs to apply |
| status | string | No | Pipeline status for all (default: Applied) |
| notes | string | null | No | Notes applied to all candidates |
| match_scores | object | No | Per-candidate scores: { candidate_id: score } |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
| id | string (uuid) | — | Application id (candidate_jobs row) |
| candidate_id | string (uuid) | — | Candidate id |
| job_id | string (uuid) | — | Job id |
| organization_id | string (uuid) | — | Organization id |
| status | string | — | Current pipeline status |
| screening_score | number | null | — | Aggregate screening score |
| match_score | number | null | — | ATS match score 0–100 |
| notes | string | null | — | Recruiter notes |
| shortlisted_by_user_id | string (uuid) | null | — | User who applied; null when applied via API key |
| shortlisted_at | string (ISO 8601 date-time) | — | When applied |
| screening_started_at | string (ISO 8601 date-time) | — | Screening started (set when status → screening) |
| screening_completed_at | string (ISO 8601 date-time) | — | Screening completed (set when status → screened) |
| status_date | string (ISO 8601 date-time) | — | When candidate entered current status |
| outreach_sent_at | string (ISO 8601 date-time) | — | Outreach email sent (first apply only) |
| interest_clicked_at | string (ISO 8601 date-time) | — | Candidate confirmed interest |
| video_interview_invite_sent_at | string (ISO 8601 date-time) | — | Video interview invite sent |
| whatsapp_started_at | string (ISO 8601 date-time) | — | First WhatsApp message received |
| closed_at | string (ISO 8601 date-time) | — | Set when status is hired / rejected / Candidate joined |
| closed_by_user_id | string (uuid) | null | — | User who closed |
| metadata | object | null | — | Custom JSON metadata |
| created_at | string (ISO 8601 date-time) | — | Created |
| updated_at | string (ISO 8601 date-time) | — | Updated |
| Field | Type | Required | Description |
|---|---|---|---|
| job_title | string | null | — | Job title (list and detail responses) |
| job_status | string | null | — | Job status (list and detail responses) |
| candidate_full_name | string | null | — | Candidate name (list and detail responses) |
| candidate_email | string | null | — | Candidate email; null if talent pool profile is masked (list and detail responses) |
| Field | Type | Required | Description |
|---|---|---|---|
| meta.total | integer | — | Total matching records |
| meta.limit | integer | — | Page size used |
| meta.offset | integer | — | Offset used |
Endpoints
/api/v1/applicationsList applications for your organization. Supports filtering by candidate, job, status, and full-text search.
/api/v1/applicationsApply a candidate to a job. Returns 201 for new applications, 200 for existing (idempotent). Sends outreach email on first apply only.
{
"candidate_id": "550e8400-e29b-41d4-a716-446655440000",
"job_id": "660e8400-e29b-41d4-a716-446655440001",
"status": "Applied",
"notes": "Imported from ATS",
"match_score": 85.5,
"metadata": { "external_application_id": "ATS-123" }
}/api/v1/applications/{id}Get an application by its id, with enriched job and candidate fields.
/api/v1/applications/{id}Partially update an application. At least one field required.
{
"status": "screening",
"notes": "Passed phone screen",
"match_score": 92,
"metadata": { "interview_round": 1 }
}/api/v1/applications/{id}Remove application. Returns 204 with no body. Screening sessions are not deleted.
/api/v1/applications/bulkApply up to 100 candidates to a single job. Per-item errors are reported without failing the whole batch.
{
"job_id": "660e8400-e29b-41d4-a716-446655440001",
"candidate_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"770e8400-e29b-41d4-a716-446655440002"
],
"status": "Applied",
"notes": "Batch import from legacy ATS",
"match_scores": {
"550e8400-e29b-41d4-a716-446655440000": 88.0
}
}Code examples
# Check whether a candidate is already applied to a job
GET /api/v1/applications?candidate_id=550e8400-...&job_id=660e8400-...
# Returns [] if not applied, or [application] if applied — safe to retry# Full ATS workflow
# 1. Create or upsert a client
POST /api/v1/clients { "name": "Acme Corp" }
# 2. Create or upsert a job
POST /api/v1/jobs { "title": "Senior Engineer", ... }
# 3. Create or upsert a candidate (deduplicated by email)
POST /api/v1/candidates { "full_name": "Jane Doe", "email": "[email protected]", ... }
# 4. Apply the candidate to the job
POST /api/v1/applications { "candidate_id": "...", "job_id": "..." }
# → 201 on first apply (outreach email sent)
# → 200 on retry (idempotent, no duplicate email)
# 5. Later, sync pipeline status from ATS
PUT /api/v1/applications/{id} { "status": "Shortlisted" }PUT /api/v1/applications/${id}
Authorization: Bearer tly_live_...
{
"status": "screening",
"notes": "Passed phone screen",
"match_score": 92,
"metadata": { "interview_round": 1 }
}POST /api/v1/applications/bulk
Authorization: Bearer tly_live_...
{
"job_id": "660e8400-e29b-41d4-a716-446655440001",
"candidate_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"770e8400-e29b-41d4-a716-446655440002"
],
"status": "Applied",
"notes": "Batch import from legacy ATS",
"match_scores": {
"550e8400-e29b-41d4-a716-446655440000": 88.0
}
}