Skip to content

Background processing and callbacks

On this page

Parsing a backlog of CVs or analysing an hour-long interview should not hold a connection open. The async endpoints accept links or uploads, answer immediately with a task_id, and run the work on our side. Collect the result by polling, or have it delivered to your server.

Async endpoints

EndpointInputResult
POST /parse/asyncUp to 25 CV links (file_urls), or uploaded filesOne parse result per CV, in order, same shape as POST /parse/
POST /evaluate/asyncUp to 25 CV links (resume_urls), or uploaded resume_pdfs, plus job_details or job_idOne evaluation per CV, in order, same shape as POST /evaluate/
POST /summary/asyncA recording link (media_url), or one uploaded media file; optional interview_id and languagetranscript_text, transcript_entries and summary
POST /transcribe/asyncA recording link (media_url), or one uploaded media filetranscript_text and transcript_entries
POST /translate/asynctext, texts or document links (file_urls), or uploaded files; source_language and target_languageSame shape as POST /translate/

Every async request may also include a callback_url.

Polling endpointReturns
GET /{op}/tasks/{task_id}The task's status and, once finished, its result. {op} is parse, evaluate, summary, transcribe or translate.

Sending a task

Send application/json with a list of links, or multipart/form-data with uploads. In multipart, a link field can be repeated or sent one link per line.

curl -X POST https://api.curriculo.me/public/parse/async \
  -H 'Authorization: Bearer key_abc123_yoursecret' \
  -H 'Content-Type: application/json' \
  -d '{"file_urls": ["https://example.com/cvs/candidate_a.pdf", "https://example.com/cvs/candidate_b.pdf"], "callback_url": "https://example.com/curriculo/callback"}'
curl -X POST https://api.curriculo.me/public/summary/async \
  -H 'Authorization: Bearer key_abc123_yoursecret' \
  -H 'Content-Type: application/json' \
  -d '{"media_url": "https://example.com/interviews/interview.mp4", "interview_id": "int_0042", "language": "ko"}'
curl -X POST https://api.curriculo.me/public/translate/async \
  -H 'Authorization: Bearer key_abc123_yoursecret' \
  -H 'Content-Type: application/json' \
  -d '{"texts": ["Senior backend engineer", "Remote within Europe"], "source_language": "en", "target_language": "fr"}'

For JSON requests to /translate/async, source_language is required. For /evaluate/async, send the job as job_details or job_id.

The API answers at once with 202:

{ "task_id": "…", "status": "queued", "callback_secret": "whsec_…" }

callback_secret is returned only when you sent a callback_url. It is shown exactly once, like an API key, so store it.

Getting the result

Polling

curl https://api.curriculo.me/public/parse/tasks/<task_id> \
  -H 'Authorization: Bearer key_abc123_yoursecret'

A task is readable with the same key that created it, whether or not a callback was delivered. The response never includes the callback URL or secret.

FieldMeaning
task_idThe task's id.
opWhich operation the task runs.
statusqueued, processing, succeeded or failed.
status_codeThe HTTP status the operation itself produced, once finished.
resultThe operation's response body once status is succeeded; null before.
errorThe error message when the task failed.
deliveryCallback delivery status: not_requested, pending, delivered or dead, with the attempt count.
created_at, completed_atTimestamps.

Callbacks

If you sent a callback_url, the finished task is POSTed there as application/json with these headers:

HeaderValue
X-Curriculo-Signaturesha256=<hex>: the lowercase hex HMAC-SHA256 of the exact raw request body, keyed with your callback_secret
X-Curriculo-Task-IdThe task's id
Content-Typeapplication/json

The body is {task_id, op, status, status_code, result, error, completed_at}.

There is no timestamp header, so de-duplicate deliveries on task_id: the same task can arrive more than once if your server answered slowly or with an error.

Verify the signature

Compute the HMAC-SHA256 of the raw request body, using your callback_secret exactly as returned (including the whsec_ prefix) as the key. Compare sha256= plus the lowercase hex digest with X-Curriculo-Signature in constant time. Use the raw bytes as received, before any JSON parsing, and reject anything that does not match.

Python:

import hashlib
import hmac

def is_valid(raw_body: bytes, signature_header: str, callback_secret: str) -> bool:
    # callback_secret is used as-is, including the "whsec_" prefix
    expected = "sha256=" + hmac.new(
        callback_secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

JavaScript (Node.js):

import crypto from "node:crypto";

// rawBody must be the unparsed request body (a Buffer)
export function isValid(rawBody, signatureHeader, callbackSecret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", callbackSecret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Then de-duplicate on the X-Curriculo-Task-Id header (or task_id in the body) and answer 2xx.

Delivery and retries

  • Any non-2xx answer counts as a failed delivery, and redirects are not followed.
  • Delivery retries with increasing delays (1, 5, 15 and 60 minutes, then 4 hours), for about 5 hours 21 minutes in total, before the delivery is marked dead.
  • The result stays readable at GET /{op}/tasks/{task_id} either way, so nothing is lost to a missed delivery.

Requirements for URLs

Callback URLs must be https, publicly reachable, and carry no credentials. Addresses on private networks are refused.

  • Links you send (file_urls, resume_urls, media_url) must be public https links. A public link or a pre-signed storage URL (for example S3) both work.
  • Links that redirect are refused.
  • Recordings by link can be up to 1 GB and 2 hours. Uploaded files are limited to 12 MB each.

Billing

  • A task is billed once, at its final outcome, as one request of its operation: an async parse of 25 links costs $0.01.
  • A task that fails (a dead link, an unreadable file) is never charged.
  • Recording tasks (/summary/async, /transcribe/async) reserve the charge for the 2-hour maximum when the task is accepted. Without enough credit for that reservation, the request returns 400. The reservation is released if the task fails.

See Pricing and billing.

Data kept by tasks

Unlike the synchronous operations, a background task keeps its result so you can poll for it, and uploaded files are stored under your account. See Data handling.

Updated