Intelligence API

Aptly Intelligence
API Reference

● Live REST API v1 JSON

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.

An active Aptly subscription is required to generate API keys. Log in at app.aptly.pro, go to Profile → API Keys, and generate your first key. Keys are shown once — store them securely.

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.

HTTP Required header
X-API-Key: aptly_your_key_here
Never expose your API key in client-side code, public repositories, or URLs. Treat it like a password. If a key is compromised, revoke it immediately from your API Keys panel and generate a new one.

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:

  1. POST /api/v1/screen — submit your job spec and candidates, receive a job_id
  2. GET /api/v1/jobs/{job_id} — poll until status is complete, then read results

If you provide a webhook_url, step 2 is optional — results will be pushed to you when ready.

cURL Step 1 — submit a screening job
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..."
      }
    ]
  }'
JSON Response — job accepted
{
  "job_id":              "3f8a2b1c-d4e5-4f67-89ab-cdef01234567",
  "status":            "pending",
  "total_candidates":  1,
  "expires_at":        "2026-04-27T01:40:45.160801"
}
cURL Step 2 — poll for results
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

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

POST /api/v1/screen Submit candidates for scoring against a job spec

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

FieldTypeRequiredDescription
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

FieldTypeDescription
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).
JSON Example request body
{
  "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

GET /api/v1/jobs/{job_id} Poll status and retrieve scored 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

ParameterTypeDescription
job_id string The UUID returned by POST /api/v1/screen.

Response

FieldTypeDescription
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.
Recommended polling interval: Check every 3 seconds for small batches (under 10 candidates), every 10 seconds for larger batches. Most jobs complete in under 60 seconds. If you are using webhooks, you do not need to poll at all.

List API keys

GET /api/v1/keys List all API keys for the authenticated account

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.

JSON Example response
[
  {
    "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

POST /api/v1/keys Generate a new 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)

FieldTypeRequiredDescription
name string Required A label for this key. Max 100 characters. e.g. Production, Staging, Integration test.
JSON Example response — key shown once
{
  "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

DELETE /api/v1/keys/{key_id} Permanently 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

ParameterTypeDescription
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.

JSON Full request schema
{
  "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.

JSON Scored candidate object
{
  "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.

JSON Failed candidate object
{
  "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 rangeVerdictMeaning
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.
Scores are absolute, not relative. A score of 80 means the candidate is a strong fit for the job spec, not that they ranked first among the batch. If you submit the same candidate against two different job specs, their scores may differ significantly.

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 complete status
  • Your endpoint must respond with a 2xx status 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

JSON Webhook POST body
{
  "job_id":  "3f8a2b1c-d4e5-4f67-89ab-cdef01234567",
  "status":  "complete",
  "results": [
    {
      "candidate_ref":  "ats-candidate-8821",
      "score":          84,
      "verdict":        "Strong shortlist",
      "match_reasons":  ["..."],
      "gaps":           ["..."]
    }
  ],
  "failed": []
}
Your webhook endpoint must be publicly accessible over HTTPS. Localhost URLs and private network addresses will not work. For local development, use a tunnelling tool like ngrok to expose a local endpoint.

Errors

Aptly uses standard HTTP status codes. Error responses always include a JSON body with a detail field describing the problem.

JSON Error response structure
{
  "detail": "Duplicate candidate_ref values found — all candidate_ref values must be unique within a request"
}
StatusMeaningCommon 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

LimitValue
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.

Python screen_candidates.py
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

JavaScript screenCandidates.mjs (Node 18+, native fetch)
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

cURL Submit screening job from file
# 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 Poll for results
curl https://aptly-backend-unk2.onrender.com/api/v1/jobs/YOUR_JOB_ID \
  -H "X-API-Key: aptly_your_key_here"
cURL List your API keys (uses JWT auth)
curl https://aptly-backend-unk2.onrender.com/api/v1/keys \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
On Windows using PowerShell, save your JSON payload to a file and use -d "@payload.json" to avoid shell escaping issues. Pass --ssl-no-revoke if you encounter SSL certificate errors.

Questions or integration issues?

Contact support →