Aptly Intelligence
API Reference
The Aptly Intelligence API lets you add AI-powered CV screening to any ATS, HR platform, or custom workflow. Submit a job specification and an array of candidate CVs and receive structured scoring, match reasoning, and gap analysis for each candidate.
The API is asynchronous — you submit a batch and receive a job_id immediately. You then poll for results or receive them via webhook when processing is complete.
Authentication
All API requests must include your API key in the X-API-Key header. Keys are prefixed with aptly_ and are tied to your Aptly account.
X-API-Key: aptly_your_key_here
Key management
You can create up to 5 active API keys per account. Keys can be created and revoked from the Profile → API Keys tab in your Aptly dashboard, or programmatically via the key management endpoints documented below.
When a key is created, the full key value is shown once and cannot be retrieved again. Only the prefix (first 20 characters) is stored and displayed for identification.
Quick start
The minimal flow to screen a batch of candidates is two API calls:
- POST /api/v1/screen — submit your job spec and candidates, receive a
job_id - GET /api/v1/jobs/{job_id} — poll until
statusiscomplete, then read results
If you provide a webhook_url, step 2 is optional — results will be pushed to you when ready.
curl -X POST https://aptly-backend-unk2.onrender.com/api/v1/screen \ -H "X-API-Key: aptly_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "job_spec": "Senior Python developer with 5+ years FastAPI experience...", "candidates": [ { "candidate_ref": "internal-id-001", "cv_text": "7 years Python experience, built APIs with FastAPI..." } ] }'
{ "job_id": "3f8a2b1c-d4e5-4f67-89ab-cdef01234567", "status": "pending", "total_candidates": 1, "expires_at": "2026-04-27T01:40:45.160801" }
curl https://aptly-backend-unk2.onrender.com/api/v1/jobs/3f8a2b1c-d4e5-4f67-89ab-cdef01234567 \ -H "X-API-Key: aptly_your_key_here"
Base URL
https://aptly-backend-unk2.onrender.com
All endpoints are prefixed with this base URL. The API accepts and returns JSON. All timestamps are in ISO 8601 format (UTC).
Submit a screening job
Submits a batch of candidates for scoring. Returns a job_id immediately. Processing happens asynchronously — poll GET /api/v1/jobs/{job_id} or supply a webhook URL to receive results.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| job_spec | string | Required | The full job description or specification. Plain text. The more detail, the more accurate the scoring. There is no strict minimum length but a thorough spec produces better results. |
| candidates | array | Required | Array of candidate objects. Minimum 1, maximum 200. All candidate_ref values must be unique within a single request. |
| candidates[].candidate_ref | string | Required | Your internal identifier for this candidate. Opaque — any string is valid. Echoed back in results so you can match scores to your own records. |
| candidates[].cv_text | string | Required | The candidate's CV as plain text. Your application is responsible for extracting text from PDFs or Word documents before submitting. The first 3,000 characters are used for scoring. |
| webhook_url | string | Optional | A URL to receive results via HTTP POST when processing is complete. Must be publicly accessible. See Webhooks for payload details and retry behaviour. |
Response
| Field | Type | Description |
|---|---|---|
| job_id | string | UUID for this job. Use this to poll for results. |
| status | string | Always pending on initial submission. |
| total_candidates | integer | Number of candidates accepted for processing. |
| expires_at | string | ISO 8601 timestamp when results will be purged (72 hours from submission). |
{ "job_spec": "Senior Python Developer with 5+ years of experience building production APIs. Must have: FastAPI or Django REST, PostgreSQL, experience with async Python. Nice to have: AWS, Docker, Redis.", "candidates": [ { "candidate_ref": "ats-candidate-8821", "cv_text": "Jane Smith. Software Engineer. 7 years Python experience..." }, { "candidate_ref": "ats-candidate-9034", "cv_text": "Mark Chen. Backend Developer. 3 years Node.js, 1 year Python..." } ], "webhook_url": "https://yourapp.com/webhooks/aptly" }
Retrieve job results
Returns the current status of a screening job. When status is complete, the results and failed arrays are included in the response. Results are available for 72 hours from submission, after which this endpoint returns 410 Gone.
Path parameters
| Parameter | Type | Description |
|---|---|---|
| job_id | string | The UUID returned by POST /api/v1/screen. |
Response
| Field | Type | Description |
|---|---|---|
| job_id | string | The job UUID. |
| status | string | One of: pending, processing, complete, failed. |
| total_candidates | integer | Total candidates submitted. |
| processed_candidates | integer | Candidates scored so far. Useful for progress tracking during processing status. |
| created_at | string | ISO 8601 timestamp of job submission. |
| completed_at | string | null | ISO 8601 timestamp of completion. null until complete. |
| expires_at | string | ISO 8601 timestamp when results will be purged. |
| results | array | Array of scored candidate objects. Only present when status is complete. See Response schema. |
| failed | array | Array of candidates that could not be scored, each with candidate_ref and error. Only present when status is complete. |
List API keys
Returns all API keys associated with the account, including revoked keys. Authentication for this endpoint uses your existing Aptly JWT token (the standard Authorization: Bearer {token} header), not an API key.
[ { "id": 1, "name": "Production", "key_prefix": "aptly_upMY--sVyRGeuY", "revoked": false, "created_at": "2026-04-24T01:33:11.984978", "last_used_at": "2026-04-24T09:12:44.000000", "monthly_cv_count": 142, "billing_month": "2026-04" } ]
Create an API key
Creates a new API key. The full key value is returned once only in the response — it cannot be retrieved again. Store it securely immediately. Uses JWT authentication.
Request body (form data)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Required | A label for this key. Max 100 characters. e.g. Production, Staging, Integration test. |
{ "id": 2, "name": "Production", "key": "aptly_upMY--sVyRGeuYGScwOWN4yqlzKRDsx3QwpA-ucNUUd-bCL7DuAFDQ", "key_prefix": "aptly_upMY--sVyRGeuY", "created_at": "2026-04-24T01:33:11.984978", "warning": "Store this key securely. It will not be shown again." }
Revoke an API key
Revokes an API key immediately. Any requests using a revoked key will receive 401 Unauthorized. This action is irreversible — you will need to generate a new key and update any integrations. Uses JWT authentication.
Path parameters
| Parameter | Type | Description |
|---|---|---|
| key_id | integer | The numeric id of the key to revoke, as returned by GET /api/v1/keys. |
Request schema
Full schema for the POST /api/v1/screen request body.
{ "job_spec": "string", // required — full job description, plain text "candidates": [ // required — 1 to 200 items { "candidate_ref": "string", // required — your internal ID, unique per request "cv_text": "string" // required — plain text CV content } ], "webhook_url": "string | null" // optional — must be a publicly accessible HTTPS URL }
Response schema
Each scored candidate in the results array has the following structure.
{ "candidate_ref": "string", // echoed from your request "score": 84, // integer 0–100 "verdict": "Strong shortlist", // see Verdicts section "match_reasons": [ "7 years Python meets the senior requirement", "FastAPI experience directly matches primary stack", "Led distributed backend teams — signals seniority" ], // up to 4 items, evidence-based "gaps": [ "No AWS or cloud infrastructure experience mentioned", "No mention of system design at distributed scale" ] // up to 3 items, specific and actionable }
Failed candidate object
Candidates in the failed array were not scored due to a processing error. Your job still completes — partial results are always returned.
{ "candidate_ref": "ats-candidate-9034", "error": "JSON decode error" }
Verdicts and scores
Every scored candidate receives both a numeric score and a categorical verdict. The verdict is derived from the score using fixed thresholds.
| Score range | Verdict | Meaning |
|---|---|---|
| 75 – 100 | Strong shortlist | Candidate meets all key requirements with strong, specific evidence in their CV. Recommend progressing. |
| 50 – 74 | Borderline | Candidate meets most requirements but has notable gaps or ambiguity. Worth reviewing the gaps field to decide whether to progress. |
| 0 – 49 | Do not progress | Significant gaps against the job specification. Not recommended to progress without further information. |
Webhooks
If you supply a webhook_url in your screening request, Aptly will POST the completed results to that URL when processing finishes. This eliminates the need to poll.
Delivery
- Aptly makes a single POST request with a JSON body when the job reaches
completestatus - Your endpoint must respond with a
2xxstatus code within 15 seconds - If your endpoint is unavailable or returns a non-2xx status, Aptly retries 3 times with exponential backoff (delays of 5s, 30s, 120s)
- After 3 failed attempts, no further retries are made — retrieve results via polling instead
Webhook payload
{ "job_id": "3f8a2b1c-d4e5-4f67-89ab-cdef01234567", "status": "complete", "results": [ { "candidate_ref": "ats-candidate-8821", "score": 84, "verdict": "Strong shortlist", "match_reasons": ["..."], "gaps": ["..."] } ], "failed": [] }
Errors
Aptly uses standard HTTP status codes. Error responses always include a JSON body with a detail field describing the problem.
{ "detail": "Duplicate candidate_ref values found — all candidate_ref values must be unique within a request" }
| Status | Meaning | Common causes |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Missing required field, duplicate candidate_ref, over 200 candidates, empty job_spec. |
| 401 | Unauthorized | Missing X-API-Key header, invalid key, or revoked key. |
| 404 | Not Found | The job_id does not exist or does not belong to your account. |
| 410 | Gone | Results have expired. Jobs are retained for 72 hours from submission. |
| 500 | Server Error | An unexpected error occurred on our side. If this persists, contact hello@aptly.pro. |
Limits and data retention
| Limit | Value |
|---|---|
| Maximum candidates per request | 200 |
| CV text used for scoring | First 3,000 characters |
| Maximum active API keys | 5 per account |
| Result retention | 72 hours from submission |
| Webhook timeout | 15 seconds per attempt |
| Webhook retry attempts | 3 (after initial attempt) |
| Monthly CV quota | Depends on plan (Starter: 1,000 / Business: 3,500) |
Data processing
CV text and job specifications submitted to the API are processed to generate scores and are not stored after the 72-hour retention window. Aptly does not use submitted content to train AI models. For full details, see our Privacy Policy and Data Processing Agreement.
Python example
A complete example using the requests library with polling until complete.
import requests import time API_BASE = "https://aptly-backend-unk2.onrender.com" API_KEY = "aptly_your_key_here" HEADERS = { "X-API-Key": API_KEY, "Content-Type": "application/json" } # 1. Submit the screening job payload = { "job_spec": "Senior Python Developer, 5+ years FastAPI...", "candidates": [ {"candidate_ref": "cand-001", "cv_text": "7 years Python, FastAPI, PostgreSQL..."}, {"candidate_ref": "cand-002", "cv_text": "3 years Node.js, some Python exposure..."}, ] } response = requests.post(f"{API_BASE}/api/v1/screen", json=payload, headers=HEADERS) response.raise_for_status() job_id = response.json()["job_id"] print(f"Job submitted: {job_id}") # 2. Poll until complete while True: time.sleep(5) result = requests.get( f"{API_BASE}/api/v1/jobs/{job_id}", headers=HEADERS ).json() if result["status"] == "complete": for candidate in result["results"]: print( f"{candidate['candidate_ref']}: " f"{candidate['score']}% — {candidate['verdict']}" ) break elif result["status"] == "failed": print("Job failed") break else: print(f"Status: {result['status']} — {result['processed_candidates']}/{result['total_candidates']}")
Node.js example
const API_BASE = "https://aptly-backend-unk2.onrender.com"; const API_KEY = "aptly_your_key_here"; const headers = { "X-API-Key": API_KEY, "Content-Type": "application/json" }; async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function screenCandidates() { // 1. Submit const submit = await fetch(`${API_BASE}/api/v1/screen`, { method: "POST", headers, body: JSON.stringify({ job_spec: "Senior Python Developer, FastAPI, 5+ years...", candidates: [ { candidate_ref: "cand-001", cv_text: "7 years Python, FastAPI..." }, { candidate_ref: "cand-002", cv_text: "3 years Node.js, some Python..." } ] }) }); const { job_id } = await submit.json(); console.log(`Job submitted: ${job_id}`); // 2. Poll while (true) { await sleep(5000); const res = await fetch(`${API_BASE}/api/v1/jobs/${job_id}`, { headers }); const data = await res.json(); if (data.status === "complete") { data.results.forEach(c => console.log(`${c.candidate_ref}: ${c.score}% — ${c.verdict}`) ); break; } else if (data.status === "failed") { console.error("Job failed"); break; } console.log(`Processing: ${data.processed_candidates}/${data.total_candidates}`); } } screenCandidates();
cURL examples
# Create payload.json first, then: curl -X POST https://aptly-backend-unk2.onrender.com/api/v1/screen \ -H "X-API-Key: aptly_your_key_here" \ -H "Content-Type: application/json" \ -d "@payload.json"
curl https://aptly-backend-unk2.onrender.com/api/v1/jobs/YOUR_JOB_ID \ -H "X-API-Key: aptly_your_key_here"
curl https://aptly-backend-unk2.onrender.com/api/v1/keys \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
-d "@payload.json" to avoid shell escaping issues. Pass --ssl-no-revoke if you encounter SSL certificate errors.Questions or integration issues?
Contact support →