API reference

Generated from the schemas the API validates against, so a field listed here is a field the API accepts.

OpenAPI 3.1: https://api.50heads.com/openapi.json. Generated from the same schemas, so it never drifts from this page. Load it into Postman, Insomnia or a client generator.

Pricing and config

Unauthenticated and rate limited. Estimate as often as you like; it never spends.

POST/v1/billing/estimate

Price, time and validation for a question draft.

No authentication.

Counts as half a request against the rate limit. validation lists anything that would stop the ask, one issue per field.

Request body (JSON)
FieldTypeNotes
draftobjectrequired
draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
draft.textstringrequired up to 600 characters
draft.contextstring up to 120 characters
draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
draft.optionsobject[] up to 8 items, default []
draft.options[].labelstringrequired up to 40 characters
draft.options[].imageUrlstring (URL)
draft.options[].imageUrlsstring (URL)[] up to 4 items
draft.scaleLabelsobject
draft.scaleLabels.lowstringrequired up to 24 characters
draft.scaleLabels.highstringrequired up to 24 characters
draft.neitherboolean default false
draft.stimulusobject
draft.stimulus.imageUrlstring (URL)
draft.stimulus.audioUrlstring (URL)
draft.stimulus.textstring up to 120 characters
draft.stimulus.exposureMsinteger 2000–10000
draft.ninteger 10–5000, default 50
draft.tier1 | 2 | 3 default 1
draft.rushboolean default false
draft.targetingobject
draft.targeting.countriesstring[] up to 50 items, default []
draft.targeting.tagsstring[] up to 20 items, default []
draft.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
draft.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
draft.targeting.verifiedAgeboolean
draft.clickTestobject
draft.clickTest.maxTapsinteger 1–5, default 1
draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
draft.publicResultsboolean default false
draft.reason"off" | "optional" | "required" default "off"
draft.goRuleobject
draft.goRule.goAbovenumberrequired 0.5–0.95
draft.goRule.minAnswersintegerrequired 5–5000
draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
draft.interestAudienceIdstring up to 40 characters
draft.minGrade1 | 2 | 3
draft.interestPriceobject
draft.interestPrice.perAnswerPenceintegerrequired 1–10000
draft.interestPrice.headShareBpsintegerrequired 0–10000
draft.answeredBy"heads" | "private" default "heads"
draft.openForDays1 | 7 | 14 | 30 default 7
draft.shownAsstring up to 40 characters
surface"app" | "web"
Response (JSON)
FieldTypeNotes
creditsPerAnswerintegerrequired
creditsTotalintegerrequired
payPerAnswerintegerrequired
etaMinutesintegerrequired
traitsintegerrequired
breakdownobject[]required
breakdown[].labelstringrequired
breakdown[].valuestringrequired
breakdownLinestringrequired
validationobject[]required
validation[].fieldstringrequired
validation[].codestringrequired
validation[].messagestringrequired
poolSizeinteger | nullrequired

GET/v1/config

Credit exchange rates, the country table and question languages.

No authentication.

Response (JSON)
FieldTypeNotes
ratesmap of numberrequired
ratesDatestringrequired
countriesmap of objectrequired
defaultCountryobjectrequired
defaultCountry.minWithdrawalPenceintegerrequired
defaultCountry.payoutRailsstring[]required
defaultCountry.otpChannel"sms" | "whatsapp"required
defaultCountry.taxFieldsstring[]required
defaultCountry.tier2Availablebooleanrequired
defaultCountry.currency"GBP" | "USD" | "EUR" | "CAD"required
defaultCountry.displayCurrency"GBP" | "USD" | "EUR" | "CAD" | "JPY" | "KRW" | "BRL" | "TRY" | "SEK" | "DKK" | "NOK" | "PLN" | "CZK" | "RON"
defaultCountry.wiseFieldsobject[]
defaultCountry.wiseFields[].keystringrequired
defaultCountry.wiseFields[].labelstringrequired
defaultCountry.wiseFields[].examplestring
defaultCountry.wiseFields[].patternstring
defaultCountry.requesterRails"card" | "bank_transfer" | "pix" | "swish" | "vipps" | "mobilepay" | "pse"[]
defaultCountry.localestring
defaultCountry.questionLanguagesstring[]
defaultCountry.legalPack"uk" | "gdpr-eu" | "gdpr-eea" | "us" | "ca" | "lgpd" | "appi" | "pipa" | "kvkk" | "cl" | "co" | "ar" | "pe" | "uy" | "other"
defaultCountry.legalPackVersionstring up to 40 characters
defaultCountry.payoutProvenAtstring up to 40 characters
defaultCountry.minAgeinteger 13–21
defaultCountry.state"known" | "answering" | "launched"
questionLanguages"en" | "fr" | "es" | "pt" | "it" | "de" | "nl" | "pl" | "ja" | "ko" | "sv" | "da" | "nb" | "cs" | "ro" | "fi" | "tr"[]required
reverifyDaysintegerrequired
tier1PayPenceintegerrequired
tier2PayMultipliernumberrequired
answeringCountriesstring[]
tremendous"off" | "on" | "default"
webTopUpStorefrontsstring[]
storeCreditPacksobject[]
storeCreditPacks[].productIdstringrequired
storeCreditPacks[].creditsintegerrequired

POST/v1/estimate

A quick price and time from a tier, a number of heads and images.

No authentication.

Never spends. Defaults: tier1, 50 heads, no images. For a real question (targeting, question type, validation) send a draft to POST /v1/billing/estimate; this route also accepts that body when it has a draft. Counts against the same rate limit.

Request body (JSON)
FieldTypeNotes
tier"tier1" | "tier2" | "tier3" default "tier1"
ninteger 10–5000, default 50
imagesinteger 0–4, default 0
Response (JSON)
FieldTypeNotes
estimateobjectrequired
estimate.tier"tier1" | "tier2" | "tier3"required
estimate.nintegerrequired
estimate.imagesintegerrequired
estimate.perAnswerPenceintegerrequired
estimate.totalPenceintegerrequired
estimate.payPerSayPenceintegerrequired
estimate.etaMinutesintegerrequired
linestringrequired
creditsPerAnswerintegerrequired
creditsTotalintegerrequired
payPerAnswerintegerrequired
etaMinutesintegerrequired
priceobjectrequired
poolSizeinteger | nullrequired
validationobject[]required
validation[].fieldstringrequired
validation[].codestringrequired
validation[].messagestringrequired

GET/v1/config/platform

Whether asking through MCP is paused, the minimum app versions and the feature flags.

No authentication.

Read by the MCP server before it asks. Safe to cache for a minute.

Response (JSON)
FieldTypeNotes
killSwitchobjectrequired
killSwitch.pauseAskingbooleanrequired
killSwitch.pauseAnsweringbooleanrequired
killSwitch.messagestring | nullrequired
killSwitch.untilstring | nullrequired
appVersionsobjectrequired
appVersions.minimumIosstringrequired
appVersions.minimumAndroidstringrequired
mcpobjectrequired
mcp.pauseAskbooleanrequired
mcp.pausedClientsstring[]required
mcp.minProtocolVersionstring | nullrequired
mcp.messagestring | nullrequired
flagsmap of objectrequired

GET/v1/templates

Fixed-price question templates.

No authentication.

Response (JSON)
FieldTypeNotes
templatesobject[]required
templates[].idstringrequired
templates[].namestringrequired
templates[].descriptionstringrequired
templates[].draftobjectrequired
templates[].draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
templates[].draft.textstringrequired up to 600 characters
templates[].draft.contextstring up to 120 characters
templates[].draft.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
templates[].draft.optionsobject[]required up to 8 items, default []
templates[].draft.scaleLabelsobject
templates[].draft.neitherbooleanrequired default false
templates[].draft.stimulusobject
templates[].draft.nintegerrequired 10–5000, default 50
templates[].draft.tier1 | 2 | 3required default 1
templates[].draft.rushbooleanrequired default false
templates[].draft.targetingobject
templates[].draft.clickTestobject
templates[].draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
templates[].draft.publicResultsbooleanrequired default false
templates[].draft.reason"off" | "optional" | "required"required default "off"
templates[].draft.goRuleobject
templates[].draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
templates[].draft.interestAudienceIdstring up to 40 characters
templates[].draft.minGrade1 | 2 | 3
templates[].draft.interestPriceobject
templates[].draft.answeredBy"heads" | "private"required default "heads"
templates[].draft.openForDays1 | 7 | 14 | 30required default 7
templates[].draft.shownAsstring up to 40 characters
templates[].priceCreditsintegerrequired

GET/v1/public/countries

Where heads answer from, the languages they read and pool size bands by tier.

No authentication.

Response (JSON)
FieldTypeNotes
countriesobject[]required
countries[].codestringrequired up to 2 characters, at least 2 characters
countries[].namestringrequired
countries[].languagesstring[]required
countries[].tier2Availablebooleanrequired
countries[].poolBandsobjectrequired
countries[].poolBands.tier1string | nullrequired
countries[].poolBands.tier2string | nullrequired
countries[].poolBands.tier3string | nullrequired
updatedAtstringrequired

GET/v1/tags

The self-declared tags you can target at any tier, by group (one trait per group).

No authentication.

Response (JSON)
FieldTypeNotes
groupsobject[]required
groups[].id"life_stage" | "household" | "income" | "education" | "occupation" | "interests" | "habits" | "shopping" | "health" | "media" | "pets" | "tech"required
groups[].labelstringrequired
groups[].whystringrequired
groups[].maxintegerrequired
tagsobject[]required
tags[].idstringrequired
tags[].group"life_stage" | "household" | "income" | "education" | "occupation" | "interests" | "habits" | "shopping" | "health" | "media" | "pets" | "tech"required
tags[].labelstringrequired

GET/v1/public/stats

Heads online now, answers this week and the median time to fifty answers.

No authentication.

Response (JSON)
FieldTypeNotes
headsOnlineNowintegerrequired
answersThisWeekintegerrequired
medianMinutesTo50numberrequired
headEarningsPerHourobjectrequired
headEarningsPerHour.lowintegerrequired
headEarningsPerHour.highintegerrequired
updatedAtstringrequired

GET/v1/public/subprocessors

Upcoming and past changes to our subprocessors, with their effective dates.

No authentication.

Cacheable for an hour. We email every account holder before adding or changing a subprocessor, and give at least 30 days' notice (noticeDays). upcoming holds announced changes that have not taken effect, soonest first; history holds the rest, newest first. Drafts are never listed. The current list itself is on the subprocessors page.

Response (JSON)
FieldTypeNotes
upcomingobject[]required
upcoming[].idstringrequired
upcoming[].kind"add" | "remove" | "change"required
upcoming[].namestringrequired
upcoming[].purposestringrequired
upcoming[].dataCategoriesstringrequired
upcoming[].locationstringrequired
upcoming[].transferMechanismstringrequired
upcoming[].linkstring | nullrequired
upcoming[].status"announced" | "effective" | "withdrawn"required
upcoming[].announcedAtstringrequired
upcoming[].effectiveAtstringrequired
upcoming[].withdrawnAtstring | nullrequired
historyobject[]required
history[].idstringrequired
history[].kind"add" | "remove" | "change"required
history[].namestringrequired
history[].purposestringrequired
history[].dataCategoriesstringrequired
history[].locationstringrequired
history[].transferMechanismstringrequired
history[].linkstring | nullrequired
history[].status"announced" | "effective" | "withdrawn"required
history[].announcedAtstringrequired
history[].effectiveAtstringrequired
history[].withdrawnAtstring | nullrequired
noticeDaysintegerrequired
updatedAtstringrequired

Status, identity and limits

Read these before you spend: whether 50heads is working and asking is open, who your key or grant acts as, and the rate limits on it. Every response that counts against a limit carries RateLimit-Policy, RateLimit, RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers; a 429 also carries Retry-After.

GET/v1/status

Whether 50heads is working, and whether asking, answering, MCP asking or top-ups are paused.

No authentication.

Cached for 15 seconds. 503 with the same body when the database is not answering. An agent should not ask while asking or mcp_asking is paused; nothing is charged while paused.

Response (JSON)
FieldTypeNotes
status"operational" | "degraded" | "paused" | "outage"required
componentsobject[]required
components[].id"api" | "database" | "asking" | "answering" | "mcp_asking" | "top_ups"required
components[].status"operational" | "degraded" | "paused" | "outage"required
components[].notestring | nullrequired
messagestring | nullrequired
untilstring | nullrequired
checkedAtstringrequired
pagestringrequired

GET/v1/whoami

Who this credential is, its scopes, its connection's caps and spend, and the account and organization it acts in.

API key or OAuth token.

Any key, connected app, service account or signed-in person; no scope needed. organization is null unless organizations are switched on for the team (and, for keys and apps, machine identities too).

Response (JSON)
FieldTypeNotes
principalobjectrequired
principal.type"person" | "service_account"required
principal.idstringrequired
principal.emailstring | nullrequired
auth"session" | "api_key" | "oauth" | "device"required
scopesstring[] | nullrequired
connectionobject | nullrequired
connection.idstringrequired
connection.kind"oauth" | "key" | "app"required
connection.clientIdstring | nullrequired
connection.clientNamestringrequired
connection.hoststring | nullrequired
connection.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
connection.capDailyPenceintegerrequired
connection.spentTodayPenceintegerrequired
connection.spentMonthPenceintegerrequired
connection.protocolVersionstring | nullrequired
connection.validatesIssboolean | nullrequired
connection.createdAtstringrequired
connection.lastUsedAtstring | nullrequired
connection.capMonthlyPenceinteger | null
connection.capQuestionPenceinteger | null
connection.teamIdstring | null
accountobjectrequired
account.kind"personal" | "team"required
account.teamIdstring | nullrequired
account.teamNamestring | nullrequired
account.role"owner" | "admin" | "member" | "viewer" | "billing" | nullrequired
organizationobject | nullrequired
organization.idstringrequired
organization.namestringrequired
organization.adminbooleanrequired

GET/v1/limits

The rate limits on this account and connection, with what is used in the current window.

API key or OAuth token.

Any credential; no scope needed. Counts are approximate (a brake on runaway loops, not a quota). The MCP server's own limits are listed with used: null.

Response (JSON)
FieldTypeNotes
policiesobject[]required
policies[].idstringrequired
policies[].appliesTostringrequired
policies[].per"account" | "connection"required
policies[].limitintegerrequired
policies[].windowSecondsintegerrequired
policies[].usedinteger | nullrequired
policies[].remaininginteger | nullrequired
policies[].resetSecondsintegerrequired
notestringrequired

Questions

Ask, wait and read results. Asking reserves the full price from your credits; answers not given are refunded when the question closes.

POST/v2/questions

Ask a question.

API key or OAuth token with questions:write. Idempotency-Key header required.

Requires an Idempotency-Key header (a UUID). The same key returns the same question for 24 hours; a different body with the same key is idempotency_conflict. Fails with insufficient_credits, spend_cap_reached or content_refused without spending anything. The portal and app are reminded once with 422 quality_reminder (nothing stored or charged) when the wording review has a strong suggestion the writer has not taken; ask again with acknowledgeQuality: true and the same key to send it as written. The API and MCP are never reminded.

Request body (JSON)
FieldTypeNotes
draftobjectrequired
draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
draft.textstringrequired up to 600 characters
draft.contextstring up to 120 characters
draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
draft.optionsobject[] up to 8 items, default []
draft.options[].labelstringrequired up to 40 characters
draft.options[].imageUrlstring (URL)
draft.options[].imageUrlsstring (URL)[] up to 4 items
draft.scaleLabelsobject
draft.scaleLabels.lowstringrequired up to 24 characters
draft.scaleLabels.highstringrequired up to 24 characters
draft.neitherboolean default false
draft.stimulusobject
draft.stimulus.imageUrlstring (URL)
draft.stimulus.audioUrlstring (URL)
draft.stimulus.textstring up to 120 characters
draft.stimulus.exposureMsinteger 2000–10000
draft.ninteger 10–5000, default 50
draft.tier1 | 2 | 3 default 1
draft.rushboolean default false
draft.targetingobject
draft.targeting.countriesstring[] up to 50 items, default []
draft.targeting.tagsstring[] up to 20 items, default []
draft.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
draft.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
draft.targeting.verifiedAgeboolean
draft.clickTestobject
draft.clickTest.maxTapsinteger 1–5, default 1
draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
draft.publicResultsboolean default false
draft.reason"off" | "optional" | "required" default "off"
draft.goRuleobject
draft.goRule.goAbovenumberrequired 0.5–0.95
draft.goRule.minAnswersintegerrequired 5–5000
draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
draft.interestAudienceIdstring up to 40 characters
draft.minGrade1 | 2 | 3
draft.interestPriceobject
draft.interestPrice.perAnswerPenceintegerrequired 1–10000
draft.interestPrice.headShareBpsintegerrequired 0–10000
draft.answeredBy"heads" | "private" default "heads"
draft.openForDays1 | 7 | 14 | 30 default 7
draft.shownAsstring up to 40 characters
fromDraftIdstring
thenobject[] up to 3 items
then[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each" default "free_text"
then[].textstringrequired up to 600 characters, at least 8 characters
then[].optionsobject[] up to 8 items, default []
then[].options[].labelstringrequired up to 40 characters
then[].options[].imageUrlstring (URL)
then[].options[].imageUrlsstring (URL)[] up to 4 items
then[].ninteger 10–5000, default 20
then[].tier1 | 2 | 3 default 2
then[].minConfidence"low" | "medium" | "high"
then[].reason"off" | "optional" | "required"
variantOfstring
followUpOfstring up to 64 characters
templateIdstring
projectIdstring | null
labelsstring[] up to 10 items
externalRefstring up to 100 characters, pattern ^[\w.:\-/#@+]+$
privateDeclarationobject
privateDeclaration.versionintegerrequired ≥ 1
acknowledgeQualityboolean
Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null

GET/v2/questions

Your questions, newest first.

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"
sincestring
limitinteger 1–100
cursorstring
projectIdstring
labelstring
externalRefstring
archived"exclude" | "only" | "include"
bookmarkedboolean
mineboolean
setIdstring
Response (JSON)
FieldTypeNotes
questionsobject[]required
questions[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
questions[].textstringrequired up to 600 characters
questions[].contextstring up to 120 characters
questions[].languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
questions[].optionsobject[]required up to 8 items, default []
questions[].options[].labelstringrequired up to 40 characters
questions[].options[].imageUrlstring (URL)
questions[].options[].imageUrlsstring (URL)[] up to 4 items
questions[].scaleLabelsobject
questions[].scaleLabels.lowstringrequired up to 24 characters
questions[].scaleLabels.highstringrequired up to 24 characters
questions[].neitherbooleanrequired default false
questions[].stimulusobject
questions[].stimulus.imageUrlstring (URL)
questions[].stimulus.audioUrlstring (URL)
questions[].stimulus.textstring up to 120 characters
questions[].stimulus.exposureMsinteger 2000–10000
questions[].nintegerrequired 10–5000, default 50
questions[].tier1 | 2 | 3required default 1
questions[].rushbooleanrequired default false
questions[].targetingobject
questions[].targeting.countriesstring[]required up to 50 items, default []
questions[].targeting.tagsstring[]required up to 20 items, default []
questions[].targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
questions[].targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
questions[].targeting.verifiedAgeboolean
questions[].clickTestobject
questions[].clickTest.maxTapsintegerrequired 1–5, default 1
questions[].scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
questions[].publicResultsbooleanrequired default false
questions[].reason"off" | "optional" | "required"required default "off"
questions[].goRuleobject
questions[].goRule.goAbovenumberrequired 0.5–0.95
questions[].goRule.minAnswersintegerrequired 5–5000
questions[].contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
questions[].interestAudienceIdstring up to 40 characters
questions[].minGrade1 | 2 | 3
questions[].interestPriceobject
questions[].interestPrice.perAnswerPenceintegerrequired 1–10000
questions[].interestPrice.headShareBpsintegerrequired 0–10000
questions[].answeredBy"heads" | "private"required default "heads"
questions[].openForDays1 | 7 | 14 | 30required default 7
questions[].shownAsstring up to 40 characters
questions[].idstringrequired
questions[].status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
questions[].source"app" | "portal" | "api" | "mcp"required
questions[].answeredintegerrequired
questions[].creditsPerAnswerintegerrequired
questions[].creditsReservedintegerrequired
questions[].creditsSpentintegerrequired
questions[].etaMinutesintegerrequired
questions[].leaderLinestring | nullrequired
questions[].refusedCategorystring | nullrequired
questions[].publicCodestring | nullrequired
questions[].createdAtstringrequired
questions[].liveAtstring | nullrequired
questions[].closedAtstring | nullrequired
questions[].teamIdstring | null
questions[].askedByobject | null
questions[].askedBy.idstringrequired
questions[].askedBy.namestring | nullrequired
questions[].projectIdstring | null
questions[].labelsstring[]
questions[].archivedboolean
questions[].bookmarkedboolean
questions[].externalRefstring | null
questions[].linkobject | null
questions[].link.urlstringrequired
questions[].link.qrSvgUrlstringrequired
questions[].link.qrPngUrlstringrequired
questions[].link.closesAtstring | nullrequired
questions[].link.viewsintegerrequired
questions[].link.startedintegerrequired
questions[].setIdstring | null
questions[].setPositioninteger | null
questions[].setCountinteger | null
nextCursorstring | nullrequired

GET/v2/questions/{id}

One question and its progress.

API key or OAuth token with questions:read.

Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null

PATCH/v2/questions/{id}

Complete early once minAnswers heads have answered, or file it: project, labels, archive, bookmark and a set-once externalRef.

API key or OAuth token with questions:write.

If that many have already answered, the question closes now and what was not answered is refunded. Changing an externalRef once set is refused with 409 external_ref_set.

Request body (JSON)
FieldTypeNotes
minAnswersinteger 1–5000
projectIdstring | null
labelsstring[] up to 10 items
archivedboolean
bookmarkedboolean
externalRefstring up to 100 characters, pattern ^[\w.:\-/#@+]+$
Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null

GET/v2/questions/labels

Labels in use in the account, most used first.

API key or OAuth token with questions:read.

Response (JSON)
FieldTypeNotes
labelsobject[]required
labels[].labelstringrequired
labels[].countintegerrequired

GET/v2/questions/{id}/result

The result so far. Partial while the question is live.

API key or OAuth token with questions:read.

With a filter, the distribution, breakdowns and answers are of matching answers only; nUnfiltered counts them all and suppressed is true when a country or age band matched too few to show. insights is the summary of written answers once there is one.

Query parameters
FieldTypeNotes
optioninteger 0–8
tierinteger 1–3
countrystring up to 2 characters, at least 2 characters
age_bandstring
qstring up to 100 characters
Response (JSON)
FieldTypeNotes
resultobjectrequired
result.questionIdstringrequired
result.status"in_progress" | "complete" | "underfilled" | "canceled"required
result.nRequestedintegerrequired
result.nAcceptedintegerrequired
result.distributionobject[]required
result.distribution[].optionstringrequired
result.distribution[].countintegerrequired
result.distribution[].sharenumberrequired
result.distribution[].averageRanknumber
result.distribution[].meannumber
result.distribution[].ratingsinteger[] up to 5 items
result.distribution[].appearancesinteger
result.distribution[].strengthnumber
result.distribution[].rankinteger
result.distribution[].lownumber
result.distribution[].highnumber
result.meannumber | nullrequired
result.scaleLabelsobject | null
result.scaleLabels.lowstringrequired up to 24 characters
result.scaleLabels.highstringrequired up to 24 characters
result.summaryobjectrequired
result.summary.winnerstring | nullrequired
result.summary.marginnumber | nullrequired
result.summary.confidence"low" | "medium" | "high" | nullrequired
result.summary.notestringrequired
result.summary.suggestedFollowUpstring | nullrequired
result.tierinteger | nullrequired
result.languagestringrequired
result.verificationLinestringrequired
result.provenanceobject
result.provenance.answeredBy"heads" | "private"required
result.provenance.access"app" | "shared_link"required
result.provenance.verifiedbooleanrequired
result.provenance.answersintegerrequired
result.provenance.issuedinteger | nullrequired
result.provenance.denominator"known" | "unknown"required
result.medianSecondsnumber | nullrequired
result.creditsSpentintegerrequired
result.answersobject[]required
result.answers[].optionstring | nullrequired
result.answers[].textstring | nullrequired
result.answers[].translatedTextstring | null
result.answers[].reasonstring | null
result.answers[].translatedReasonstring | null
result.answers[].tierinteger | nullrequired
result.answers[].attestationRefstring | nullrequired
result.answers[].answeredAtstringrequired
result.answers[].pinnedboolean
result.answers[].flagobject | null
result.answers[].tapsobject[]
result.answers[].choiceobject
result.breakdownobject[]
result.breakdown[].tagIdstringrequired
result.breakdown[].labelstringrequired
result.breakdown[].nintegerrequired
result.breakdown[].distributionobject[]required
result.refundCreditsintegerrequired
result.breakdownsobject[]
result.breakdowns[].dimension"country" | "tier" | "age_band" | "gender" | "tag" | "language"required
result.breakdowns[].segmentsobject[]required
result.clicksobject
result.clicks.imageUrlstring | nullrequired
result.clicks.tapsintegerrequired
result.clicks.pointsobject[]required
result.clicks.gridinteger[]required
result.clicks.gridSizeintegerrequired
result.clicks.hotspotsobject[]required
result.clicks.heatmapUrlstring | nullrequired
result.sentimentobject
result.sentiment.positivenumberrequired
result.sentiment.neutralnumberrequired
result.sentiment.negativenumberrequired
result.translationLanguagestring | null
result.insightsobject | null
result.insights.questionIdstringrequired
result.insights.languagestringrequired
result.insights.sourceLanguagestringrequired
result.insights.takeawaystringrequired
result.insights.themesobject[]required up to 8 items
result.insights.findingsobject[] up to 5 items
result.insights.byOptionobject[] up to 9 items
result.insights.followUpobject | null
result.insights.sentimentobjectrequired
result.insights.basedOnintegerrequired
result.insights.answersAtTimeintegerrequired
result.insights.originalobject | nullrequired
result.insights.kind"interim" | "final"required
result.insights.generatedAtstringrequired
result.insights.refreshAfterstring | nullrequired
result.insights.enginestringrequired
result.filterobject
result.filter.optioninteger 0–8
result.filter.tierinteger 1–3
result.filter.countryobject
result.filter.ageBandstring pattern ^[0-9]{2}(-[0-9]{2}|\+)$
result.filter.gender"woman" | "man" | "non_binary"
result.filter.qstring up to 100 characters
result.nUnfilteredinteger
result.suppressedboolean
result.facetsobject
result.facets.tiersobject[]required
result.facets.countriesobject[]required
result.facets.ageBandsobject[]required
result.facets.gendersobject[]
result.verdictobject
result.verdict.verdict"go" | "no_go" | "unclear"required
result.verdict.reason"too_few" | "too_close" | nullrequired
result.verdict.yesSharenumberrequired
result.verdict.lownumberrequired
result.verdict.highnumberrequired
result.verdict.goAbovenumberrequired
result.verdict.minAnswersintegerrequired
result.verdict.nintegerrequired
result.headToHeadobject
result.headToHead.optionsstring[]required
result.headToHead.sharenumber | null[][]required
result.headToHead.ninteger[][]required
result.cohortHashstring

GET/v2/questions/{id}/answers

Matching answers, newest first, a page at a time, with reasons and translations.

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
optioninteger 0–8
tierinteger 1–3
countrystring up to 2 characters, at least 2 characters
age_bandstring
qstring up to 100 characters
limitinteger 1–200
cursorstring up to 200 characters
Response (JSON)
FieldTypeNotes
questionIdstringrequired
filterobjectrequired
filter.optioninteger 0–8
filter.tierinteger 1–3
filter.countryobject
filter.ageBandstring pattern ^[0-9]{2}(-[0-9]{2}|\+)$
filter.gender"woman" | "man" | "non_binary"
filter.qstring up to 100 characters
totalintegerrequired
nUnfilteredintegerrequired
suppressedbooleanrequired
answersobject[]required
answers[].optionstring | nullrequired
answers[].textstring | nullrequired
answers[].translatedTextstring | null
answers[].reasonstring | null
answers[].translatedReasonstring | null
answers[].tierinteger | nullrequired
answers[].attestationRefstring | nullrequired
answers[].answeredAtstringrequired
answers[].pinnedboolean
answers[].flagobject | null
answers[].flag.reason"off_topic" | "low_effort" | "abusive" | "automated"required
answers[].flag.status"open" | "upheld" | "dismissed"required
answers[].flag.refundCreditsintegerrequired
answers[].flag.createdAtstringrequired
answers[].flag.decidedAtstring | nullrequired
answers[].tapsobject[]
answers[].taps[].xnumberrequired 0–1
answers[].taps[].ynumberrequired 0–1
answers[].choiceobject
answers[].choice.indexinteger
answers[].choice.indexesinteger[]
answers[].choice.orderinteger[]
answers[].choice.pairarray up to 2 items
answers[].choice.ratingsinteger[]
translationLanguagestring | nullrequired
nextCursorstring | nullrequired

GET/v2/questions/{id}/insights

The summary of the written answers: themes with counts and quotes, sentiment, a takeaway.

API key or OAuth token with questions:read.

Made by a machine from the reasons and free text, never from who answered; counts and quotes are computed from the answers. Written in the account's language, with the question-language original.

Response (JSON)
FieldTypeNotes
insightsobject | nullrequired
insights.questionIdstringrequired
insights.languagestringrequired
insights.sourceLanguagestringrequired
insights.takeawaystringrequired
insights.themesobject[]required up to 8 items
insights.themes[].labelstringrequired
insights.themes[].countintegerrequired
insights.themes[].sharenumberrequired
insights.themes[].sentiment"positive" | "neutral" | "negative"required
insights.themes[].optionstring | nullrequired
insights.themes[].quotesstring[]required up to 3 items
insights.findingsobject[] up to 5 items
insights.findings[].textstringrequired
insights.findings[].countintegerrequired
insights.findings[].optionstring | nullrequired
insights.findings[].quotesstring[]required up to 3 items
insights.byOptionobject[] up to 9 items
insights.byOption[].optionstringrequired
insights.byOption[].nintegerrequired
insights.byOption[].likesobject[]required up to 4 items
insights.byOption[].dislikesobject[]required up to 4 items
insights.followUpobject | null
insights.followUp.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
insights.followUp.textstringrequired
insights.followUp.optionsstring[]required up to 8 items
insights.followUp.nintegerrequired 10–5000
insights.followUp.tier1 | 2 | 3required
insights.followUp.reason"off" | "optional" | "required"required
insights.sentimentobjectrequired
insights.sentiment.positiveintegerrequired
insights.sentiment.neutralintegerrequired
insights.sentiment.negativeintegerrequired
insights.basedOnintegerrequired
insights.answersAtTimeintegerrequired
insights.originalobject | nullrequired
insights.original.takeawaystringrequired
insights.original.themesobject[]required
insights.original.findingsobject[]
insights.original.followUpobject | null
insights.kind"interim" | "final"required
insights.generatedAtstringrequired
insights.refreshAfterstring | nullrequired
insights.enginestringrequired

POST/v2/questions/{id}/insights

Make a fresh summary now: once every 10 minutes while live; the final one when it closes.

API key or OAuth token with questions:read.

429 rate_limited with retryAfterMs while a recent summary stands; 409 too_few_answers under three written answers.

Response (JSON)
FieldTypeNotes
insightsobject | nullrequired
insights.questionIdstringrequired
insights.languagestringrequired
insights.sourceLanguagestringrequired
insights.takeawaystringrequired
insights.themesobject[]required up to 8 items
insights.themes[].labelstringrequired
insights.themes[].countintegerrequired
insights.themes[].sharenumberrequired
insights.themes[].sentiment"positive" | "neutral" | "negative"required
insights.themes[].optionstring | nullrequired
insights.themes[].quotesstring[]required up to 3 items
insights.findingsobject[] up to 5 items
insights.findings[].textstringrequired
insights.findings[].countintegerrequired
insights.findings[].optionstring | nullrequired
insights.findings[].quotesstring[]required up to 3 items
insights.byOptionobject[] up to 9 items
insights.byOption[].optionstringrequired
insights.byOption[].nintegerrequired
insights.byOption[].likesobject[]required up to 4 items
insights.byOption[].dislikesobject[]required up to 4 items
insights.followUpobject | null
insights.followUp.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
insights.followUp.textstringrequired
insights.followUp.optionsstring[]required up to 8 items
insights.followUp.nintegerrequired 10–5000
insights.followUp.tier1 | 2 | 3required
insights.followUp.reason"off" | "optional" | "required"required
insights.sentimentobjectrequired
insights.sentiment.positiveintegerrequired
insights.sentiment.neutralintegerrequired
insights.sentiment.negativeintegerrequired
insights.basedOnintegerrequired
insights.answersAtTimeintegerrequired
insights.originalobject | nullrequired
insights.original.takeawaystringrequired
insights.original.themesobject[]required
insights.original.findingsobject[]
insights.original.followUpobject | null
insights.kind"interim" | "final"required
insights.generatedAtstringrequired
insights.refreshAfterstring | nullrequired
insights.enginestringrequired

GET/v2/questions/{id}/wait

Hold the request until the question is answered, up to 25 seconds a call.

API key or OAuth token with questions:read.

Call again while done is false. minAnswers completes early once that many heads have answered.

Query parameters
FieldTypeNotes
secondsinteger 1–25
minAnswersinteger ≥ 1
Response (JSON)
FieldTypeNotes
resultobjectrequired
result.questionIdstringrequired
result.status"in_progress" | "complete" | "underfilled" | "canceled"required
result.nRequestedintegerrequired
result.nAcceptedintegerrequired
result.distributionobject[]required
result.distribution[].optionstringrequired
result.distribution[].countintegerrequired
result.distribution[].sharenumberrequired
result.distribution[].averageRanknumber
result.distribution[].meannumber
result.distribution[].ratingsinteger[] up to 5 items
result.distribution[].appearancesinteger
result.distribution[].strengthnumber
result.distribution[].rankinteger
result.distribution[].lownumber
result.distribution[].highnumber
result.meannumber | nullrequired
result.scaleLabelsobject | null
result.scaleLabels.lowstringrequired up to 24 characters
result.scaleLabels.highstringrequired up to 24 characters
result.summaryobjectrequired
result.summary.winnerstring | nullrequired
result.summary.marginnumber | nullrequired
result.summary.confidence"low" | "medium" | "high" | nullrequired
result.summary.notestringrequired
result.summary.suggestedFollowUpstring | nullrequired
result.tierinteger | nullrequired
result.languagestringrequired
result.verificationLinestringrequired
result.provenanceobject
result.provenance.answeredBy"heads" | "private"required
result.provenance.access"app" | "shared_link"required
result.provenance.verifiedbooleanrequired
result.provenance.answersintegerrequired
result.provenance.issuedinteger | nullrequired
result.provenance.denominator"known" | "unknown"required
result.medianSecondsnumber | nullrequired
result.creditsSpentintegerrequired
result.answersobject[]required
result.answers[].optionstring | nullrequired
result.answers[].textstring | nullrequired
result.answers[].translatedTextstring | null
result.answers[].reasonstring | null
result.answers[].translatedReasonstring | null
result.answers[].tierinteger | nullrequired
result.answers[].attestationRefstring | nullrequired
result.answers[].answeredAtstringrequired
result.answers[].pinnedboolean
result.answers[].flagobject | null
result.answers[].tapsobject[]
result.answers[].choiceobject
result.breakdownobject[]
result.breakdown[].tagIdstringrequired
result.breakdown[].labelstringrequired
result.breakdown[].nintegerrequired
result.breakdown[].distributionobject[]required
result.refundCreditsintegerrequired
result.breakdownsobject[]
result.breakdowns[].dimension"country" | "tier" | "age_band" | "gender" | "tag" | "language"required
result.breakdowns[].segmentsobject[]required
result.clicksobject
result.clicks.imageUrlstring | nullrequired
result.clicks.tapsintegerrequired
result.clicks.pointsobject[]required
result.clicks.gridinteger[]required
result.clicks.gridSizeintegerrequired
result.clicks.hotspotsobject[]required
result.clicks.heatmapUrlstring | nullrequired
result.sentimentobject
result.sentiment.positivenumberrequired
result.sentiment.neutralnumberrequired
result.sentiment.negativenumberrequired
result.translationLanguagestring | null
result.insightsobject | null
result.insights.questionIdstringrequired
result.insights.languagestringrequired
result.insights.sourceLanguagestringrequired
result.insights.takeawaystringrequired
result.insights.themesobject[]required up to 8 items
result.insights.findingsobject[] up to 5 items
result.insights.byOptionobject[] up to 9 items
result.insights.followUpobject | null
result.insights.sentimentobjectrequired
result.insights.basedOnintegerrequired
result.insights.answersAtTimeintegerrequired
result.insights.originalobject | nullrequired
result.insights.kind"interim" | "final"required
result.insights.generatedAtstringrequired
result.insights.refreshAfterstring | nullrequired
result.insights.enginestringrequired
result.filterobject
result.filter.optioninteger 0–8
result.filter.tierinteger 1–3
result.filter.countryobject
result.filter.ageBandstring pattern ^[0-9]{2}(-[0-9]{2}|\+)$
result.filter.gender"woman" | "man" | "non_binary"
result.filter.qstring up to 100 characters
result.nUnfilteredinteger
result.suppressedboolean
result.facetsobject
result.facets.tiersobject[]required
result.facets.countriesobject[]required
result.facets.ageBandsobject[]required
result.facets.gendersobject[]
result.verdictobject
result.verdict.verdict"go" | "no_go" | "unclear"required
result.verdict.reason"too_few" | "too_close" | nullrequired
result.verdict.yesSharenumberrequired
result.verdict.lownumberrequired
result.verdict.highnumberrequired
result.verdict.goAbovenumberrequired
result.verdict.minAnswersintegerrequired
result.verdict.nintegerrequired
result.headToHeadobject
result.headToHead.optionsstring[]required
result.headToHead.sharenumber | null[][]required
result.headToHead.ninteger[][]required
result.cohortHashstring
donebooleanrequired

POST/v2/questions/{id}/cancel

Stop a live question and refund the answers not given.

API key or OAuth token with questions:write.

Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null
refundCreditsintegerrequired

POST/v2/questions/{id}/rating

Say how useful the answers were, from 1 to 5.

API key or OAuth token with questions:write.

Once the question has at least one answer (409 nothing_to_rate before). Sending it again replaces the earlier rating.

Request body (JSON)
FieldTypeNotes
usefulnessintegerrequired 1–5
Response (JSON)
FieldTypeNotes
oktruerequired

POST/v2/questions/{id}/share

Turn the public results page on or off.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
publicbooleanrequired
Response (JSON)
FieldTypeNotes
publicCodestring | nullrequired
urlstring | nullrequired

GET/v2/questions/{id}/export.csv

Every answer as CSV: option or text, reason, translations, tier, attestation reference, time, and taps for a click test.

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
optioninteger 0–8
tierinteger 1–3
countrystring up to 2 characters, at least 2 characters
age_bandstring
qstring up to 100 characters

GET/v2/questions/{id}/export.jsonl

Every answer as JSON lines (application/x-ndjson), for scripts and model training: the CSV's fields plus `choice` (option indexes) and `cohortHash`.

API key or OAuth token with questions:read.

One JSON object per answer, each on its own line and newline-terminated; an empty body when no answers match. Takes the same filters as export.csv. Nothing identifies a head.

Query parameters
FieldTypeNotes
optioninteger 0–8
tierinteger 1–3
countrystring up to 2 characters, at least 2 characters
age_bandstring
qstring up to 100 characters

GET/v2/questions/{id}/link/qr.svg

Your audience: the share link's QR code as SVG (quiet zone of four modules, error correction M).

API key or OAuth token with questions:read.

GET/v2/questions/{id}/link/qr.png

Your audience: the share link's QR code as a 1024-pixel PNG.

API key or OAuth token with questions:read.

GET/v2/questions/{id}/export.pdf

A one-page PDF report: the question, the chart, what it means and the answers (pinned first).

API key or OAuth token with questions:read.

GET/v2/questions/{id}/export.png

The chart as a PNG to share: size=square (1080×1080, default), wide (1200×675) or story (1080×1920).

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
size"square" | "wide" | "story"

POST/v2/questions/{id}/heads

Reopen a finished question for n more heads, same wording and targeting; answers merge. dryRun quotes only.

API key or OAuth token with questions:write. Idempotency-Key header required.

Needs an Idempotency-Key unless dryRun. The question must be complete or underfilled.

Request body (JSON)
FieldTypeNotes
nintegerrequired 1–5000
dryRunboolean
Response (JSON)
FieldTypeNotes
questionobject
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null
quoteobjectrequired
quote.nintegerrequired
quote.creditsPerAnswerintegerrequired
quote.creditsTotalintegerrequired
quote.etaMinutesintegerrequired
quote.newTargetintegerrequired
quote.linestringrequired
creditsReservedinteger

POST/v2/questions/{id}/answers/{ref}/flag

Flag one answer, named by its attestation reference, for review. Upheld: removed and refunded.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
reason"off_topic" | "low_effort" | "abusive" | "automated"required
notestring up to 500 characters
Response (JSON)
FieldTypeNotes
flagobjectrequired
flag.reason"off_topic" | "low_effort" | "abusive" | "automated"required
flag.status"open" | "upheld" | "dismissed"required
flag.refundCreditsintegerrequired
flag.createdAtstringrequired
flag.decidedAtstring | nullrequired
flag.idstringrequired
flag.attestationRefstringrequired

POST/v2/questions/{id}/answers/{ref}/pin

Pin an answer to the top of the results and the PDF report, or unpin it.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
pinnedbooleanrequired
Response (JSON)
FieldTypeNotes
pinnedbooleanrequired

POST/v1/public/link/{code}/answers

Your audience: one answer from the browser.

No authentication.

Needs a Turnstile token in production and a fresh UUID nonce per page load: a retry with the same nonce returns the first outcome. Fails with human_check_failed (403) or rate_limited (429). An answer after the link closed or filled returns accepted: false with the state to show.

Request body (JSON)
FieldTypeNotes
answerobjectrequired
answer.optionIndexinteger 0–8
answer.optionIndexesinteger[] up to 9 items
answer.rankinginteger[] up to 9 items
answer.ratingsinteger[] up to 8 items
answer.pairarray up to 2 items
answer.tapsobject[] up to 5 items
answer.taps[].xnumberrequired 0–1
answer.taps[].ynumberrequired 0–1
dwellMsinteger 0–86400000, default 0
interruptedboolean default false
noncestring (uuid)required pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
turnstileTokenstring up to 2048 characters
localestring up to 12 characters
positioninteger 1–10
visitstring up to 40 characters
Response (JSON)
FieldTypeNotes
acceptedbooleanrequired
state"live" | "scheduled" | "closed" | "full" | "paused"required
showResultbooleanrequired
resultCodestring | nullrequired
visitstring | nullrequired
remaininginteger[]required
answeredobject[]required
answered[].positionintegerrequired
answered[].resultCodestring | nullrequired

POST/v1/public/link/{code}/started

Your audience: an option was chosen on the answer page.

No authentication. Idempotency-Key header required.

Counted once per page load (the nonce), so the link's started figure means people, not loads.

Request body (JSON)
FieldTypeNotes
noncestring (uuid)required pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
Response (JSON)
FieldTypeNotes
oktruerequired

GET/v1/public/shared/{token}

The results behind a private share link. No sign-in; never cached; noindex.

No authentication.

Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null
resultobjectrequired
result.questionIdstringrequired
result.status"in_progress" | "complete" | "underfilled" | "canceled"required
result.nRequestedintegerrequired
result.nAcceptedintegerrequired
result.distributionobject[]required
result.distribution[].optionstringrequired
result.distribution[].countintegerrequired
result.distribution[].sharenumberrequired
result.distribution[].averageRanknumber
result.distribution[].meannumber
result.distribution[].ratingsinteger[] up to 5 items
result.distribution[].appearancesinteger
result.distribution[].strengthnumber
result.distribution[].rankinteger
result.distribution[].lownumber
result.distribution[].highnumber
result.meannumber | nullrequired
result.scaleLabelsobject | null
result.scaleLabels.lowstringrequired up to 24 characters
result.scaleLabels.highstringrequired up to 24 characters
result.summaryobjectrequired
result.summary.winnerstring | nullrequired
result.summary.marginnumber | nullrequired
result.summary.confidence"low" | "medium" | "high" | nullrequired
result.summary.notestringrequired
result.summary.suggestedFollowUpstring | nullrequired
result.tierinteger | nullrequired
result.languagestringrequired
result.verificationLinestringrequired
result.provenanceobject
result.provenance.answeredBy"heads" | "private"required
result.provenance.access"app" | "shared_link"required
result.provenance.verifiedbooleanrequired
result.provenance.answersintegerrequired
result.provenance.issuedinteger | nullrequired
result.provenance.denominator"known" | "unknown"required
result.medianSecondsnumber | nullrequired
result.creditsSpentintegerrequired
result.answersobject[]required
result.answers[].optionstring | nullrequired
result.answers[].textstring | nullrequired
result.answers[].translatedTextstring | null
result.answers[].reasonstring | null
result.answers[].translatedReasonstring | null
result.answers[].tierinteger | nullrequired
result.answers[].attestationRefstring | nullrequired
result.answers[].answeredAtstringrequired
result.answers[].pinnedboolean
result.answers[].flagobject | null
result.answers[].tapsobject[]
result.answers[].choiceobject
result.breakdownobject[]
result.breakdown[].tagIdstringrequired
result.breakdown[].labelstringrequired
result.breakdown[].nintegerrequired
result.breakdown[].distributionobject[]required
result.refundCreditsintegerrequired
result.breakdownsobject[]
result.breakdowns[].dimension"country" | "tier" | "age_band" | "gender" | "tag" | "language"required
result.breakdowns[].segmentsobject[]required
result.clicksobject
result.clicks.imageUrlstring | nullrequired
result.clicks.tapsintegerrequired
result.clicks.pointsobject[]required
result.clicks.gridinteger[]required
result.clicks.gridSizeintegerrequired
result.clicks.hotspotsobject[]required
result.clicks.heatmapUrlstring | nullrequired
result.sentimentobject
result.sentiment.positivenumberrequired
result.sentiment.neutralnumberrequired
result.sentiment.negativenumberrequired
result.translationLanguagestring | null
result.insightsobject | null
result.insights.questionIdstringrequired
result.insights.languagestringrequired
result.insights.sourceLanguagestringrequired
result.insights.takeawaystringrequired
result.insights.themesobject[]required up to 8 items
result.insights.findingsobject[] up to 5 items
result.insights.byOptionobject[] up to 9 items
result.insights.followUpobject | null
result.insights.sentimentobjectrequired
result.insights.basedOnintegerrequired
result.insights.answersAtTimeintegerrequired
result.insights.originalobject | nullrequired
result.insights.kind"interim" | "final"required
result.insights.generatedAtstringrequired
result.insights.refreshAfterstring | nullrequired
result.insights.enginestringrequired
result.filterobject
result.filter.optioninteger 0–8
result.filter.tierinteger 1–3
result.filter.countryobject
result.filter.ageBandstring pattern ^[0-9]{2}(-[0-9]{2}|\+)$
result.filter.gender"woman" | "man" | "non_binary"
result.filter.qstring up to 100 characters
result.nUnfilteredinteger
result.suppressedboolean
result.facetsobject
result.facets.tiersobject[]required
result.facets.countriesobject[]required
result.facets.ageBandsobject[]required
result.facets.gendersobject[]
result.verdictobject
result.verdict.verdict"go" | "no_go" | "unclear"required
result.verdict.reason"too_few" | "too_close" | nullrequired
result.verdict.yesSharenumberrequired
result.verdict.lownumberrequired
result.verdict.highnumberrequired
result.verdict.goAbovenumberrequired
result.verdict.minAnswersintegerrequired
result.verdict.nintegerrequired
result.headToHeadobject
result.headToHead.optionsstring[]required
result.headToHead.sharenumber | null[][]required
result.headToHead.ninteger[][]required
result.cohortHashstring
expiresAtstringrequired

GET/v2/questions/{id}/heatmap.png

A click test's heatmap as a transparent PNG to lay over the image. Add ?aspect=width/height to match it.

API key or OAuth token with questions:read.

POST/v2/questions/{id}/dispute

Dispute a result within 7 days of it closing.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
reasonstringrequired up to 2000 characters, at least 10 characters
Response (JSON)
FieldTypeNotes
oktruerequired

GET/v1/public/results/{code}

A result its requester made public.

No authentication.

Response (JSON)
FieldTypeNotes
questionobjectrequired
question.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
question.textstringrequired up to 600 characters
question.contextstring up to 120 characters
question.languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
question.optionsobject[]required up to 8 items, default []
question.options[].labelstringrequired up to 40 characters
question.options[].imageUrlstring (URL)
question.options[].imageUrlsstring (URL)[] up to 4 items
question.scaleLabelsobject
question.scaleLabels.lowstringrequired up to 24 characters
question.scaleLabels.highstringrequired up to 24 characters
question.neitherbooleanrequired default false
question.stimulusobject
question.stimulus.imageUrlstring (URL)
question.stimulus.audioUrlstring (URL)
question.stimulus.textstring up to 120 characters
question.stimulus.exposureMsinteger 2000–10000
question.nintegerrequired 10–5000, default 50
question.tier1 | 2 | 3required default 1
question.rushbooleanrequired default false
question.targetingobject
question.targeting.countriesstring[]required up to 50 items, default []
question.targeting.tagsstring[]required up to 20 items, default []
question.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
question.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
question.targeting.verifiedAgeboolean
question.clickTestobject
question.clickTest.maxTapsintegerrequired 1–5, default 1
question.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
question.publicResultsbooleanrequired default false
question.reason"off" | "optional" | "required"required default "off"
question.goRuleobject
question.goRule.goAbovenumberrequired 0.5–0.95
question.goRule.minAnswersintegerrequired 5–5000
question.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
question.interestAudienceIdstring up to 40 characters
question.minGrade1 | 2 | 3
question.interestPriceobject
question.interestPrice.perAnswerPenceintegerrequired 1–10000
question.interestPrice.headShareBpsintegerrequired 0–10000
question.answeredBy"heads" | "private"required default "heads"
question.openForDays1 | 7 | 14 | 30required default 7
question.shownAsstring up to 40 characters
question.idstringrequired
question.status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
question.source"app" | "portal" | "api" | "mcp"required
question.answeredintegerrequired
question.creditsPerAnswerintegerrequired
question.creditsReservedintegerrequired
question.creditsSpentintegerrequired
question.etaMinutesintegerrequired
question.leaderLinestring | nullrequired
question.refusedCategorystring | nullrequired
question.publicCodestring | nullrequired
question.createdAtstringrequired
question.liveAtstring | nullrequired
question.closedAtstring | nullrequired
question.teamIdstring | null
question.askedByobject | null
question.askedBy.idstringrequired
question.askedBy.namestring | nullrequired
question.projectIdstring | null
question.labelsstring[]
question.archivedboolean
question.bookmarkedboolean
question.externalRefstring | null
question.linkobject | null
question.link.urlstringrequired
question.link.qrSvgUrlstringrequired
question.link.qrPngUrlstringrequired
question.link.closesAtstring | nullrequired
question.link.viewsintegerrequired
question.link.startedintegerrequired
question.setIdstring | null
question.setPositioninteger | null
question.setCountinteger | null
resultobjectrequired
result.questionIdstringrequired
result.status"in_progress" | "complete" | "underfilled" | "canceled"required
result.nRequestedintegerrequired
result.nAcceptedintegerrequired
result.distributionobject[]required
result.distribution[].optionstringrequired
result.distribution[].countintegerrequired
result.distribution[].sharenumberrequired
result.distribution[].averageRanknumber
result.distribution[].meannumber
result.distribution[].ratingsinteger[] up to 5 items
result.distribution[].appearancesinteger
result.distribution[].strengthnumber
result.distribution[].rankinteger
result.distribution[].lownumber
result.distribution[].highnumber
result.meannumber | nullrequired
result.scaleLabelsobject | null
result.scaleLabels.lowstringrequired up to 24 characters
result.scaleLabels.highstringrequired up to 24 characters
result.summaryobjectrequired
result.summary.winnerstring | nullrequired
result.summary.marginnumber | nullrequired
result.summary.confidence"low" | "medium" | "high" | nullrequired
result.summary.notestringrequired
result.summary.suggestedFollowUpstring | nullrequired
result.tierinteger | nullrequired
result.languagestringrequired
result.verificationLinestringrequired
result.provenanceobject
result.provenance.answeredBy"heads" | "private"required
result.provenance.access"app" | "shared_link"required
result.provenance.verifiedbooleanrequired
result.provenance.answersintegerrequired
result.provenance.issuedinteger | nullrequired
result.provenance.denominator"known" | "unknown"required
result.medianSecondsnumber | nullrequired
result.creditsSpentintegerrequired
result.answersobject[]required
result.answers[].optionstring | nullrequired
result.answers[].textstring | nullrequired
result.answers[].translatedTextstring | null
result.answers[].reasonstring | null
result.answers[].translatedReasonstring | null
result.answers[].tierinteger | nullrequired
result.answers[].attestationRefstring | nullrequired
result.answers[].answeredAtstringrequired
result.answers[].pinnedboolean
result.answers[].flagobject | null
result.answers[].tapsobject[]
result.answers[].choiceobject
result.breakdownobject[]
result.breakdown[].tagIdstringrequired
result.breakdown[].labelstringrequired
result.breakdown[].nintegerrequired
result.breakdown[].distributionobject[]required
result.refundCreditsintegerrequired
result.breakdownsobject[]
result.breakdowns[].dimension"country" | "tier" | "age_band" | "gender" | "tag" | "language"required
result.breakdowns[].segmentsobject[]required
result.clicksobject
result.clicks.imageUrlstring | nullrequired
result.clicks.tapsintegerrequired
result.clicks.pointsobject[]required
result.clicks.gridinteger[]required
result.clicks.gridSizeintegerrequired
result.clicks.hotspotsobject[]required
result.clicks.heatmapUrlstring | nullrequired
result.sentimentobject
result.sentiment.positivenumberrequired
result.sentiment.neutralnumberrequired
result.sentiment.negativenumberrequired
result.translationLanguagestring | null
result.insightsobject | null
result.insights.questionIdstringrequired
result.insights.languagestringrequired
result.insights.sourceLanguagestringrequired
result.insights.takeawaystringrequired
result.insights.themesobject[]required up to 8 items
result.insights.findingsobject[] up to 5 items
result.insights.byOptionobject[] up to 9 items
result.insights.followUpobject | null
result.insights.sentimentobjectrequired
result.insights.basedOnintegerrequired
result.insights.answersAtTimeintegerrequired
result.insights.originalobject | nullrequired
result.insights.kind"interim" | "final"required
result.insights.generatedAtstringrequired
result.insights.refreshAfterstring | nullrequired
result.insights.enginestringrequired
result.filterobject
result.filter.optioninteger 0–8
result.filter.tierinteger 1–3
result.filter.countryobject
result.filter.ageBandstring pattern ^[0-9]{2}(-[0-9]{2}|\+)$
result.filter.gender"woman" | "man" | "non_binary"
result.filter.qstring up to 100 characters
result.nUnfilteredinteger
result.suppressedboolean
result.facetsobject
result.facets.tiersobject[]required
result.facets.countriesobject[]required
result.facets.ageBandsobject[]required
result.facets.gendersobject[]
result.verdictobject
result.verdict.verdict"go" | "no_go" | "unclear"required
result.verdict.reason"too_few" | "too_close" | nullrequired
result.verdict.yesSharenumberrequired
result.verdict.lownumberrequired
result.verdict.highnumberrequired
result.verdict.goAbovenumberrequired
result.verdict.minAnswersintegerrequired
result.verdict.nintegerrequired
result.headToHeadobject
result.headToHead.optionsstring[]required
result.headToHead.sharenumber | null[][]required
result.headToHead.ninteger[][]required
result.cohortHashstring

Sets

Your audience: two to ten questions behind one link, answered in order. Each question in a set is an ordinary question with its own result and exports; the set owns the link and the closing date.

POST/v2/sets

Your audience: publish a set of two to ten questions behind one link.

API key or OAuth token with questions:write. Idempotency-Key header required.

Requires an Idempotency-Key header (a UUID); the same key returns the same set. Every question is reserved at 5 credits an answer times maxAnswers. Fails with insufficient_credits, private_link_limit, private_declaration_required or content_refused without spending anything.

Request body (JSON)
FieldTypeNotes
questionsobject[]required up to 10 items
questions[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
questions[].textstringrequired up to 600 characters
questions[].contextstring up to 120 characters
questions[].languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
questions[].optionsobject[] up to 8 items, default []
questions[].options[].labelstringrequired up to 40 characters
questions[].options[].imageUrlstring (URL)
questions[].options[].imageUrlsstring (URL)[] up to 4 items
questions[].scaleLabelsobject
questions[].scaleLabels.lowstringrequired up to 24 characters
questions[].scaleLabels.highstringrequired up to 24 characters
questions[].neitherboolean default false
questions[].stimulusobject
questions[].stimulus.imageUrlstring (URL)
questions[].stimulus.audioUrlstring (URL)
questions[].stimulus.textstring up to 120 characters
questions[].stimulus.exposureMsinteger 2000–10000
questions[].clickTestobject
questions[].clickTest.maxTapsinteger 1–5, default 1
questions[].goRuleobject
questions[].goRule.goAbovenumberrequired 0.5–0.95
questions[].goRule.minAnswersintegerrequired 5–5000
maxAnswersinteger 10–5000, default 100
openForDays1 | 7 | 14 | 30 default 7
shownAsstring up to 40 characters
publicResultsboolean default false
projectIdstring | null
labelsstring[] up to 10 items
privateDeclarationobject
privateDeclaration.versionintegerrequired ≥ 1
Response (JSON)
FieldTypeNotes
setobjectrequired
set.idstringrequired
set.status"open" | "closed"required
set.shownAsstring | nullrequired
set.maxAnswersintegerrequired
set.openForDays1 | 7 | 14 | 30required
set.publicResultsbooleanrequired
set.expiresAtstringrequired
set.questionIdsstring[]required
set.answeredintegerrequired
set.creditsReservedintegerrequired
set.creditsSpentintegerrequired
set.linkobject | nullrequired
set.link.urlstringrequired
set.link.qrSvgUrlstringrequired
set.link.qrPngUrlstringrequired
set.link.closesAtstring | nullrequired
set.link.viewsintegerrequired
set.link.startedintegerrequired
set.teamIdstring | nullrequired
set.createdAtstringrequired
set.closedAtstring | nullrequired
questionsobject[]required
questions[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
questions[].textstringrequired up to 600 characters
questions[].contextstring up to 120 characters
questions[].languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
questions[].optionsobject[]required up to 8 items, default []
questions[].options[].labelstringrequired up to 40 characters
questions[].options[].imageUrlstring (URL)
questions[].options[].imageUrlsstring (URL)[] up to 4 items
questions[].scaleLabelsobject
questions[].scaleLabels.lowstringrequired up to 24 characters
questions[].scaleLabels.highstringrequired up to 24 characters
questions[].neitherbooleanrequired default false
questions[].stimulusobject
questions[].stimulus.imageUrlstring (URL)
questions[].stimulus.audioUrlstring (URL)
questions[].stimulus.textstring up to 120 characters
questions[].stimulus.exposureMsinteger 2000–10000
questions[].nintegerrequired 10–5000, default 50
questions[].tier1 | 2 | 3required default 1
questions[].rushbooleanrequired default false
questions[].targetingobject
questions[].targeting.countriesstring[]required up to 50 items, default []
questions[].targeting.tagsstring[]required up to 20 items, default []
questions[].targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
questions[].targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
questions[].targeting.verifiedAgeboolean
questions[].clickTestobject
questions[].clickTest.maxTapsintegerrequired 1–5, default 1
questions[].scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
questions[].publicResultsbooleanrequired default false
questions[].reason"off" | "optional" | "required"required default "off"
questions[].goRuleobject
questions[].goRule.goAbovenumberrequired 0.5–0.95
questions[].goRule.minAnswersintegerrequired 5–5000
questions[].contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
questions[].interestAudienceIdstring up to 40 characters
questions[].minGrade1 | 2 | 3
questions[].interestPriceobject
questions[].interestPrice.perAnswerPenceintegerrequired 1–10000
questions[].interestPrice.headShareBpsintegerrequired 0–10000
questions[].answeredBy"heads" | "private"required default "heads"
questions[].openForDays1 | 7 | 14 | 30required default 7
questions[].shownAsstring up to 40 characters
questions[].idstringrequired
questions[].status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
questions[].source"app" | "portal" | "api" | "mcp"required
questions[].answeredintegerrequired
questions[].creditsPerAnswerintegerrequired
questions[].creditsReservedintegerrequired
questions[].creditsSpentintegerrequired
questions[].etaMinutesintegerrequired
questions[].leaderLinestring | nullrequired
questions[].refusedCategorystring | nullrequired
questions[].publicCodestring | nullrequired
questions[].createdAtstringrequired
questions[].liveAtstring | nullrequired
questions[].closedAtstring | nullrequired
questions[].teamIdstring | null
questions[].askedByobject | null
questions[].askedBy.idstringrequired
questions[].askedBy.namestring | nullrequired
questions[].projectIdstring | null
questions[].labelsstring[]
questions[].archivedboolean
questions[].bookmarkedboolean
questions[].externalRefstring | null
questions[].linkobject | null
questions[].link.urlstringrequired
questions[].link.qrSvgUrlstringrequired
questions[].link.qrPngUrlstringrequired
questions[].link.closesAtstring | nullrequired
questions[].link.viewsintegerrequired
questions[].link.startedintegerrequired
questions[].setIdstring | null
questions[].setPositioninteger | null
questions[].setCountinteger | null

GET/v2/sets

Your sets, newest first.

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
status"open" | "closed"
limitinteger 1–100
cursorstring
Response (JSON)
FieldTypeNotes
setsobject[]required
sets[].idstringrequired
sets[].status"open" | "closed"required
sets[].shownAsstring | nullrequired
sets[].maxAnswersintegerrequired
sets[].openForDays1 | 7 | 14 | 30required
sets[].publicResultsbooleanrequired
sets[].expiresAtstringrequired
sets[].questionIdsstring[]required
sets[].answeredintegerrequired
sets[].creditsReservedintegerrequired
sets[].creditsSpentintegerrequired
sets[].linkobject | nullrequired
sets[].link.urlstringrequired
sets[].link.qrSvgUrlstringrequired
sets[].link.qrPngUrlstringrequired
sets[].link.closesAtstring | nullrequired
sets[].link.viewsintegerrequired
sets[].link.startedintegerrequired
sets[].teamIdstring | nullrequired
sets[].createdAtstringrequired
sets[].closedAtstring | nullrequired
nextCursorstring | nullrequired

GET/v2/sets/{id}

A set and its questions in order.

API key or OAuth token with questions:read.

Response (JSON)
FieldTypeNotes
setobjectrequired
set.idstringrequired
set.status"open" | "closed"required
set.shownAsstring | nullrequired
set.maxAnswersintegerrequired
set.openForDays1 | 7 | 14 | 30required
set.publicResultsbooleanrequired
set.expiresAtstringrequired
set.questionIdsstring[]required
set.answeredintegerrequired
set.creditsReservedintegerrequired
set.creditsSpentintegerrequired
set.linkobject | nullrequired
set.link.urlstringrequired
set.link.qrSvgUrlstringrequired
set.link.qrPngUrlstringrequired
set.link.closesAtstring | nullrequired
set.link.viewsintegerrequired
set.link.startedintegerrequired
set.teamIdstring | nullrequired
set.createdAtstringrequired
set.closedAtstring | nullrequired
questionsobject[]required
questions[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
questions[].textstringrequired up to 600 characters
questions[].contextstring up to 120 characters
questions[].languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
questions[].optionsobject[]required up to 8 items, default []
questions[].options[].labelstringrequired up to 40 characters
questions[].options[].imageUrlstring (URL)
questions[].options[].imageUrlsstring (URL)[] up to 4 items
questions[].scaleLabelsobject
questions[].scaleLabels.lowstringrequired up to 24 characters
questions[].scaleLabels.highstringrequired up to 24 characters
questions[].neitherbooleanrequired default false
questions[].stimulusobject
questions[].stimulus.imageUrlstring (URL)
questions[].stimulus.audioUrlstring (URL)
questions[].stimulus.textstring up to 120 characters
questions[].stimulus.exposureMsinteger 2000–10000
questions[].nintegerrequired 10–5000, default 50
questions[].tier1 | 2 | 3required default 1
questions[].rushbooleanrequired default false
questions[].targetingobject
questions[].targeting.countriesstring[]required up to 50 items, default []
questions[].targeting.tagsstring[]required up to 20 items, default []
questions[].targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
questions[].targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
questions[].targeting.verifiedAgeboolean
questions[].clickTestobject
questions[].clickTest.maxTapsintegerrequired 1–5, default 1
questions[].scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
questions[].publicResultsbooleanrequired default false
questions[].reason"off" | "optional" | "required"required default "off"
questions[].goRuleobject
questions[].goRule.goAbovenumberrequired 0.5–0.95
questions[].goRule.minAnswersintegerrequired 5–5000
questions[].contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
questions[].interestAudienceIdstring up to 40 characters
questions[].minGrade1 | 2 | 3
questions[].interestPriceobject
questions[].interestPrice.perAnswerPenceintegerrequired 1–10000
questions[].interestPrice.headShareBpsintegerrequired 0–10000
questions[].answeredBy"heads" | "private"required default "heads"
questions[].openForDays1 | 7 | 14 | 30required default 7
questions[].shownAsstring up to 40 characters
questions[].idstringrequired
questions[].status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
questions[].source"app" | "portal" | "api" | "mcp"required
questions[].answeredintegerrequired
questions[].creditsPerAnswerintegerrequired
questions[].creditsReservedintegerrequired
questions[].creditsSpentintegerrequired
questions[].etaMinutesintegerrequired
questions[].leaderLinestring | nullrequired
questions[].refusedCategorystring | nullrequired
questions[].publicCodestring | nullrequired
questions[].createdAtstringrequired
questions[].liveAtstring | nullrequired
questions[].closedAtstring | nullrequired
questions[].teamIdstring | null
questions[].askedByobject | null
questions[].askedBy.idstringrequired
questions[].askedBy.namestring | nullrequired
questions[].projectIdstring | null
questions[].labelsstring[]
questions[].archivedboolean
questions[].bookmarkedboolean
questions[].externalRefstring | null
questions[].linkobject | null
questions[].link.urlstringrequired
questions[].link.qrSvgUrlstringrequired
questions[].link.qrPngUrlstringrequired
questions[].link.closesAtstring | nullrequired
questions[].link.viewsintegerrequired
questions[].link.startedintegerrequired
questions[].setIdstring | null
questions[].setPositioninteger | null
questions[].setCountinteger | null

POST/v2/sets/{id}/close

Close the set now: every open question closes with the answers it has and the unused reserve comes back.

API key or OAuth token with questions:write.

Response (JSON)
FieldTypeNotes
setobjectrequired
set.idstringrequired
set.status"open" | "closed"required
set.shownAsstring | nullrequired
set.maxAnswersintegerrequired
set.openForDays1 | 7 | 14 | 30required
set.publicResultsbooleanrequired
set.expiresAtstringrequired
set.questionIdsstring[]required
set.answeredintegerrequired
set.creditsReservedintegerrequired
set.creditsSpentintegerrequired
set.linkobject | nullrequired
set.link.urlstringrequired
set.link.qrSvgUrlstringrequired
set.link.qrPngUrlstringrequired
set.link.closesAtstring | nullrequired
set.link.viewsintegerrequired
set.link.startedintegerrequired
set.teamIdstring | nullrequired
set.createdAtstringrequired
set.closedAtstring | nullrequired
questionsobject[]required
questions[].type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
questions[].textstringrequired up to 600 characters
questions[].contextstring up to 120 characters
questions[].languagestringrequired pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
questions[].optionsobject[]required up to 8 items, default []
questions[].options[].labelstringrequired up to 40 characters
questions[].options[].imageUrlstring (URL)
questions[].options[].imageUrlsstring (URL)[] up to 4 items
questions[].scaleLabelsobject
questions[].scaleLabels.lowstringrequired up to 24 characters
questions[].scaleLabels.highstringrequired up to 24 characters
questions[].neitherbooleanrequired default false
questions[].stimulusobject
questions[].stimulus.imageUrlstring (URL)
questions[].stimulus.audioUrlstring (URL)
questions[].stimulus.textstring up to 120 characters
questions[].stimulus.exposureMsinteger 2000–10000
questions[].nintegerrequired 10–5000, default 50
questions[].tier1 | 2 | 3required default 1
questions[].rushbooleanrequired default false
questions[].targetingobject
questions[].targeting.countriesstring[]required up to 50 items, default []
questions[].targeting.tagsstring[]required up to 20 items, default []
questions[].targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
questions[].targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
questions[].targeting.verifiedAgeboolean
questions[].clickTestobject
questions[].clickTest.maxTapsintegerrequired 1–5, default 1
questions[].scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
questions[].publicResultsbooleanrequired default false
questions[].reason"off" | "optional" | "required"required default "off"
questions[].goRuleobject
questions[].goRule.goAbovenumberrequired 0.5–0.95
questions[].goRule.minAnswersintegerrequired 5–5000
questions[].contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
questions[].interestAudienceIdstring up to 40 characters
questions[].minGrade1 | 2 | 3
questions[].interestPriceobject
questions[].interestPrice.perAnswerPenceintegerrequired 1–10000
questions[].interestPrice.headShareBpsintegerrequired 0–10000
questions[].answeredBy"heads" | "private"required default "heads"
questions[].openForDays1 | 7 | 14 | 30required default 7
questions[].shownAsstring up to 40 characters
questions[].idstringrequired
questions[].status"draft" | "scheduled" | "live" | "complete" | "underfilled" | "canceled" | "refused" | "pending_approval" | "closed"required
questions[].source"app" | "portal" | "api" | "mcp"required
questions[].answeredintegerrequired
questions[].creditsPerAnswerintegerrequired
questions[].creditsReservedintegerrequired
questions[].creditsSpentintegerrequired
questions[].etaMinutesintegerrequired
questions[].leaderLinestring | nullrequired
questions[].refusedCategorystring | nullrequired
questions[].publicCodestring | nullrequired
questions[].createdAtstringrequired
questions[].liveAtstring | nullrequired
questions[].closedAtstring | nullrequired
questions[].teamIdstring | null
questions[].askedByobject | null
questions[].askedBy.idstringrequired
questions[].askedBy.namestring | nullrequired
questions[].projectIdstring | null
questions[].labelsstring[]
questions[].archivedboolean
questions[].bookmarkedboolean
questions[].externalRefstring | null
questions[].linkobject | null
questions[].link.urlstringrequired
questions[].link.qrSvgUrlstringrequired
questions[].link.qrPngUrlstringrequired
questions[].link.closesAtstring | nullrequired
questions[].link.viewsintegerrequired
questions[].link.startedintegerrequired
questions[].setIdstring | null
questions[].setPositioninteger | null
questions[].setCountinteger | null
refundCreditsintegerrequired

GET/v2/sets/{id}/export.csv

Every accepted answer across the set as CSV: position, question, time, answer, visit (the random id grouping one browser's pass), answered_by and access.

API key or OAuth token with questions:read.

GET/v2/sets/{id}/export.jsonl

Every accepted answer across the set as JSON lines: position, question, options, answer, `choice`, each question's `cohortHash` and the visit.

API key or OAuth token with questions:read.

GET/v2/sets/{id}/link/qr.svg

The set link's QR code as SVG.

API key or OAuth token with questions:read.

GET/v2/sets/{id}/link/qr.png

The set link's QR code as a 1024-pixel PNG.

API key or OAuth token with questions:read.

Drafts and uploads

Save work in progress and upload images before you ask.

GET/v1/drafts

Saved drafts.

API key or OAuth token with questions:read.

Response (JSON)
FieldTypeNotes
draftsobject[]required
drafts[].idstringrequired
drafts[].draftobjectrequired
drafts[].draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
drafts[].draft.textstring up to 600 characters
drafts[].draft.contextstring up to 120 characters
drafts[].draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
drafts[].draft.optionsobject[] up to 8 items, default []
drafts[].draft.scaleLabelsobject
drafts[].draft.neitherboolean default false
drafts[].draft.stimulusobject
drafts[].draft.ninteger 10–5000, default 50
drafts[].draft.tier1 | 2 | 3 default 1
drafts[].draft.rushboolean default false
drafts[].draft.targetingobject
drafts[].draft.clickTestobject
drafts[].draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
drafts[].draft.publicResultsboolean default false
drafts[].draft.reason"off" | "optional" | "required" default "off"
drafts[].draft.goRuleobject
drafts[].draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
drafts[].draft.interestAudienceIdstring up to 40 characters
drafts[].draft.minGrade1 | 2 | 3
drafts[].draft.interestPriceobject
drafts[].draft.answeredBy"heads" | "private" default "heads"
drafts[].draft.openForDays1 | 7 | 14 | 30 default 7
drafts[].draft.shownAsstring up to 40 characters
drafts[].updatedAtstringrequired
drafts[].origin"zapier_test" | null

POST/v1/drafts

Save a draft. PUT /v1/drafts/{id} updates one; DELETE removes it.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
textstring up to 600 characters
contextstring up to 120 characters
languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
optionsobject[] up to 8 items, default []
options[].labelstringrequired up to 40 characters
options[].imageUrlstring (URL)
options[].imageUrlsstring (URL)[] up to 4 items
scaleLabelsobject
scaleLabels.lowstringrequired up to 24 characters
scaleLabels.highstringrequired up to 24 characters
neitherboolean default false
stimulusobject
stimulus.imageUrlstring (URL)
stimulus.audioUrlstring (URL)
stimulus.textstring up to 120 characters
stimulus.exposureMsinteger 2000–10000
ninteger 10–5000, default 50
tier1 | 2 | 3 default 1
rushboolean default false
targetingobject
targeting.countriesstring[] up to 50 items, default []
targeting.tagsstring[] up to 20 items, default []
targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
targeting.verifiedAgeboolean
clickTestobject
clickTest.maxTapsinteger 1–5, default 1
scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
publicResultsboolean default false
reason"off" | "optional" | "required" default "off"
goRuleobject
goRule.goAbovenumberrequired 0.5–0.95
goRule.minAnswersintegerrequired 5–5000
contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
interestAudienceIdstring up to 40 characters
minGrade1 | 2 | 3
interestPriceobject
interestPrice.perAnswerPenceintegerrequired 1–10000
interestPrice.headShareBpsintegerrequired 0–10000
answeredBy"heads" | "private" default "heads"
openForDays1 | 7 | 14 | 30 default 7
shownAsstring up to 40 characters
origin"zapier_test"
Response (JSON)
FieldTypeNotes
idstringrequired
draftobjectrequired
draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
draft.textstring up to 600 characters
draft.contextstring up to 120 characters
draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
draft.optionsobject[] up to 8 items, default []
draft.options[].labelstringrequired up to 40 characters
draft.options[].imageUrlstring (URL)
draft.options[].imageUrlsstring (URL)[] up to 4 items
draft.scaleLabelsobject
draft.scaleLabels.lowstringrequired up to 24 characters
draft.scaleLabels.highstringrequired up to 24 characters
draft.neitherboolean default false
draft.stimulusobject
draft.stimulus.imageUrlstring (URL)
draft.stimulus.audioUrlstring (URL)
draft.stimulus.textstring up to 120 characters
draft.stimulus.exposureMsinteger 2000–10000
draft.ninteger 10–5000, default 50
draft.tier1 | 2 | 3 default 1
draft.rushboolean default false
draft.targetingobject
draft.targeting.countriesstring[]required up to 50 items, default []
draft.targeting.tagsstring[]required up to 20 items, default []
draft.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
draft.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
draft.targeting.verifiedAgeboolean
draft.clickTestobject
draft.clickTest.maxTapsintegerrequired 1–5, default 1
draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
draft.publicResultsboolean default false
draft.reason"off" | "optional" | "required" default "off"
draft.goRuleobject
draft.goRule.goAbovenumberrequired 0.5–0.95
draft.goRule.minAnswersintegerrequired 5–5000
draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
draft.interestAudienceIdstring up to 40 characters
draft.minGrade1 | 2 | 3
draft.interestPriceobject
draft.interestPrice.perAnswerPenceintegerrequired 1–10000
draft.interestPrice.headShareBpsintegerrequired 0–10000
draft.answeredBy"heads" | "private" default "heads"
draft.openForDays1 | 7 | 14 | 30 default 7
draft.shownAsstring up to 40 characters
updatedAtstringrequired
origin"zapier_test" | null

PUT/v1/drafts/{id}

Replace a saved draft.

API key or OAuth token with questions:write.

404 not_found when the draft is not in the account the caller acts for.

Request body (JSON)
FieldTypeNotes
type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
textstring up to 600 characters
contextstring up to 120 characters
languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
optionsobject[] up to 8 items, default []
options[].labelstringrequired up to 40 characters
options[].imageUrlstring (URL)
options[].imageUrlsstring (URL)[] up to 4 items
scaleLabelsobject
scaleLabels.lowstringrequired up to 24 characters
scaleLabels.highstringrequired up to 24 characters
neitherboolean default false
stimulusobject
stimulus.imageUrlstring (URL)
stimulus.audioUrlstring (URL)
stimulus.textstring up to 120 characters
stimulus.exposureMsinteger 2000–10000
ninteger 10–5000, default 50
tier1 | 2 | 3 default 1
rushboolean default false
targetingobject
targeting.countriesstring[] up to 50 items, default []
targeting.tagsstring[] up to 20 items, default []
targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
targeting.verifiedAgeboolean
clickTestobject
clickTest.maxTapsinteger 1–5, default 1
scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
publicResultsboolean default false
reason"off" | "optional" | "required" default "off"
goRuleobject
goRule.goAbovenumberrequired 0.5–0.95
goRule.minAnswersintegerrequired 5–5000
contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
interestAudienceIdstring up to 40 characters
minGrade1 | 2 | 3
interestPriceobject
interestPrice.perAnswerPenceintegerrequired 1–10000
interestPrice.headShareBpsintegerrequired 0–10000
answeredBy"heads" | "private" default "heads"
openForDays1 | 7 | 14 | 30 default 7
shownAsstring up to 40 characters
Response (JSON)
FieldTypeNotes
idstringrequired
draftobjectrequired
draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
draft.textstring up to 600 characters
draft.contextstring up to 120 characters
draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
draft.optionsobject[] up to 8 items, default []
draft.options[].labelstringrequired up to 40 characters
draft.options[].imageUrlstring (URL)
draft.options[].imageUrlsstring (URL)[] up to 4 items
draft.scaleLabelsobject
draft.scaleLabels.lowstringrequired up to 24 characters
draft.scaleLabels.highstringrequired up to 24 characters
draft.neitherboolean default false
draft.stimulusobject
draft.stimulus.imageUrlstring (URL)
draft.stimulus.audioUrlstring (URL)
draft.stimulus.textstring up to 120 characters
draft.stimulus.exposureMsinteger 2000–10000
draft.ninteger 10–5000, default 50
draft.tier1 | 2 | 3 default 1
draft.rushboolean default false
draft.targetingobject
draft.targeting.countriesstring[]required up to 50 items, default []
draft.targeting.tagsstring[]required up to 20 items, default []
draft.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
draft.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
draft.targeting.verifiedAgeboolean
draft.clickTestobject
draft.clickTest.maxTapsintegerrequired 1–5, default 1
draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
draft.publicResultsboolean default false
draft.reason"off" | "optional" | "required" default "off"
draft.goRuleobject
draft.goRule.goAbovenumberrequired 0.5–0.95
draft.goRule.minAnswersintegerrequired 5–5000
draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
draft.interestAudienceIdstring up to 40 characters
draft.minGrade1 | 2 | 3
draft.interestPriceobject
draft.interestPrice.perAnswerPenceintegerrequired 1–10000
draft.interestPrice.headShareBpsintegerrequired 0–10000
draft.answeredBy"heads" | "private" default "heads"
draft.openForDays1 | 7 | 14 | 30 default 7
draft.shownAsstring up to 40 characters
updatedAtstringrequired
origin"zapier_test" | null

DELETE/v1/drafts/{id}

Delete a saved draft.

API key or OAuth token with questions:write.

Deleting a draft that is already gone still answers { ok: true }.

Response (JSON)
FieldTypeNotes
oktruerequired

POST/v1/question-reviews

Start a background review of a draft's wording, or find the one already done.

API key or OAuth token with questions:write.

Advice only: it never spends and never changes the price. Returns at once with pending; ask again (GET) after retryMs. The same words are reviewed once a day, so calling it on every save is fine. skipped means there was nothing to review, with the reason in review.skippedReason. Each suggestion is one change to the draft (op) that you apply yourself; a tidy suggestion fixes capitals and spacing.

Request body (JSON)
FieldTypeNotes
draftobjectrequired
draft.type"single_choice" | "multi_choice" | "ab_image" | "pairwise" | "scale_1_5" | "ranking" | "yes_no" | "yes_mostly_no" | "free_text" | "click_test" | "reaction" | "rate_each"required
draft.textstringrequired up to 600 characters
draft.contextstring up to 120 characters
draft.languagestring pattern ^[a-z]{2}(-[A-Z]{2})?$, default "en"
draft.optionsobject[] up to 8 items, default []
draft.options[].labelstringrequired up to 40 characters
draft.options[].imageUrlstring (URL)
draft.options[].imageUrlsstring (URL)[] up to 4 items
draft.scaleLabelsobject
draft.scaleLabels.lowstringrequired up to 24 characters
draft.scaleLabels.highstringrequired up to 24 characters
draft.neitherboolean default false
draft.stimulusobject
draft.stimulus.imageUrlstring (URL)
draft.stimulus.audioUrlstring (URL)
draft.stimulus.textstring up to 120 characters
draft.stimulus.exposureMsinteger 2000–10000
draft.ninteger 10–5000, default 50
draft.tier1 | 2 | 3 default 1
draft.rushboolean default false
draft.targetingobject
draft.targeting.countriesstring[] up to 50 items, default []
draft.targeting.tagsstring[] up to 20 items, default []
draft.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
draft.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
draft.targeting.verifiedAgeboolean
draft.clickTestobject
draft.clickTest.maxTapsinteger 1–5, default 1
draft.scheduledForstring (date-time) pattern ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$
draft.publicResultsboolean default false
draft.reason"off" | "optional" | "required" default "off"
draft.goRuleobject
draft.goRule.goAbovenumberrequired 0.5–0.95
draft.goRule.minAnswersintegerrequired 5–5000
draft.contentFlag"none" | "medical" | "violence" | "distressing" | "alcohol_gambling" | "political"
draft.interestAudienceIdstring up to 40 characters
draft.minGrade1 | 2 | 3
draft.interestPriceobject
draft.interestPrice.perAnswerPenceintegerrequired 1–10000
draft.interestPrice.headShareBpsintegerrequired 0–10000
draft.answeredBy"heads" | "private" default "heads"
draft.openForDays1 | 7 | 14 | 30 default 7
draft.shownAsstring up to 40 characters
Response (JSON)
FieldTypeNotes
inputHashstringrequired
status"pending" | "ready" | "skipped" | "failed"required
reviewobject
review.status"ok" | "skipped" | "unavailable"required
review.rubricVersionstringrequired
review.modelstring
review.inputHashstring
review.overall"pass" | "suggest"
review.scoresobject
review.scores.clarityintegerrequired 1–5
review.scores.singleIdeaintegerrequired 1–5
review.scores.neutralintegerrequired 1–5
review.scores.optionsintegerrequired 1–5
review.scores.answerableintegerrequired 1–5
review.scores.lengthintegerrequired 1–5
review.suggestionsobject[]
review.suggestions[].idstringrequired
review.suggestions[].code"tidy" | "spelling" | "clarity" | "single_idea" | "leading" | "overlap" | "neither" | "length" | "jargon" | "not_a_preference" | "show_the_thing" | "parallel" | "policy"required
review.suggestions[].severity"info" | "suggest" | "strong"required
review.suggestions[].fieldstringrequired
review.suggestions[].reasonstringrequired up to 180 characters
review.suggestions[].opobject | object | object | object | object | object | object | object | object | object
review.skippedReason"flag_off" | "incomplete" | "no_text" | "content_refused" | "ai_off" | "asks_killed"
retryMsinteger

GET/v1/question-reviews/{hash}

Where a draft's review stands: pending, ready, skipped or failed.

API key or OAuth token with questions:write.

Response (JSON)
FieldTypeNotes
inputHashstringrequired
status"pending" | "ready" | "skipped" | "failed"required
reviewobject
review.status"ok" | "skipped" | "unavailable"required
review.rubricVersionstringrequired
review.modelstring
review.inputHashstring
review.overall"pass" | "suggest"
review.scoresobject
review.scores.clarityintegerrequired 1–5
review.scores.singleIdeaintegerrequired 1–5
review.scores.neutralintegerrequired 1–5
review.scores.optionsintegerrequired 1–5
review.scores.answerableintegerrequired 1–5
review.scores.lengthintegerrequired 1–5
review.suggestionsobject[]
review.suggestions[].idstringrequired
review.suggestions[].code"tidy" | "spelling" | "clarity" | "single_idea" | "leading" | "overlap" | "neither" | "length" | "jargon" | "not_a_preference" | "show_the_thing" | "parallel" | "policy"required
review.suggestions[].severity"info" | "suggest" | "strong"required
review.suggestions[].fieldstringrequired
review.suggestions[].reasonstringrequired up to 180 characters
review.suggestions[].opobject | object | object | object | object | object | object | object | object | object
review.skippedReason"flag_off" | "incomplete" | "no_text" | "content_refused" | "ai_off" | "asks_killed"
retryMsinteger

POST/v1/uploads

A pre-signed URL to upload an image or sound clip.

API key or OAuth token with questions:write.

PUT the file to uploadUrl with the returned headers (the URL is the credential, for 15 minutes), then use publicUrl in the question. Images are checked and their metadata stripped before heads see them.

Request body (JSON)
FieldTypeNotes
kind"image" | "audio"required
contentTypestringrequired
bytesintegerrequired
Response (JSON)
FieldTypeNotes
uploadUrlstringrequired
publicUrlstringrequired
headersmap of stringrequired

POST/v1/uploads/import

Copy an image from a public https link into 50heads.

API key or OAuth token with questions:write.

The image is fetched on our side (public https hosts only, three redirects at most), checked like an upload and stored. JPEG, PNG or WebP up to 5 MB.

Request body (JSON)
FieldTypeNotes
urlstring (URL)required
Response (JSON)
FieldTypeNotes
publicUrlstringrequired
widthinteger | nullrequired
heightinteger | nullrequired

POST/v1/mockups

Draw a store listing or search result mockup from your details.

API key or OAuth token with questions:write.

A listing or a search result in 50heads styling (never a marketplace's), 1200 × 1600, drawn from a title, a price, a rating, a review count and your image, then stored like an upload. Use imageUrl on an option or the stimulus within 15 minutes. See docs/specs/mockups.md.

Request body (JSON)
FieldTypeNotes
kind"listing" | "search_result"required
titlestringrequired up to 120 characters
priceobjectrequired
price.amountnumberrequired 0–1000000
price.currency"GBP" | "USD" | "EUR" | "CAD" | "JPY" | "KRW" | "SEK" | "DKK" | "NOK" | "PLN" | "BRL" | "CZK" | "RON" | "TRY" | "CLP" | "COP" | "ARS" | "PEN" | "UYU"required
ratingnumber 0–5
reviewsinteger 0–100000000
imageUrlstring (URL)required
linestring up to 40 characters
Response (JSON)
FieldTypeNotes
kind"listing" | "search_result"required
imageUrlstringrequired
widthintegerrequired
heightintegerrequired
expiresAtstringrequired

Projects

Group questions into projects in the account the key belongs to (yours or a team's). Label, archive and give each question your own reference with the question routes.

GET/v1/projects

Projects in use, or the archived ones.

API key or OAuth token with questions:read.

Query parameters
FieldTypeNotes
archivedboolean
Response (JSON)
FieldTypeNotes
projectsobject[]required
projects[].idstringrequired
projects[].namestringrequired
projects[].descriptionstring | nullrequired
projects[].goalstring | nullrequired
projects[].bookmarkedbooleanrequired
projects[].archivedbooleanrequired
projects[].costCenterstring | null
projects[].questionCountintegerrequired
projects[].createdAtstringrequired
projects[].updatedAtstringrequired

POST/v1/projects

Make a project.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 80 characters
descriptionstring | null
goalstring | null
Response (JSON)
FieldTypeNotes
projectobjectrequired
project.idstringrequired
project.namestringrequired
project.descriptionstring | nullrequired
project.goalstring | nullrequired
project.bookmarkedbooleanrequired
project.archivedbooleanrequired
project.costCenterstring | null
project.questionCountintegerrequired
project.createdAtstringrequired
project.updatedAtstringrequired

PATCH/v1/projects/{id}

Rename, bookmark or archive a project.

API key or OAuth token with questions:write.

Request body (JSON)
FieldTypeNotes
namestring up to 80 characters
descriptionstring | null
goalstring | null
bookmarkedboolean
archivedboolean
costCenterstring | null
Response (JSON)
FieldTypeNotes
projectobjectrequired
project.idstringrequired
project.namestringrequired
project.descriptionstring | nullrequired
project.goalstring | nullrequired
project.bookmarkedbooleanrequired
project.archivedbooleanrequired
project.costCenterstring | null
project.questionCountintegerrequired
project.createdAtstringrequired
project.updatedAtstringrequired

DELETE/v1/projects/{id}

Delete a project. Its questions stay, outside any project.

API key or OAuth token with questions:write.

Response (JSON)
FieldTypeNotes
oktruerequired

Audiences

Panels of heads who proved they fit something (UK commuter cyclists, for example). Ask one by putting its id in a draft's `interestAudienceId`, with a `minGrade`: it replaces targeting, and the price per answer is the audience's at that grade.

GET/v1/interest-audiences

Audiences you can ask, with their size band and price per answer by grade.

No authentication.

open=1 keeps only audiences open to join from the caller's country. With a key and mine=asking, private audiences your team sponsors are listed too. Sizes are bands, never counts.

Query parameters
FieldTypeNotes
qstring up to 60 characters
countrystring pattern ^[A-Z]{2}$
langstring up to 5 characters
category"outdoors" | "home" | "family" | "food_drink" | "games_tech" | "travel" | "motoring" | "work" | "shopping" | "pets" | "seasonal"
open"0" | "1"
mine"asking"
Response (JSON)
FieldTypeNotes
audiencesobject[]required
audiences[].idstringrequired
audiences[].slugstringrequired
audiences[].namestringrequired
audiences[].summarystringrequired
audiences[].imageUrlstring | nullrequired
audiences[].status"draft" | "recruiting" | "live" | "closed"required
audiences[].categorystringrequired
audiences[].geoAllowstring[]required
audiences[].languagesstring[]required
audiences[].poolBand"under_50" | "50_200" | "200_500" | "500_plus"required
audiences[].pricesobjectrequired
audiences[].prices.grade1integerrequired
audiences[].prices.grade2integerrequired
audiences[].prices.grade3integerrequired
audiences[].headShareBpsintegerrequired
audiences[].payFromPenceintegerrequired
audiences[].geoOkbooleanrequired
audiences[].sponsoredbooleanrequired
audiences[].expiresAtstring | nullrequired

GET/v1/interest-audiences/{slug}

One audience: what it is, the tags it stamps, grades and pay.

No authentication.

Response (JSON)
FieldTypeNotes
audienceobjectrequired
audience.slugstringrequired
audience.namestringrequired
audience.summarystringrequired
audience.overviewstring[]required
audience.whoForstringrequired
audience.imageUrlstring | nullrequired
audience.status"draft" | "recruiting" | "live" | "closed"required
audience.geoAllowstring[]required
audience.languagesstring[]required
audience.tagsobject[]required
audience.tags[].idstringrequired
audience.tags[].labelstringrequired
audience.gradesobject[]required
audience.grades[].grade1 | 2 | 3required
audience.grades[].namestringrequired
audience.grades[].pricePenceintegerrequired
audience.grades[].headPayPenceintegerrequired
audience.grades[].how"quiz" | "work_email_domain" | "org_code" | "review"required
audience.poolBand"under_50" | "50_200" | "200_500" | "500_plus"required
audience.countrystring | nullrequired
audience.geoOkbooleanrequired
audience.quizobjectrequired
audience.quiz.itemsintegerrequired
audience.quiz.passMarkintegerrequired
audience.quiz.timeLimitSintegerrequired
audience.trustedDomainsstring[]required

Saved audiences

A targeting setup you reuse: how many heads, tier, rush, language and who to ask, saved under a name in the account the caller acts for (yours or a team's). They are for the portal and the app composer, so these routes need a signed-in person; API keys and connected apps get 403 `session_required`. To ask the same audience from a key, send the same fields in the draft.

GET/v1/audiences

Saved audiences in the account.

Portal session only.

A person signed in at 50heads.com or in the app. Keys and connected apps get 403 session_required.

Response (JSON)
FieldTypeNotes
audiencesobject[]required
audiences[].idstringrequired
audiences[].namestringrequired up to 60 characters
audiences[].nintegerrequired 10–5000
audiences[].tier1 | 2 | 3required
audiences[].rushbooleanrequired
audiences[].languagestringrequired
audiences[].targetingobjectrequired
audiences[].targeting.countriesstring[]required up to 50 items, default []
audiences[].targeting.tagsstring[]required up to 20 items, default []
audiences[].targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
audiences[].targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
audiences[].targeting.verifiedAgeboolean

POST/v1/audiences

Save an audience under a name.

Portal session only.

A person signed in. Team members who can ask can save for the team. At most 50 per account: 409 too_many_audiences after that.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 60 characters
nintegerrequired 10–5000
tier1 | 2 | 3required
rushbooleanrequired
languagestringrequired
targetingobjectrequired
targeting.countriesstring[] up to 50 items, default []
targeting.tagsstring[] up to 20 items, default []
targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
targeting.verifiedAgeboolean
Response (JSON)
FieldTypeNotes
audienceobjectrequired
audience.idstringrequired
audience.namestringrequired up to 60 characters
audience.nintegerrequired 10–5000
audience.tier1 | 2 | 3required
audience.rushbooleanrequired
audience.languagestringrequired
audience.targetingobjectrequired
audience.targeting.countriesstring[]required up to 50 items, default []
audience.targeting.tagsstring[]required up to 20 items, default []
audience.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
audience.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
audience.targeting.verifiedAgeboolean

PUT/v1/audiences/{id}

Replace a saved audience.

Portal session only.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 60 characters
nintegerrequired 10–5000
tier1 | 2 | 3required
rushbooleanrequired
languagestringrequired
targetingobjectrequired
targeting.countriesstring[] up to 50 items, default []
targeting.tagsstring[] up to 20 items, default []
targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
targeting.verifiedAgeboolean
Response (JSON)
FieldTypeNotes
audienceobjectrequired
audience.idstringrequired
audience.namestringrequired up to 60 characters
audience.nintegerrequired 10–5000
audience.tier1 | 2 | 3required
audience.rushbooleanrequired
audience.languagestringrequired
audience.targetingobjectrequired
audience.targeting.countriesstring[]required up to 50 items, default []
audience.targeting.tagsstring[]required up to 20 items, default []
audience.targeting.ageBands"18-24" | "25-34" | "35-44" | "45-54" | "55-64" | "65+"[] up to 6 items
audience.targeting.genders"woman" | "man" | "non_binary"[] up to 3 items
audience.targeting.verifiedAgeboolean

DELETE/v1/audiences/{id}

Delete a saved audience.

Portal session only.

Response (JSON)
FieldTypeNotes
oktruerequired

Credits and account

Balance, ledger, invoices and statements. Credits are bought on the web; the API spends them.

GET/v1/billing/balance

Credits, what is reserved by live questions, and what is left under this key's daily cap.

API key or OAuth token with account:read.

Response (JSON)
FieldTypeNotes
creditsintegerrequired
reservedintegerrequired
expiringobject[]required
expiring[].creditsintegerrequired
expiring[].expiresAtstringrequired
currency"GBP" | "USD" | "EUR" | "CAD" | "JPY" | "KRW" | "BRL" | "TRY" | "SEK" | "DKK" | "NOK" | "PLN" | "CZK" | "RON"required
capRemaininginteger | nullrequired
freeQuestionTodaybooleanrequired
privateDeclaredAtstring | null
privateFreeAnswersLeftinteger

GET/v1/billing/ledger

Every credit movement, newest first.

API key or OAuth token with account:read.

Query parameters
FieldTypeNotes
cursorstring
Response (JSON)
FieldTypeNotes
rowsobject[]required
rows[].idstringrequired
rows[].atstringrequired
rows[].kind"purchase" | "ask" | "refund" | "say" | "bonus" | "referral" | "payout" | "payout_returned" | "fee" | "expiry" | "clawback" | "adjustment" | "tier2_fee" | "not_paid" | "founder_credits"required
rows[].titlestringrequired
rows[].state"pending" | "available" | "in_flight" | "paid" | "reversed" | "not_paid"required
rows[].amountintegerrequired
rows[].refstring | nullrequired
rows[].notPaidobject | null
rows[].notPaid.idstringrequired
rows[].notPaid.source"rejected" | "clawback"required
rows[].notPaid.reason"too_fast" | "repeat" | "check_question" | "automated" | "linked_accounts" | "shared_payout_method" | "confirmed_fraud"required
rows[].notPaid.amountintegerrequired
rows[].notPaid.status"not_paid" | "appealed" | "upheld" | "not_upheld"required
rows[].notPaid.appealBystring | nullrequired
rows[].notPaid.decisionBystring | nullrequired
rows[].bystring | null
rows[].source"web" | "apple" | "google"
rows[].releaseAtstring | null
nextCursorstring | nullrequired

POST/v1/billing/checkout

Start a card or bank-transfer purchase (portal session only).

Portal session only.

Not available to API keys: buying credits needs a signed-in person.

Request body (JSON)
FieldTypeNotes
packstringrequired
currency"GBP" | "USD" | "EUR" | "CAD"required
method"card" | "bank_transfer"required
returnPathstring pattern ^\/app(\/[\w\-/]*)?$

POST/v1/billing/store-purchases/intent

Start an App Store or Google Play credit-pack purchase. Returns the account token to pass to the store.

Portal session only.

The app sends appAccountToken to StoreKit and obfuscatedAccountId to Play Billing. Credits are granted by storePurchase after the store returns a signed transaction or purchase token.

Request body (JSON)
FieldTypeNotes
store"apple" | "google"required
Response (JSON)
FieldTypeNotes
appAccountTokenstringrequired
obfuscatedAccountIdstringrequired

POST/v1/billing/store-purchases

Verify an App Store or Google Play credit-pack purchase and grant the credits once.

Portal session only.

Apple: signedTransaction is the StoreKit 2 JWS. Google: purchaseToken is checked with the Play Developer API, then consumed. A repeat of the same transaction returns status duplicate and does not grant again. Finish the store transaction only after this succeeds. The stores are the merchant of record, so this does not create a VAT invoice.

Request body (JSON)
FieldTypeNotes
store"apple" | "google"required
productIdstringrequired
signedTransactionstring at least 20 characters
purchaseTokenstring at least 8 characters
Response (JSON)
FieldTypeNotes
status"granted" | "duplicate"required
creditsintegerrequired
transactionIdstringrequired
productIdstringrequired
balanceintegerrequired
testPurchaseboolean

GET/v1/billing/store-purchases

Recent App Store and Google Play credit-pack purchases on the account, newest first.

Portal session only.

Each purchase has the credits added, when, its state (granted or refunded) and the receipt currency and price in minor units. Currency and price are null where the store's receipt does not state them (Google Play). Test purchases that added no credits are left out.

Response (JSON)
FieldTypeNotes
purchasesobject[]required
purchases[].idstringrequired
purchases[].store"apple" | "google"required
purchases[].productIdstringrequired
purchases[].creditsintegerrequired
purchases[].state"granted" | "refunded"required
purchases[].atstringrequired
purchases[].refundedAtstring | nullrequired
purchases[].currencystring | nullrequired
purchases[].priceMinorinteger | nullrequired
purchases[].orderIdstringrequired

GET/v1/billing/invoices

Invoices, credit notes and self-billing statements, newest first.

API key or OAuth token with account:read.

Each has a pdfUrl on this API; fetch it with the same key.

Response (JSON)
FieldTypeNotes
invoicesobject[]required
invoices[].idstringrequired
invoices[].numberstringrequired
invoices[].kind"invoice" | "credit_note" | "self_billing"required
invoices[].issuedAtstringrequired
invoices[].currencystringrequired
invoices[].netnumberrequired
invoices[].vatnumberrequired
invoices[].totalnumberrequired
invoices[].vatTreatment"uk_standard" | "reverse_charge" | "outside_scope" | "none"required
invoices[].pdfUrlstringrequired

GET/v1/billing/invoices/{id}/pdf

An invoice or credit note as a PDF.

API key or OAuth token with account:read.

GET/v1/billing/profile

The legal name, address and VAT number printed on invoices.

API key or OAuth token with account:read.

Changing it (PUT) needs a signed-in person.

Response (JSON)
FieldTypeNotes
legalNamestring | nullrequired
addressstring | nullrequired
countrystring | nullrequired
vatNumberstring | nullrequired
vatValidboolean | nullrequired

GET/v1/billing/statements

Monthly and annual credit statements.

API key or OAuth token with account:read.

Response (JSON)
FieldTypeNotes
statementsobject[]required
statements[].idstringrequired
statements[].periodstringrequired
statements[].kind"monthly" | "annual"required
statements[].totalintegerrequired
statements[].pdfUrlstringrequired
statements[].csvUrlstring | null

GET/v1/billing/statements/{id}/{format}

One statement as a PDF, or as CSV (`format` is `pdf` or `csv`).

API key or OAuth token with account:read.

format csv answers text/csv instead.

GET/v1/billing/transfers

Bank transfers waiting for the money to arrive, or matched.

API key or OAuth token with account:read.

Response (JSON)
FieldTypeNotes
transfersobject[]required
transfers[].referencestringrequired
transfers[].packstringrequired
transfers[].creditsintegerrequired
transfers[].amountnumberrequired
transfers[].currency"GBP" | "USD" | "EUR" | "CAD"required
transfers[].status"awaiting" | "received" | "expired"required
transfers[].createdAtstringrequired
transfers[].expiresAtstringrequired

GET/v1/billing/invoice-terms

Whether you have 14-day invoice terms for the larger packs.

API key or OAuth token with account:read.

Asking for terms (POST) needs a signed-in person.

Response (JSON)
FieldTypeNotes
status"none" | "pending" | "approved" | "declined"required
termsDaysinteger | nullrequired
decidedAtstring | nullrequired
purchaseOrderstring | null
billingContactNamestring | null
billingContactEmailstring | null

GET/v1/billing/disputes

Disputes you opened on questions, and how they ended.

API key or OAuth token with account:read.

Response (JSON)
FieldTypeNotes
disputesobject[]required
disputes[].idstringrequired
disputes[].questionIdstringrequired
disputes[].titlestringrequired
disputes[].reasonstringrequired
disputes[].status"open" | "upheld" | "partly_upheld" | "not_upheld"required
disputes[].refundCreditsintegerrequired
disputes[].creditNoteIdstring | nullrequired
disputes[].createdAtstringrequired
disputes[].resolvedAtstring | nullrequired

GET/v1/billing/packs

The credit packs on sale and their price in a currency.

No authentication.

Query parameters
FieldTypeNotes
currencystring
Response (JSON)
FieldTypeNotes
packsobject[]required
packs[].idstringrequired
packs[].creditsintegerrequired
packs[].bonusCreditsintegerrequired
packs[].pricenumberrequired
packs[].currencystringrequired
packs[].bankTransferbooleanrequired

GET/v1/me

The account behind the key: name, tier, languages, credits and sign-in providers.

API key or OAuth token with account:read.

Response (JSON)
FieldTypeNotes
userobjectrequired
user.idstringrequired
user.emailstring (email)required pattern ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
user.namestring | nullrequired
user.tier0 | 1 | 2 | 3required
user.countrystring | nullrequired
user.phonestring | nullrequired
user.displayCurrency"GBP" | "USD" | "EUR" | "CAD" | "JPY" | "KRW" | "BRL" | "TRY" | "SEK" | "DKK" | "NOK" | "PLN" | "CZK" | "RON" | nullrequired
user.languages"en" | "fr" | "es" | "pt" | "it" | "de" | "nl" | "pl" | "ja" | "ko" | "sv" | "da" | "nb" | "cs" | "ro" | "fi" | "tr"[]required
user.localestring | nullrequired
user.creditsPenceintegerrequired
user.earningsPenceintegerrequired
user.pendingPenceintegerrequired
user.providers"apple" | "google" | "email"[]required
user.createdAtstringrequired
user.demoboolean
user.captureAllowedboolean
user.storeReviewboolean

GET/v1/connections/current

The connection behind this key or token: its scopes, its caps and what it has spent today and this month.

API key or OAuth token.

Any key or token can read its own; no scope is needed. This is what MCP balance uses to show an agent its cap. 404 not_found for a first-party session, which has no connection.

Response (JSON)
FieldTypeNotes
connectionobjectrequired
connection.idstringrequired
connection.kind"oauth" | "key" | "app"required
connection.clientIdstring | nullrequired
connection.clientNamestringrequired
connection.hoststring | nullrequired
connection.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
connection.capDailyPenceintegerrequired
connection.spentTodayPenceintegerrequired
connection.spentMonthPenceintegerrequired
connection.protocolVersionstring | nullrequired
connection.validatesIssboolean | nullrequired
connection.createdAtstringrequired
connection.lastUsedAtstring | nullrequired
connection.capMonthlyPenceinteger | null
connection.capQuestionPenceinteger | null
connection.teamIdstring | null

POST/v1/connections/calls

Report tool calls made on this connection, for the usage and logs on the Developers page.

API key or OAuth token.

Used by the MCP server. Never send question text; calls are kept 90 days. 400 no_connection when the caller has no connection.

Request body (JSON)
FieldTypeNotes
callsobject[]required up to 100 items
calls[].toolstringrequired up to 64 characters
calls[].questionIdstring | null
calls[].taskIdstring | null
calls[].outcome"ok" | "error"required
calls[].codestring | null
calls[].latencyMsintegerrequired 0–3600000
calls[].traceIdstringrequired up to 64 characters
calls[].regionstring | null
calls[].creditsinteger ≥ 0
calls[].cacheHitboolean
hoststring up to 100 characters
protocolVersionstring up to 20 characters
validatesIssboolean
Response (JSON)
FieldTypeNotes
oktruerequired

Support

Open a ticket from a script or an agent. Replies come by email and in the portal.

POST/v1/support/tickets

Open a support ticket.

API key or OAuth token.

Signed in (key, OAuth or session), the reply goes to the account's email. Without a token, send email. 10 tickets an hour.

Request body (JSON)
FieldTypeNotes
subjectstringrequired up to 140 characters, at least 3 characters
bodystringrequired up to 5000 characters, at least 10 characters
appVersionstring up to 40 characters
category"payout" | "payout_failed" | "account" | "account_frozen" | "verification" | "question" | "billing" | "api" | "security" | "privacy" | "press" | "legal" | "support" | "other"
emailstring (email) pattern ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
localestring up to 10 characters
websitestring up to 2000 characters
openedAtinteger ≥ 0
Response (JSON)
FieldTypeNotes
ticketIdstringrequired

Webhook endpoints

Get an HTTPS POST when something happens, instead of polling. Manage endpoints in the portal under Developers, or with a key or OAuth grant that has webhooks:read (list) and webhooks:write (change). Keys made before scopes cannot manage webhooks.

GET/v1/webhooks/schemas

The JSON Schema of every event's data, and the payload versions.

No authentication.

Payloads are additive inside a version: new optional fields may appear; existing fields never change meaning or disappear.

Response (JSON)
FieldTypeNotes
apiVersions"2026-10-10"[]required
latest"2026-10-10"required
eventsobjectrequired
events.question.reservedmap of objectrequired
events.question.completedmap of objectrequired
events.question.underfilledmap of objectrequired
events.question.canceledmap of objectrequired
events.question.refusedmap of objectrequired
events.credits.purchasedmap of objectrequired
events.credits.lowmap of objectrequired
events.credits.expiringmap of objectrequired
events.dispute.resolvedmap of objectrequired
events.connection.cap_warningmap of objectrequired
events.webhook.testmap of objectrequired
events.member.provisionedmap of objectrequired
events.member.deprovisionedmap of objectrequired
events.policy.changedmap of objectrequired
events.approval.requestedmap of objectrequired
events.approval.decidedmap of objectrequired
events.approval.expiredmap of objectrequired
events.approval.refusedmap of objectrequired
events.workspace.createdmap of objectrequired
events.workspace.movedmap of objectrequired
events.workspace.closedmap of objectrequired

GET/v1/webhooks/events

The latest events of one type in this account, newest first, as deliveries carry them.

API key or OAuth token with webhooks:read.

Each event is the exact body a delivery sends (id, type, createdAt, apiVersion, data), so a receiver can use one as sample data or poll with it and drop ids it has seen. Events are kept whether or not an endpoint listens, for 90 days. A personal account lists the person's events, including their asks in a team; a team lists the team's. A key acts for its own account (personal or team); a session acts for the account in the account header. Team members who can ask can read; the owner and admins can change.

Query parameters
FieldTypeNotes
type"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "webhook.test" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"required
limitinteger 1–25, default 3
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].typestringrequired
events[].createdAtstringrequired
events[].apiVersion"2026-10-10"
events[].datamap of objectrequired

GET/v1/webhooks/endpoints

The endpoints in this account (yours, or the team's). Secrets are masked.

API key or OAuth token with webhooks:read.

A key acts for its own account (personal or team); a session acts for the account in the account header. Team members who can ask can read; the owner and admins can change.

Response (JSON)
FieldTypeNotes
endpointsobject[]required
endpoints[].idstringrequired
endpoints[].urlstring (URL)required
endpoints[].events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]required
endpoints[].secretstringrequired
endpoints[].createdAtstringrequired
endpoints[].lastDeliveryAtstring | nullrequired
endpoints[].lastStatusinteger | nullrequired
endpoints[].descriptionstring | null
endpoints[].enabledboolean
endpoints[].disabledReasonstring | null
endpoints[].disabledAtstring | null
endpoints[].lastSuccessAtstring | null
endpoints[].teamIdstring | null
endpoints[].apiVersion"2026-10-10"
endpoints[].filterobject | null
endpoints[].filter.projectIdsstring[] up to 20 items
endpoints[].filter.labelsstring[] up to 20 items
endpoints[].previousSecretobject | null
endpoints[].previousSecret.secretstringrequired
endpoints[].previousSecret.expiresAtstringrequired

POST/v1/webhooks/endpoints

Add an endpoint. The signing secret is in the response, once.

API key or OAuth token with webhooks:write.

https only, public hosts only. Up to 10 endpoints on a personal account and 100 on a team. In a team, only the owner or an admin can add one. apiVersion defaults to the latest. An endpoint made with a key or OAuth grant is deleted when that key or grant is revoked. Zapier's REST hooks count against its connection (up to 50 each), not the account's limit.

Request body (JSON)
FieldTypeNotes
urlstring (URL)required up to 500 characters
events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]required
descriptionstring up to 100 characters
filterobject
filter.projectIdsstring[] up to 20 items
filter.labelsstring[] up to 20 items
apiVersion"2026-10-10"
Response (JSON)
FieldTypeNotes
endpointobjectrequired
endpoint.idstringrequired
endpoint.urlstring (URL)required
endpoint.events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]required
endpoint.secretstringrequired
endpoint.createdAtstringrequired
endpoint.lastDeliveryAtstring | nullrequired
endpoint.lastStatusinteger | nullrequired
endpoint.descriptionstring | null
endpoint.enabledboolean
endpoint.disabledReasonstring | null
endpoint.disabledAtstring | null
endpoint.lastSuccessAtstring | null
endpoint.teamIdstring | null
endpoint.apiVersion"2026-10-10"
endpoint.filterobject | null
endpoint.filter.projectIdsstring[] up to 20 items
endpoint.filter.labelsstring[] up to 20 items
endpoint.previousSecretobject | null
endpoint.previousSecret.secretstringrequired
endpoint.previousSecret.expiresAtstringrequired

PATCH/v1/webhooks/endpoints/{endpointId}

Change an endpoint's address, events, description or filter, or pause and resume it.

API key or OAuth token with webhooks:write.

filter: null clears the filter. enabled: true resumes an endpoint that was switched off after failures.

Request body (JSON)
FieldTypeNotes
urlstring (URL) up to 500 characters
events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]
descriptionstring | null
enabledboolean
filterobject | null
filter.projectIdsstring[] up to 20 items
filter.labelsstring[] up to 20 items
Response (JSON)
FieldTypeNotes
endpointobjectrequired
endpoint.idstringrequired
endpoint.urlstring (URL)required
endpoint.events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]required
endpoint.secretstringrequired
endpoint.createdAtstringrequired
endpoint.lastDeliveryAtstring | nullrequired
endpoint.lastStatusinteger | nullrequired
endpoint.descriptionstring | null
endpoint.enabledboolean
endpoint.disabledReasonstring | null
endpoint.disabledAtstring | null
endpoint.lastSuccessAtstring | null
endpoint.teamIdstring | null
endpoint.apiVersion"2026-10-10"
endpoint.filterobject | null
endpoint.filter.projectIdsstring[] up to 20 items
endpoint.filter.labelsstring[] up to 20 items
endpoint.previousSecretobject | null
endpoint.previousSecret.secretstringrequired
endpoint.previousSecret.expiresAtstringrequired

DELETE/v1/webhooks/endpoints/{endpointId}

Delete an endpoint. Nothing more is sent to it.

API key or OAuth token with webhooks:write.

Response (JSON)
FieldTypeNotes
oktruerequired

POST/v1/webhooks/endpoints/{endpointId}/rotate-secret

A new signing secret, shown once. The old one keeps signing for the overlap.

API key or OAuth token with webhooks:write.

overlapHours defaults to 24 (0 to 168). During the overlap every delivery carries a signature from each secret; previousSecret.expiresAt says when the old one stops. 0 cuts over at once.

Request body (JSON)
FieldTypeNotes
overlapHoursinteger 0–168, default 24
Response (JSON)
FieldTypeNotes
endpointobjectrequired
endpoint.idstringrequired
endpoint.urlstring (URL)required
endpoint.events"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed"[]required
endpoint.secretstringrequired
endpoint.createdAtstringrequired
endpoint.lastDeliveryAtstring | nullrequired
endpoint.lastStatusinteger | nullrequired
endpoint.descriptionstring | null
endpoint.enabledboolean
endpoint.disabledReasonstring | null
endpoint.disabledAtstring | null
endpoint.lastSuccessAtstring | null
endpoint.teamIdstring | null
endpoint.apiVersion"2026-10-10"
endpoint.filterobject | null
endpoint.filter.projectIdsstring[] up to 20 items
endpoint.filter.labelsstring[] up to 20 items
endpoint.previousSecretobject | null
endpoint.previousSecret.secretstringrequired
endpoint.previousSecret.expiresAtstringrequired

POST/v1/webhooks/endpoints/{endpointId}/test

Send a signed webhook.test event to this endpoint now.

API key or OAuth token with webhooks:write.

Signed and logged like any other delivery, and retried the same way if it fails.

Response (JSON)
FieldTypeNotes
outcome"succeeded" | "retrying" | "failed" | "skipped"required
deliveryobjectrequired
delivery.idstringrequired
delivery.endpointIdstringrequired
delivery.event"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed" | "webhook.test"required
delivery.statusinteger | nullrequired
delivery.attemptsintegerrequired
delivery.nextAttemptAtstring | nullrequired
delivery.createdAtstringrequired
delivery.eventIdstring
delivery.state"pending" | "delivering" | "succeeded" | "failed"
delivery.lastErrorstring | null
delivery.deliveredAtstring | null
delivery.replayOfstring | null

GET/v1/webhooks/deliveries

The latest 100 deliveries, newest first.

API key or OAuth token with webhooks:read.

Query parameters
FieldTypeNotes
endpointIdstring
state"pending" | "delivering" | "succeeded" | "failed"
Response (JSON)
FieldTypeNotes
deliveriesobject[]required
deliveries[].idstringrequired
deliveries[].endpointIdstringrequired
deliveries[].event"question.reserved" | "question.completed" | "question.underfilled" | "question.canceled" | "question.refused" | "credits.purchased" | "credits.low" | "credits.expiring" | "dispute.resolved" | "connection.cap_warning" | "member.provisioned" | "member.deprovisioned" | "policy.changed" | "approval.requested" | "approval.decided" | "approval.expired" | "approval.refused" | "workspace.created" | "workspace.moved" | "workspace.closed" | "webhook.test"required
deliveries[].statusinteger | nullrequired
deliveries[].attemptsintegerrequired
deliveries[].nextAttemptAtstring | nullrequired
deliveries[].createdAtstringrequired
deliveries[].eventIdstring
deliveries[].state"pending" | "delivering" | "succeeded" | "failed"
deliveries[].lastErrorstring | null
deliveries[].deliveredAtstring | null
deliveries[].replayOfstring | null

GET/v1/webhooks/deliveries/{deliveryId}/attempts

Each attempt at one delivery: status, error and time taken.

API key or OAuth token with webhooks:read.

Response (JSON)
FieldTypeNotes
attemptsobject[]required
attempts[].attemptintegerrequired
attempts[].statusinteger | nullrequired
attempts[].errorstring | nullrequired
attempts[].durationMsintegerrequired
attempts[].atstringrequired

POST/v1/webhooks/deliveries/{deliveryId}/replay

Send a delivery's event again, as a new delivery.

API key or OAuth token with webhooks:write.

The body and webhook-id are the original event's, so a receiver that keeps event ids ignores a duplicate. Signed with the endpoint's current secrets.

Response (JSON)
FieldTypeNotes
oktruerequired
deliveryIdstringrequired

A2A push notifications

The Agent2Agent (A2A) server at https://mcp.50heads.com/a2a stores the push notification configs its peers set on a task here, and you can manage them with a key too. A task id is the question id. Each config gets a POST for every change of state, signed with its token. Reads need webhooks:read, changes webhooks:write, and both questions:read. Being introduced: 404 `not_found` until A2A is on for your account.

GET/v1/a2a/status

Whether the A2A server is on for anyone.

No authentication.

The agent card is at https://mcp.50heads.com/.well-known/agent-card.json while this is true.

Response (JSON)
FieldTypeNotes
enabledbooleanrequired

GET/v1/a2a/access

Whether the A2A server is on for the caller.

API key or OAuth token.

Response (JSON)
FieldTypeNotes
enabledbooleanrequired

GET/v1/a2a/tasks/{taskId}/push-configs

Your push notification configs on a task.

API key or OAuth token with webhooks:read.

Also needs questions:read. A task in another account is 404 not_found.

Response (JSON)
FieldTypeNotes
configsobject[]required
configs[].idstringrequired
configs[].taskIdstringrequired
configs[].contextIdstringrequired
configs[].urlstringrequired
configs[].tokenstringrequired
configs[].authenticationobject | nullrequired
configs[].authentication.schemestringrequired
configs[].protocolVersion"1.0" | "0.3"required
configs[].createdAtstringrequired

POST/v1/a2a/tasks/{taskId}/push-configs

Send a task's changes of state to a URL.

API key or OAuth token with webhooks:write.

Also needs questions:read. https on a public host. Up to 5 configs on a task; the same id again replaces yours. Each push carries Authorization: <scheme> <credentials> when you set authentication, X-A2A-Notification-Token: <token>, and Standard Webhooks headers signed with the secret whsec_ + base64(token), so you verify it with the token alone. protocolVersion 1.0 sends a StreamResponse with a statusUpdate (application/a2a+json); 0.3 sends the Task. Call GetTask (or GET /v2/questions/{id}/result) for the results.

Request body (JSON)
FieldTypeNotes
idstring pattern ^[A-Za-z0-9._:-]{1,64}$
urlstringrequired up to 2000 characters
tokenstring up to 512 characters, at least 16 characters
authenticationobject
authentication.schemestringrequired up to 40 characters, pattern ^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$
authentication.credentialsstring up to 4096 characters
protocolVersion"1.0" | "0.3" default "1.0"
Response (JSON)
FieldTypeNotes
configobjectrequired
config.idstringrequired
config.taskIdstringrequired
config.contextIdstringrequired
config.urlstringrequired
config.tokenstringrequired
config.authenticationobject | nullrequired
config.authentication.schemestringrequired
config.protocolVersion"1.0" | "0.3"required
config.createdAtstringrequired

GET/v1/a2a/tasks/{taskId}/push-configs/{configId}

One of your push notification configs on a task.

API key or OAuth token with webhooks:read.

Also needs questions:read. Credentials are never returned; the token is.

Response (JSON)
FieldTypeNotes
configobjectrequired
config.idstringrequired
config.taskIdstringrequired
config.contextIdstringrequired
config.urlstringrequired
config.tokenstringrequired
config.authenticationobject | nullrequired
config.authentication.schemestringrequired
config.protocolVersion"1.0" | "0.3"required
config.createdAtstringrequired

DELETE/v1/a2a/tasks/{taskId}/push-configs/{configId}

Stop a push notification config. Nothing more is sent to it.

API key or OAuth token with webhooks:write.

Also needs questions:read. Deleting one that is already gone answers ok.

Response (JSON)
FieldTypeNotes
oktruerequired

Workspace apps

Ask from Slack, Microsoft Teams, Jira, Linear or Zapier. Install 50heads into a Slack workspace, a Jira site or a Linear workspace from the portal under Settings, Integrations; asks from it spend this account's credits up to a daily cap. In Jira and Linear, a comment that starts with `/50heads ask` asks from the issue, and the results come back to the issue as a comment. Zapier connects with OAuth like any other app and is listed beside the Slack installs. List apps with apps:read; change the cap or remove one with apps:write.

GET/v1/apps

The workspace apps installed in this account (yours, or the team's), and its Zapier connections, with today's spend.

API key or OAuth token with apps:read.

Keys made before scopes cannot list apps. In a team, any role that can ask may list them. zapier lists live Zapier connections with their daily cap, today's spend and live REST hooks; removing one in the portal revokes the connection and deletes its hooks.

Response (JSON)
FieldTypeNotes
appsobject[]required
apps[].idstringrequired
apps[].provider"slack" | "teams" | "jira" | "linear"required
apps[].workspaceIdstringrequired
apps[].workspaceNamestringrequired
apps[].defaultChannelNamestring | nullrequired
apps[].capDailyPenceintegerrequired
apps[].spentTodayPenceintegerrequired
apps[].installedAtstringrequired
apps[].installedByobjectrequired
apps[].installedBy.namestring | nullrequired
apps[].installedBy.emailstring | nullrequired
apps[].status"active" | "revoked"required
zapierobject[]required default []
zapier[].connectionIdstringrequired
zapier[].connectedAtstringrequired
zapier[].lastUsedAtstring | nullrequired
zapier[].capDailyPenceintegerrequired
zapier[].spentTodayPenceintegerrequired
zapier[].hooksintegerrequired
zapier[].connectedByobjectrequired
zapier[].connectedBy.namestring | nullrequired
zapier[].connectedBy.emailstring | nullrequired
teamsAvailablebooleanrequired default false
jiraAvailablebooleanrequired default false
linearAvailablebooleanrequired default false

GET/v1/apps/slack/install

Slack's authorize page, with a signed state that works once, for 10 minutes.

Portal session only.

Portal session only; in a team, the owner or an admin. Slack sends the person back to the portal's Integrations page with installed=slack, or error= denied, expired, already_installed or failed. 503 slack_not_configured when Slack is not set up.

Response (JSON)
FieldTypeNotes
urlstringrequired

GET/v1/apps/jira/install

Atlassian's authorize page for the 50heads Jira app, with a signed state that works once, for 10 minutes.

Portal session only.

Portal session only; in a team, the owner or an admin. Atlassian sends the person back to the portal's Integrations page with installed=jira, or error= denied, expired, already_installed or failed. One Jira site per install. 503 jira_not_configured when the Atlassian app is not set up; 404 while the Jira app is off on this server.

Response (JSON)
FieldTypeNotes
urlstringrequired

GET/v1/apps/linear/install

Linear's authorize page for the 50heads Linear app, with a signed state that works once, for 10 minutes.

Portal session only.

Portal session only; in a team, the owner or an admin. Linear sends the person back to the portal's Integrations page with installed=linear, or error= denied, expired, already_installed or failed. 503 linear_not_configured when the Linear app is not set up; 404 while the Linear app is off on this server.

Response (JSON)
FieldTypeNotes
urlstringrequired

PATCH/v1/apps/{appId}

Change an app's daily cap, in credits.

API key or OAuth token with apps:write.

In a team, the owner or an admin. Asks over the cap are refused with spend_cap_reached.

Request body (JSON)
FieldTypeNotes
capDailyPenceinteger 100–10000000
Response (JSON)
FieldTypeNotes
appobjectrequired
app.idstringrequired
app.provider"slack" | "teams" | "jira" | "linear"required
app.workspaceIdstringrequired
app.workspaceNamestringrequired
app.defaultChannelNamestring | nullrequired
app.capDailyPenceintegerrequired
app.spentTodayPenceintegerrequired
app.installedAtstringrequired
app.installedByobjectrequired
app.installedBy.namestring | nullrequired
app.installedBy.emailstring | nullrequired
app.status"active" | "revoked"required

DELETE/v1/apps/{appId}

Uninstall: revokes the workspace's token and the app's connection.

API key or OAuth token with apps:write.

Questions already asked keep running and are refunded as usual if they underfill; their results cards are not posted.

Response (JSON)
FieldTypeNotes
oktruerequired

Event stream and warehouse export

Every event webhooks deliver, as a stream you page through: oldest first by `updatedAt`, with a cursor that never skips or repeats an event, so a scheduled job or an ELT tool (Airbyte, Fivetran, Hevo) reads only what is new. Read the account's stream with events:read; an organization admin reads every workspace's at /v1/organizations/current/events. Organizations can also have it, and their audit trail, written to their own S3, R2 or Google Cloud Storage bucket as JSON lines on a schedule, with a manifest per run (Organization settings, Warehouse export). Being introduced: 404 `not_found` until it is on for your account.

GET/v1/events

The account's events, oldest first by updatedAt, paged by a stable cursor.

API key or OAuth token with events:read.

A key acts for its own account; a session for the account in the account header. A personal account reads its own events (not its teams'); a team reads the team's. Keys made before scopes cannot read it (403 insufficient_scope). The page is oldest first by updatedAt, then sequence. nextCursor is always set: pass it back for the next page, or keep it and poll; an empty page still moves it on, so the next call reads only new events. Events are the webhook catalog's, with the same data; they are kept 90 days. Load by id to drop the rare repeat. Behind the warehouseExport flag: 404 not_found until it is on for your account.

Query parameters
FieldTypeNotes
cursorstring up to 40 characters
sincestring up to 40 characters
typestring up to 60 characters, pattern ^[a-z_]+(\.[a-z_]+)?$
limitinteger 1–500
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].typestringrequired
events[].sequenceintegerrequired
events[].createdAtstringrequired
events[].updatedAtstringrequired
events[].workspaceIdstring | nullrequired
events[].datamap of objectrequired
nextCursorstringrequired
hasMorebooleanrequired

GET/v1/organizations/current/events

Every workspace's events in the organization, oldest first by updatedAt, paged by a stable cursor.

API key or OAuth token with events:read.

Organization admins only (403 organization_admin_required); API keys, connected apps and service accounts with events:read where the organization has machine identities (otherwise 403 session_required). The page is oldest first by updatedAt, then sequence. nextCursor is always set: pass it back for the next page, or keep it and poll; an empty page still moves it on, so the next call reads only new events. Events are the webhook catalog's, with the same data; they are kept 90 days. Load by id to drop the rare repeat. Behind the warehouseExport flag: 404 not_found until it is on for your account.

Query parameters
FieldTypeNotes
cursorstring up to 40 characters
sincestring up to 40 characters
typestring up to 60 characters, pattern ^[a-z_]+(\.[a-z_]+)?$
limitinteger 1–500
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].typestringrequired
events[].sequenceintegerrequired
events[].createdAtstringrequired
events[].updatedAtstringrequired
events[].workspaceIdstring | nullrequired
events[].datamap of objectrequired
nextCursorstringrequired
hasMorebooleanrequired

GET/v1/organizations/current/warehouse/destinations

The organization's warehouse export destinations: buckets, schedules, state and how far each data set has been written.

API key or OAuth token with organization:read.

Organization admins, or a key, connected app or service account with organization:read. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
destinationsobject[]required
destinations[].idstringrequired
destinations[].namestringrequired
destinations[].provider"s3" | "r2" | "gcs"required
destinations[].bucketstringrequired
destinations[].regionstring | nullrequired
destinations[].accountIdstring | nullrequired
destinations[].jurisdiction"default" | "eu" | "fedramp" | nullrequired
destinations[].prefixstringrequired
destinations[].accessKeyHintstringrequired
destinations[].datasets"events" | "audit"[]required
destinations[].intervalMinutesintegerrequired
destinations[].state"active" | "paused" | "failing"required
destinations[].consecutiveFailuresintegerrequired
destinations[].lastErrorstring | nullrequired
destinations[].lastRunAtstring | nullrequired
destinations[].lastSuccessAtstring | nullrequired
destinations[].nextRunAtstring | nullrequired
destinations[].exportedThroughobjectrequired
destinations[].exportedThrough.eventsstring | nullrequired
destinations[].exportedThrough.auditstring | nullrequired
destinations[].createdAtstringrequired
destinations[].createdByobjectrequired
destinations[].createdBy.idstringrequired
destinations[].createdBy.namestring | nullrequired

GET/v1/organizations/current/warehouse/destinations/{id}

One destination.

API key or OAuth token with organization:read.

As the list. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
destinationobjectrequired
destination.idstringrequired
destination.namestringrequired
destination.provider"s3" | "r2" | "gcs"required
destination.bucketstringrequired
destination.regionstring | nullrequired
destination.accountIdstring | nullrequired
destination.jurisdiction"default" | "eu" | "fedramp" | nullrequired
destination.prefixstringrequired
destination.accessKeyHintstringrequired
destination.datasets"events" | "audit"[]required
destination.intervalMinutesintegerrequired
destination.state"active" | "paused" | "failing"required
destination.consecutiveFailuresintegerrequired
destination.lastErrorstring | nullrequired
destination.lastRunAtstring | nullrequired
destination.lastSuccessAtstring | nullrequired
destination.nextRunAtstring | nullrequired
destination.exportedThroughobjectrequired
destination.exportedThrough.eventsstring | nullrequired
destination.exportedThrough.auditstring | nullrequired
destination.createdAtstringrequired
destination.createdByobjectrequired
destination.createdBy.idstringrequired
destination.createdBy.namestring | nullrequired

GET/v1/organizations/current/warehouse/destinations/{id}/runs

The destination's last 50 runs, newest first: rows and files written, the manifest, and the error of a run that failed.

API key or OAuth token with organization:read.

As the list. Runs are kept 90 days. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
runsobject[]required
runs[].idstringrequired
runs[].destinationIdstringrequired
runs[].trigger"schedule" | "manual"required
runs[].status"running" | "succeeded" | "failed"required
runs[].startedAtstringrequired
runs[].finishedAtstring | nullrequired
runs[].rowsobjectrequired
runs[].rows.eventsintegerrequired
runs[].rows.auditintegerrequired
runs[].bytesintegerrequired
runs[].filesintegerrequired
runs[].manifestKeystring | nullrequired
runs[].morebooleanrequired
runs[].errorstring | nullrequired

POST/v1/organizations/current/warehouse/destinations

Add a bucket: AWS S3 (with its region), Cloudflare R2 (with the account ID) or Google Cloud Storage (HMAC keys).

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Saving writes a small test file under the prefix first; if the bucket refuses it nothing is saved (422 destination_unreachable, with the test in details.test). The keys need permission to put objects under the prefix and nothing else. The first run is within five minutes. Up to 5 destinations (409 too_many_destinations). Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 60 characters
provider"s3" | "r2" | "gcs"required
bucketstringrequired pattern ^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$
regionstring pattern ^[a-z]{2}(-[a-z]+)+-\d{1,2}$
accountIdstring pattern ^[a-f0-9]{32}$
jurisdiction"default" | "eu" | "fedramp"
prefixstring up to 200 characters, pattern ^(?:[A-Za-z0-9!_.*'()=-]+\/){0,8}$, default "50heads/"
datasets"events" | "audit"[] up to 2 items, default ["events"]
intervalMinutes60 | 360 | 1440 default 60
accessKeyIdstringrequired up to 128 characters, at least 4 characters, pattern ^[\x21-\x7e]+$
secretAccessKeystringrequired up to 256 characters, at least 8 characters, pattern ^[\x21-\x7e]+$
Response (JSON)
FieldTypeNotes
destinationobjectrequired
destination.idstringrequired
destination.namestringrequired
destination.provider"s3" | "r2" | "gcs"required
destination.bucketstringrequired
destination.regionstring | nullrequired
destination.accountIdstring | nullrequired
destination.jurisdiction"default" | "eu" | "fedramp" | nullrequired
destination.prefixstringrequired
destination.accessKeyHintstringrequired
destination.datasets"events" | "audit"[]required
destination.intervalMinutesintegerrequired
destination.state"active" | "paused" | "failing"required
destination.consecutiveFailuresintegerrequired
destination.lastErrorstring | nullrequired
destination.lastRunAtstring | nullrequired
destination.lastSuccessAtstring | nullrequired
destination.nextRunAtstring | nullrequired
destination.exportedThroughobjectrequired
destination.exportedThrough.eventsstring | nullrequired
destination.exportedThrough.auditstring | nullrequired
destination.createdAtstringrequired
destination.createdByobjectrequired
destination.createdBy.idstringrequired
destination.createdBy.namestring | nullrequired
testobjectrequired
test.okbooleanrequired
test.statusinteger | nullrequired
test.code"access_denied" | "no_such_bucket" | "invalid_credentials" | "wrong_region" | "unreachable" | "bucket_error" | nullrequired
test.messagestringrequired
test.keystringrequired

PATCH/v1/organizations/current/warehouse/destinations/{id}

Rename, change the prefix, data sets or schedule, pause or resume, or replace the keys.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. New keys or a new prefix are tested with a write first (422 destination_unreachable). Resuming a failing destination runs it within five minutes. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Request body (JSON)
FieldTypeNotes
namestring up to 60 characters
prefixstring up to 200 characters, pattern ^(?:[A-Za-z0-9!_.*'()=-]+\/){0,8}$
datasets"events" | "audit"[] up to 2 items
intervalMinutes60 | 360 | 1440
pausedboolean
credentialsobject
credentials.accessKeyIdstringrequired up to 128 characters, at least 4 characters, pattern ^[\x21-\x7e]+$
credentials.secretAccessKeystringrequired up to 256 characters, at least 8 characters, pattern ^[\x21-\x7e]+$
Response (JSON)
FieldTypeNotes
destinationobjectrequired
destination.idstringrequired
destination.namestringrequired
destination.provider"s3" | "r2" | "gcs"required
destination.bucketstringrequired
destination.regionstring | nullrequired
destination.accountIdstring | nullrequired
destination.jurisdiction"default" | "eu" | "fedramp" | nullrequired
destination.prefixstringrequired
destination.accessKeyHintstringrequired
destination.datasets"events" | "audit"[]required
destination.intervalMinutesintegerrequired
destination.state"active" | "paused" | "failing"required
destination.consecutiveFailuresintegerrequired
destination.lastErrorstring | nullrequired
destination.lastRunAtstring | nullrequired
destination.lastSuccessAtstring | nullrequired
destination.nextRunAtstring | nullrequired
destination.exportedThroughobjectrequired
destination.exportedThrough.eventsstring | nullrequired
destination.exportedThrough.auditstring | nullrequired
destination.createdAtstringrequired
destination.createdByobjectrequired
destination.createdBy.idstringrequired
destination.createdBy.namestring | nullrequired

DELETE/v1/organizations/current/warehouse/destinations/{id}

Stop exporting to a bucket and delete its keys. Files already written stay in the bucket.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
oktruerequired

POST/v1/organizations/current/warehouse/destinations/{id}/test

Test the connection: write a small file with the stored keys.

Portal session only.

Organization admins, signed in. Answers 200 whether or not the write worked; test.ok says which, with the bucket's error code. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
testobjectrequired
test.okbooleanrequired
test.statusinteger | nullrequired
test.code"access_denied" | "no_such_bucket" | "invalid_credentials" | "wrong_region" | "unreachable" | "bucket_error" | nullrequired
test.messagestringrequired
test.keystringrequired

POST/v1/organizations/current/warehouse/destinations/{id}/run

Run an export now (within five minutes) instead of waiting for the schedule.

Portal session only.

Organization admins, signed in. A paused destination answers 409 destination_paused. Behind the warehouseExport flag (and the organizations flag): 404 not_found until it is on for your organization. Each run writes JSON lines under <prefix><dataset>/dt=<date>/<run>-<n>.jsonl and then a manifest at <prefix>manifests/<time>-<run>.json listing the files, their rows and SHA-256; load the files a manifest lists (a file with no manifest is from a run that failed and is written again). Parquet is not written. Keys are stored encrypted and never returned.

Response (JSON)
FieldTypeNotes
destinationobjectrequired
destination.idstringrequired
destination.namestringrequired
destination.provider"s3" | "r2" | "gcs"required
destination.bucketstringrequired
destination.regionstring | nullrequired
destination.accountIdstring | nullrequired
destination.jurisdiction"default" | "eu" | "fedramp" | nullrequired
destination.prefixstringrequired
destination.accessKeyHintstringrequired
destination.datasets"events" | "audit"[]required
destination.intervalMinutesintegerrequired
destination.state"active" | "paused" | "failing"required
destination.consecutiveFailuresintegerrequired
destination.lastErrorstring | nullrequired
destination.lastRunAtstring | nullrequired
destination.lastSuccessAtstring | nullrequired
destination.nextRunAtstring | nullrequired
destination.exportedThroughobjectrequired
destination.exportedThrough.eventsstring | nullrequired
destination.exportedThrough.auditstring | nullrequired
destination.createdAtstringrequired
destination.createdByobjectrequired
destination.createdBy.idstringrequired
destination.createdBy.namestring | nullrequired

Spend approvals

An organization can ask that some asks wait for a person: over an amount per ask, day or month, at a tier, with images, or always. An ask a policy holds comes back as 202 `approval_required` with its credits held and nothing sent to heads; an approver decides in the 50heads app or at 50heads.com, and the agent reads the outcome here (or waits on the question). Being introduced: 404 `not_found` until it is on for your organization.

GET/v1/approvals

Approvals for the signed-in person: waiting for them (`inbox`), decided (`history`) or the whole workspace's (`workspace`, for owners, admins and billing).

Portal session only.

A person, in the app or at 50heads.com. pendingForYou is the badge on the Answer tab. An empty list while approvals are off.

Query parameters
FieldTypeNotes
view"inbox" | "history" | "workspace"
state"pending" | "approved" | "denied" | "expired" | "canceled"
limitinteger 1–100
cursorstring
Response (JSON)
FieldTypeNotes
approvalsobject[]required
approvals[].idstringrequired
approvals[].state"pending" | "approved" | "denied" | "expired" | "canceled"required
approvals[].organizationIdstringrequired
approvals[].organizationNamestringrequired
approvals[].workspaceIdstringrequired
approvals[].workspaceNamestringrequired
approvals[].questionIdstringrequired
approvals[].questionobjectrequired
approvals[].question.textstringrequired
approvals[].question.typestringrequired
approvals[].question.optionsobject[]required
approvals[].question.contextstring | nullrequired
approvals[].amountPenceintegerrequired
approvals[].headsintegerrequired
approvals[].tierintegerrequired
approvals[].etaMinutesinteger | nullrequired
approvals[].reasons"per_ask" | "per_day" | "per_month" | "tier" | "images" | "always"[]required
approvals[].requesterobjectrequired
approvals[].requester.kind"key" | "app" | "service_account" | "person"required
approvals[].requester.namestringrequired
approvals[].requester.keyTailstring | nullrequired
approvals[].projectIdstring | nullrequired
approvals[].projectNamestring | nullrequired
approvals[].quorum1 | 2required
approvals[].approvedCountintegerrequired
approvals[].approverRoles"owner" | "admin" | "billing"[]required
approvals[].approverCountintegerrequired
approvals[].votesobject[]required
approvals[].votes[].userIdstring | nullrequired
approvals[].votes[].namestring | nullrequired
approvals[].votes[].rolestringrequired
approvals[].votes[].decision"approve" | "deny"required
approvals[].votes[].reasonstring | nullrequired
approvals[].votes[].channel"app" | "web"required
approvals[].votes[].atstringrequired
approvals[].expiresAtstringrequired
approvals[].createdAtstringrequired
approvals[].decidedAtstring | nullrequired
approvals[].canDecidebooleanrequired
approvals[].blocked"own_ask" | "not_approver" | "already_voted" | "decided" | "expired" | nullrequired
approvals[].heldPenceintegerrequired
approvals[].confirmRequiredbooleanrequired
approvals[].approveUrlstringrequired
pendingForYouintegerrequired
nextCursorstring | nullrequired
statsobject | null
stats.decidedintegerrequired
stats.approvedintegerrequired
stats.deniedintegerrequired
stats.expiredintegerrequired
stats.approvedPenceintegerrequired
stats.releasedPenceintegerrequired
stats.medianMinutesnumber | nullrequired
stats.slowestMinutesnumber | nullrequired
stats.monthlyLimitPenceinteger | nullrequired
enabledboolean

GET/v1/approvals/{id}

What an agent can read about its own approval: the state and, once decided, the approver's role and typed reason.

API key or OAuth token with questions:read.

Poll this (the 202 says how often), or wait on the question. Denied: code: refused_by_policy with the approver's reason and role (owner, admin or billing), never their name. Nobody decided in time: code: approval_expired. Only the account that asked, or a person who can see the approval, can read it; anyone else gets 404.

Response (JSON)
FieldTypeNotes
approvalIdstringrequired
questionIdstringrequired
status"pending" | "approved" | "denied" | "expired" | "canceled"required
amountPenceintegerrequired
approveUrlstringrequired
expiresAtstringrequired
quorum1 | 2required
approvedCountintegerrequired
code"refused_by_policy" | "approval_expired" | nullrequired
decisionobject | nullrequired
decision.outcome"approved" | "denied" | "expired" | "canceled"required
decision.reasonstring | nullrequired
decision.decidedByRolestring | nullrequired
decision.decidedAtstringrequired

GET/v1/approvals/{id}/review

The approver's view of one approval: the question, the amount, who asked, who has decided. Opening it is written to the audit trail.

Portal session only.

A person who can see the approval; anyone else gets 404.

Response (JSON)
FieldTypeNotes
approvalobjectrequired
approval.idstringrequired
approval.state"pending" | "approved" | "denied" | "expired" | "canceled"required
approval.organizationIdstringrequired
approval.organizationNamestringrequired
approval.workspaceIdstringrequired
approval.workspaceNamestringrequired
approval.questionIdstringrequired
approval.questionobjectrequired
approval.question.textstringrequired
approval.question.typestringrequired
approval.question.optionsobject[]required
approval.question.contextstring | nullrequired
approval.amountPenceintegerrequired
approval.headsintegerrequired
approval.tierintegerrequired
approval.etaMinutesinteger | nullrequired
approval.reasons"per_ask" | "per_day" | "per_month" | "tier" | "images" | "always"[]required
approval.requesterobjectrequired
approval.requester.kind"key" | "app" | "service_account" | "person"required
approval.requester.namestringrequired
approval.requester.keyTailstring | nullrequired
approval.projectIdstring | nullrequired
approval.projectNamestring | nullrequired
approval.quorum1 | 2required
approval.approvedCountintegerrequired
approval.approverRoles"owner" | "admin" | "billing"[]required
approval.approverCountintegerrequired
approval.votesobject[]required
approval.votes[].userIdstring | nullrequired
approval.votes[].namestring | nullrequired
approval.votes[].rolestringrequired
approval.votes[].decision"approve" | "deny"required
approval.votes[].reasonstring | nullrequired
approval.votes[].channel"app" | "web"required
approval.votes[].atstringrequired
approval.expiresAtstringrequired
approval.createdAtstringrequired
approval.decidedAtstring | nullrequired
approval.canDecidebooleanrequired
approval.blocked"own_ask" | "not_approver" | "already_voted" | "decided" | "expired" | nullrequired
approval.heldPenceintegerrequired
approval.confirmRequiredbooleanrequired
approval.approveUrlstringrequired

POST/v1/approvals/{id}/confirm

The app: Face ID or the passcode succeeded on the phone. Returns a one-use confirmation and opens a five-minute window.

Portal session only.

App sessions only. Every decision over £100.00 needs its own confirmation (confirmationId); smaller ones are covered by a confirm in the last five minutes.

Request body (JSON)
FieldTypeNotes
method"biometric" | "passcode"required
Response (JSON)
FieldTypeNotes
confirmationIdstringrequired
expiresAtstringrequired
windowEndsAtstringrequired

POST/v1/approvals/{id}/decision

Approve or deny. Needs a person: never an API key, a connected app or a service account.

Portal session only.

An approver on a browser session with a fresh proof in x-50h-reauth (spent by the decision), or in the app with a confirmationId. The typed reason goes to the agent and the audit trail. 403 separation_of_duties when the approver's own key made the ask (unless the policy allows it under an amount), 403 not_an_approver, 403 confirm_required (the app has not confirmed it is them, or the confirmation was used or is too old), 409 approval_decided or approval_expired. Approving the last approval a policy needs makes the question live; denying releases the held credits.

Request body (JSON)
FieldTypeNotes
decision"approve" | "deny"required
reasonstring up to 200 characters
confirmationIdstring up to 100 characters
Response (JSON)
FieldTypeNotes
approvalobjectrequired
approval.idstringrequired
approval.state"pending" | "approved" | "denied" | "expired" | "canceled"required
approval.organizationIdstringrequired
approval.organizationNamestringrequired
approval.workspaceIdstringrequired
approval.workspaceNamestringrequired
approval.questionIdstringrequired
approval.questionobjectrequired
approval.question.textstringrequired
approval.question.typestringrequired
approval.question.optionsobject[]required
approval.question.contextstring | nullrequired
approval.amountPenceintegerrequired
approval.headsintegerrequired
approval.tierintegerrequired
approval.etaMinutesinteger | nullrequired
approval.reasons"per_ask" | "per_day" | "per_month" | "tier" | "images" | "always"[]required
approval.requesterobjectrequired
approval.requester.kind"key" | "app" | "service_account" | "person"required
approval.requester.namestringrequired
approval.requester.keyTailstring | nullrequired
approval.projectIdstring | nullrequired
approval.projectNamestring | nullrequired
approval.quorum1 | 2required
approval.approvedCountintegerrequired
approval.approverRoles"owner" | "admin" | "billing"[]required
approval.approverCountintegerrequired
approval.votesobject[]required
approval.votes[].userIdstring | nullrequired
approval.votes[].namestring | nullrequired
approval.votes[].rolestringrequired
approval.votes[].decision"approve" | "deny"required
approval.votes[].reasonstring | nullrequired
approval.votes[].channel"app" | "web"required
approval.votes[].atstringrequired
approval.expiresAtstringrequired
approval.createdAtstringrequired
approval.decidedAtstring | nullrequired
approval.canDecidebooleanrequired
approval.blocked"own_ask" | "not_approver" | "already_voted" | "decided" | "expired" | nullrequired
approval.heldPenceintegerrequired
approval.confirmRequiredbooleanrequired
approval.approveUrlstringrequired
outcome"approved" | "denied" | "recorded"required

GET/v1/approvals/notifications

The person's own push, email, quiet hours and daily digest settings for approvals.

Portal session only.

Response (JSON)
FieldTypeNotes
settingsobjectrequired
settings.pushbooleanrequired
settings.emailbooleanrequired
settings.quietFrominteger | nullrequired
settings.quietTointeger | nullrequired
settings.digestbooleanrequired
settings.digestHourintegerrequired 0–23
settings.timezonestring | nullrequired

PUT/v1/approvals/notifications

Change them. Quiet hours are both set or both null.

Portal session only.

Request body (JSON)
FieldTypeNotes
pushboolean
emailboolean
quietFrominteger | null
quietTointeger | null
digestboolean
digestHourinteger 0–23
timezonestring | null
Response (JSON)
FieldTypeNotes
settingsobjectrequired
settings.pushbooleanrequired
settings.emailbooleanrequired
settings.quietFrominteger | nullrequired
settings.quietTointeger | nullrequired
settings.digestbooleanrequired
settings.digestHourintegerrequired 0–23
settings.timezonestring | nullrequired

GET/v1/approvals/policies

The organization's approval policies, and the workspaces, projects, keys and people they can name.

Portal session only.

Organization admins only (403 organization_admin_required).

Response (JSON)
FieldTypeNotes
policiesobject[]required
policies[].idstringrequired
policies[].organizationIdstringrequired
policies[].scope"organization" | "workspace" | "project" | "key"required
policies[].scopeIdstring | nullrequired
policies[].scopeNamestring | nullrequired
policies[].enabledbooleanrequired
policies[].alwaysbooleanrequired
policies[].perAskPenceinteger | nullrequired
policies[].perDayPenceinteger | nullrequired
policies[].perMonthPenceinteger | nullrequired
policies[].requireForTiersinteger[]required
policies[].requireForImagesbooleanrequired
policies[].approversobjectrequired
policies[].approvers.roles"owner" | "admin" | "billing"[]required
policies[].approvers.peopleobject[]required
policies[].quorum1 | 2required
policies[].expiresInMinutesintegerrequired
policies[].onExpiry"deny"required
policies[].allowSelfApprovalUnderPenceinteger | nullrequired
policies[].notify"app" | "email"[]required
policies[].createdAtstringrequired
policies[].updatedAtstring | nullrequired
targetsobjectrequired
targets.workspacesobject[]required
targets.workspaces[].idstringrequired
targets.workspaces[].namestringrequired
targets.projectsobject[]required
targets.projects[].idstringrequired
targets.projects[].namestringrequired
targets.projects[].workspaceIdstring | nullrequired
targets.keysobject[]required
targets.keys[].idstringrequired
targets.keys[].namestringrequired
targets.keys[].workspaceIdstring | nullrequired
targets.peopleobject[]required
targets.people[].userIdstringrequired
targets.people[].namestring | nullrequired
targets.people[].emailstring | nullrequired

POST/v1/approvals/policies

Make a policy for the organization, a workspace, a project or a key. One per scope; the narrowest wins.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Approvers must be people in the organization (in the workspace, for a workspace, project or key policy).

Request body (JSON)
FieldTypeNotes
enabledboolean default true
alwaysboolean default false
perAskPenceinteger | null default null
perDayPenceinteger | null default null
perMonthPenceinteger | null default null
requireForTiersinteger[] up to 3 items, default []
requireForImagesboolean default false
approversobject default {"roles":["owner","admin"],"people":[]}
approvers.roles"owner" | "admin" | "billing"[] up to 3 items, default ["owner","admin"]
approvers.peoplestring[] up to 25 items, default []
quorum1 | 2 default 1
expiresInMinutesinteger 5–1440, default 60
onExpiry"deny" default "deny"
allowSelfApprovalUnderPenceinteger | null default null
notify"app" | "email"[] up to 2 items, default ["app","email"]
scope"organization" | "workspace" | "project" | "key"required
scopeIdstring | null default null
Response (JSON)
FieldTypeNotes
policyobjectrequired
policy.idstringrequired
policy.organizationIdstringrequired
policy.scope"organization" | "workspace" | "project" | "key"required
policy.scopeIdstring | nullrequired
policy.scopeNamestring | nullrequired
policy.enabledbooleanrequired
policy.alwaysbooleanrequired
policy.perAskPenceinteger | nullrequired
policy.perDayPenceinteger | nullrequired
policy.perMonthPenceinteger | nullrequired
policy.requireForTiersinteger[]required
policy.requireForImagesbooleanrequired
policy.approversobjectrequired
policy.approvers.roles"owner" | "admin" | "billing"[]required
policy.approvers.peopleobject[]required
policy.quorum1 | 2required
policy.expiresInMinutesintegerrequired
policy.onExpiry"deny"required
policy.allowSelfApprovalUnderPenceinteger | nullrequired
policy.notify"app" | "email"[]required
policy.createdAtstringrequired
policy.updatedAtstring | nullrequired

PATCH/v1/approvals/policies/{id}

Change a policy. Only what you send changes; asks already waiting keep the approvers and quorum they were held with.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit.

Request body (JSON)
FieldTypeNotes
enabledboolean
alwaysboolean
perAskPenceinteger | null
perDayPenceinteger | null
perMonthPenceinteger | null
requireForTiersinteger[] up to 3 items
requireForImagesboolean
approversobject
approvers.roles"owner" | "admin" | "billing"[] up to 3 items
approvers.peoplestring[] up to 25 items
quorum1 | 2
expiresInMinutesinteger 5–1440
allowSelfApprovalUnderPenceinteger | null
notify"app" | "email"[] up to 2 items
Response (JSON)
FieldTypeNotes
policyobjectrequired
policy.idstringrequired
policy.organizationIdstringrequired
policy.scope"organization" | "workspace" | "project" | "key"required
policy.scopeIdstring | nullrequired
policy.scopeNamestring | nullrequired
policy.enabledbooleanrequired
policy.alwaysbooleanrequired
policy.perAskPenceinteger | nullrequired
policy.perDayPenceinteger | nullrequired
policy.perMonthPenceinteger | nullrequired
policy.requireForTiersinteger[]required
policy.requireForImagesbooleanrequired
policy.approversobjectrequired
policy.approvers.roles"owner" | "admin" | "billing"[]required
policy.approvers.peopleobject[]required
policy.quorum1 | 2required
policy.expiresInMinutesintegerrequired
policy.onExpiry"deny"required
policy.allowSelfApprovalUnderPenceinteger | nullrequired
policy.notify"app" | "email"[]required
policy.createdAtstringrequired
policy.updatedAtstring | nullrequired

DELETE/v1/approvals/policies/{id}

Delete a policy. Asks already waiting are decided as before.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit.

Response (JSON)
FieldTypeNotes
oktruerequired

Organizations

Being introduced for teams that ask for it. An organization holds a team's workspaces, proves the email domains it owns with a DNS record, and sets rules for people on those domains. Signed in at 50heads.com, changes need an organization admin (an owner or admin of one of its workspaces) who has just confirmed it is them. Where the organization has machine identities, API keys and connected apps of an organization admin, and the organization's service accounts, can use the routes marked with a scope (organization:read, organization:write, members:read, members:write, audit:read); the rest stay with a person.

GET/v1/organizations/current

The organization of the team account in use, with your role in it.

API key or OAuth token with organization:read.

The organization of the team named in the account header. A personal account has none: 404 not_in_organization. Any member of its workspaces can read this; role says whether you are an admin.

Response (JSON)
FieldTypeNotes
organizationobjectrequired
organization.idstringrequired
organization.namestringrequired
organization.createdAtstringrequired
organization.role"admin" | "member"required
organization.defaultWorkspaceIdstring | nullrequired
organization.workspacesobject[]required
organization.workspaces[].idstringrequired
organization.workspaces[].namestringrequired
organization.workspaces[].costCenterstring | null
organization.workspaces[].closedboolean
organization.flagsmap of objectrequired
organization.policiesobjectrequired
organization.policies.autoJoinbooleanrequired
organization.policies.claimWithNoticebooleanrequired
organization.policies.restrictPersonalSignupbooleanrequired
organization.policies.noticeDaysintegerrequired 7–90
organization.verifiedDomainsstring[]required
organization.ssoobject | null
organization.sso.state"draft" | "tested" | "enabled"required
organization.sso.enforcebooleanrequired
organization.scimobject
organization.scim.activebooleanrequired
organization.securityPoliciesobject
organization.securityPolicies.requireTwoStepbooleanrequired
organization.securityPolicies.maxSessionAgeMinutesinteger | nullrequired
organization.securityPolicies.idleTimeoutMinutesinteger | nullrequired
organization.securityPolicies.reauthWindowMinutesinteger | nullrequired
organization.machineIdentitiesboolean
organization.warehouseExportboolean

PATCH/v1/organizations/current

Rename the organization. Its workspaces keep their names.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account).

Request body (JSON)
FieldTypeNotes
namestringrequired up to 80 characters
Response (JSON)
FieldTypeNotes
organizationobjectrequired
organization.idstringrequired
organization.namestringrequired
organization.createdAtstringrequired
organization.role"admin" | "member"required
organization.defaultWorkspaceIdstring | nullrequired
organization.workspacesobject[]required
organization.workspaces[].idstringrequired
organization.workspaces[].namestringrequired
organization.workspaces[].costCenterstring | null
organization.workspaces[].closedboolean
organization.flagsmap of objectrequired
organization.policiesobjectrequired
organization.policies.autoJoinbooleanrequired
organization.policies.claimWithNoticebooleanrequired
organization.policies.restrictPersonalSignupbooleanrequired
organization.policies.noticeDaysintegerrequired 7–90
organization.verifiedDomainsstring[]required
organization.ssoobject | null
organization.sso.state"draft" | "tested" | "enabled"required
organization.sso.enforcebooleanrequired
organization.scimobject
organization.scim.activebooleanrequired
organization.securityPoliciesobject
organization.securityPolicies.requireTwoStepbooleanrequired
organization.securityPolicies.maxSessionAgeMinutesinteger | nullrequired
organization.securityPolicies.idleTimeoutMinutesinteger | nullrequired
organization.securityPolicies.reauthWindowMinutesinteger | nullrequired
organization.machineIdentitiesboolean
organization.warehouseExportboolean

GET/v1/organizations/current/domains

The organization's domains, each with the TXT record that proves it.

API key or OAuth token with organization:read.

Organization admins only. Removed domains are not listed.

Response (JSON)
FieldTypeNotes
domainsobject[]required
domains[].idstringrequired
domains[].domainstringrequired
domains[].state"pending" | "verified" | "warning" | "failed" | "revoked"required
domains[].challengeobjectrequired
domains[].challenge.recordNamestringrequired
domains[].challenge.recordType"TXT"required
domains[].challenge.valuestringrequired
domains[].createdAtstringrequired
domains[].verifiedAtstring | nullrequired
domains[].checkedAtstring | nullrequired
domains[].lastErrorstring | nullrequired
domains[].warningSincestring | nullrequired
domains[].graceEndsAtstring | nullrequired

POST/v1/organizations/current/domains

Add a domain to prove. It starts as pending, with its TXT record.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). The exact domain only: example.com covers addresses @example.com, not @mail.example.com and never @evil-example.com. Public suffixes (co.uk), free email providers and disposable email domains are refused with domain_not_allowed. Up to 20 domains.

Request body (JSON)
FieldTypeNotes
domainstringrequired up to 253 characters, at least 3 characters
Response (JSON)
FieldTypeNotes
domainobjectrequired
domain.idstringrequired
domain.domainstringrequired
domain.state"pending" | "verified" | "warning" | "failed" | "revoked"required
domain.challengeobjectrequired
domain.challenge.recordNamestringrequired
domain.challenge.recordType"TXT"required
domain.challenge.valuestringrequired
domain.createdAtstringrequired
domain.verifiedAtstring | nullrequired
domain.checkedAtstring | nullrequired
domain.lastErrorstring | nullrequired
domain.warningSincestring | nullrequired
domain.graceEndsAtstring | nullrequired

POST/v1/organizations/current/domains/{domainId}/verify

Look the TXT record up now and verify the domain if it is there.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). outcome says what the lookup found: verified, already_verified, record_missing, value_mismatch, lookup_failed (try again later) or held_elsewhere (another organization has verified it). Only one organization can hold a domain. Verified domains are checked again daily; a missing record moves the domain to warning and emails the admins, and after 7 days to failed, when its policies stop applying.

Response (JSON)
FieldTypeNotes
outcome"verified" | "already_verified" | "record_missing" | "value_mismatch" | "lookup_failed" | "held_elsewhere"required
domainobjectrequired
domain.idstringrequired
domain.domainstringrequired
domain.state"pending" | "verified" | "warning" | "failed" | "revoked"required
domain.challengeobjectrequired
domain.challenge.recordNamestringrequired
domain.challenge.recordType"TXT"required
domain.challenge.valuestringrequired
domain.createdAtstringrequired
domain.verifiedAtstring | nullrequired
domain.checkedAtstring | nullrequired
domain.lastErrorstring | nullrequired
domain.warningSincestring | nullrequired
domain.graceEndsAtstring | nullrequired

DELETE/v1/organizations/current/domains/{domainId}

Remove a domain. Its policies stop applying at once.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account).

Response (JSON)
FieldTypeNotes
oktruerequired

GET/v1/organizations/current/policies

The rules for people on the organization's verified domains. All off by default.

API key or OAuth token with organization:read.

Organization admins only.

Response (JSON)
FieldTypeNotes
policiesobjectrequired
policies.autoJoinbooleanrequired
policies.claimWithNoticebooleanrequired
policies.restrictPersonalSignupbooleanrequired
policies.noticeDaysintegerrequired 7–90

PATCH/v1/organizations/current/policies

Turn a policy on or off.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Turning a policy on needs a verified domain (409 domain_not_verified). Policies never delete, merge or move an existing personal account.

Request body (JSON)
FieldTypeNotes
autoJoinboolean
claimWithNoticeboolean
restrictPersonalSignupboolean
noticeDaysinteger 7–90
Response (JSON)
FieldTypeNotes
policiesobjectrequired
policies.autoJoinbooleanrequired
policies.claimWithNoticebooleanrequired
policies.restrictPersonalSignupbooleanrequired
policies.noticeDaysintegerrequired 7–90

GET/v1/organizations/current/audit

Who did what in the organization, newest first.

API key or OAuth token with audit:read.

Organization admins only. Read-only: entries cannot be changed or deleted. 50 a page.

Query parameters
FieldTypeNotes
cursorstring
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].atstringrequired
events[].actorobjectrequired
events[].actor.idstring | nullrequired
events[].actor.namestring | nullrequired
events[].action"organization.created" | "organization.renamed" | "domain.added" | "domain.removed" | "domain.verified" | "domain.check_failed" | "domain.warning" | "domain.restored" | "domain.failed" | "policy.changed" | "member.joined" | "sso.configured" | "sso.updated" | "sso.test_started" | "sso.tested" | "sso.enabled" | "sso.disabled" | "sso.enforced" | "sso.unenforced" | "sso.enforcement_suspended" | "sso.linked" | "sso.unlinked" | "break_glass.added" | "break_glass.removed" | "break_glass.used" | "scim.token_created" | "scim.token_rotated" | "scim.token_revoked" | "scim.settings_changed" | "scim.mapping_created" | "scim.mapping_changed" | "scim.mapping_removed" | "group.created" | "group.renamed" | "group.members_changed" | "group.deleted" | "member.provisioned" | "member.linked" | "member.updated" | "member.role_changed" | "member.removed" | "member.deprovisioned" | "member.reactivated" | "member.deleted" | "member.sessions_revoked" | "member.credentials_revoked" | "member.webhooks_stopped" | "deprovision.failing" | "workspace.owner_changed" | "sso.sessions_ended" | "service_account.created" | "service_account.updated" | "service_account.disabled" | "service_account.enabled" | "service_account.deleted" | "service_account.secret_rotated" | "service_account.secret_revoked" | "service_account.keys_changed" | "approval.requested" | "approval.viewed" | "approval.reauthenticated" | "approval.decided" | "approval.refused" | "approval.expired" | "approval.canceled" | "approval.policy_changed" | "workspace.created" | "workspace.renamed" | "workspace.cost_center_changed" | "workspace.closed" | "workspace.move_requested" | "workspace.move_canceled" | "workspace.move_declined" | "workspace.moved_in" | "workspace.moved_out" | "organization.default_workspace_changed" | "organization.closed" | "invitation.resent" | "invitation.revoked" | "warehouse.destination_created" | "warehouse.destination_updated" | "warehouse.destination_deleted" | "warehouse.export_failing" | "warehouse.export_recovered"required
events[].subjectstringrequired
events[].datamap of objectrequired
nextCursorstring | nullrequired

GET/v1/organizations/current/audit/events

Every audit event of the organization and its workspaces, newest first, with filters.

API key or OAuth token with audit:read.

Organization admins only (403 organization_admin_required). API keys, connected apps and service accounts with audit:read can read and export where the organization has machine identities (otherwise 403 session_required); retention and legal holds change only with a person. Events come from the organization's log and its workspaces' logs and never hold secrets, codes or anything that identifies a head. Being introduced: 404 not_found until it is on for your organization. Ordered by at, then id; page with nextCursor (no gaps or repeats when events are written between pages).

Query parameters
FieldTypeNotes
cursorstring up to 200 characters
fromstring up to 40 characters
tostring up to 40 characters
actorstring up to 100 characters
actionstring up to 60 characters
workspacestring up to 64 characters
limitinteger 1–200
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].atstringrequired
events[].organizationIdstringrequired
events[].workspaceIdstring | nullrequired
events[].source"organization" | "workspace"required
events[].actorobjectrequired
events[].actor.type"person" | "system" | "api_key" | "oauth" | "scim" | "service_account"required
events[].actor.idstring | nullrequired
events[].actor.namestring | nullrequired
events[].action"organization.created" | "organization.renamed" | "domain.added" | "domain.removed" | "domain.verified" | "domain.check_failed" | "domain.warning" | "domain.restored" | "domain.failed" | "policy.changed" | "member.joined" | "sso.configured" | "sso.updated" | "sso.test_started" | "sso.tested" | "sso.enabled" | "sso.disabled" | "sso.enforced" | "sso.unenforced" | "sso.enforcement_suspended" | "sso.linked" | "sso.unlinked" | "break_glass.added" | "break_glass.removed" | "break_glass.used" | "scim.token_created" | "scim.token_rotated" | "scim.token_revoked" | "scim.settings_changed" | "scim.mapping_created" | "scim.mapping_changed" | "scim.mapping_removed" | "group.created" | "group.renamed" | "group.members_changed" | "group.deleted" | "member.provisioned" | "member.linked" | "member.updated" | "member.role_changed" | "member.removed" | "member.deprovisioned" | "member.reactivated" | "member.deleted" | "member.sessions_revoked" | "member.credentials_revoked" | "member.webhooks_stopped" | "deprovision.failing" | "workspace.owner_changed" | "sso.sessions_ended" | "service_account.created" | "service_account.updated" | "service_account.disabled" | "service_account.enabled" | "service_account.deleted" | "service_account.secret_rotated" | "service_account.secret_revoked" | "service_account.keys_changed" | "approval.requested" | "approval.viewed" | "approval.reauthenticated" | "approval.decided" | "approval.refused" | "approval.expired" | "approval.canceled" | "approval.policy_changed" | "workspace.created" | "workspace.renamed" | "workspace.cost_center_changed" | "workspace.closed" | "workspace.move_requested" | "workspace.move_canceled" | "workspace.move_declined" | "workspace.moved_in" | "workspace.moved_out" | "organization.default_workspace_changed" | "organization.closed" | "invitation.resent" | "invitation.revoked" | "warehouse.destination_created" | "warehouse.destination_updated" | "warehouse.destination_deleted" | "warehouse.export_failing" | "warehouse.export_recovered" | "question.asked" | "question.canceled" | "credits.purchased" | "key.created" | "key.revoked" | "key.updated" | "member.invited" | "member.cap_changed" | "connection.created" | "connection.revoked" | "connection.cap_changed" | "team.created" | "team.moved" | "team.closed" | "team.cost_center_changed" | "project.cost_center_changed" | "member.invite_resent" | "member.invite_revoked" | "member.invite_declined" | "credits.refunded" | "credits.adjusted" | "credits.invoice_terms_changed" | "audit.retention_changed" | "audit.retention_applied" | "audit.hold_created" | "audit.hold_released" | "audit.exported"required
events[].subjectstringrequired
events[].datamap of objectrequired
events[].ipstring | nullrequired
nextCursorstring | nullrequired

GET/v1/organizations/current/audit/export.jsonl

The audit trail as JSON lines (application/x-ndjson), oldest first, streamed.

API key or OAuth token with audit:read.

Organization admins only (403 organization_admin_required). API keys, connected apps and service accounts with audit:read can read and export where the organization has machine identities (otherwise 403 session_required); retention and legal holds change only with a person. Events come from the organization's log and its workspaces' logs and never hold secrets, codes or anything that identifies a head. Being introduced: 404 not_found until it is on for your organization. One AuditEvent per line, newline-terminated. Events written after the export starts are left for the next one. Up to 1,000,000 events per request: to resume, pass cursor as the last line's <at>|<id>. Each export is itself recorded (audit.exported).

Query parameters
FieldTypeNotes
cursorstring up to 200 characters
fromstring up to 40 characters
tostring up to 40 characters
actorstring up to 100 characters
actionstring up to 60 characters
workspacestring up to 64 characters

GET/v1/organizations/current/audit/export.csv

The audit trail as CSV, oldest first, streamed.

API key or OAuth token with audit:read.

Organization admins only (403 organization_admin_required). API keys, connected apps and service accounts with audit:read can read and export where the organization has machine identities (otherwise 403 session_required); retention and legal holds change only with a person. Events come from the organization's log and its workspaces' logs and never hold secrets, codes or anything that identifies a head. Being introduced: 404 not_found until it is on for your organization. Columns: id, at, organization_id, workspace_id, source, actor_type, actor_id, actor_name, action, subject, data (JSON), ip. RFC 4180 with CRLF; a cell starting with =, +, - or @ is prefixed with a single quote so spreadsheets do not run it. Same limits and resuming as the JSONL export.

Query parameters
FieldTypeNotes
cursorstring up to 200 characters
fromstring up to 40 characters
tostring up to 40 characters
actorstring up to 100 characters
actionstring up to 60 characters
workspacestring up to 64 characters

GET/v1/organizations/current/audit/retention

How long the organization's audit events are kept.

API key or OAuth token with audit:read.

Organization admins only (403 organization_admin_required). API keys, connected apps and service accounts with audit:read can read and export where the organization has machine identities (otherwise 403 session_required); retention and legal holds change only with a person. Events come from the organization's log and its workspaces' logs and never hold secrets, codes or anything that identifies a head. Being introduced: 404 not_found until it is on for your organization. Six years until an admin sets a period; never less than one year.

Response (JSON)
FieldTypeNotes
retentionobjectrequired
retention.daysintegerrequired
retention.isDefaultbooleanrequired
retention.floorDaysintegerrequired
retention.defaultDaysintegerrequired
retention.maxDaysintegerrequired
retention.setByobject | nullrequired
retention.setBy.idstringrequired
retention.setBy.namestring | nullrequired
retention.setAtstring | nullrequired

PUT/v1/organizations/current/audit/retention

Set how long audit events are kept (365 to 3,650 days).

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. A daily job deletes events older than the period, except those a legal hold covers. Once set, the period applies to the workspace logs too. 50heads keeps its own records (payments, payouts, fraud checks) for as long as it must, whatever is set here.

Request body (JSON)
FieldTypeNotes
daysintegerrequired 365–3650
Response (JSON)
FieldTypeNotes
retentionobjectrequired
retention.daysintegerrequired
retention.isDefaultbooleanrequired
retention.floorDaysintegerrequired
retention.defaultDaysintegerrequired
retention.maxDaysintegerrequired
retention.setByobject | nullrequired
retention.setBy.idstringrequired
retention.setBy.namestring | nullrequired
retention.setAtstring | nullrequired

GET/v1/organizations/joinable

Organizations you may join because your verified email is on one of their domains.

Portal session only.

Only organizations with auto-join on, and only for an email address a sign-in has proved.

Response (JSON)
FieldTypeNotes
organizationsobject[]required
organizations[].idstringrequired
organizations[].namestringrequired
organizations[].domainstringrequired
organizations[].workspaceIdstringrequired
organizations[].workspaceNamestringrequired
organizations[].role"viewer"required

POST/v1/organizations/join

Join an organization's default workspace as a viewer.

Portal session only.

Your personal account and its credits stay as they are. 403 not_joinable when the organization does not list you.

Request body (JSON)
FieldTypeNotes
organizationIdstringrequired up to 64 characters
Response (JSON)
FieldTypeNotes
organizationIdstringrequired
workspaceIdstringrequired
role"viewer"required

GET/v1/organizations/current/security-policies

Sign-in rules for everyone in the organization's workspaces. All off by default.

API key or OAuth token with organization:read.

Organization admins only.

Response (JSON)
FieldTypeNotes
policiesobjectrequired
policies.requireTwoStepbooleanrequired
policies.maxSessionAgeMinutesinteger | nullrequired
policies.idleTimeoutMinutesinteger | nullrequired
policies.reauthWindowMinutesinteger | nullrequired

PATCH/v1/organizations/current/security-policies

Require two-step sign-in, limit session age and idle time, shorten the re-authentication window.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Policies apply to new and existing sessions: a session older than a new maximum ends on its next request (401 session_ended), and an OAuth refresh older than it answers invalid_grant. With two-step required, sensitive actions answer 403 two_step_required until the person adds two-step sign-in or a passkey; adding one is never blocked. Sends a policy.changed event.

Request body (JSON)
FieldTypeNotes
requireTwoStepboolean
maxSessionAgeMinutesinteger | null
idleTimeoutMinutesinteger | null
reauthWindowMinutesinteger | null
Response (JSON)
FieldTypeNotes
policiesobjectrequired
policies.requireTwoStepbooleanrequired
policies.maxSessionAgeMinutesinteger | nullrequired
policies.idleTimeoutMinutesinteger | nullrequired
policies.reauthWindowMinutesinteger | nullrequired

GET/v1/organizations/requirements

What your organizations ask of you: two-step sign-in, session limits.

Portal session only.

Read after sign-in. With twoStep.required and not enrolled, set up two-step sign-in or a passkey first.

Response (JSON)
FieldTypeNotes
requirementsobjectrequired
requirements.twoStepobjectrequired
requirements.twoStep.requiredbooleanrequired
requirements.twoStep.enrolledbooleanrequired
requirements.twoStep.organizationIdsstring[]required
requirements.maxSessionAgeMinutesinteger | nullrequired
requirements.idleTimeoutMinutesinteger | nullrequired
requirements.reauthWindowMinutesinteger | nullrequired

GET/v1/organizations/current/people

Everyone in the organization, across every team.

Portal session only.

Organization admins only (403 organization_admin_required). Active members of any of its teams, and people its identity provider provisioned (still listed when SCIM turned them off). Each with their teams and roles, SCIM or not, single sign-on linked or not, last sign-in and live sessions; no IP addresses. Ordered by email; q matches part of a name or address; page with nextCursor.

Query parameters
FieldTypeNotes
qstring up to 100 characters
cursorstring up to 400 characters
limitinteger 1–200
Response (JSON)
FieldTypeNotes
peopleobject[]required
people[].userIdstringrequired
people[].namestring | nullrequired
people[].emailstringrequired
people[].teamsobject[]required
people[].teams[].workspaceIdstringrequired
people[].teams[].workspaceNamestringrequired
people[].teams[].role"owner" | "admin" | "member" | "viewer" | "billing"required
people[].teams[].scimManagedbooleanrequired
people[].scimobject | nullrequired
people[].scim.activebooleanrequired
people[].ssoLinkedbooleanrequired
people[].lastSignInAtstring | nullrequired
people[].activeSessionsintegerrequired
nextCursorstring | nullrequired

GET/v1/organizations/current/members/{userId}/sessions

A member's live sessions, on the web and in the app.

API key or OAuth token with members:read.

Organization admins only. No IP addresses. 404 for someone not in the organization.

Response (JSON)
FieldTypeNotes
userIdstringrequired
sessionsobject[]required
sessions[].idstringrequired
sessions[].kind"web" | "app"required
sessions[].labelstringrequired
sessions[].countrystring | nullrequired
sessions[].authMethod"email" | "apple" | "google" | "passkey" | "sso" | "break_glass" | nullrequired
sessions[].createdAtstringrequired
sessions[].lastSeenAtstring | nullrequired

DELETE/v1/organizations/current/members/{userId}/sessions

Sign a member out everywhere.

API key or OAuth token with members:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Every session ends on its next request.

Response (JSON)
FieldTypeNotes
oktruerequired
revokedintegerrequired

POST/v1/organizations/current/workspaces/{workspaceId}/owner

Hand a workspace to another of its members.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Only the workspace's owner or the owner of the organization's main workspace (403 owner_required). The new owner must be an active member (400 not_a_member) who owns no other open workspace in this organization (409 owns_workspace_in_organization; a person may own one workspace in each organization). The previous owner becomes an admin. An owner cannot be deprovisioned until this is done.

Request body (JSON)
FieldTypeNotes
userIdstringrequired up to 64 characters
Response (JSON)
FieldTypeNotes
workspaceIdstringrequired
ownerIdstringrequired
previousOwnerIdstringrequired

GET/v1/organizations

The organizations you are in, with the workspaces you are in and your role.

API key or OAuth token with organization:read.

Signed in: every organization the flag serves where you are an active member of a workspace; role is admin when you own or administer one of its open workspaces. With an API key, a connected app or a service account: only the organization of the workspace it works in.

Response (JSON)
FieldTypeNotes
organizationsobject[]required
organizations[].idstringrequired
organizations[].namestringrequired
organizations[].role"admin" | "member"required
organizations[].defaultWorkspaceIdstring | nullrequired
organizations[].workspacesobject[]required
organizations[].workspaces[].idstringrequired
organizations[].workspaces[].namestringrequired
organizations[].workspaces[].role"owner" | "admin" | "member" | "viewer" | "billing"required
organizations[].workspaces[].closedbooleanrequired

GET/v1/organizations/current/workspaces

Every workspace of the organization: owner, members, pending invitations, cost center.

API key or OAuth token with organization:read.

Organization admins only. Closed workspaces are listed last, with closedAt; they are read-only.

Response (JSON)
FieldTypeNotes
workspacesobject[]required
workspaces[].idstringrequired
workspaces[].namestringrequired
workspaces[].ownerIdstringrequired
workspaces[].ownerNamestring | nullrequired
workspaces[].ownerEmailstring | nullrequired
workspaces[].membersintegerrequired
workspaces[].pendingInvitationsintegerrequired
workspaces[].costCenterstring | nullrequired
workspaces[].isDefaultbooleanrequired
workspaces[].createdAtstringrequired
workspaces[].closedAtstring | nullrequired

POST/v1/organizations/current/workspaces

Make a workspace in the organization.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The owner is you, or ownerId: someone already in one of its open workspaces (400 not_a_member). A person may own one open workspace in each organization (409 owns_workspace_in_organization). Sends workspace.created.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 80 characters
ownerIdstring up to 64 characters
costCenterstring | null
Response (JSON)
FieldTypeNotes
workspaceobjectrequired
workspace.idstringrequired
workspace.namestringrequired
workspace.ownerIdstringrequired
workspace.ownerNamestring | nullrequired
workspace.ownerEmailstring | nullrequired
workspace.membersintegerrequired
workspace.pendingInvitationsintegerrequired
workspace.costCenterstring | nullrequired
workspace.isDefaultbooleanrequired
workspace.createdAtstringrequired
workspace.closedAtstring | nullrequired

PATCH/v1/organizations/current/workspaces/{workspaceId}

Rename a workspace, or set its cost center label.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. costCenter null removes the label. The label is printed on the workspace's invoices from then on and carried on spend reports and exports. A closed workspace cannot change (409 workspace_closed).

Request body (JSON)
FieldTypeNotes
namestring up to 80 characters
costCenterstring | null
Response (JSON)
FieldTypeNotes
workspaceobjectrequired
workspace.idstringrequired
workspace.namestringrequired
workspace.ownerIdstringrequired
workspace.ownerNamestring | nullrequired
workspace.ownerEmailstring | nullrequired
workspace.membersintegerrequired
workspace.pendingInvitationsintegerrequired
workspace.costCenterstring | nullrequired
workspace.isDefaultbooleanrequired
workspace.createdAtstringrequired
workspace.closedAtstring | nullrequired

PUT/v1/organizations/current/default-workspace

Choose the organization's main workspace.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The workspace auto-join, SCIM and single sign-on put people in by default. Only the owner of the current main workspace can choose (403 owner_required), or any admin once it is closed. A closed workspace cannot be chosen (409 workspace_closed).

Request body (JSON)
FieldTypeNotes
workspaceIdstringrequired up to 64 characters
Response (JSON)
FieldTypeNotes
defaultWorkspaceIdstringrequired

GET/v1/organizations/current/moves

Requests to move teams into the organization.

API key or OAuth token with organization:read.

Organization admins only. Newest first. A pending request past expiresAt reads as expired.

Response (JSON)
FieldTypeNotes
movesobject[]required
moves[].idstringrequired
moves[].workspaceIdstringrequired
moves[].workspaceNamestringrequired
moves[].fromOrganizationIdstringrequired
moves[].toOrganizationIdstringrequired
moves[].toOrganizationNamestringrequired
moves[].requestedByobjectrequired
moves[].requestedBy.idstringrequired
moves[].requestedBy.namestring | nullrequired
moves[].status"pending" | "accepted" | "declined" | "canceled" | "expired"required
moves[].createdAtstringrequired
moves[].expiresAtstringrequired
moves[].decidedAtstring | nullrequired

POST/v1/organizations/current/moves

Ask a team's owner to move the team into the organization.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The team's owner reads its id on their Team page. Refused when the team's organization has other open workspaces (409 source_has_other_workspaces), holds a verified domain, single sign-on or SCIM (409 source_has_identity), when its owner already owns a workspace here (409 owns_workspace_in_organization), when asks wait for a spend approval (409 pending_approvals), or a request is already open (409 move_exists). The owner is emailed and has 14 days to accept, with a fresh re-authentication, on their Team page. When they do, the team keeps its members, credits, questions and history, and its old organization is closed with its audit kept.

Request body (JSON)
FieldTypeNotes
workspaceIdstringrequired up to 64 characters
Response (JSON)
FieldTypeNotes
moveobjectrequired
move.idstringrequired
move.workspaceIdstringrequired
move.workspaceNamestringrequired
move.fromOrganizationIdstringrequired
move.toOrganizationIdstringrequired
move.toOrganizationNamestringrequired
move.requestedByobjectrequired
move.requestedBy.idstringrequired
move.requestedBy.namestring | nullrequired
move.status"pending" | "accepted" | "declined" | "canceled" | "expired"required
move.createdAtstringrequired
move.expiresAtstringrequired
move.decidedAtstring | nullrequired

DELETE/v1/organizations/current/moves/{id}

Withdraw a request to move a team in.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Only a pending request (409 move_not_allowed).

Response (JSON)
FieldTypeNotes
moveobjectrequired
move.idstringrequired
move.workspaceIdstringrequired
move.workspaceNamestringrequired
move.fromOrganizationIdstringrequired
move.toOrganizationIdstringrequired
move.toOrganizationNamestringrequired
move.requestedByobjectrequired
move.requestedBy.idstringrequired
move.requestedBy.namestring | nullrequired
move.status"pending" | "accepted" | "declined" | "canceled" | "expired"required
move.createdAtstringrequired
move.expiresAtstringrequired
move.decidedAtstring | nullrequired

GET/v1/organizations/current/invitations

Invitations not yet accepted, in every workspace of the organization.

API key or OAuth token with members:read.

Organization admins only. Newest first; expired invitations stay listed until they are resent or revoked.

Response (JSON)
FieldTypeNotes
invitationsobject[]required
invitations[].idstringrequired
invitations[].emailstringrequired
invitations[].role"owner" | "admin" | "member" | "viewer" | "billing"required
invitations[].workspaceIdstringrequired
invitations[].workspaceNamestringrequired
invitations[].invitedByobject | nullrequired
invitations[].invitedBy.idstringrequired
invitations[].invitedBy.namestring | nullrequired
invitations[].invitedAtstringrequired
invitations[].expiresAtstring | nullrequired
invitations[].expiredbooleanrequired

POST/v1/organizations/current/invitations/{id}/resend

Send an invitation again, with a new link and seven more days.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The old link stops working. At most 30 invitations an hour per person.

Response (JSON)
FieldTypeNotes
invitationobjectrequired
invitation.idstringrequired
invitation.emailstringrequired
invitation.role"owner" | "admin" | "member" | "viewer" | "billing"required
invitation.workspaceIdstringrequired
invitation.workspaceNamestringrequired
invitation.invitedByobject | nullrequired
invitation.invitedBy.idstringrequired
invitation.invitedBy.namestring | nullrequired
invitation.invitedAtstringrequired
invitation.expiresAtstring | nullrequired
invitation.expiredbooleanrequired

DELETE/v1/organizations/current/invitations/{id}

Revoke an invitation.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit.

Response (JSON)
FieldTypeNotes
oktruerequired

DELETE/v1/organizations/current/people/{userId}

Take someone out of every workspace of the organization.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The keys and app grants they made for those workspaces stop; their questions stay with the workspaces. Refused while they own a workspace here (409 owns_workspace_in_organization: hand it over first) and for people your identity provider manages (409 scim_managed: remove them there). Their own account is untouched.

Response (JSON)
FieldTypeNotes
userIdstringrequired
workspacesLeftstring[]required

PUT/v1/organizations/current/people/{userId}/role

Change someone's role in one workspace of the organization.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Not the owner's role (409 owner_role); not in a closed workspace (409 workspace_closed). An admin's choice replaces the identity provider's group mapping for that person from then on.

Request body (JSON)
FieldTypeNotes
workspaceIdstringrequired up to 64 characters
role"admin" | "member" | "viewer" | "billing"required
Response (JSON)
FieldTypeNotes
userIdstringrequired
workspaceIdstringrequired
rolestringrequired

GET/v1/organizations/current/spend

Spend by cost center for a month.

API key or OAuth token with organization:read.

Organization admins only. Credits the workspaces' questions took in the month (UTC), less what came back, by workspace and cost center: a project's label wins over its workspace's; questions with neither have costCenter null.

Query parameters
FieldTypeNotes
monthstring pattern ^\d{4}-(0[1-9]|1[0-2])$
Response (JSON)
FieldTypeNotes
monthstringrequired
totalSpentPenceintegerrequired
totalQuestionsintegerrequired
rowsobject[]required
rows[].costCenterstring | nullrequired
rows[].workspaceIdstringrequired
rows[].workspaceNamestringrequired
rows[].questionsintegerrequired
rows[].spentPenceintegerrequired

GET/v1/organizations/current/spend.csv

Spend by cost center as CSV.

API key or OAuth token with organization:read.

Organization admins only. Columns: month, cost_center, workspace_id, workspace_name, questions, spent_credits. RFC 4180 with CRLF; formula cells are quoted.

Query parameters
FieldTypeNotes
monthstring pattern ^\d{4}-(0[1-9]|1[0-2])$

GET/v1/organizations/current/scim

SCIM provisioning: the base URL, the default role and counts.

API key or OAuth token with organization:read.

Organization admins only. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
settingsobjectrequired
settings.baseUrlstringrequired
settings.defaultRole"admin" | "member" | "viewer" | "billing"required
settings.defaultWorkspaceIdstring | nullrequired
settings.provisionedUsersintegerrequired
settings.activeUsersintegerrequired
settings.groupsintegerrequired

PATCH/v1/organizations/current/scim

Set the role a provisioned person gets when none of their groups is mapped.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Request body (JSON)
FieldTypeNotes
defaultRole"admin" | "member" | "viewer" | "billing"required
Response (JSON)
FieldTypeNotes
defaultRole"admin" | "member" | "viewer" | "billing"required

GET/v1/organizations/current/scim/tokens

The SCIM tokens that work now (never the tokens themselves).

API key or OAuth token with organization:read.

Organization admins only. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
tokensobject[]required
tokens[].idstringrequired
tokens[].labelstringrequired
tokens[].prefixstringrequired
tokens[].createdAtstringrequired
tokens[].createdBystring | nullrequired
tokens[].expiresAtstring | nullrequired
tokens[].lastUsedAtstring | nullrequired

POST/v1/organizations/current/scim/tokens

Make a SCIM token for the identity provider. It is shown once.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Stored as a SHA-256 hash. Up to 5 live tokens (409 too_many_tokens). Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Request body (JSON)
FieldTypeNotes
labelstring up to 60 characters, default "Identity provider"
Response (JSON)
FieldTypeNotes
tokenobjectrequired
token.idstringrequired
token.labelstringrequired
token.prefixstringrequired
token.createdAtstringrequired
token.createdBystring | nullrequired
token.expiresAtstring | nullrequired
token.lastUsedAtstring | nullrequired
secretstringrequired
baseUrlstringrequired

POST/v1/organizations/current/scim/tokens/{tokenId}/rotate

Replace a SCIM token; the old one keeps working for the overlap.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. overlapHours 0 ends the old token at once; up to 168. 409 already_rotated. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Request body (JSON)
FieldTypeNotes
overlapHoursinteger 0–168, default 24
Response (JSON)
FieldTypeNotes
tokenobjectrequired
token.idstringrequired
token.labelstringrequired
token.prefixstringrequired
token.createdAtstringrequired
token.createdBystring | nullrequired
token.expiresAtstring | nullrequired
token.lastUsedAtstring | nullrequired
secretstringrequired
baseUrlstringrequired

DELETE/v1/organizations/current/scim/tokens/{tokenId}

End a SCIM token at once.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
oktruerequired

GET/v1/organizations/current/scim/groups

The groups the identity provider has pushed.

API key or OAuth token with organization:read.

Organization admins only. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
groupsobject[]required
groups[].idstringrequired
groups[].displayNamestringrequired
groups[].externalIdstring | nullrequired
groups[].membersintegerrequired
groups[].createdAtstringrequired
groups[].updatedAtstringrequired

GET/v1/organizations/current/scim/mappings

Which group gives which role in which workspace.

API key or OAuth token with organization:read.

Organization admins only. A person in several mapped groups gets the highest role in each workspace (admin, member, billing, viewer); a person in no mapped group gets the default role in the default workspace. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
mappingsobject[]required
mappings[].idstringrequired
mappings[].groupIdstringrequired
mappings[].groupNamestringrequired
mappings[].workspaceIdstringrequired
mappings[].workspaceNamestringrequired
mappings[].role"admin" | "member" | "viewer" | "billing"required
mappings[].createdAtstringrequired
mappings[].updatedAtstringrequired

POST/v1/organizations/current/scim/mappings

Give a group's members a role in a workspace.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). The group's members get the role at once. 409 mapping_exists. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Request body (JSON)
FieldTypeNotes
groupIdstringrequired up to 64 characters
workspaceIdstringrequired up to 64 characters
role"admin" | "member" | "viewer" | "billing"required
Response (JSON)
FieldTypeNotes
mappingobjectrequired
mapping.idstringrequired
mapping.groupIdstringrequired
mapping.groupNamestringrequired
mapping.workspaceIdstringrequired
mapping.workspaceNamestringrequired
mapping.role"admin" | "member" | "viewer" | "billing"required
mapping.createdAtstringrequired
mapping.updatedAtstringrequired

PATCH/v1/organizations/current/scim/mappings/{mappingId}

Change the role a group gives.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Request body (JSON)
FieldTypeNotes
role"admin" | "member" | "viewer" | "billing"required
Response (JSON)
FieldTypeNotes
mappingobjectrequired
mapping.idstringrequired
mapping.groupIdstringrequired
mapping.groupNamestringrequired
mapping.workspaceIdstringrequired
mapping.workspaceNamestringrequired
mapping.role"admin" | "member" | "viewer" | "billing"required
mapping.createdAtstringrequired
mapping.updatedAtstringrequired

DELETE/v1/organizations/current/scim/mappings/{mappingId}

Stop a group giving a role. Its members lose the role at once.

API key or OAuth token with organization:write.

Organization admins only (403 organization_admin_required). Signed in, with a fresh proof in the x-50h-reauth header (401 reauth_required); with an API key, a connected app or a service account, with the scope named here and no proof, where the organization has machine identities (otherwise 403 session_required). The key's or app's person must be an organization admin. Every change is written to the organization's audit, with who made it (a person, a key, an app or a service account). Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Response (JSON)
FieldTypeNotes
oktruerequired

GET/v1/organizations/current/scim/log

The last SCIM requests and what came of each.

API key or OAuth token with organization:read.

Organization admins only. No request bodies and no tokens. Kept 90 days. Behind the scim sub-flag: 404 not_found while it is off. The SCIM protocol itself is at /scim/v2 for the identity provider (Okta, Microsoft Entra ID), with the token as its bearer; see the SCIM setup guide.

Query parameters
FieldTypeNotes
limitinteger 1–500
Response (JSON)
FieldTypeNotes
entriesobject[]required
entries[].idstringrequired
entries[].atstringrequired
entries[].methodstringrequired
entries[].pathstringrequired
entries[].statusintegerrequired
entries[].outcomestringrequired
entries[].scimTypestring | nullrequired
entries[].resourceIdstring | nullrequired
entries[].tokenIdstring | nullrequired

GET/v1/organizations/current/sso

The organization's single sign-on connection, the OIDC redirect URI, and the SAML values (entity ID, ACS URL, metadata URL) to give your identity provider.

API key or OAuth token with organization:read.

Organization admins only. connection is null until one is saved. The client secret is never returned. connection.protocol is oidc or saml; for SAML, connection.saml has the identity provider's entity ID, single sign-on URL, metadata URL and certificates (fingerprints and expiry, never the certificate itself). saml is this organization's service provider values whatever the protocol in use.

Response (JSON)
FieldTypeNotes
connectionobject | nullrequired
connection.idstringrequired
connection.protocol"oidc" | "saml"required
connection.issuerstringrequired
connection.clientIdstringrequired
connection.clientSecretSetbooleanrequired
connection.clientSecretHintstring | nullrequired
connection.tokenAuthMethod"client_secret_basic" | "client_secret_post"required
connection.defaultWorkspaceIdstring | nullrequired
connection.defaultRole"admin" | "member" | "viewer"required
connection.state"draft" | "tested" | "enabled"required
connection.enforcebooleanrequired
connection.enforceEnabledAtstring | nullrequired
connection.configChangedAtstringrequired
connection.lastTestobject | nullrequired
connection.lastTest.atstringrequired
connection.lastTest.byobjectrequired
connection.lastTest.resultstringrequired
connection.lastTest.matchedTesterbooleanrequired
connection.lastTest.currentbooleanrequired
connection.lastSignInAtstring | nullrequired
connection.createdAtstringrequired
connection.updatedAtstring | nullrequired
connection.samlobject | nullrequired
connection.saml.idpEntityIdstringrequired
connection.saml.ssoUrlstringrequired
connection.saml.metadataUrlstring | nullrequired
connection.saml.certificatesobject[]required
connection.saml.allowIdpInitiatedbooleanrequired
redirectUristringrequired
scopesstring[]required
samlobjectrequired
saml.entityIdstringrequired
saml.acsUrlstringrequired
saml.metadataUrlstringrequired
saml.nameIdFormatstringrequired

PUT/v1/organizations/current/sso

Save the connection: OpenID Connect (issuer, client id and secret, token method) or SAML 2.0 (the identity provider's metadata), and where new people join.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. 201 the first time. OIDC (protocol left out or oidc): the issuer's discovery document is read before saving (400 sso_discovery_failed); the secret is required the first time and kept when left out. SAML (protocol: "saml"): metadataUrl (read now, https on a public host, 6-second timeout, no redirects) or metadataXml, one of them; metadata that cannot be read, wants signed requests or has no HTTP-Redirect single sign-on URL is 400 saml_metadata_invalid; allowIdpInitiated (default false) accepts sign-ins started at the identity provider. A provider change (OIDC issuer, client id, secret or token method; SAML entity ID, single sign-on URL or certificates replaced; or a switch of protocol) needs a new test before people can sign in, and is refused while enforcement is on (409 sso_enforced). defaultRole is admin, member or viewer, never owner.

Response (JSON)
FieldTypeNotes
connectionobject | nullrequired
connection.idstringrequired
connection.protocol"oidc" | "saml"required
connection.issuerstringrequired
connection.clientIdstringrequired
connection.clientSecretSetbooleanrequired
connection.clientSecretHintstring | nullrequired
connection.tokenAuthMethod"client_secret_basic" | "client_secret_post"required
connection.defaultWorkspaceIdstring | nullrequired
connection.defaultRole"admin" | "member" | "viewer"required
connection.state"draft" | "tested" | "enabled"required
connection.enforcebooleanrequired
connection.enforceEnabledAtstring | nullrequired
connection.configChangedAtstringrequired
connection.lastTestobject | nullrequired
connection.lastTest.atstringrequired
connection.lastTest.byobjectrequired
connection.lastTest.resultstringrequired
connection.lastTest.matchedTesterbooleanrequired
connection.lastTest.currentbooleanrequired
connection.lastSignInAtstring | nullrequired
connection.createdAtstringrequired
connection.updatedAtstring | nullrequired
connection.samlobject | nullrequired
connection.saml.idpEntityIdstringrequired
connection.saml.ssoUrlstringrequired
connection.saml.metadataUrlstring | nullrequired
connection.saml.certificatesobject[]required
connection.saml.allowIdpInitiatedbooleanrequired
redirectUristringrequired
scopesstring[]required
samlobjectrequired
saml.entityIdstringrequired
saml.acsUrlstringrequired
saml.metadataUrlstringrequired
saml.nameIdFormatstringrequired

PATCH/v1/organizations/current/sso

Change part of the connection, or turn single sign-on on or off with `enabled`.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Turning it on needs a successful test sign-in since the last provider change (409 sso_test_required) and a verified domain. Turning it off is refused while enforcement is on. OIDC fields on a SAML connection, or SAML fields on an OIDC one, are 400 protocol_mismatch. SAML certificate rotation: new metadata with keepPreviousCertificate: true keeps the certificate in use beside the new one (at most two, 409 too_many_certificates), and removeCertificate (a fingerprint) drops one later (409 last_certificate for the only one); neither needs a new test or turning enforcement off.

Request body (JSON)
FieldTypeNotes
issuerstring up to 300 characters, at least 8 characters
clientIdstring up to 300 characters
clientSecretstring up to 1000 characters
tokenAuthMethod"client_secret_basic" | "client_secret_post"
defaultWorkspaceIdstring | null
defaultRole"admin" | "member" | "viewer"
enabledboolean
metadataUrlstring up to 1000 characters, at least 8 characters
metadataXmlstring up to 200000 characters, at least 20 characters
keepPreviousCertificateboolean
removeCertificatestring pattern ^[0-9a-f]{64}$
allowIdpInitiatedboolean
Response (JSON)
FieldTypeNotes
connectionobject | nullrequired
connection.idstringrequired
connection.protocol"oidc" | "saml"required
connection.issuerstringrequired
connection.clientIdstringrequired
connection.clientSecretSetbooleanrequired
connection.clientSecretHintstring | nullrequired
connection.tokenAuthMethod"client_secret_basic" | "client_secret_post"required
connection.defaultWorkspaceIdstring | nullrequired
connection.defaultRole"admin" | "member" | "viewer"required
connection.state"draft" | "tested" | "enabled"required
connection.enforcebooleanrequired
connection.enforceEnabledAtstring | nullrequired
connection.configChangedAtstringrequired
connection.lastTestobject | nullrequired
connection.lastTest.atstringrequired
connection.lastTest.byobjectrequired
connection.lastTest.resultstringrequired
connection.lastTest.matchedTesterbooleanrequired
connection.lastTest.currentbooleanrequired
connection.lastSignInAtstring | nullrequired
connection.createdAtstringrequired
connection.updatedAtstring | nullrequired
connection.samlobject | nullrequired
connection.saml.idpEntityIdstringrequired
connection.saml.ssoUrlstringrequired
connection.saml.metadataUrlstring | nullrequired
connection.saml.certificatesobject[]required
connection.saml.allowIdpInitiatedbooleanrequired
redirectUristringrequired
scopesstring[]required
samlobjectrequired
saml.entityIdstringrequired
saml.acsUrlstringrequired
saml.metadataUrlstringrequired
saml.nameIdFormatstringrequired

POST/v1/organizations/current/sso/test

Start your own test sign-in: open `url`, sign in at your identity provider, and come back to organization settings.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The result lands on the connection (lastTest) and in the error log; the page you come back to has sso_test=ok or an error code.

Response (JSON)
FieldTypeNotes
urlstringrequired
expiresAtstringrequired

GET/v1/organizations/current/sso/events

The single sign-on log: tests, failed sign-ins and account links, newest first.

API key or OAuth token with organization:read.

Organization admins only. Codes and plain messages only: no token is ever kept.

Query parameters
FieldTypeNotes
cursorstring
Response (JSON)
FieldTypeNotes
eventsobject[]required
events[].idstringrequired
events[].atstringrequired
events[].kind"sign_in" | "test" | "link"required
events[].outcome"ok" | "error"required
events[].codestring | nullrequired
events[].messagestring | nullrequired
events[].emailstring | nullrequired
nextCursorstring | nullrequired

PATCH/v1/organizations/current/sso/enforce

Require single sign-on for addresses on the verified domains, or stop requiring it.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. On: email codes stop for those addresses and passkey sign-in is kept only for break-glass owners. It can only be turned on by an admin whose own test sign-in since the last change worked (409 sso_test_required), so nobody locks themselves out. Off is always allowed.

Request body (JSON)
FieldTypeNotes
enforcebooleanrequired
Response (JSON)
FieldTypeNotes
connectionobject | nullrequired
connection.idstringrequired
connection.protocol"oidc" | "saml"required
connection.issuerstringrequired
connection.clientIdstringrequired
connection.clientSecretSetbooleanrequired
connection.clientSecretHintstring | nullrequired
connection.tokenAuthMethod"client_secret_basic" | "client_secret_post"required
connection.defaultWorkspaceIdstring | nullrequired
connection.defaultRole"admin" | "member" | "viewer"required
connection.state"draft" | "tested" | "enabled"required
connection.enforcebooleanrequired
connection.enforceEnabledAtstring | nullrequired
connection.configChangedAtstringrequired
connection.lastTestobject | nullrequired
connection.lastTest.atstringrequired
connection.lastTest.byobjectrequired
connection.lastTest.resultstringrequired
connection.lastTest.matchedTesterbooleanrequired
connection.lastTest.currentbooleanrequired
connection.lastSignInAtstring | nullrequired
connection.createdAtstringrequired
connection.updatedAtstring | nullrequired
connection.samlobject | nullrequired
connection.saml.idpEntityIdstringrequired
connection.saml.ssoUrlstringrequired
connection.saml.metadataUrlstring | nullrequired
connection.saml.certificatesobject[]required
connection.saml.allowIdpInitiatedbooleanrequired
redirectUristringrequired
scopesstring[]required
samlobjectrequired
saml.entityIdstringrequired
saml.acsUrlstringrequired
saml.metadataUrlstringrequired
saml.nameIdFormatstringrequired

GET/v1/organizations/current/sso/break-glass

Owners who keep passkey sign-in while single sign-on is required.

API key or OAuth token with organization:read.

Organization admins only. Every use is written to the audit and emailed to every admin.

Response (JSON)
FieldTypeNotes
membersobject[]required
members[].userIdstringrequired
members[].emailstringrequired
members[].namestring | nullrequired
members[].addedAtstringrequired
members[].hasPasskeybooleanrequired
maxintegerrequired

POST/v1/organizations/current/sso/break-glass

Give an owner break-glass access (up to two).

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Owners only (403 organization_owner_required). The person must be an owner of one of the organization's workspaces with a passkey on their account.

Request body (JSON)
FieldTypeNotes
userIdstringrequired up to 64 characters
Response (JSON)
FieldTypeNotes
membersobject[]required
members[].userIdstringrequired
members[].emailstringrequired
members[].namestring | nullrequired
members[].addedAtstringrequired
members[].hasPasskeybooleanrequired
maxintegerrequired

DELETE/v1/organizations/current/sso/break-glass/{userId}

Remove an owner's break-glass access.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Owners only.

Response (JSON)
FieldTypeNotes
membersobject[]required
members[].userIdstringrequired
members[].emailstringrequired
members[].namestring | nullrequired
members[].addedAtstringrequired
members[].hasPasskeybooleanrequired
maxintegerrequired

GET/v1/organizations/current/service-accounts

The organization's service accounts: machine identities it owns, each with its scopes, caps, workspace and client secrets (never the secrets themselves).

API key or OAuth token with organization:read.

Organization admins, or a key, connected app or service account with organization:read. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
serviceAccountsobject[]required
serviceAccounts[].idstringrequired
serviceAccounts[].namestringrequired
serviceAccounts[].organizationIdstringrequired
serviceAccounts[].workspaceIdstringrequired
serviceAccounts[].workspaceNamestringrequired
serviceAccounts[].status"active" | "disabled"required
serviceAccounts[].scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccounts[].capDailyPenceintegerrequired
serviceAccounts[].capPerQuestionPenceinteger | nullrequired
serviceAccounts[].spentTodayPenceintegerrequired
serviceAccounts[].clientIdstringrequired
serviceAccounts[].authMethod"client_secret" | "private_key_jwt"required
serviceAccounts[].secretsobject[]required
serviceAccounts[].secrets[].idstringrequired
serviceAccounts[].secrets[].hintstringrequired
serviceAccounts[].secrets[].createdAtstringrequired
serviceAccounts[].secrets[].expiresAtstring | nullrequired
serviceAccounts[].secrets[].lastUsedAtstring | nullrequired
serviceAccounts[].jwksobject | nullrequired
serviceAccounts[].jwks.keysmap of object[]required up to 5 items
serviceAccounts[].createdAtstringrequired
serviceAccounts[].createdByobject | nullrequired
serviceAccounts[].createdBy.idstringrequired
serviceAccounts[].createdBy.namestring | nullrequired
serviceAccounts[].lastUsedAtstring | nullrequired
serviceAccounts[].disabledAtstring | nullrequired

GET/v1/organizations/current/service-accounts/{id}

One service account.

API key or OAuth token with organization:read.

As the list. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

POST/v1/organizations/current/service-accounts

Make a service account. Its client secret is in this response only.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. It acts in the workspace you choose (the organization's main workspace by default) with the admin role there, limited by its scopes, and spends up to its daily and per-question caps. Up to 50 (409 too_many_service_accounts). With authMethod: private_key_jwt, send your public keys in jwks and no secret is made. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Request body (JSON)
FieldTypeNotes
namestringrequired up to 60 characters
scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
capDailyPenceintegerrequired 100–10000000
capPerQuestionPenceinteger | null
workspaceIdstring up to 64 characters
authMethod"client_secret" | "private_key_jwt" default "client_secret"
jwksobject
jwks.keysmap of object[]required up to 5 items
jwks.keys[].kty"EC" | "RSA"required
jwks.keys[].kidstring up to 200 characters
jwks.keys[].alg"ES256" | "RS256"
jwks.keys[].use"sig"
jwks.keys[].crvstring up to 20 characters
jwks.keys[].xstring up to 2000 characters
jwks.keys[].ystring up to 2000 characters
jwks.keys[].nstring up to 2000 characters
jwks.keys[].estring up to 20 characters
Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired
clientSecretstring | nullrequired
tokenEndpointstringrequired

PATCH/v1/organizations/current/service-accounts/{id}

Rename a service account, or change its scopes, caps or workspace.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. New scopes and caps apply to its next request. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Request body (JSON)
FieldTypeNotes
namestring up to 60 characters
scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]
capDailyPenceinteger 100–10000000
capPerQuestionPenceinteger | null
workspaceIdstring up to 64 characters
Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

POST/v1/organizations/current/service-accounts/{id}/disable

Turn a service account off: its tokens fail on their next request and it gets no new ones.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

POST/v1/organizations/current/service-accounts/{id}/enable

Turn a service account back on.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

DELETE/v1/organizations/current/service-accounts/{id}

Delete a service account. Its tokens, secrets and the webhook endpoints it made stop; the questions it asked stay with the workspace.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
oktruerequired

POST/v1/organizations/current/service-accounts/{id}/secrets

A new client secret, shown once. The old one keeps working for `overlapHours` (24 by default, 0 ends it now, up to 168).

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. At most two secrets work at once. A service account on private_key_jwt moves to its client secret. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Request body (JSON)
FieldTypeNotes
overlapHoursinteger 0–168, default 24
Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired
clientSecretstring | nullrequired
tokenEndpointstringrequired

DELETE/v1/organizations/current/service-accounts/{id}/secrets/{secretId}

End one client secret now.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

PUT/v1/organizations/current/service-accounts/{id}/jwks

Use private_key_jwt with these public keys (ES256 or RS256, up to 5). Every client secret stops working.

Portal session only.

Organization admins only (403 organization_admin_required), on a browser session with a fresh proof in the x-50h-reauth header (401 reauth_required). It needs a person: API keys, connected apps and service accounts get 403 session_required. Every change is written to the organization's audit. The assertion's iss and sub are the client id, its aud the token endpoint or the issuer, at most five minutes old, each jti once. Behind the machineIdentities sub-flag: 404 not_found while it is off. A service account gets tokens from https://auth.50heads.com/token with grant_type=client_credentials and its client id and secret (HTTP Basic or the body) or a private_key_jwt assertion; no refresh token, ask again when it expires.

Request body (JSON)
FieldTypeNotes
jwksobjectrequired
jwks.keysmap of object[]required up to 5 items
jwks.keys[].kty"EC" | "RSA"required
jwks.keys[].kidstring up to 200 characters
jwks.keys[].alg"ES256" | "RS256"
jwks.keys[].use"sig"
jwks.keys[].crvstring up to 20 characters
jwks.keys[].xstring up to 2000 characters
jwks.keys[].ystring up to 2000 characters
jwks.keys[].nstring up to 2000 characters
jwks.keys[].estring up to 20 characters
Response (JSON)
FieldTypeNotes
serviceAccountobjectrequired
serviceAccount.idstringrequired
serviceAccount.namestringrequired
serviceAccount.organizationIdstringrequired
serviceAccount.workspaceIdstringrequired
serviceAccount.workspaceNamestringrequired
serviceAccount.status"active" | "disabled"required
serviceAccount.scopes"questions:read" | "questions:write" | "templates:read" | "account:read" | "webhooks:read" | "webhooks:write" | "apps:read" | "apps:write" | "organization:read" | "organization:write" | "members:read" | "members:write" | "audit:read" | "events:read"[]required
serviceAccount.capDailyPenceintegerrequired
serviceAccount.capPerQuestionPenceinteger | nullrequired
serviceAccount.spentTodayPenceintegerrequired
serviceAccount.clientIdstringrequired
serviceAccount.authMethod"client_secret" | "private_key_jwt"required
serviceAccount.secretsobject[]required
serviceAccount.secrets[].idstringrequired
serviceAccount.secrets[].hintstringrequired
serviceAccount.secrets[].createdAtstringrequired
serviceAccount.secrets[].expiresAtstring | nullrequired
serviceAccount.secrets[].lastUsedAtstring | nullrequired
serviceAccount.jwksobject | nullrequired
serviceAccount.jwks.keysmap of object[]required up to 5 items
serviceAccount.createdAtstringrequired
serviceAccount.createdByobject | nullrequired
serviceAccount.createdBy.idstringrequired
serviceAccount.createdBy.namestring | nullrequired
serviceAccount.lastUsedAtstring | nullrequired
serviceAccount.disabledAtstring | nullrequired

JSON lines exports

GET /v2/questions/{id}/export.jsonl and GET /v2/sets/{id}/export.jsonl return every answer as JSON lines (application/x-ndjson): one JSON object per answer, each on its own line, for scripts and model training. They carry the same fields as the CSV export, plus choice and cohortHash, and nothing that identifies a head. The question export takes the same filters as the CSV; an empty body means no answers match.

curl -s https://api.50heads.com/v2/questions/q_8f2k1x/export.jsonl \
  -H "Authorization: Bearer $FIFTYHEADS_API_KEY" > answers.jsonl

One line:

{"questionId":"q_8f2k1x","type":"ranking","question":"Rank these names for a bakery","options":["Crumb","Loafers","Proof"],"cohortHash":"ch1_3f9a0c2e71b4d856","answeredAt":"2026-10-10T09:41:12.000Z","answer":"Crumb > Proof > Loafers","choice":{"order":[0,2,1]},"text":null,"translatedText":null,"reason":"Short and warm","translatedReason":null,"tier":1,"attestationRef":"att_5d0e7a91c2b4f3e86a17"}

The choice field

choice is the answer as indexes into options, counted from 0, so you never parse a label. It is null for free text and for a click test, whose taps are in taps. The same field is on each answer in the result (answers[].choice) and the answers pages.

KeyWhen
indexOne pick: single choice, yes or no, A or B, a reaction. A 1 to 5 rating reads 0 to 4.
indexesPick several (multi choice), in option order.
orderRanking, best first.
pairPairwise with three or more options: the two options shown; index is the one picked.
ratingsRate each: one rating from 1 to 5 per statement, in option order.

The cohort hash

Cohort hash: the same value means the same kind of audience was asked (who, how many, which tier and targeting). It never identifies anyone. It is ch1_ and 16 hex characters, on every line and on the result as cohortHash. It also covers the question type, the language, an interest audience and its minimum grade, but never the words asked: two questions put to the same audience share it, so you can group their answers. Change who is asked and it changes. For a shared link, tier and targeting do not apply.

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