Authentication
Use an API key in either header:
Authorization: Bearer rpk_xxxx.yyyyy X-API-Key: rpk_xxxx.yyyyy
API keys are created by an admin in the /admin UI under "API Keys". The admin logs in with ADMIN_PASSWORD and then shares the generated key with the client.
Register Webhook URL (One-Time)
Register a default webhook once, then you can omit callback_url on uploads.
curl -X POST https://YOUR_DOMAIN/v1/webhooks/register \
-H "Authorization: Bearer rpk_xxxx.yyyyy" \
-H "Content-Type: application/json" \
-d '{"callback_url":"https://yourapp.com/webhook"}'
If your API key isn't linked to a client, ask an admin to assign it in /admin.
Upload a Resume
POST /v1/resumes (multipart/form-data)
curl -X POST https://YOUR_DOMAIN/v1/resumes \ -H "Authorization: Bearer rpk_xxxx.yyyyy" \ -F "file=@/path/to/resume.pdf" \ -F "callback_url=https://yourapp.com/webhook"
If a webhook URL is registered, callback_url is optional. Optional headers: Idempotency-Key, X-Bypass-Cache.
Check Job Status
GET /v1/resumes/{job_id}
curl -H "Authorization: Bearer rpk_xxxx.yyyyy" \
https://YOUR_DOMAIN/v1/resumes/{job_id}
Get Parsed Result
GET /v1/resumes/{job_id}/result
curl -H "Authorization: Bearer rpk_xxxx.yyyyy" \
https://YOUR_DOMAIN/v1/resumes/{job_id}/result
Webhook
Results are delivered to your registered webhook URL (or the per-request callback_url if you pass one).
Signature headers:
X-ResumeParser-Timestamp: <unix_seconds> X-ResumeParser-Signature: sha256=<hex> X-ResumeParser-Event: resume.completed | resume.failed
Signing: HMAC_SHA256(WEBHOOK_SECRET, f"{timestamp}.{raw_body}")
Full webhook docs: /v1/webhooks/docs
Response Shape
Results include: candidate details, education, experience, skills, and summary.
{
"job_id": "...",
"status": "completed",
"result": {
"schema_version": "1.0",
"candidate": { "name": "...", "email": "...", "phone": "..." },
"education": [],
"experience": [],
"skills": []
}
}
Webhook Verification (Short)
Set WEBHOOK_SECRET on the server (worker container) and share that secret with clients. Clients verify the HMAC signature using the raw JSON bytes.
1) Read raw request body bytes
2) Read headers:
- X-ResumeParser-Timestamp
- X-ResumeParser-Signature
3) Compute: sha256 = HMAC_SHA256(WEBHOOK_SECRET, "{timestamp}.{raw_body}")
4) Constant-time compare with signature header
Webhook Verification (Python)
import hmac
import hashlib
import time
WEBHOOK_SECRET = b"your_shared_secret"
MAX_AGE_SECONDS = 300
def verify_signature(raw_body: bytes, timestamp: str, signature_header: str) -> bool:
try:
ts = int(timestamp)
except Exception:
return False
now = int(time.time())
if abs(now - ts) > MAX_AGE_SECONDS:
return False
if not signature_header.startswith("sha256="):
return False
expected_hex = signature_header.split("=", 1)[1]
msg = str(ts).encode("utf-8") + b"." + raw_body
digest = hmac.new(WEBHOOK_SECRET, msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, expected_hex)
Webhook Verification (Node)
import crypto from "crypto";
const WEBHOOK_SECRET = "your_shared_secret";
const MAX_AGE_SECONDS = 300;
export function verifySignature(rawBody, timestamp, signatureHeader) {
const ts = Number(timestamp);
if (!Number.isFinite(ts)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > MAX_AGE_SECONDS) return false;
if (!signatureHeader?.startsWith("sha256=")) return false;
const expectedHex = signatureHeader.slice("sha256=".length);
const msg = Buffer.concat([Buffer.from(String(ts)), Buffer.from("."), rawBody]);
const digest = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(msg)
.digest("hex");
const a = Buffer.from(digest, "hex");
const b = Buffer.from(expectedHex, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}