ZeltoHumans

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 530 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/quotePrice, 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/queueReviewers 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

POST/v1/tasks

Submit a task. Returns immediately with a task id.

FieldRequiredNotes
instructionsyesPlain language, read by a person. Enumerate the questions.
response_schemayesFlat JSON Schema — see below.
audio_urlone ofMust be fetchable now; we copy it immediately.
transcriptone ofCheaper than audio. Send both if you have both.
audio_duration_secondsnoAlways pass it if you know it — tightens the hold.
webhook_urlnoOverrides the account default.
metadatanoEchoed back untouched. Useful for your call id.
modeno"live" (default) or "sandbox".
GET/v1/tasks/:id

Poll 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.

DELETE/v1/tasks/:id

Cancel 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.

Supported
  • Flat object, up to 25 properties
  • string, with or without enum
  • number, integer
  • boolean
  • array of strings, optionally enum'd
  • description — shown to the reviewer
Rejected
  • 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

WhatPrice
Transcript review$0.75
Audio review, first 5 min$2.00
Each additional minute$0.25
Re-review of a wrong answerFree
Quotes, sandbox, polling, cancelsFree

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

POST/v1/tasks/:id/redo

Free. 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"
ToolKey requiredWhat it does
quote_human_reviewnoPrice, ETA, schema check
request_human_reviewlive onlySubmit; sandbox works keyless
get_human_reviewlive onlyPoll status and result
wait_for_human_reviewlive onlyBlock up to 120s
check_credit_balanceyesAvailable 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.

StatusTypeWhat to do
401unauthorizedBad or missing key.
402insufficient_creditsTop up. Do not retry as-is.
400unsupported_schemaRead details.problems, flatten, retry.
422audio_unreachableRe-issue the URL with a longer expiry.
429rate_limitedRespect reset_at. Don't spin.
409conflictState 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 tasks60 per hour per account by default — email to raise it
Quotes600 per hour
Sandbox tasksUnlimited
Audio file size200 MB
Audio duration60 minutes
Schema properties25
Instructions20,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.