Kumo developer documentation · Recruiting

Plug your assessment into Kumo recruiting.

For AI interviewers, skills tests, background checks and sourcing tools. When a recruiter moves a candidate into your stage, Kumo tells you. You run your process and post the result back, and Kumo moves the candidate on. You build one receiver and two API calls, and Kumo never needs your credentials. If you already have an API for scheduling interviews, Kumo can call it instead. You can also create jobs, screening questions and AI interviews with Kumo's AI from your product.

integration.sh
# 1. Subscribe to your stage
POST /api/v1/webhooks
{ "url": "https://you.example/kumo",
  "events": ["ats.application.stage_entered"],
  "filters": { "stages": ["AI Interview"] } }

# 2. Kumo → you, signed, when a card
#    enters "AI Interview"

# 3. Post the result
POST /api/v1/ats/assessments
{ "application_id": "…",
  "provider": "your-product",
  "external_id": "session-123",
  "status": "completed",
  "score": 78, "passed": true }
34 endpointsSigned, retried webhooksIdempotent resultsIsolated sandbox
How it works

The recruiter moves a card. Your product does the rest.

Every job in Kumo has a pipeline board. The customer adds a stage for your product, such as "AI Interview", and sets where a pass and a fail should go. From then on, the recruiter's only job is dragging the card into your stage.

  1. 01
    Card enters your stage

    By drag, bulk move, the Kumo agent or the API. All of them emit the same event.

  2. 02
    Kumo sends you a signed webhook

    It includes the candidate's contact details, the full job description and a link for fetching the CV.

  3. 03
    You run your process

    Invite the candidate, run the interview or test. Post progress as it happens if you like.

  4. 04
    You post the result

    Score, recommendation, summary, and a link to your full report.

  5. 05
    Kumo routes the card

    Pass goes to the customer's pass stage and fail to their fail stage. Recruiters can review and override any move.

Two ways to connect

Kumo calls you, or you call Kumo.

Either way the recruiter's experience is the same: they move a card into your stage, and your result appears on the candidate with the customer's pass and fail rules applied.

ApproachYou buildBest when
WebhooksA receiver and two API calls

You want to start interviews from Kumo's event and post results back. This guide covers it step by step, and Kumo never holds your credentials.

Kumo connectorNothing new

You already have an API to create candidates, schedule interviews and return results. Kumo calls it with credentials the customer connects in their settings, and checks for results every ten minutes until you offer status notifications.

Connectors available today

Genia-AI. Customers connect it under Settings → Automation → Integrations and choose the stage it runs on. To add a connector for your product, send us your API documentation and a test account.

Sandbox and tokens

Build against a sandbox, go live with the customer's token.

Kumo gives partners a sandbox workspace with sample jobs and candidates. Email from a sandbox is suppressed except to the test inboxes you name, so nothing reaches a real person. Sandbox tokens start kumo_app_test_ and are refused by every live workspace. Live tokens are refused by every sandbox. Call https://kumohr.com directly, not www.kumohr.com: the redirect from www drops the Authorization header. The limit is 60 requests a minute per token; on a 429, wait for the Retry-After seconds and try again.

ScopeNeeded forGrants
ats:readReading

Jobs, candidates, applications, pipeline stages, assessment results, CV links

ats:assessments:writeResults

Posting assessment results against applications

webhooks:manageEvents

Subscribing, listing, testing and removing webhooks

ats:writeOptional

Adding candidates, creating applications, moving cards, setting routes

Check your token
curl https://kumohr.com/api/v1/me \
  -H "Authorization: Bearer $KUMO_TOKEN"
A token acts as a real user in one workspace

An integration token is created by a workspace admin in Kumo settings and shown once. It can never do more than both its scopes and its user's role allow, and it only works in the workspace where it was created. The limit is 60 requests per minute per token; on HTTP 429, respect Retry-After.

Step 1

Subscribe to the stage you own.

Register one https endpoint per customer workspace. Filter by stage so you only hear about candidates moving into your stage. You can also filter by job. The signing secret is returned once; store it the way you store passwords.

Subscribe
curl -X POST https://kumohr.com/api/v1/webhooks \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.your-product.com/kumo/events",
    "events": ["ats.application.stage_entered"],
    "filters": { "stages": ["AI Interview"] },
    "label": "AI interviews"
  }'

# → { "data": { "endpoint": { "id": "…", … }, "signing_secret": "kumo_wh_…" } }
Send yourself a test event
curl -X POST https://kumohr.com/api/v1/webhooks/test \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "id": "<endpoint id>" }'

# → { "data": { "delivered": true, "response_status": 200, … } }
ats.application.stage_enteredats.application.createdats.assessment.recordedats.assessment.invitedats.assessment.startedats.assessment.declinedats.job.createdats.job.published
Step 2

Verify, acknowledge, then work.

Each delivery is signed with HMAC-SHA256 over the timestamp and the raw body. Check the signature and the timestamp, reply 2xx within 10 seconds, and do the slow work afterwards. A failed delivery is retried after 5 minutes, doubling each time, up to 8 attempts. Retries reuse the event id, so use it to ignore duplicates.

Headers
Kumo-Webhook-Id: <delivery id>
Kumo-Webhook-Timestamp: 1790326442
Kumo-Webhook-Signature: v1=<hex of HMAC_SHA256(signing_secret, timestamp + "." + raw_body)>
ats.application.stage_entered
{
  "id": "evt_Q2x9…",
  "type": "ats.application.stage_entered",
  "createdAt": "2026-09-25T09:14:02.000Z",
  "data": {
    "application": {
      "id": "8a3f…", "candidate_id": "c71e…", "job_id": "5d02…",
      "stage": "AI Interview", "status": "NEW"
    },
    "from_stage": "Matched Profile",
    "to_stage": "AI Interview",
    "candidate": {
      "id": "c71e…", "first_name": "Jane", "last_name": "Doe",
      "email": "jane.doe@example.com", "phone": "+62 812 0000 0000",
      "location": "Jakarta", "current_title": "Backend Engineer",
      "current_company": "Acme", "linkedin_url": null,
      "skills": ["Python", "Django"], "has_resume": true
    },
    "job": {
      "id": "5d02…", "title": "Senior Backend Engineer",
      "department": "Engineering", "location": "Jakarta",
      "experience_level": "Senior",
      "description": "We are looking for…",
      "requirements": "5+ years of Python…",
      "skills_required": ["Python", "PostgreSQL"]
    },
    "actor": { "type": "user", "id": "…", "label": null },
    "links": {
      "resume": "/api/v1/ats/candidates/resume?candidate_id=c71e…",
      "application": "/api/v1/ats/applications?candidate_id=c71e…"
    }
  }
}
Verify (TypeScript)
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(request: Request) {
  const raw = await request.text();                 // verify the raw bytes
  const ts = request.headers.get("Kumo-Webhook-Timestamp") ?? "";
  const sig = (request.headers.get("Kumo-Webhook-Signature") ?? "").replace(/^v1=/, "");
  const expected = createHmac("sha256", process.env.KUMO_WEBHOOK_SECRET!)
    .update(`${ts}.${raw}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  const valid = sig.length === expected.length &&
    timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!fresh || !valid) return new Response("invalid signature", { status: 401 });

  const event = JSON.parse(raw);
  if (await alreadyProcessed(event.id)) return new Response("ok");  // retries reuse the id
  await queueInterview(event.data);                  // do the slow work async
  return new Response("ok");                         // 2xx within 10 seconds
}
Verify (Python)
import hmac, hashlib, time

def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(timestamp)) < 300
    return fresh and hmac.compare_digest(signature.removeprefix("v1="), expected)

Fetch the CV when you need it

Payloads never contain files or long-lived links. Call the path in links.resume for a download link that expires after 15 minutes.

CV link
curl "https://kumohr.com/api/v1/ats/candidates/resume?candidate_id=c71e…" \
  -H "Authorization: Bearer $KUMO_TOKEN"

# → { "data": { "url": "https://…", "filename": "jane-doe.pdf", "expires_at": "…" } }
Read the raw body once

Re-serializing JSON changes the bytes and breaks the signature. Verify first, then parse.

Step 3

Post progress and results to one record.

A result is keyed by your provider and external_id. Post each status change for the same session and it updates one record, which recruiters see on the candidate's profile. Sending the same result twice is safe.

pendinginvitedin_progresscompletedexpireddeclinedcancellederror
Invite sent
curl -X POST https://kumohr.com/api/v1/ats/assessments \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "application_id": "8a3f…",
    "provider": "your-product",
    "external_id": "session-f47ac10b",
    "status": "invited"
  }'
Completed
curl -X POST https://kumohr.com/api/v1/ats/assessments \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "application_id": "8a3f…",
    "provider": "your-product",
    "external_id": "session-f47ac10b",
    "status": "completed",
    "score": 78,
    "passed": true,
    "summary": "Strong problem-solving and clear communication…",
    "strengths": "System design, communication",
    "weaknesses": "Limited large-scale database experience",
    "report_url": "https://app.your-product.com/sessions/f47ac10b",
    "raw": { "rank": "3/27", "cefr": "B2" }
  }'

# → { "data": { "assessment": { … },
#               "routing": { "action": "moved", "outcome": "passed",
#                            "to_stage": "Passed Interview" } } }
FieldTypeMeaning
score0–100

Overall score, on your own scale normalised to 0–100

passedboolean

Your recommendation. Omit it to let the customer's pass mark decide

summarytext

The conclusion recruiters read first

report_urlhttps

Your full report, transcript or recording

rawJSON

Anything else, up to 256 KB, kept for recruiters

disclosureobject

What you told the candidate before starting: text, and optionally version, language, mode, shown_at. Recruiters see it as “What they were told”

preferred_humanboolean

With status: "declined": the candidate asked to speak with a person. It is never scored, and the hiring team is told

Candidates must know they are talking to an AI and must be able to ask for a person instead. Send the words you showed them as disclosure with your first post, and when someone asks for a person, post status: "declined" with preferred_human: true.

Asked for a person
curl -X POST https://kumohr.com/api/v1/ats/assessments \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "application_id": "8a3f…",
    "provider": "your-product",
    "external_id": "session-f47ac10b",
    "status": "declined",
    "preferred_human": true,
    "disclosure": {
      "text": "You are about to speak with an AI interviewer. A person on the hiring team reviews every interview, and you can ask for a person instead.",
      "version": "2026-10",
      "language": "en",
      "mode": "voice"
    }
  }'
Step 4

Kumo moves the card, and recruiters stay in control.

The customer sets a route for your stage: where a pass goes and where a fail goes, optionally with a pass mark. A completed result moves the application once. It uses passed when you send it, otherwise the score against the pass mark.

routing.actionWhenWhat happens
movedPass or fail known

The application moves to the pass or fail stage, and the move is logged as made by your integration

needs_reviewNo pass signal

No passed and no pass mark. The card waits for a recruiter

noneNothing to do

Not completed yet, no route set, already routed, or a recruiter already moved the card

Set a route (workspace admin)
curl -X POST https://kumohr.com/api/v1/ats/assessment-routes \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_stage": "AI Interview",
    "pass_stage": "Passed Interview",
    "fail_stage": "Failed Interview",
    "min_score": 70,
    "provider": "your-product"
  }'
A recruiter move always wins

If a recruiter has already moved the card out of your stage, your result is still recorded and shown, but the card stays where the recruiter put it.

Candidates and jobs

Read the board, or feed it.

Applicant tracking systems, sourcing tools and job boards can add candidates, with their CVs, and applications directly. Send source_system (for example "greenhouse") and external_id (the candidate's id in your system): sending the same pair again updates that candidate, even if their email changed. Without them, Kumo matches on email. Either way, a match is updated with the fields you send and returned with created: false.

Attach the CV with resume_url, an https link Kumo downloads once, or resume_base64. Kumo accepts PDF and Word files up to 10 MB, checked by their content. Requests are limited to about 4.5 MB, so send larger CVs as links. A new or changed CV is scored against the job and the company; sending the same CV again does not re-score it. Pass job_id to apply them to a job, and stage when creating an application to place it in a stage.

Add or update a candidate, with their CV
curl -X POST https://kumohr.com/api/v1/ats/candidates \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane", "last_name": "Doe",
    "email": "jane.doe@example.com",
    "current_title": "Backend Engineer",
    "source_system": "client-ats",
    "external_id": "cand_48213",
    "resume_url": "https://files.client-ats.com/cv/48213.pdf",
    "job_id": "5d02…"
  }'

To move a whole talent pool or a day's applicants at once, send up to 100 candidates per call. One bad candidate never fails the rest; each result says what happened.

Up to 100 at once
curl -X POST https://kumohr.com/api/v1/ats/candidates/import \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source_system": "client-ats",
    "job_id": "5d02…",
    "candidates": [
      { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com",
        "external_id": "cand_48213", "resume_url": "https://files.client-ats.com/cv/48213.pdf" },
      { "first_name": "Budi", "last_name": "Santoso", "email": "budi@example.com",
        "external_id": "cand_48214" }
    ]
  }'

# → { "data": { "results": [ { "index": 0, "ok": true, "created": true, "resume": "attached", … }, … ],
#              "summary": { "received": 2, "created": 2, "updated": 0, "failed": 0 } } }
Pipeline stages for a job
curl "https://kumohr.com/api/v1/ats/pipeline/stages?job_id=5d02…" \
  -H "Authorization: Bearer $KUMO_TOKEN"
Candidates from an ATS

Any applicant tracking system can feed Kumo, in its own format.

A customer who keeps their applicant tracking system can connect it without anyone writing code against Kumo's API. Under Settings → Automation → Integrations → ATS inboxes they create an inbox and choose how candidates arrive.

  1. 01
    The ATS sends

    The ATS, or a tool such as Zapier, Make or n8n, posts JSON in its own format to the inbox's private address. Kumo answers 202 at once.

  2. 02
    Or Kumo fetches

    For an ATS that can't send, Kumo calls its candidate list on a schedule, with an access key, an optional "changed since" parameter and page numbers, cursors or next links.

  3. 03
    The fields are matched once

    The first payload is kept as a sample. Kumo suggests where the email, names, CV link, the ATS's candidate id and the job are; an admin confirms with a live preview.

  4. 04
    Every candidate is added or updated

    Through the same rules as the API above: matched on the ATS id, then email, with CVs attached and scored, and placed on the chosen job.

What an ATS sends (any shape)
curl -X POST "https://kumohr.com/api/inbound/ats/{workspace}/{inbox}?key=…" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "candidate.applied",
    "candidate": {
      "id": "cand_48213",
      "name": "Jane Doe",
      "email": "jane.doe@example.com",
      "resume": { "url": "https://files.client-ats.com/cv/48213.pdf" }
    },
    "job": { "code": "ENG-7" }
  }'

# → 202 { "ok": true, "accepted": true, "mapped": true }
Keys and safety

The inbox key is stored only as a hash and can be replaced at any time; deleting an inbox revokes it at once. Kumo only fetches from public https addresses, with time and size limits on every request.

Build with Kumo AI

Create jobs, screening questions and AI interviews from your product.

Everything Kumo's own job creator does is available to you: describe a role and Kumo drafts the ad, hiring criteria, application questions, the stages and an assessment for each stage. You can run the assessments yourself (post results as above) or let Kumo run them — a screening form or a Kumo AI interview — and receive the result as ats.assessment.recorded.

Candidates are always told they are talking to an AI, and every AI interview offers them the choice to speak with a person instead. That choice is never scored and never counts against them: the assessment is marked declined with preferred_human: true, lands in the review queue, and you receive ats.assessment.declined so you can arrange another way to talk. Every Kumo assessment also carries disclosure: the exact words the candidate was shown before starting, the wording version, the language, how they took part and when.

  1. 01
    Draft the job

    POST /api/v1/ats/jobs/draft with a plain-language brief. If a fact Kumo won't guess is missing (title, seniority, where they work) you get needs_input and up to three follow_ups; send the answers back in answers. Nothing is saved.

  2. 02
    Create it

    POST /api/v1/ats/jobs with the (edited) draft. Stages, criteria, application questions and each stage's assessment are saved together; stage assessments become automations that send on arrival and route passes on. Approval policies still apply.

  3. 03
    Approve AI-written answers

    Screening forms Kumo writes start as status: draft and are never sent until a person approves them: POST /api/v1/ats/assessment-templates with id and status: active.

  4. 04
    Publish and send

    POST /api/v1/ats/jobs/publish. Candidates entering an automated stage get their assessment automatically, or send one yourself with POST /api/v1/ats/assessments/send — set send_email: false to get the private link and deliver it in your own product.

Draft a job with Kumo AI
curl -X POST https://kumohr.com/api/v1/ats/jobs/draft \
  -H "Authorization: Bearer $KUMO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "brief": "Senior data engineer for our lending platform. Hybrid in Jakarta, 25-35jt a month. Python, Spark and Airflow." }'
Create it, then send the screening form yourself
curl -X POST https://kumohr.com/api/v1/ats/jobs \
  -H "Authorization: Bearer $KUMO_TOKEN" -H "Content-Type: application/json" \
  -d '{ "draft": { …the draft you received, edited… } }'

curl -X POST https://kumohr.com/api/v1/ats/assessments/send \
  -H "Authorization: Bearer $KUMO_TOKEN" -H "Content-Type: application/json" \
  -d '{ "application_id": "8a3f…", "template_id": "2c1d…", "send_email": false }'
# → { "data": { "link": "https://kumohr.com/assess/…", "expires_at": "…" } }
Fair by construction

Every candidate for a job gets the same core questions. Questions or criteria about age, gender, nationality, religion, marital status, health or appearance are refused. Written answers and interviews are scored only on words the candidate actually wrote or said — a score without real quotes is not trusted and goes to a person. Fails wait for a recruiter by default.

Credits and billing

AI work is billed to the customer's workspace.

Calls that use Kumo's AI draw from the customer workspace's AI credits, never from you. Each charge is recorded against your token, so the customer can see what your integration used. When a workspace is out of credits, AI calls return 402 credits_empty; everything else keeps working.

WorkTypical creditsNotes
draft_job_with_ai8–12

Reading the brief (1), the job and stages (2 each), each stage's assessment (2) and an answer-key check (1).

draft_assessment_with_ai2–3

Forms include the answer-key check.

Screening form0–2 per candidate

Choice, yes/no and number questions are scored in code for free; written answers are one AI call.

Kumo AI interview (text)≈ 14 per candidate

One small call per turn plus scoring; capped by the plan's questions and minutes.

search_candidates_ai0–2

Free when you pass structured skills and locations; 1 to read a plain-language query, 1 more for reasons.

Going live

The customer connects you from their own workspace.

  1. 01
    The customer adds your stage

    They add it to their pipeline, for example "AI Interview", plus the pass and fail stages.

  2. 02
    Their admin creates an integration token

    Under Settings → Automation → Integrations, with the scopes listed above. They send it to you securely.

  3. 03
    You subscribe their workspace

    One POST /api/v1/webhooks with their token, then /webhooks/test.

  4. 04
    They set the route

    Where a pass and a fail go, and the pass mark if you don't send passed.

Revocation is instant

The customer can revoke your token at any time. Calls then return 401 and webhooks stop.

Endpoints

Everything in this guide.

Full request and response shapes are in the endpoint reference and the OpenAPI spec. The same operations are also available as MCP tools at /api/mcp.

EndpointScopeWhat it does
GET /api/v1/meany token

Introspect the token

GET /api/v1/ats/jobsats:read

List jobs

GET /api/v1/ats/candidatesats:read

List or find candidates

POST /api/v1/ats/candidatesats:write

Add a candidate

POST /api/v1/ats/candidates/importats:write

Add or update many candidates

GET /api/v1/ats/applicationsats:read

List applications

POST /api/v1/ats/applicationsats:write

Apply a candidate to a job

POST /api/v1/ats/applications/moveats:write

Move an application to a stage

GET /api/v1/ats/pipeline/stagesats:read

List pipeline stages

POST /api/v1/ats/candidates/searchats:read

Find candidates (AI search)

GET /api/v1/webhookswebhooks:manage

List webhooks

POST /api/v1/webhookswebhooks:manage

Subscribe to events

DELETE /api/v1/webhookswebhooks:manage

Remove a webhook

POST /api/v1/webhooks/testwebhooks:manage

Send a test event

POST /api/v1/ats/assessmentsats:assessments:write

Post an assessment result

GET /api/v1/ats/assessmentsats:read

List assessment results

GET /api/v1/ats/assessment-routesats:read

List assessment routes

POST /api/v1/ats/assessment-routesats:write

Set an assessment route

GET /api/v1/ats/candidates/resumeats:read

Get a candidate’s CV

GET /api/v1/ats/assessments/detailats:read

Get one assessment

POST /api/v1/ats/assessments/decisionats:write

Record a recruiter decision

GET /api/v1/ats/jobs/criteriaats:read

Get a job’s hiring criteria

POST /api/v1/ats/jobs/criteriaats:write

Set a job’s hiring criteria

POST /api/v1/ats/jobs/match-poolats:write

Match the talent pool against a job

GET /api/v1/ats/jobs/match-poolats:read

Get talent-pool match results

POST /api/v1/ats/jobs/match-pool/placeats:write

Add matched candidates to the pipeline

POST /api/v1/ats/jobs/draftats:jobs:write

Draft a job with Kumo AI

POST /api/v1/ats/jobsats:jobs:write

Create a job

POST /api/v1/ats/jobs/publishats:jobs:write

Publish a job

POST /api/v1/ats/jobs/stagesats:jobs:write

Set a job’s hiring stages and assessments

GET /api/v1/ats/assessment-templatesats:read

List assessment templates

POST /api/v1/ats/assessment-templatesats:assessments:manage

Create or update an assessment template

POST /api/v1/ats/assessment-templates/draftats:assessments:manage

Draft an assessment with Kumo AI

POST /api/v1/ats/assessments/sendats:assessments:send

Send a Kumo assessment to a candidate

Recruiting integrations · Kumo API