API reference
Send a recording or transcript to a vetted human. Get structured JSON back that validates against a schema you define. Base URL https://humans.zelto.ai.
This is asynchronous. Median turnaround is minutes, not seconds — target 5–30 minutes. Do not block a live call on it. Current capacity is public at /v1/queue.
Quickstart
Sign up for a key, then run this. It costs nothing — mode: "sandbox" returns a schema-valid synthetic result immediately so you can confirm your parsing before spending anything.
curl https://humans.zelto.ai/v1/tasks \
-H "Authorization: Bearer $ZELTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transcript": "AGENT: Are we set for Thursday?\nCALLER: Yes, that works.",
"instructions": "Did the caller confirm the appointment?",
"response_schema": {
"type": "object",
"properties": {
"confirmed": { "type": "boolean" },
"confidence": { "type": "string", "enum": ["low","medium","high"] }
},
"required": ["confirmed", "confidence"]
},
"mode": "sandbox"
}'
Drop mode and it goes to a real person. That needs credits.
Free before paid
Two things cost nothing, and you should use both before you spend a cent.
| POST /v1/quote | Price, ETA, and schema validation for the exact body you're about to submit. Submits nothing. |
| mode: "sandbox" | Full round trip — real validation, real response shape — with a synthetic result. No key needed on MCP. |
| GET /v1/queue | Reviewers online, queue depth, observed median turnaround. No auth. |
A sandbox result is not a human answer. It is labelled in the response, and if you surface it to a user, label it there too.
Tasks
/v1/tasksSubmit a task. Returns immediately with a task id.
| Field | Required | Notes |
|---|---|---|
| instructions | yes | Plain language, read by a person. Enumerate the questions. |
| response_schema | yes | Flat JSON Schema — see below. |
| audio_url | one of | Must be fetchable now; we copy it immediately. |
| transcript | one of | Cheaper than audio. Send both if you have both. |
| audio_duration_seconds | no | Always pass it if you know it — tightens the hold. |
| webhook_url | no | Overrides the account default. |
| metadata | no | Echoed back untouched. Useful for your call id. |
| mode | no | "live" (default) or "sandbox". |
/v1/tasks/:idPoll for status and result. Status is one of queued, in_progress, completed, failed, cancelled.
{
"task_id": "task_7k2m...",
"status": "completed",
"result": {
"demo_booked": false,
"objections": ["incumbent", "price", "authority"],
"confidence": "high"
},
"review": {
"notes": "Not a booking — 'send it over, no promises', and he needs his VP who is out until the 8th.",
"confidence": "high",
"reviewed_by": "Dani R.",
"audio_duration_seconds": 214
},
"price": { "held_cents": 200, "charged_cents": 200 },
"audio": { "retained_until": "2026-08-24T00:00:00Z", "retention_days": 30 }
}Read review.notes. Reviewers put their reasoning there and it frequently contains the thing you actually needed.
/v1/tasks/:idCancel while still queued and the hold is released in full. Once a reviewer has claimed it, cancellation fails with a 409.
Response schemas
A human fills your schema in as a form, so it must be renderable as one. Unsupported schemas are rejected at submit with a message naming the exact path — you will never get a silently mangled form.
- Flat object, up to 25 properties
string, with or withoutenumnumber,integerbooleanarrayof strings, optionally enum'ddescription— shown to the reviewer
- Nested objects — flatten them
- Arrays of objects
oneOf,anyOf,allOf$ref,if/then- More than 25 properties
Two things measurably improve answer quality: put a descriptionon any field where "true" is debatable, and use an enum for confidence rather than a float. No human produces 0.94.
{
"type": "object",
"properties": {
"demo_booked": {
"type": "boolean",
"description": "True only if they committed to a specific time. 'Send me some times' is not a commitment."
},
"objections": {
"type": "array",
"items": { "type": "string", "enum": ["price","integration","timing","authority"] }
},
"confidence": { "type": "string", "enum": ["low","medium","high"] }
},
"required": ["demo_booked", "objections", "confidence"]
}Pricing & credits
| What | Price |
|---|---|
| Transcript review | $0.75 |
| Audio review, first 5 min | $2.00 |
| Each additional minute | $0.25 |
| Re-review of a wrong answer | Free |
| Quotes, sandbox, polling, cancels | Free |
Credits are held when you submit and chargedwhen the task completes, against the duration the reviewer's player actually measured. If you omit audio_duration_seconds we hold against an assumed 10-minute call and release the difference — which ties up more of your balance than necessary, so pass the duration when you have it.
Cancelled tasks and tasks a reviewer reports as unreviewable are never charged. Check GET /v1/balance before a batch.
Webhooks
Set webhook_url per task or a default in the dashboard. On completion we POST the same body as GET /v1/tasks/:id, wrapped in an event envelope, with header zelto-signature: t=<unix>,v1=<hex>.
The signature is an HMAC-SHA256 of `${timestamp}.${rawBody}` keyed with your webhook secret. Verify it, and reject timestamps older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // replay guard
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
We retry up to three times on 5xx and network errors, with backoff. A 4xx is treated as final. Delivery attempts are visible on the task page.
When an answer is wrong
/v1/tasks/:id/redoFree. A different reviewer answers independently, with your reason attached if you send one. The original task is left untouched so you can compare.
curl -X POST https://humans.zelto.ai/v1/tasks/task_7k2m.../redo \
-H "Authorization: Bearer $ZELTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Caller said Unit 7 twice — this says Unit 4."}'
Use this rather than resubmitting as a new task: resubmitting charges you again and loses the link between the two answers. We track redo rate as our primary quality metric, so it is genuinely useful to us.
MCP
claude mcp add --transport http zelto-humans \
https://humans.zelto.ai/mcp \
--header "Authorization: Bearer $ZELTO_API_KEY"
| Tool | Key required | What it does |
|---|---|---|
| quote_human_review | no | Price, ETA, schema check |
| request_human_review | live only | Submit; sandbox works keyless |
| get_human_review | live only | Poll status and result |
| wait_for_human_review | live only | Block up to 120s |
| check_credit_balance | yes | Available and held credits |
Connecting and listing tools works with no key at all, so an agent can discover the server and prove the integration before anyone signs up.
Agent skill
A single versioned file that teaches an agent when a decision warrants a human, how to quote and sandbox for free first, and to report costs before incurring them.
mkdir -p ~/.claude/skills/zelto-humans
curl -sL https://humans.zelto.ai/skill -o ~/.claude/skills/zelto-humans/SKILL.md
Served from our origin and versioned in its frontmatter, so an agent reading it can replace a stale copy on its own. Nothing to install, nothing to upgrade.
Errors
Every error carries a hints array of machine-readable next actions, so an agent that hits a wall can recover without a human reading prose.
| Status | Type | What to do |
|---|---|---|
| 401 | unauthorized | Bad or missing key. |
| 402 | insufficient_credits | Top up. Do not retry as-is. |
| 400 | unsupported_schema | Read details.problems, flatten, retry. |
| 422 | audio_unreachable | Re-issue the URL with a longer expiry. |
| 429 | rate_limited | Respect reset_at. Don't spin. |
| 409 | conflict | State changed — already claimed or completed. |
{
"error": {
"type": "insufficient_credits",
"message": "This task costs $2.00 but only $0.40 is available.",
"docs_url": "https://humans.zelto.ai/docs",
"details": { "required_cents": 200, "available_cents": 40 }
},
"hints": [
{ "action": "Add credits at https://humans.zelto.ai/dashboard/billing, then retry." },
{ "action": "Set \"mode\": \"sandbox\" to run the full loop for free." }
]
}Data handling
Audio is copied to our storage at submit time — customer signed URLs routinely expire before a human opens the task, and a reviewer staring at a dead player helps nobody.
Recordings are deleted after 30 days. Every task reports its own audio.retained_until. Reviewers are under NDA and see only the material attached to the task.
If you are sending recordings from healthcare, finance, or collections, sort out a DPA before you send volume — either a@zelto.ai or book 15 minutes. We would rather have that conversation first than after.
Limits
| Live tasks | 60 per hour per account by default — email to raise it |
| Quotes | 600 per hour |
| Sandbox tasks | Unlimited |
| Audio file size | 200 MB |
| Audio duration | 60 minutes |
| Schema properties | 25 |
| Instructions | 20,000 characters |
The hourly cap exists because reviewers are people. Without it, one overnight backlog loop would swamp the queue and every other customer's turnaround along with it — including yours.
Running steady volume above it is a capacity conversation, not a paywall. Book 15 minutes and we'll raise the cap, staff for your pattern, and agree a turnaround you can actually build against.
Something unclear or missing?
Email a@zelto.ai. A person reads it, which is somewhat the point.
