Resume Parser API

Simple guide for uploading resumes and retrieving parsed results.

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);
}