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_at is set when status becomes screening; screening_completed_at when it becomes screened; closed_at for hired / rejected / Candidate joined.
  • DELETE — removes the candidate from the job pipeline. candidate_job_status_events and marketing_campaign_recipients are cascade-deleted. Screening sessions and video interviews are retained.
  • shortlisted_by_user_id is null when 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)

GET /api/v1/applications
FieldTypeRequiredDescription
candidate_idstring (uuid)NoFilter by candidate
job_idstring (uuid)NoFilter by job
statusstringNoExact pipeline status match
searchstringNoILIKE on candidate name, email, job title
limitintegerNoPage size (1–100, default 50)
offsetintegerNoSkip N records (default 0)

Request body — Create (POST)

POST /api/v1/applications
FieldTypeRequiredDescription
candidate_idstring (uuid)YesCandidate to apply (must belong to your org)
job_idstring (uuid)YesJob to apply to (must belong to your org)
statusstringNoPipeline status (default: Applied; must match org settings)
notesstring | nullNoRecruiter notes
match_scorenumber | nullNoMatch score 0–100
metadataobjectNoCustom JSON (e.g. ATS application ID)

Request body — Update (PUT)

At least one field is required. Omitted fields are left unchanged.

PUT /api/v1/applications/{id}
FieldTypeRequiredDescription
statusstringNoNew pipeline status. Triggers timestamp side effects.
notesstring | nullNoUpdated recruiter notes
match_scorenumber | nullNoUpdated match score 0–100
metadataobjectNoShallow-merged with existing metadata

Request body — Bulk create (POST)

POST /api/v1/applications/bulk
FieldTypeRequiredDescription
job_idstring (uuid)YesJob to apply all candidates to
candidate_idsstring[] (uuid, max 100)YesCandidate UUIDs to apply
statusstringNoPipeline status for all (default: Applied)
notesstring | nullNoNotes applied to all candidates
match_scoresobjectNoPer-candidate scores: { candidate_id: score }

Response body

Application resource (data)
FieldTypeRequiredDescription
idstring (uuid)Application id (candidate_jobs row)
candidate_idstring (uuid)Candidate id
job_idstring (uuid)Job id
organization_idstring (uuid)Organization id
statusstringCurrent pipeline status
screening_scorenumber | nullAggregate screening score
match_scorenumber | nullATS match score 0–100
notesstring | nullRecruiter notes
shortlisted_by_user_idstring (uuid) | nullUser who applied; null when applied via API key
shortlisted_atstring (ISO 8601 date-time)When applied
screening_started_atstring (ISO 8601 date-time)Screening started (set when status → screening)
screening_completed_atstring (ISO 8601 date-time)Screening completed (set when status → screened)
status_datestring (ISO 8601 date-time)When candidate entered current status
outreach_sent_atstring (ISO 8601 date-time)Outreach email sent (first apply only)
interest_clicked_atstring (ISO 8601 date-time)Candidate confirmed interest
video_interview_invite_sent_atstring (ISO 8601 date-time)Video interview invite sent
whatsapp_started_atstring (ISO 8601 date-time)First WhatsApp message received
closed_atstring (ISO 8601 date-time)Set when status is hired / rejected / Candidate joined
closed_by_user_idstring (uuid) | nullUser who closed
metadataobject | nullCustom JSON metadata
created_atstring (ISO 8601 date-time)Created
updated_atstring (ISO 8601 date-time)Updated
Enriched fields (list and detail responses only)
FieldTypeRequiredDescription
job_titlestring | nullJob title (list and detail responses)
job_statusstring | nullJob status (list and detail responses)
candidate_full_namestring | nullCandidate name (list and detail responses)
candidate_emailstring | nullCandidate email; null if talent pool profile is masked (list and detail responses)
List meta object
FieldTypeRequiredDescription
meta.totalintegerTotal matching records
meta.limitintegerPage size used
meta.offsetintegerOffset used

Endpoints

GET/api/v1/applications

List applications for your organization. Supports filtering by candidate, job, status, and full-text search.

POST/api/v1/applications

Apply a candidate to a job. Returns 201 for new applications, 200 for existing (idempotent). Sends outreach email on first apply only.

Example
{
  "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" }
}
GET/api/v1/applications/{id}

Get an application by its id, with enriched job and candidate fields.

PUT/api/v1/applications/{id}

Partially update an application. At least one field required.

Example
{
  "status": "screening",
  "notes": "Passed phone screen",
  "match_score": 92,
  "metadata": { "interview_round": 1 }
}
DELETE/api/v1/applications/{id}

Remove application. Returns 204 with no body. Screening sessions are not deleted.

POST/api/v1/applications/bulk

Apply up to 100 candidates to a single job. Per-item errors are reported without failing the whole batch.

Example
{
  "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 if already applied (GET)
# 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
# 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" }
Update pipeline status (PUT)
PUT /api/v1/applications/${id}
Authorization: Bearer tly_live_...

{
  "status": "screening",
  "notes": "Passed phone screen",
  "match_score": 92,
  "metadata": { "interview_round": 1 }
}
Bulk import candidates (POST)
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
  }
}