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
| Endpoint | Input | Result |
|---|---|---|
POST /parse/async | Up to 25 CV links (file_urls), or uploaded files | One parse result per CV, in order, same shape as POST /parse/ |
POST /evaluate/async | Up to 25 CV links (resume_urls), or uploaded resume_pdfs, plus job_details or job_id | One evaluation per CV, in order, same shape as POST /evaluate/ |
POST /summary/async | A recording link (media_url), or one uploaded media file; optional interview_id and language | transcript_text, transcript_entries and summary |
POST /transcribe/async | A recording link (media_url), or one uploaded media file | transcript_text and transcript_entries |
POST /translate/async | text, texts or document links (file_urls), or uploaded files; source_language and target_language | Same shape as POST /translate/ |
Every async request may also include a callback_url.
| Polling endpoint | Returns |
|---|---|
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_languageis required. For/evaluate/async, send the job asjob_detailsorjob_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.
| Field | Meaning |
|---|---|
task_id | The task's id. |
op | Which operation the task runs. |
status | queued, processing, succeeded or failed. |
status_code | The HTTP status the operation itself produced, once finished. |
result | The operation's response body once status is succeeded; null before. |
error | The error message when the task failed. |
delivery | Callback delivery status: not_requested, pending, delivered or dead, with the attempt count. |
created_at, completed_at | Timestamps. |
Callbacks
If you sent a callback_url, the finished task is POSTed there as application/json with these headers:
| Header | Value |
|---|---|
X-Curriculo-Signature | sha256=<hex>: the lowercase hex HMAC-SHA256 of the exact raw request body, keyed with your callback_secret |
X-Curriculo-Task-Id | The task's id |
Content-Type | application/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-
2xxanswer 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 publichttpslinks. 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 returns400. 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