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 organisation.
X-API-Key: aptly_your_key_here
Key management
You can create up to 5 active API keys per organisation. 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://api.aptly.pro/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": "YYYY-MM-DDTHH:MM:SS.ssssss" }
curl https://api.aptly.pro/api/v1/jobs/3f8a2b1c-d4e5-4f67-89ab-cdef01234567 \ -H "X-API-Key: aptly_your_key_here"
Downloads
Skip writing requests by hand. Use these artifacts to start integrating in under two minutes.
- aptly-api.http Open in VS Code with the REST Client extension
- aptly-api.postman_collection.json Import into Postman (Collection v2.1)
Base URL
https://api.aptly.pro
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 10,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.
{ "api_access_enabled": true, "org_api_cvs_this_month": 142, "committed_scans": 1000, "hard_cap_scans": 1500, "keys": [ { "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." }
Error responses
| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Maximum 5 active API keys allowed. Revoke an existing key before creating a new one. |
| 401 | Unauthorized | Missing or invalid JWT. |
| 403 | Forbidden | Email address has not been verified, or API access is not enabled for your organisation. |
| 422 | Unprocessable Entity | Missing or invalid name form field. |
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. |
Error responses
| Status | Meaning | Common causes |
|---|---|---|
| 401 | Unauthorized | Missing or invalid JWT. |
| 403 | Forbidden | The authenticated user has no organisation associated with their account. |
| 404 | Not Found | The key_id does not exist or does not belong to your organisation. |
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 up to 3 times after the initial attempt, with backoff delays of 5 seconds, 30 seconds, and 120 seconds between attempts (4 total attempts).
- After 4 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 include a JSON body. For most errors the body contains a single detail string. For 422 (validation errors), detail is an array of validation error objects; see Validation errors (422).
{ "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. |
| 403 | Forbidden | API access is not enabled for your organisation. Contact hello@aptly.pro to get started. |
| 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. |
| 422 | Unprocessable Entity | Request body failed validation (missing required field, wrong type, malformed JSON). See Validation errors (422) for the response body shape. |
| 429 | Too Many Requests | Monthly CV quota would be exceeded for your organisation. See Quota exceeded (429) for the response body shape. |
| 500 | Server Error | An unexpected error occurred on our side. If this persists, contact hello@aptly.pro. |
Quota exceeded (429)
When a request would push your monthly usage over your hard cap, the API rejects the request with status 429. The response body lets your integration surface a clear message to the user or queue the request for next month.
{ "detail": "Monthly API quota would be exceeded. Used 3400 of 3500 CVs this month; this request of 200 CVs would push you over the limit. Please contact hello@aptly.pro to increase your limit.", "current_usage": 3400, "hard_cap": 3500, "requested_count": 200, "billing_month": "2026-05" }
Validation errors (422)
When a request body fails validation (missing required fields, wrong types, malformed JSON), the API returns status 422 with a body whose detail field is an array of validation error objects, one per failed field.
{ "detail": [ { "loc": ["body", "candidates", 0, "cv_text"], "msg": "field required", "type": "value_error.missing" } ] }
Limits and data retention
| Limit | Value |
|---|---|
| Maximum candidates per request | 200 |
| CV text used for scoring | First 10,000 characters |
| Maximum active API keys | 5 per organisation |
| Result retention | 72 hours from submission |
| Webhook timeout | 15 seconds per attempt |
| Webhook retry attempts | 3 retries after initial attempt (4 total) |
| Monthly CV quota | Configured per organisation as part of your API plan. |
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://api.aptly.pro" 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://api.aptly.pro"; 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://api.aptly.pro/api/v1/screen \ -H "X-API-Key: aptly_your_key_here" \ -H "Content-Type: application/json" \ -d "@payload.json"
curl https://api.aptly.pro/api/v1/jobs/YOUR_JOB_ID \ -H "X-API-Key: aptly_your_key_here"
curl https://api.aptly.pro/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 →