Skip to main content

Screening API

Submit a candidate against a job spec, get an AI score, and get a human-reviewed result on your webhook. Every score is approved, rejected, or edited by a human reviewer before delivery — the API never auto-delivers a model's raw output.

Quickstart

Submit a candidate with one request. The credit is charged and the request queued immediately; scoring and review happen asynchronously.

curl -X POST "https://lokerdollar.com/api/screening/v1/screen" \
  -H "Authorization: Bearer lk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"idempotencyKey":"req-backend-eng-jane-2026-08-03","candidate":{"kind":"structured","fullName":"Jane Doe","email":"jane.doe@example.com","summary":"5 years backend engineering focused on Node.js and PostgreSQL; led a 4-person team at a Series A logistics startup.","skills":["Node.js","PostgreSQL","TypeScript","AWS"],"experienceYears":5},"jobSpec":{"title":"Senior Backend Engineer","mustHaves":["Node.js","PostgreSQL","3+ years backend experience"],"niceToHaves":["AWS","Prior team-lead experience"],"salaryBandMinIdr":25000000,"salaryBandMaxIdr":40000000,"description":"Own the payments service for a Series B fintech."}}'

Authentication

Pass an employer API key as a bearer token on every request. A key resolves to exactly one tenant, so a request for another tenant's data always fails, regardless of the ID in the URL.

Authorization: Bearer lk_live_…

Request states

Every screening request settles on exactly one terminal state: delivered, failed, or expired. Nothing hangs indefinitely.

StateMeaning
receivedAccepted, credit charged, queued for parsing.
parsedResume/candidate text resolved.
scoredAI score attached, waiting for a human reviewer.
pending_reviewSitting in the reviewer queue (expires after 72h if untouched).
approved / rejected / editedA human reviewer decided. Every decision is a delivered result.
deliveredThe webhook attempt succeeded.
failedTerminal failure (e.g. the AI produced no usable output). Credit refunded.
expiredSat in pending_review past 72h with no decision. Credit refunded.

Endpoints

POST /api/screening/v1/screen

Submit a candidate. Body: idempotencyKey, candidate (a resume reference or structured fields), jobSpec (title, mustHaves, niceToHaves, salary band, description). Returns 201 with the new request's id and state, or 200 with the same id when idempotencyKey was already accepted.

{
  "requestId": "scr_7c1e0a9b4f2d4c8e",
  "state": "received"
}

402 when the free-tier monthly cap or the paid credit wallet is exhausted. The body always carries error.upsell.upgradeUrl, never a silent degrade.

GET /api/screening/v1/usage

Your current credit wallet, so you can check before submitting instead of finding out via a failed request.

{
  "tenantId": "biz_9f3a2c",
  "planTier": "free",
  "creditsRemaining": 37,
  "creditsCap": 50,
  "usagePercent": 0.26,
  "nearLimit": false,
  "resetAt": "2026-09-01T00:00:00.000Z",
  "freeScreenCap": 50
}

GET /api/screening/v1/audit

Paginated export of every state transition for your tenant: the audit ledger. Query: since (ISO-8601), cursor (from a prior page's nextCursor), limit (1–500, default 100). Append-only, so nothing here is ever updated or deleted.

{
  "events": [
    {
      "id": "scrv_a1b2c3d4",
      "requestId": "scr_7c1e0a9b4f2d4c8e",
      "fromState": "pending_review",
      "toState": "approved",
      "actor": "reviewer:412",
      "payload": null,
      "createdAt": "2026-08-03T09:14:10.000Z"
    }
  ],
  "nextCursor": null
}

Score result shape

What a completed screen looks like: a 0–100 score, per-dimension breakdowns with evidence quotes, a short rationale, model provenance (modelId/promptVersion), and the reviewer's decision once one exists.

{
  "schemaVersion": 1,
  "score": 78,
  "dimensions": [
    {
      "name": "must-haves",
      "score": 85,
      "evidence": "5 years backend engineering focused on Node.js and PostgreSQL"
    },
    {
      "name": "seniority fit",
      "score": 70,
      "evidence": "led a 4-person team at a Series A logistics startup"
    }
  ],
  "rationale": "Meets every must-have and has one prior lead role; salary expectations were not provided so band fit is unconfirmed.",
  "confidence": 0.82,
  "modelId": "@cf/google/gemma-4-26b-a4b-it",
  "promptVersion": "screening-scorer-v1"
}

After review, the same request also carries a review decision:

{
  "schemaVersion": 1,
  "decision": "approved",
  "reviewerId": "reviewer_412",
  "reviewerRole": "recruiter",
  "latencyMs": 46000,
  "note": "Strong must-have coverage, forward to the hiring manager."
}

Delivery webhook

Once a score or a review decision exists, we POST it to your webhookUrl and sign the raw body with HMAC-SHA256 using your webhookSecret. Verify X-Screening-Signature before trusting the payload.

X-Screening-Signature: sha256=<hex>
X-Screening-Event: screening.review_decided
X-Screening-Request-Id: scr_7c1e0a9b4f2d4c8e
{
  "requestId": "scr_7c1e0a9b4f2d4c8e",
  "tenantId": "biz_9f3a2c",
  "event": "screening.review_decided",
  "result": {
    "schemaVersion": 1,
    "decision": "approved",
    "reviewerId": "reviewer_412",
    "reviewerRole": "recruiter",
    "latencyMs": 46000,
    "note": "Strong must-have coverage, forward to the hiring manager."
  },
  "deliveredAt": "2026-08-03T09:14:22.000Z"
}

A non-2xx response or a timeout is retried with backoff; delivery attempts are also recorded in the audit ledger, so a stalled endpoint is visible from GET /audit even before you fix it.

Errors

Every error returns the same envelope with a stable code, the same contract the jobs API uses:

{ "error": { "code": "payment_required", "message": "…" } }
unauthorized401
forbidden403
payment_required402
not_found404
invalid_request400
internal500

This page and the OpenAPI spec are generated from the same request/response contract the server validates every call against — a shape change to the API updates both without a separate rewrite.

By using the API you agree to the API Terms of Service.