SDKs and examples
The API is small enough to call directly. These examples estimate, ask and wait for one question.
Command line
@50heads/cli is the 50heads command for scripts and CI: ask from flags, a JSON file or a CSV of many questions, wait on the server, and read results and answers. Every command takes --json, and exit codes tell a script what happened. Asks are keyed by a hash of the question, so re-running a script never asks or charges twice.
npx -y @50heads/cli estimate --text "Which name sounds friendlier?" --option Pip --option Moss
# 50 answers, 1,000 credits. 20 credits an answer.
export FIFTYHEADS_API_KEY=fh_live_…
npx -y @50heads/cli ask --text "Which name sounds friendlier?" --option Pip --option Moss --wait
npx -y @50heads/cli ask --csv asks.csv --json > asked.jsonl
npx -y @50heads/cli answers q_123 --q price --all --jsonCommands: ask, estimate, wait and cancel; results, answers and list; projects, labels and file; balance and templates; webhooks (create, update, test, rotate, deliveries, replay, events and an offline verify); apps; read-only org and audit for organization admins; and login, logout, status and schema. 50heads --help lists them, and the CLI README has an example for each. Webhook, apps and organization commands need the matching scope on the key.
Exit codes: 0 done, 2 usage, 3 not signed in, 4 not enough credits or over the cap, 5 the question needs fixing, 6 not found, 7 try again, 8 some bulk asks failed, 124 still live when wait timed out. On your own machine, 50heads login signs in once in the browser, for the CLI and the MCP server alike.
TypeScript
import { randomUUID } from "node:crypto";
const api = "https://api.50heads.com";
const headers = {
Authorization: `Bearer ${process.env.FIFTYHEADS_API_KEY}`,
"Content-Type": "application/json",
};
const draft = {
type: "single_choice",
text: "Which menu would you order from tonight?",
options: [{ label: "Menu A" }, { label: "Menu B" }],
n: 50,
tier: 1,
language: "en",
};
// 1. Price and time, free.
const quote = await fetch(`${api}/v1/billing/estimate`, {
method: "POST",
headers,
body: JSON.stringify({ draft }),
}).then((r) => r.json());
console.log(`${quote.creditsTotal} credits`);
// 2. Ask. The idempotency key makes a retry safe.
const { question } = await fetch(`${api}/v2/questions`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": randomUUID() },
body: JSON.stringify({ draft }),
}).then((r) => r.json());
// 3. Wait on our side, 25 seconds a call, until it is done.
let done = false;
let result;
while (!done) {
({ result, done } = await fetch(`${api}/v2/questions/${question.id}/wait?seconds=25`, {
headers,
}).then((r) => r.json()));
}
console.log(result.summary.note);Python
import os, uuid, requests
API = "https://api.50heads.com"
HEADERS = {"Authorization": f"Bearer {os.environ['FIFTYHEADS_API_KEY']}"}
draft = {
"type": "single_choice",
"text": "Which menu would you order from tonight?",
"options": [{"label": "Menu A"}, {"label": "Menu B"}],
"n": 50,
"tier": 1,
"language": "en",
}
quote = requests.post(f"{API}/v1/billing/estimate", json={"draft": draft}).json()
print(quote["creditsTotal"], "credits")
question = requests.post(
f"{API}/v2/questions",
json={"draft": draft},
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
).json()["question"]
done = False
while not done:
body = requests.get(
f"{API}/v2/questions/{question['id']}/wait", params={"seconds": 25}, headers=HEADERS
).json()
result, done = body["result"], body["done"]
print(result["summary"]["note"])Python and Go SDKs (preview)
Typed SDKs for Python (fiftyheads, Python 3.11 or later) and Go (Go 1.24 or later) are in preview. They are generated from the OpenAPI document, so they cover every endpoint, and they add the parts you would otherwise write yourself:
ask(…).wait(), which waits on the server 25 seconds a call until the question is done.- An
Idempotency-Keyon every ask, kept the same on retries, so a retry never asks twice. - Retries on 429 and 5xx with backoff that respects
Retry-After. A request that is not safe to repeat is not retried after a 5xx. - The
x-50h-bookmarkheader sent back on each call, so you always read your own last write. - Iterators that follow
nextCursor, and typed errors you can branch on bycode. - Webhook verification of both signature formats:
verify_webhookin Python,VerifyWebhookin Go.
from fiftyheads import FiftyHeads
client = FiftyHeads() # reads FIFTYHEADS_API_KEY
draft = {
"type": "single_choice",
"text": "Which menu would you order from tonight?",
"options": [{"label": "Menu A"}, {"label": "Menu B"}],
"n": 50,
"tier": 1,
}
quote = client.estimate(draft) # never spends
print(quote["creditsTotal"], "credits,", quote["etaMinutes"], "minutes estimated")
result = client.ask(draft).wait() # a fresh Idempotency-Key, then waits 25 seconds a call
print(result["summary"]["note"])client := fiftyheads.NewClient() // reads FIFTYHEADS_API_KEY
draft := fiftyheads.QuestionDraft{
Type: "single_choice",
Text: "Which menu would you order from tonight?",
Options: []fiftyheads.QuestionDraftOptionsItem{{Label: "Menu A"}, {Label: "Menu B"}},
N: fiftyheads.Ptr(int64(50)),
Tier: fiftyheads.Ptr(int64(1)),
}
quote, err := client.Estimate(ctx, draft) // never spends
if err != nil {
return err
}
fmt.Println(quote.CreditsTotal, "credits,", quote.ETAMinutes, "minutes estimated")
asked, err := client.Ask(ctx, draft, nil) // a fresh Idempotency-Key
if err != nil {
return err
}
result, err := asked.Wait(ctx, nil) // 25 seconds a call until it is done
if err != nil {
return err
}
fmt.Println(result.Summary.Note)MCP, locally
FIFTYHEADS_API_KEY=fh_live_… npx -y @50heads/mcpThe package is on npm as @50heads/mcp and also carries the 50heads Agent Skill: the rules for writing a short, clear question.