Developer docs

Ask fifty verified people a question from your code or your agent, and get a distribution back in about ten minutes. Everything is plain JSON over HTTPS.

Quick start

  1. Sign in at 50heads.com/app/developers and make an API key. It starts fh_live_ and is shown once.
  2. Estimate the question. It's free and never spends.
  3. Ask it with an idempotency key, then wait for the result.
curl https://api.50heads.com/v1/billing/estimate \
  -H "Content-Type: application/json" \
  -d '{"draft":{"type":"single_choice","text":"Which menu would you order from tonight?",
       "options":[{"label":"Menu A"},{"label":"Menu B"}],"n":50,"tier":1}}'

curl https://api.50heads.com/v2/questions \
  -H "Authorization: Bearer $FIFTYHEADS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"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"}}'

curl "https://api.50heads.com/v2/questions/q_8k2f/wait?seconds=25" \
  -H "Authorization: Bearer $FIFTYHEADS_API_KEY"

Question types

draft.type is one of single_choice, multi_choice, ab_image, pairwise, scale_1_5, ranking, yes_no, yes_mostly_no, free_text, click_test, reaction or rate_each. The reference below lists every field; three shapes are worth a sample.

Rate each: several statements on one scale

Two to eight statements travel as options (text only), and heads rate each from 1 to 5 in one go. scaleLabels says what 1 and 5 mean; it is also accepted on scale_1_5. Each statement comes back in distribution[] with its mean (1 to 5), ratings (how many chose 1 to 5) and share (who chose 4 or 5). Priced like ranking (format × 1.3).

{
  "draft": {
    "type": "rate_each",
    "text": "How far do you agree about this homepage?",
    "language": "en",
    "options": [
      {
        "label": "It is easy to read"
      },
      {
        "label": "It looks trustworthy"
      },
      {
        "label": "I know what to do next"
      }
    ],
    "scaleLabels": {
      "low": "Disagree",
      "high": "Agree"
    },
    "stimulus": {
      "imageUrl": "https://uploads.50heads.com/q/…/home.png"
    },
    "n": 50,
    "tier": 1
  }
}

Up to four images on an option

options[].imageUrls holds 1 to 4 https URLs in the order heads swipe through them; imageUrl is kept as the first so older clients still show one. Every image is priced, and the answer is accepted only once every image has been seen. Not with a five-second test (stimulus.exposureMs) and not on rate_each.

{
  "draft": {
    "type": "single_choice",
    "text": "Which box would you pick up in a shop?",
    "language": "en",
    "options": [
      {
        "label": "Box A",
        "imageUrls": [
          "https://…/a-front.jpg",
          "https://…/a-back.jpg",
          "https://…/a-side.jpg"
        ]
      },
      {
        "label": "Box B",
        "imageUrls": [
          "https://…/b-front.jpg",
          "https://…/b-back.jpg",
          "https://…/b-side.jpg"
        ]
      }
    ],
    "n": 50,
    "tier": 1
  }
}

Mockups: a store listing or a search result from your details

POST /v1/mockups draws a listing or a search_result from a title, a price, a rating, a review count and an image, in 50heads styling (never a marketplace's), and stores the PNG like an upload. Use the returned imageUrl on an option or the stimulus within 15 minutes. It needs questions:write and counts against the upload rate limit.

{
  "kind": "listing",
  "title": "Oak desk lamp, warm white, USB-C",
  "price": {
    "amount": 39,
    "currency": "GBP"
  },
  "rating": 4.6,
  "reviews": 1284,
  "imageUrl": "https://…/lamp.jpg",
  "line": "Free delivery"
}

{
  "kind": "listing",
  "imageUrl": "https://uploads.50heads.com/q/…/upl_m4k2.png",
  "width": 1200,
  "height": 1600,
  "expiresAt": "2026-10-13T11:05:00Z"
}

Base URL and versions

https://api.50heads.com. Question and set endpoints are under /v2; pricing, config, drafts, uploads, projects, saved audiences, billing, webhooks and apps under /v1. The reference below lists every route a key or token can call, and the few portal-only routes it documents on purpose (saved audiences), each marked with who can call it. The same catalogue is the OpenAPI 3.1 document at https://api.50heads.com/openapi.json. We add fields without notice and never remove or rename one within a version.

Authentication

Send Authorization: Bearer fh_live_…. Keys carry scopes and a daily spend cap, set when you make them. Keys are for servers, CI and scripts: requests from a browser origin are refused. MCP hosts use OAuth instead; see MCP server.

ScopeGrants
questions:readResults, answers, insights, exports, heatmaps, question and set lists, projects, labels, saved drafts and share links
questions:writeEstimate, ask, add heads, edit, cancel, rate, dispute, share, flag or pin an answer, sets, projects, drafts and uploads
templates:readTemplates (also available without a key)
account:readBalance, ledger, invoices, statements, bank transfers, billing profile, invoice terms, disputes, the account itself (GET /v1/me), and what is left under the cap
webhooks:readWebhook endpoints, deliveries and attempts (only when asked for by name)
webhooks:writeCreate, change, rotate, test, replay and delete webhook endpoints (only when asked for by name)
apps:readThe Slack workspaces 50heads is added to, with each one's daily cap and today's spend (only when asked for by name)
apps:writeChange a Slack workspace's daily cap, or remove it (only when asked for by name)
organization:readThe organization: workspaces, domains, sign-in policies, single sign-on, SCIM settings and service accounts (organization admins; only when asked for by name)
organization:writeAdd, check and remove domains; change sign-in policies and SCIM settings (organization admins; only when asked for by name). Never the single sign-on connection
members:readA member's live sessions (organization admins; only when asked for by name)
members:writeSign a member out everywhere (organization admins; only when asked for by name)
audit:readThe organization's audit events, as a list or a JSONL or CSV export (organization admins; only when asked for by name)
events:readThe event stream: the account's events, oldest first with a stable cursor, and every workspace's for organization admins and service accounts (only when asked for by name)

A key with no scopes stored (made before scopes existed) holds exactly questions:read, questions:write, templates:read and account:read, and never a scope added since. No existing key or grant gains a scope when we add one.

Organization scopes

Being introduced for organizations that ask for it. The organization routes take the scope named on each one in the reference. A person's key or OAuth token acts with that person's role at the time of the request: if they stop being an organization admin, the admin routes refuse the key with organization_admin_required. Changes made with a scope need no re-authentication and are recorded in the audit log as api_key, oauth or service_account. Where the organization does not have them yet, keys get session_required.

These acts always need a person on a browser session who has just confirmed it is them, whatever the scopes:

  • Setting up or changing the single sign-on connection, and turning it on or off (PUT, PATCH /v1/organizations/current/sso)
  • Making single sign-on required or not (PATCH /v1/organizations/current/sso/enforce)
  • Testing single sign-on (POST /v1/organizations/current/sso/test)
  • Adding or removing break-glass owners (/v1/organizations/current/sso/break-glass)
  • Making or replacing SCIM tokens (POST /v1/organizations/current/scim/tokens)
  • Changing how long audit events are kept (PUT /v1/organizations/current/audit/retention)
  • Placing or releasing legal holds (/v1/organizations/current/audit/holds)
  • Handing a workspace to a new owner (POST /v1/organizations/current/workspaces/{id}/owner)
  • Making, changing, turning off or deleting service accounts (/v1/organizations/current/service-accounts)
  • Adding, changing, pausing or deleting warehouse export destinations, and their bucket keys (/v1/organizations/current/warehouse/destinations)
  • Making, rotating or revoking API keys (/v2/keys)
  • Inviting, removing or changing team members (/v1/team)
  • Adding payment methods and buying credits (/v1/billing/checkout)
  • Changing your sign-in email, unlinking Apple or Google, removing your saved card or deleting your account (/v1/me, /v1/billing/payment-method)

Service accounts and client_credentials

A service account is a machine identity your organization owns, not a person: an agent that keeps working when the person who set it up leaves. An organization admin makes one in the portal (Settings, Organization, Service accounts), with its scopes, a daily cap and an optional per-question cap in credits, and the workspace it acts in. It never appears as a team member.

It gets a token from https://auth.50heads.com/token with grant_type=client_credentials, authenticating with its client secret (HTTP Basic, or client_id and client_secret in the body) or with a private_key_jwt assertion against the public keys registered for it. Tokens last an hour; there is no refresh token. Send scope to narrow the token, never to widen it.

curl -s https://auth.50heads.com/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials -d scope="questions:read questions:write"

# {"access_token":"eyJ…","token_type":"Bearer","expires_in":3600,"scope":"questions:read questions:write"}

curl -s https://api.50heads.com/v2/questions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{"draft":{"type":"single_choice","text":"Which name would you remember?","options":[{"label":"Pip"},{"label":"Moss"}],"n":20}}'

Asks go through the same service as keys, so the caps, Idempotency-Key, credits and results behave exactly the same. Turning the service account off or deleting it fails its tokens on their next request. A wrong secret, an unknown client and a service account that is off all answer invalid_client; other clients asking for client_credentials get unauthorized_client.

For private_key_jwt, the assertion is signed with ES256 or RS256, its iss and sub are the client id, its aud is the token endpoint or the issuer, it is at most five minutes old, and each jti is accepted once. Rotating a client secret keeps the old one working for an overlap you choose (24 hours by default, up to 168).

Money

Everything is in credits, as integers, and the portal and the apps show what an ask costs and what a balance holds in credits too. Asking reserves the full price (per answer × heads); answers not given are refunded when the question closes, is canceled or is refused.

Idempotency

POST /v2/questions needs an Idempotency-Key header, a UUID you make per question. If a request times out, send it again with the same key: you get the original question back, never a second ask. Keys are kept for 24 hours. The same key with a different body is idempotency_conflict.

Errors

Errors are JSON: { "error": { "code": "…", "message": "…" } }. The message is one plain sentence you can show a person or hand to a model. Branch on code, never on the message.

CodeHTTPMeaning
validation400The body or query failed validation. error.validation lists each field and a fix.
unauthorized401No key or token, or it was revoked. MCP responses carry WWW-Authenticate with the resource metadata URL.
insufficient_scope403The key or token lacks the scope this call needs. Re-authorize with the scope named in the error.
not_found404No such question, draft or result, or it isn't yours.
idempotency_conflict409The Idempotency-Key was used with a different body in the last 24 hours.
insufficient_credits402Not enough credits for the full price. The error says how many are needed and links to top up.
spend_cap_reached402The ask would take this key or connection past its daily, monthly or per-question cap. The error says what is left and when it resets. MCP calls report it as spend_cap_exceeded.
content_refused422The question breaks the acceptable use policy. Nothing is spent; the error names the category.
rate_limited429Too many requests. Wait for Retry-After seconds.
unavailable503Asking is paused for maintenance. The error says until when.

Rate limits

60 requests a minute per key or connection, and 10 asks a minute. Estimates count half. Over the limit you get 429 with Retry-After. Ask us to raise a key's limits.

Pagination

Lists return nextCursor. Pass it back as cursor for the next page; it is null on the last one.

Waiting for results

Don't poll. GET /v2/questions/{id}/wait?seconds=25 holds the request until the question is answered or 25 seconds pass; call it again while done is false. Or register a webhook and we'll tell you.

Keyboard shortcuts

Press ? on any page to see these. They are off while you type in a box.

General

Show keyboard shortcuts
?
Search help
/
Ask a question
n
Close menus
Esc

Go to

Home
gthenh
Pricing
gthenp
How it works
gthenw
How we verify
gthenv
For AI agents
gthena
For teams
gthent
Earn
gthene
Blog
gthenb
API docs
gthend
Your account
gtheno