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.
# 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 }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.
- 01Card enters your stage
By drag, bulk move, the Kumo agent or the API. All of them emit the same event.
- 02Kumo sends you a signed webhook
It includes the candidate's contact details, the full job description and a link for fetching the CV.
- 03You run your process
Invite the candidate, run the interview or test. Post progress as it happens if you like.
- 04You post the result
Score, recommendation, summary, and a link to your full report.
- 05Kumo routes the card
Pass goes to the customer's pass stage and fail to their fail stage. Recruiters can review and override any move.
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.
WebhooksA receiver and two API callsYou 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 newYou 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.
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.
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.
ats:readReadingJobs, candidates, applications, pipeline stages, assessment results, CV links
ats:assessments:writeResultsPosting assessment results against applications
webhooks:manageEventsSubscribing, listing, testing and removing webhooks
ats:writeOptionalAdding candidates, creating applications, moving cards, setting routes
curl https://kumohr.com/api/v1/me \
-H "Authorization: Bearer $KUMO_TOKEN"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.
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.
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_…" } }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.publishedVerify, 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.
Kumo-Webhook-Id: <delivery id>
Kumo-Webhook-Timestamp: 1790326442
Kumo-Webhook-Signature: v1=<hex of HMAC_SHA256(signing_secret, timestamp + "." + raw_body)>{
"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…"
}
}
}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
}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.
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": "…" } }Re-serializing JSON changes the bytes and breaks the signature. Verify first, then parse.
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_progresscompletedexpireddeclinedcancellederrorcurl -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"
}'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" } } }score0–100Overall score, on your own scale normalised to 0–100
passedbooleanYour recommendation. Omit it to let the customer's pass mark decide
summarytextThe conclusion recruiters read first
report_urlhttpsYour full report, transcript or recording
rawJSONAnything else, up to 256 KB, kept for recruiters
disclosureobjectWhat you told the candidate before starting: text, and optionally version, language, mode, shown_at. Recruiters see it as “What they were told”
preferred_humanbooleanWith 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.
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"
}
}'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.
movedPass or fail knownThe application moves to the pass or fail stage, and the move is logged as made by your integration
needs_reviewNo pass signalNo passed and no pass mark. The card waits for a recruiter
noneNothing to doNot completed yet, no route set, already routed, or a recruiter already moved the card
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"
}'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.
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.
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.
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 } } }curl "https://kumohr.com/api/v1/ats/pipeline/stages?job_id=5d02…" \
-H "Authorization: Bearer $KUMO_TOKEN"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.
- 01The 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.
- 02Or 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.
- 03The 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.
- 04Every 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.
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 }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.
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.
- 01Draft the job
POST /api/v1/ats/jobs/draftwith a plain-languagebrief. If a fact Kumo won't guess is missing (title, seniority, where they work) you getneeds_inputand up to threefollow_ups; send the answers back inanswers. Nothing is saved. - 02Create it
POST /api/v1/ats/jobswith 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. - 03Approve AI-written answers
Screening forms Kumo writes start as
status: draftand are never sent until a person approves them:POST /api/v1/ats/assessment-templateswithidandstatus: active. - 04Publish and send
POST /api/v1/ats/jobs/publish. Candidates entering an automated stage get their assessment automatically, or send one yourself withPOST /api/v1/ats/assessments/send— setsend_email: falseto get the private link and deliver it in your own product.
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." }'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": "…" } }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.
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.
draft_job_with_ai8–12Reading the brief (1), the job and stages (2 each), each stage's assessment (2) and an answer-key check (1).
draft_assessment_with_ai2–3Forms include the answer-key check.
Screening form0–2 per candidateChoice, yes/no and number questions are scored in code for free; written answers are one AI call.
Kumo AI interview (text)≈ 14 per candidateOne small call per turn plus scoring; capped by the plan's questions and minutes.
search_candidates_ai0–2Free when you pass structured skills and locations; 1 to read a plain-language query, 1 more for reasons.
The customer connects you from their own workspace.
- 01The customer adds your stage
They add it to their pipeline, for example "AI Interview", plus the pass and fail stages.
- 02Their admin creates an integration token
Under Settings → Automation → Integrations, with the scopes listed above. They send it to you securely.
- 03You subscribe their workspace
One
POST /api/v1/webhookswith their token, then/webhooks/test. - 04They set the route
Where a pass and a fail go, and the pass mark if you don't send
passed.
The customer can revoke your token at any time. Calls then return 401 and webhooks stop.
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.
GET /api/v1/meany tokenIntrospect the token
GET /api/v1/ats/jobsats:readList jobs
GET /api/v1/ats/candidatesats:readList or find candidates
POST /api/v1/ats/candidatesats:writeAdd a candidate
POST /api/v1/ats/candidates/importats:writeAdd or update many candidates
GET /api/v1/ats/applicationsats:readList applications
POST /api/v1/ats/applicationsats:writeApply a candidate to a job
POST /api/v1/ats/applications/moveats:writeMove an application to a stage
GET /api/v1/ats/pipeline/stagesats:readList pipeline stages
POST /api/v1/ats/candidates/searchats:readFind candidates (AI search)
GET /api/v1/webhookswebhooks:manageList webhooks
POST /api/v1/webhookswebhooks:manageSubscribe to events
DELETE /api/v1/webhookswebhooks:manageRemove a webhook
POST /api/v1/webhooks/testwebhooks:manageSend a test event
POST /api/v1/ats/assessmentsats:assessments:writePost an assessment result
GET /api/v1/ats/assessmentsats:readList assessment results
GET /api/v1/ats/assessment-routesats:readList assessment routes
POST /api/v1/ats/assessment-routesats:writeSet an assessment route
GET /api/v1/ats/candidates/resumeats:readGet a candidate’s CV
GET /api/v1/ats/assessments/detailats:readGet one assessment
POST /api/v1/ats/assessments/decisionats:writeRecord a recruiter decision
GET /api/v1/ats/jobs/criteriaats:readGet a job’s hiring criteria
POST /api/v1/ats/jobs/criteriaats:writeSet a job’s hiring criteria
POST /api/v1/ats/jobs/match-poolats:writeMatch the talent pool against a job
GET /api/v1/ats/jobs/match-poolats:readGet talent-pool match results
POST /api/v1/ats/jobs/match-pool/placeats:writeAdd matched candidates to the pipeline
POST /api/v1/ats/jobs/draftats:jobs:writeDraft a job with Kumo AI
POST /api/v1/ats/jobsats:jobs:writeCreate a job
POST /api/v1/ats/jobs/publishats:jobs:writePublish a job
POST /api/v1/ats/jobs/stagesats:jobs:writeSet a job’s hiring stages and assessments
GET /api/v1/ats/assessment-templatesats:readList assessment templates
POST /api/v1/ats/assessment-templatesats:assessments:manageCreate or update an assessment template
POST /api/v1/ats/assessment-templates/draftats:assessments:manageDraft an assessment with Kumo AI
POST /api/v1/ats/assessments/sendats:assessments:sendSend a Kumo assessment to a candidate