{"openapi":"3.0.3","info":{"title":"Terac External API","description":"Programmatic access to Terac's recruitment, interview, and insight delivery platform. Authenticate using a Bearer token via the Authorization header.","version":"2.0.0"},"servers":[{"url":"https://terac.com/api/external/v2"}],"tags":[{"name":"Organizations","description":"The organization your API key belongs to: its balance, dashboard links and operating guide."},{"name":"Feasibility","description":"Ask Terac to price an audience and task before you build an opportunity for it."},{"name":"Projects","description":"Folders that group an organization's opportunities."},{"name":"Opportunities","description":"Studies you draft, launch, pause, resume and stop; launching charges your balance and starts recruiting."},{"name":"Submissions","description":"Participants' work on an opportunity, your review of it, and the applicants waiting on your invite decision."},{"name":"Hours","description":"Time experts tracked working on an opportunity, per person and date."},{"name":"Filters","description":"The audience filters an opportunity can target, and the values each one accepts."},{"name":"Webhooks","description":"Subscriptions that push signed event notifications to your URL, their signing secrets, and the delivery log."}],"paths":{"/projects":{"get":{"operationId":"listProjects","summary":"List projects","description":"Lists your organization's projects, alphabetically by name. Returns up to `limit` per page; while `pagination.has_more` is true, pass `pagination.next_cursor` back as `cursor` for the next page. Deleted and internal-only projects are left out.","tags":["Projects"],"security":[{"apiKey":[]}],"parameters":[{"in":"query","name":"limit","description":"Most items to return in one page, from 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"description":"Most items to return in one page, from 1 to 100. Defaults to 25."}},{"in":"query","name":"cursor","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page.","schema":{"type":"string","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page."}}],"responses":{"200":{"description":"One page of projects.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"created_at":{"type":"string"},"dashboard_url":{"type":"string","nullable":true,"description":"The opportunities that belong to one project. Give this when the customer is working inside a single project rather than the whole organization, or when they want to pause or resume one of its opportunities - those two controls live on a list row here. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","name","slug","created_at","dashboard_url"]}},"pagination":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]}},"required":["data","pagination"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"createProject","summary":"Create a project","description":"Creates a project to file opportunities under. Every opportunity belongs to one, so pass the returned `id` as `project_id` to `POST /opportunities`. The `slug` is generated from `name` with a unique suffix, and each call creates a new project even when the name is already in use.","tags":["Projects"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Name of the new project"}},"required":["name"]}}}},"responses":{"200":{"description":"The new project.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"created_at":{"type":"string"},"dashboard_url":{"type":"string","nullable":true,"description":"The opportunities that belong to one project. Give this when the customer is working inside a single project rather than the whole organization, or when they want to pause or resume one of its opportunities - those two controls live on a list row here. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","name","slug","created_at","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/projects/{projectId}":{"get":{"operationId":"getProject","summary":"Get project details","description":"Returns one project with `opportunity_count`, the number of opportunities filed under it, drafts included and deleted ones not. List them with `GET /opportunities` and `projectId`.","tags":["Projects"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"projectId","description":"ID of the project, as returned by `GET /projects` or `POST /projects`.","schema":{"type":"string","description":"ID of the project, as returned by `GET /projects` or `POST /projects`."},"required":true}],"responses":{"200":{"description":"The project, with its opportunity count.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"created_at":{"type":"string"},"dashboard_url":{"type":"string","nullable":true,"description":"The opportunities that belong to one project. Give this when the customer is working inside a single project rather than the whole organization, or when they want to pause or resume one of its opportunities - those two controls live on a list row here. Null if Terac cannot resolve the organization or project this link belongs to."},"opportunity_count":{"type":"integer"}},"required":["id","name","slug","created_at","dashboard_url","opportunity_count"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"updateProject","summary":"Update a project","description":"Renames a project. `name` is the only field you can change, and a request without it returns 400. The `slug` and `dashboard_url` stay the same.","tags":["Projects"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"projectId","description":"ID of the project to update","schema":{"type":"string","description":"ID of the project to update"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"New project name"}}}}}},"responses":{"200":{"description":"The renamed project.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"created_at":{"type":"string"},"dashboard_url":{"type":"string","nullable":true,"description":"The opportunities that belong to one project. Give this when the customer is working inside a single project rather than the whole organization, or when they want to pause or resume one of its opportunities - those two controls live on a list row here. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","name","slug","created_at","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/filters":{"get":{"operationId":"getFilters","summary":"List available filters","description":"Lists the audience filters an opportunity's `filters` can use, with each one's slug, type and supported operators. The catalog is the same for every organization and is not paginated. A filter with an `options_url` takes values from a fixed list: read them from `GET /filters/{filter_slug}/options` and send their `id`s. `integer--age` takes a `$gte` / `$lte` range within `min` and `max`, and the two `reference--` filters take opportunity ids.","tags":["Filters"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Every filter in the catalog.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"category":{"type":"string"},"operators":{"type":"array","items":{"type":"string"}},"min":{"type":"number"},"max":{"type":"number"},"options_url":{"type":"string"}},"required":["slug","name","type","category","operators"]}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/filters/{filter_slug}/options":{"get":{"operationId":"getFilterOptions","summary":"List options for a filter","description":"Lists the allowed values for one filter; send an option's `id`, not its `name`, as the value in an opportunity's `filters`. `search` matches ids, names and alternate names, ignoring case and accents. Results are paged by number with `page` and `per_page`, and `meta.total` counts every match. State options need `country_id`, and city options need `country_id` with an optional `state_id`; without `country_id` both lists are empty. An unknown `filter_slug` returns 400.","tags":["Filters"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"filter_slug","description":"A filter's `slug` from `GET /filters`, such as `multi_select--country`. A filter with no `options_url` returns an empty list.","schema":{"type":"string","description":"A filter's `slug` from `GET /filters`, such as `multi_select--country`. A filter with no `options_url` returns an empty list."},"required":true},{"in":"query","name":"search","description":"Text to match against option ids, names and alternate names, ignoring case and accents.","schema":{"type":"string","description":"Text to match against option ids, names and alternate names, ignoring case and accents."}},{"in":"query","name":"page","description":"1-based page number. Defaults to 1.","schema":{"type":"integer","minimum":1,"default":1,"description":"1-based page number. Defaults to 1."}},{"in":"query","name":"per_page","description":"Options per page, from 1 to 100. Defaults to 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50,"description":"Options per page, from 1 to 100. Defaults to 50."}},{"in":"query","name":"country_id","description":"ISO 3166-1 alpha-2 country code, such as `US`. `multi_select--state` and `multi_select--city` list nothing without it; other filters ignore it.","schema":{"type":"string","description":"ISO 3166-1 alpha-2 country code, such as `US`. `multi_select--state` and `multi_select--city` list nothing without it; other filters ignore it."}},{"in":"query","name":"state_id","description":"Narrows `multi_select--city` to one state or province: the state option's `id` (`US-CA`) or its bare code (`CA`). Other filters ignore it.","schema":{"type":"string","description":"Narrows `multi_select--city` to one state or province: the state option's `id` (`US-CA`) or its bare code (`CA`). Other filters ignore it."}}],"responses":{"200":{"description":"One page of the filter's allowed values, with the total match count.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]}},"meta":{"type":"object","properties":{"total":{"type":"number"},"page":{"type":"number"},"per_page":{"type":"number"}},"required":["total","page","per_page"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities":{"post":{"operationId":"createOpportunity","summary":"Create a draft opportunity","description":"Creates the opportunity as a DRAFT. Nothing is charged and no recruitment starts until you call\n`POST /opportunities/{opportunityId}/launch`.\n\n**The incentive is derived, not sent.** There is no field for participant pay, and the amount is\nfixed when the draft is created. Two paths decide which number you get:\n\n- **Omit `feasibility_request_id`** and the incentive is an automatic estimate made during this\n  call. It is priced from the whole brief, not just its size: the participant count, the task\n  duration, the audience your `filters` and `screening_questions` describe, and the recruitment\n  window from `expected_days_to_complete`. Changing any of those changes the price, so read the\n  result back from `pricing` on the response; do not quote a price to anyone before you have.\n- **Pass a `feasibility_request_id`** and that request's confirmed CPI is honored exactly, with no\n  re-estimate. Submit the brief to `POST /feasibility/requests` first and poll\n  `GET /feasibility/requests/{requestId}` until it reads `RESPONDED`, which is when a price exists.\n  This is the way to control what participants are paid.\n\nThe platform fee follows the same split. Priced by feasibility, the confirmed recruitment fee\nbecomes this opportunity's fee, as a flat per-participant amount, so the all-in CPI you agreed is\nthe one you are charged. Priced automatically, it comes from the organization's configuration\ninstead. Either way it is not a per-opportunity input, and neither is the currency (always USD) or\nthe pay cadence (always `one_time`). `pricing` on the response carries the all-in cost per\nparticipant and the total, and `funding` says whether the balance covers a launch.\n\nEditing after this call is draft-only: `PATCH /opportunities/{opportunityId}` returns 409 once the\nopportunity is launched, and a new recruitment window moves the deadline without re-pricing.","tags":["Opportunities"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"Participant-facing name of the opportunity"},"internal_title":{"type":"string","maxLength":200,"description":"Internal-only name (not shown to participants)"},"description":{"type":"string","maxLength":8000,"description":"What the opportunity is about; shown to participants"},"project_id":{"type":"string","description":"ID of the project to file this under (from GET /projects or POST /projects)"},"num_participants":{"type":"integer","minimum":1,"maximum":1000,"description":"Number of completed submissions you need (1-1000 for self-serve)"},"business_type":{"type":"string","enum":["b2c","b2b"],"description":"\"b2c\" = consumer audience, \"b2b\" = professional/business audience"},"customer_screening_review":{"type":"string","enum":["auto_invite","manual_review"],"description":"What happens to an applicant who qualifies: \"auto_invite\" (the default) invites them and starts their tasks immediately, \"manual_review\" holds them for your decision. Held applicants are read with GET /opportunities/{opportunityId}/applicants and resolved with POST /submissions/{submissionId}/invite or /decline; they carry no submission status until you decide."},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer","minimum":1,"description":"1-based order in which the participant performs this task"},"task_type":{"type":"string","enum":["interview","file_upload","activity"],"description":"Pick by WHERE the answer lands, not by what the work is called. Work through these in order and stop at the first that fits: 1. You have a page where they do the work and submit it -> \"interview\". REQUIRES task_url, that page. 2. No such page, and the deliverable is a file (document, recording, screenshot) -> \"file_upload\": the expert uploads files through Terac and you read them off the submission, so it needs no task_url. REQUIRES instructions and accepted_mime_types. 3. No such page and the deliverable is not a file -> ask the customer for a URL. The task is still an \"interview\"; you simply cannot create it until they give you that page. Do not fall through to another type while you wait. 4. You want no output at all -> \"activity\". It lands nowhere: Terac records the button press and whatever the expert read, decided or wrote is gone. Research, auditing a site, judging output, writing something: all real work, so all end at step 1, 2 or 3. Never reach step 4 to escape step 3, because \"activity\" does not collect the work, it discards it. None of these book a human-moderated call; that is not available through this API."},"review_type":{"type":"string","enum":["auto_approve","manual_review","self_report"],"description":"How completion is reviewed and when the expert is paid: \"auto_approve\" = accepted and paid automatically, but ONLY when the task has a task_url whose provider redirects to the completion callback below; a task with no URL has no way to fire it and falls back to manual review; \"self_report\" = the participant attests completion, which auto-approves and pays them (you cannot withhold payment based on your own quality signal); \"manual_review\" = the submission goes to AWAITING_REVIEW and is paid only when you call approve (reject withholds payment). Choose manual_review when you need to gate payout on your own verification."},"task_url":{"type":"string","format":"uri","description":"Fully-qualified URL (https://...) of the page where the expert BOTH does the work AND submits their answer. REQUIRED on \"interview\". Not used by \"file_upload\", which collects its files in Terac. The customer hosts that page, not Terac, and it owes two things: SHOW whatever the expert has to read, and give them an INPUT to answer with. The answer arrives there. If they have no such page, that is what to ask them for. Not a reference to read: a repo, doc or homepage is not a task, because there is nothing to submit from it. A Terac task screen hosts no form and no text box, so a task without this returns only a button press and the expert's work is lost. Files are the exception: use \"file_upload\". Pasting the material into description does not collect it, and neither does screening_questions. Terac appends tracking params per participant (submissionId, teracSubmissionId, taskId) so you can attribute an answer; the tool description carries the callback contract."},"title":{"type":"string","description":"Short label for this task"},"description":{"type":"string","description":"What the participant should do in this task"},"duration_minutes":{"type":"integer","minimum":1,"description":"Estimated minutes to complete this task; drives pricing, so a duration you guess is a price you invent. Take it from whoever is commissioning the work. Required except for \"file_upload\", which carries no time estimate."},"instructions":{"type":"string","maxLength":4000,"description":"REQUIRED on \"file_upload\": what the expert should upload, in words shown above the drop zone. Ignored by other task types."},"accepted_mime_types":{"type":"array","items":{"type":"string","minLength":1},"description":"REQUIRED on \"file_upload\": MIME types the expert may upload. Exact types (\"application/pdf\"), family wildcards (\"image/*\") and \"*/*\" for any file all work. Ignored by other task types."},"min_files":{"type":"integer","minimum":1,"maximum":20,"description":"For \"file_upload\": fewest files the expert must upload before they can submit. Defaults to 1."},"max_files":{"type":"integer","minimum":1,"maximum":20,"description":"For \"file_upload\": most files the expert may upload; must be >= min_files and at most 20. Defaults to 1."},"max_file_size_bytes":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"For \"file_upload\": per-file size ceiling in bytes. Defaults to 25 MB."}},"required":["sequence","task_type","review_type"]},"minItems":1,"description":"Ordered list of tasks each participant performs (at least one)"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."},"description":"Structured hard filters for targeting; each entry maps a filter slug to an operator object (see GET /filters). Omitting this recruits worldwide, at any age, in any language, and the draft shows that as no restriction rather than as a mistake, so a create carrying neither these nor unrestricted_audience is REJECTED."},"unrestricted_audience":{"type":"boolean","description":"Set true to record that recruiting everyone (worldwide, any age, any language) is the sample the customer wants. It is the only way to create without filters, and it is REJECTED alongside filters. Nothing checks that you asked, so reaching for it to clear the rejection defeats the point of the field."},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable identifier for this question, referenced by quotas[].screening_question and display conditions. Defaults to q0, q1, ... by position if omitted."},"text":{"type":"string","minLength":1,"description":"The screening question text"},"question_rich_text":{"type":"string","description":"Optional Markdown (GFM) version of the prompt, used as the display copy when set (bold, bullet lists, ...). `text` stays required and remains the plaintext source of truth for the voice agent and search, so keep the two in sync."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"],"description":"\"one\" = single-select, \"any\" = multi-select, \"boolean\" = yes/no, \"text\" = open-ended freeform response (no answer options; responses route to manual review), \"grid\" = matrix (put rows/columns on `grid`, omit answers)."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","minLength":1,"description":"The answer option shown to the participant"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"How this answer affects qualification: \"may\" = acceptable; \"must\" = this exact answer is required (valid with pick \"one\" or \"any\"); \"must_one_of\" = counts toward a required any-of group (multi-select, pick: \"any\"); \"reject\" = disqualifies; \"review\" = flag for manual review (routes an otherwise-qualified applicant to customer review instead of auto-inviting; only acts on auto-invite studies). IMPORTANT, single-select collapse: on pick \"one\" and pick \"boolean\" only one answer can be selected, so an answer can be neither individually required nor ignored - \"may\", \"must\" and \"must_one_of\" all persist as the same qualifying disposition and all read back as \"must\". A single-select whose answers are all \"may\"/\"must\"/\"must_one_of\" therefore screens nobody out. Put \"reject\" on the answers that must screen an applicant out (or \"review\" to route them to a human), or use pick \"any\" when you need a different disposition per answer."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\") whose typed value is captured with the response. Eligibility is unchanged - the answer keeps its qualify_logic. Applies to pick \"one\" and \"any\"; ignored for pick \"boolean\"."}},"required":["text","qualify_logic"]},"minItems":2,"description":"Answer options, each with qualify_logic. Required for choice questions (pick \"one\" | \"any\" | \"boolean\"): at least two. Omit for pick: \"text\" and pick: \"grid\"."},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"],"description":"How many columns per row: \"one\" (default) = a single column per row; \"any\" = any number of columns per row (select all that apply per row)."},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable row key; defaults to r0, r1, ... by position."},"text":{"type":"string","minLength":1,"description":"Row label, e.g. a platform name"}},"required":["text"]},"minItems":1},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"Stable column value; defaults to c0, c1, ... by position."},"text":{"type":"string","minLength":1,"description":"Column label shared across rows"}},"required":["text"]},"minItems":2},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string","description":"A row key from rows[]"},"column":{"type":"string","description":"A column value from columns[]"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject"]}},"required":["row","column","qualify_logic"]},"description":"Per-cell qualify logic. Omit a cell for neutral (may)."}},"required":["rows","columns"],"description":"Required when pick is \"grid\"; omit otherwise."},"min_qualifying":{"type":"integer","minimum":1,"description":"Multi-select (pick: \"any\") only. Require at least this many \"must_one_of\" answers to be selected to qualify, e.g. min_qualifying: 2 with three must_one_of answers means \"select 2+ of these three\". Omitted or 1 = the default \"at least one of\" behavior. Ignored for other pick types."},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions"],"description":"Show this question only if earlier answers match (\"ask only if\"). Conditions reference EARLIER questions by key."},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of the question this condition reads. Omit to test an answer on THIS question (a self-check)."},"answer":{"type":"string","description":"An answer by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["answer"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"},"outcome":{"type":"string","enum":["reject","review"],"description":"\"reject\" screens the participant out, \"review\" flags for a human, when the rule matches."}},"required":["conditions","outcome"]},"description":"Cross-question consistency / anti-falsification checks that reject or flag when answers match a pattern (e.g. \"if role is Store Manager but none of the management tasks are selected here, flag for review\"). Write one all-\"and\" rule per role. Reference an earlier question with screening_question; omit it to test this question's own selections."},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching: when a rule's conditions hold, jump straight to target_question (or end the screener when it is null), skipping everything in between. Conditions test THIS question's answers, and the target must be a later question. Not available for pick \"text\" or \"grid\"."},"allow_paste":{"type":"boolean","description":"Open-ended (pick: \"text\") only. Whether participants may paste into the freeform field. Pasting is blocked by default to discourage low-effort copied responses; set true for questions that ask for a pasted value (e.g. a link). Ignored for choice questions."}},"required":["text","pick"]},"description":"Soft qualification questions with answer options. These decide WHO is accepted; they are not where the study's work is done. Ask what makes someone the right fit (experience, tools, role, context). NEVER put the research questions here: screening runs BEFORE an applicant is accepted and is UNPAID, so it collects the study's answers for free from everyone who applies, including everyone you turn away. The work belongs at task_url. Optional here, but a study cannot LAUNCH with a question no answer can pass, and launches with no screener only when Terac has enabled that for your organization and the launch sends without_screener: true. Otherwise a draft created without these has to be updated before it can go live."},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]},"description":"Per-answer target counts; each quota references a screening_questions[].key. Requires screening_questions."},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","target"]},"maxItems":200,"description":"Cross-question (interlocked cross-tab) quotas: one cell per answer combination across two or more screening questions, cells of one cross-tab sharing a dimension. Requires screening_questions."},"device_types":{"type":"array","items":{"type":"string","enum":["desktop","mobile_ios","mobile_android","tablet_ios","tablet_android","other"]},"description":"Allowed device types. Omitting this allows desktop, mobile_ios, mobile_android and other, but NOT either tablet, so name the tablet values explicitly when a tablet audience matters."},"expected_days_to_complete":{"type":"integer","minimum":5,"maximum":50000000,"description":"Recruitment window in calendar days (min 5, default 7)"},"feasibility_request_id":{"type":"string","description":"ID of a priced (RESPONDED) feasibility request. When set, the opportunity uses that confirmed CPI instead of an autonomous estimate. The request stays RESPONDED and is only closed as won once the study launches."}},"required":["title","project_id","num_participants","business_type","tasks"]},"example":{"title":"How finance teams choose spend software","description":"Tell us how your team chose and rolled out its spend management software. Eight written questions, about 30 minutes.","project_id":"m4q8x2zc7tn1v0bk5r9wh3jd","num_participants":12,"business_type":"b2b","expected_days_to_complete":10,"feasibility_request_id":"tz4a98xxat96iws9zmbrgj3a","filters":[{"multi_select--country":{"$in":["US","CA"]}},{"multi_select--seniority":{"$in":["manager","senior-manager","director"]}}],"screening_questions":[{"key":"chose_tool","text":"Were you involved in choosing your company's current spend management software?","pick":"one","answers":[{"text":"Yes, I led or approved the decision","qualify_logic":"must"},{"text":"Yes, I gave input","qualify_logic":"must"},{"text":"No","qualify_logic":"reject"}]}],"tasks":[{"sequence":1,"task_type":"interview","review_type":"manual_review","task_url":"https://research.example.com/studies/spend-software","title":"Written interview","description":"Answer the eight questions on the interview page, then submit it.","duration_minutes":30}]}}}},"responses":{"200":{"description":"The new draft, with its price and whether the balance covers a launch.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]},"example":{"id":"h7k2p9w4c1v6n3x8b5m0q2rt","title":"How finance teams choose spend software","internal_title":null,"description":"Tell us how your team chose and rolled out its spend management software. Eight written questions, about 30 minutes.","status":"draft","num_participants":12,"estimated_duration_minutes":30,"filters":[{"multi_select--country":{"$in":["US","CA"]}},{"multi_select--seniority":{"$in":["manager","senior-manager","director"]}}],"screening_questions":[{"key":"chose_tool","text":"Were you involved in choosing your company's current spend management software?","pick":"one","answers":[{"text":"Yes, I led or approved the decision","qualify_logic":"must"},{"text":"Yes, I gave input","qualify_logic":"must"},{"text":"No","qualify_logic":"reject"}]}],"quotas":[],"cross_quotas":[],"tasks":[{"sequence":1,"task_type":"interview","review_type":"manual_review","task_url":"https://research.example.com/studies/spend-software","participant_url_template":"https://research.example.com/studies/spend-software?submissionId={submissionId}&teracSubmissionId={submissionId}&taskId={taskId}","title":"Written interview","description":"Answer the eight questions on the interview page, then submit it.","duration_minutes":30}],"pricing":{"cost_per_participant_cents":6250,"total_cost_cents":75000,"currency":"usd"},"device_types":["desktop","mobile_ios","mobile_android","other"],"business_type":"b2b","candidate_profile":null,"required_accounts":[],"video_required":"none","screen_share_required":"none","customer_screening_review":"auto_invite","pay_frequency":"one_time","screening_option_randomization":"disabled","forwarding_email_enabled":"disabled","max_responses_per_expert":null,"feasibility_request_id":"tz4a98xxat96iws9zmbrgj3a","project_id":"m4q8x2zc7tn1v0bk5r9wh3jd","created_at":"2026-09-28T15:10:05.903Z","funding":{"affordable":true,"draws_on_credit":false,"balance_cents":240000,"shortfall_cents":0,"finance_url":"https://terac.com/acme-research/settings/finance"},"updated_at":"2026-09-28T15:10:06.412Z","ending_at":"2026-10-08T15:10:05.903Z","launched_at":null,"links":{"self":"/api/external/v2/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt","launch":"/api/external/v2/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/launch","submissions":"/api/external/v2/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions","dashboard":{"study":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt","draft_editor":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/create?id=h7k2p9w4c1v6n3x8b5m0q2rt","submissions":null,"recruitment":null,"task":null,"settings":null}}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"get":{"operationId":"listOpportunities","summary":"List opportunities","description":"Lists your organization's opportunities, newest first, with each one's status, target and pricing. Filter by `projectId` or by one `status`. Returns up to `limit` per page; while `pagination.has_more` is true, pass `pagination.next_cursor` back as `cursor` for the next page. Deleted opportunities are left out. `pricing` is what launching charged, so it is null on a draft; read a draft's estimate from `GET /opportunities/{opportunityId}`.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"query","name":"limit","description":"Most items to return in one page, from 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"description":"Most items to return in one page, from 1 to 100. Defaults to 25."}},{"in":"query","name":"cursor","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page.","schema":{"type":"string","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page."}},{"in":"query","name":"status","description":"Return only opportunities in this status: `draft`, `pre_recruiting`, `active`, `fulfilled`, `paused`, `stopped`, `completed`, `expired`, `cancelled` or `failed`. `draft` includes opportunities Terac is still preparing.","schema":{"type":"string","description":"Return only opportunities in this status: `draft`, `pre_recruiting`, `active`, `fulfilled`, `paused`, `stopped`, `completed`, `expired`, `cancelled` or `failed`. `draft` includes opportunities Terac is still preparing."}},{"in":"query","name":"projectId","description":"Return only opportunities filed under this project ID.","schema":{"type":"string","description":"Return only opportunities filed under this project ID."}}],"responses":{"200":{"description":"One page of opportunity summaries.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"created_at":{"type":"string"},"dashboard_url":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","title","status","num_participants","pricing","created_at","dashboard_url"]}},"pagination":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]}},"required":["data","pagination"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}":{"get":{"operationId":"getOpportunity","summary":"Get opportunity details","description":"Returns one opportunity in full: its brief, audience, screener, tasks and pricing. A draft also carries `funding`, which says whether the balance covers a launch. A launched one carries `submission_stats`, `quota_progress` and `screening_stats` instead, so poll this to follow recruitment. `screening_questions` reads back in the shape `POST /opportunities` accepts, ready to send back on `PATCH`, and `links.dashboard` holds its pages in the researcher dashboard.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"responses":{"200":{"description":"The opportunity in full.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"updateOpportunity","summary":"Update opportunity","description":"Edits a draft opportunity; send only the fields you are changing. A list you send (`tasks`, `filters`, `screening_questions`, `quotas`, `cross_quotas`) replaces the stored one rather than merging into it, so read the opportunity first and send the whole list back. `quotas` and `cross_quotas` are only accepted alongside `screening_questions`. A new `expected_days_to_complete` moves the deadline without re-pricing. A request that changes nothing returns 400, and a launched opportunity returns 409.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity to update","schema":{"type":"string","description":"ID of the opportunity to update"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"New participant-facing name"},"internal_title":{"type":"string","maxLength":200,"description":"New internal-only name"},"description":{"type":"string","maxLength":8000,"description":"New description"},"num_participants":{"type":"integer","minimum":1,"maximum":1000,"description":"New number of completed submissions you need (1-1000 for self-serve)."},"business_type":{"type":"string","enum":["b2c","b2b"],"description":"New audience type: \"b2c\" = consumer, \"b2b\" = professional/business"},"customer_screening_review":{"type":"string","enum":["auto_invite","manual_review"],"description":"Whether qualified applicants wait for your decision (\"manual_review\") or are invited automatically (\"auto_invite\"). Applies to applicants who qualify after this call; anyone already invited stays invited."},"device_types":{"type":"array","items":{"type":"string","enum":["desktop","mobile_ios","mobile_android","tablet_ios","tablet_android","other"]},"description":"REPLACES the allowed device list wholesale. Omitting this leaves the current restriction untouched (unlike create, where omitting means desktop, mobile_ios, mobile_android and other)."},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer","minimum":1,"description":"1-based order in which the participant performs this task"},"task_type":{"type":"string","enum":["interview","file_upload","activity"],"description":"Pick by WHERE the answer lands, not by what the work is called. Work through these in order and stop at the first that fits: 1. You have a page where they do the work and submit it -> \"interview\". REQUIRES task_url, that page. 2. No such page, and the deliverable is a file (document, recording, screenshot) -> \"file_upload\": the expert uploads files through Terac and you read them off the submission, so it needs no task_url. REQUIRES instructions and accepted_mime_types. 3. No such page and the deliverable is not a file -> ask the customer for a URL. The task is still an \"interview\"; you simply cannot create it until they give you that page. Do not fall through to another type while you wait. 4. You want no output at all -> \"activity\". It lands nowhere: Terac records the button press and whatever the expert read, decided or wrote is gone. Research, auditing a site, judging output, writing something: all real work, so all end at step 1, 2 or 3. Never reach step 4 to escape step 3, because \"activity\" does not collect the work, it discards it. None of these book a human-moderated call; that is not available through this API."},"review_type":{"type":"string","enum":["auto_approve","manual_review","self_report"],"description":"How completion is reviewed and when the expert is paid: \"auto_approve\" = accepted and paid automatically, but ONLY when the task has a task_url whose provider redirects to the completion callback below; a task with no URL has no way to fire it and falls back to manual review; \"self_report\" = the participant attests completion, which auto-approves and pays them (you cannot withhold payment based on your own quality signal); \"manual_review\" = the submission goes to AWAITING_REVIEW and is paid only when you call approve (reject withholds payment). Choose manual_review when you need to gate payout on your own verification."},"task_url":{"type":"string","format":"uri","description":"Fully-qualified URL (https://...) of the page where the expert BOTH does the work AND submits their answer. REQUIRED on \"interview\". Not used by \"file_upload\", which collects its files in Terac. The customer hosts that page, not Terac, and it owes two things: SHOW whatever the expert has to read, and give them an INPUT to answer with. The answer arrives there. If they have no such page, that is what to ask them for. Not a reference to read: a repo, doc or homepage is not a task, because there is nothing to submit from it. A Terac task screen hosts no form and no text box, so a task without this returns only a button press and the expert's work is lost. Files are the exception: use \"file_upload\". Pasting the material into description does not collect it, and neither does screening_questions. Terac appends tracking params per participant (submissionId, teracSubmissionId, taskId) so you can attribute an answer; the tool description carries the callback contract."},"title":{"type":"string","description":"Short label for this task"},"description":{"type":"string","description":"What the participant should do in this task"},"duration_minutes":{"type":"integer","minimum":1,"description":"Estimated minutes to complete this task; drives pricing, so a duration you guess is a price you invent. Take it from whoever is commissioning the work. Required except for \"file_upload\", which carries no time estimate."},"instructions":{"type":"string","maxLength":4000,"description":"REQUIRED on \"file_upload\": what the expert should upload, in words shown above the drop zone. Ignored by other task types."},"accepted_mime_types":{"type":"array","items":{"type":"string","minLength":1},"description":"REQUIRED on \"file_upload\": MIME types the expert may upload. Exact types (\"application/pdf\"), family wildcards (\"image/*\") and \"*/*\" for any file all work. Ignored by other task types."},"min_files":{"type":"integer","minimum":1,"maximum":20,"description":"For \"file_upload\": fewest files the expert must upload before they can submit. Defaults to 1."},"max_files":{"type":"integer","minimum":1,"maximum":20,"description":"For \"file_upload\": most files the expert may upload; must be >= min_files and at most 20. Defaults to 1."},"max_file_size_bytes":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"For \"file_upload\": per-file size ceiling in bytes. Defaults to 25 MB."}},"required":["sequence","task_type","review_type"]},"minItems":1,"description":"REPLACES the entire task list; it is not merged. Send every task you want to keep, so read the current tasks with terac_get_opportunity first."},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."},"description":"REPLACES every hard filter; it is not merged. Send the full targeting list you want to keep. An empty array drops all of them and leaves the study recruiting everyone, so it is accepted only alongside unrestricted_audience: true."},"unrestricted_audience":{"type":"boolean","description":"Set true in the same call that sends an empty filters array, to record that the customer wants every restriction gone. It decides what THIS call does to the audience, so it is rejected on a call that leaves filters alone, and rejected alongside a non-empty filters list."},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable identifier for this question, referenced by quotas[].screening_question and display conditions. Defaults to q0, q1, ... by position if omitted."},"text":{"type":"string","minLength":1,"description":"The screening question text"},"question_rich_text":{"type":"string","description":"Optional Markdown (GFM) version of the prompt, used as the display copy when set (bold, bullet lists, ...). `text` stays required and remains the plaintext source of truth for the voice agent and search, so keep the two in sync."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"],"description":"\"one\" = single-select, \"any\" = multi-select, \"boolean\" = yes/no, \"text\" = open-ended freeform response (no answer options; responses route to manual review), \"grid\" = matrix (put rows/columns on `grid`, omit answers)."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","minLength":1,"description":"The answer option shown to the participant"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"How this answer affects qualification: \"may\" = acceptable; \"must\" = this exact answer is required (valid with pick \"one\" or \"any\"); \"must_one_of\" = counts toward a required any-of group (multi-select, pick: \"any\"); \"reject\" = disqualifies; \"review\" = flag for manual review (routes an otherwise-qualified applicant to customer review instead of auto-inviting; only acts on auto-invite studies). IMPORTANT, single-select collapse: on pick \"one\" and pick \"boolean\" only one answer can be selected, so an answer can be neither individually required nor ignored - \"may\", \"must\" and \"must_one_of\" all persist as the same qualifying disposition and all read back as \"must\". A single-select whose answers are all \"may\"/\"must\"/\"must_one_of\" therefore screens nobody out. Put \"reject\" on the answers that must screen an applicant out (or \"review\" to route them to a human), or use pick \"any\" when you need a different disposition per answer."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\") whose typed value is captured with the response. Eligibility is unchanged - the answer keeps its qualify_logic. Applies to pick \"one\" and \"any\"; ignored for pick \"boolean\"."}},"required":["text","qualify_logic"]},"minItems":2,"description":"Answer options, each with qualify_logic. Required for choice questions (pick \"one\" | \"any\" | \"boolean\"): at least two. Omit for pick: \"text\" and pick: \"grid\"."},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"],"description":"How many columns per row: \"one\" (default) = a single column per row; \"any\" = any number of columns per row (select all that apply per row)."},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable row key; defaults to r0, r1, ... by position."},"text":{"type":"string","minLength":1,"description":"Row label, e.g. a platform name"}},"required":["text"]},"minItems":1},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"Stable column value; defaults to c0, c1, ... by position."},"text":{"type":"string","minLength":1,"description":"Column label shared across rows"}},"required":["text"]},"minItems":2},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string","description":"A row key from rows[]"},"column":{"type":"string","description":"A column value from columns[]"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject"]}},"required":["row","column","qualify_logic"]},"description":"Per-cell qualify logic. Omit a cell for neutral (may)."}},"required":["rows","columns"],"description":"Required when pick is \"grid\"; omit otherwise."},"min_qualifying":{"type":"integer","minimum":1,"description":"Multi-select (pick: \"any\") only. Require at least this many \"must_one_of\" answers to be selected to qualify, e.g. min_qualifying: 2 with three must_one_of answers means \"select 2+ of these three\". Omitted or 1 = the default \"at least one of\" behavior. Ignored for other pick types."},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions"],"description":"Show this question only if earlier answers match (\"ask only if\"). Conditions reference EARLIER questions by key."},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of the question this condition reads. Omit to test an answer on THIS question (a self-check)."},"answer":{"type":"string","description":"An answer by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["answer"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"},"outcome":{"type":"string","enum":["reject","review"],"description":"\"reject\" screens the participant out, \"review\" flags for a human, when the rule matches."}},"required":["conditions","outcome"]},"description":"Cross-question consistency / anti-falsification checks that reject or flag when answers match a pattern (e.g. \"if role is Store Manager but none of the management tasks are selected here, flag for review\"). Write one all-\"and\" rule per role. Reference an earlier question with screening_question; omit it to test this question's own selections."},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching: when a rule's conditions hold, jump straight to target_question (or end the screener when it is null), skipping everything in between. Conditions test THIS question's answers, and the target must be a later question. Not available for pick \"text\" or \"grid\"."},"allow_paste":{"type":"boolean","description":"Open-ended (pick: \"text\") only. Whether participants may paste into the freeform field. Pasting is blocked by default to discourage low-effort copied responses; set true for questions that ask for a pasted value (e.g. a link). Ignored for choice questions."}},"required":["text","pick"]},"description":"REPLACES the entire screener; it is not merged. Send every question you want to keep, so read the current screener with terac_get_opportunity first."},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]},"description":"REPLACES every per-answer target count. Only accepted alongside screening_questions in the SAME call, because a quota attaches to one of those answers."},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","target"]},"maxItems":200,"description":"REPLACES every cross-question (cross-tab) quota. Only accepted alongside screening_questions in the SAME call, because each cell references those answers. Send an empty array to drop all cross-quotas."},"expected_days_to_complete":{"type":"integer","minimum":5,"maximum":50000000,"description":"New recruitment window in calendar days counted from today (min 5). Moves the deadline only; it does not re-price the opportunity."}}}}}},"responses":{"200":{"description":"The draft as stored after the edit.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"delete":{"operationId":"deleteOpportunity","summary":"Delete a draft opportunity","description":"Deletes a draft opportunity, which was never charged, and drops it from `GET /opportunities`. A launched opportunity returns 409; end one with `POST /opportunities/{opportunityId}/stop` instead.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"responses":{"200":{"description":"Confirms the deletion, with a link to the organization's remaining opportunities.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]},"dashboard_url":{"type":"string","nullable":true,"description":"Every opportunity in the organization, across all projects, filterable by status. Give this when the customer asks where they can see everything they have running, or when they want to pause or resume something - those two controls live on a list row here, not on the study's own pages. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","deleted","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/launch":{"post":{"operationId":"launchOpportunity","summary":"Launch opportunity","description":"Launches a draft opportunity: charges your organization's balance and starts recruiting.\n\nThe charge is the participant incentive for every one of `num_participants` plus the platform\nfee, as `pricing` on the draft shows, and `funding` says beforehand whether the balance covers\nit. The response is the opportunity, already `active`, so there is nothing to poll for.\n\nOnly a `draft` can be launched; any other status returns 409. A draft that cannot run is\nrefused with 412 before anything is charged, and the message says what to fix: the balance\ndoes not cover the charge, the screener is missing or nobody can pass it, the participant\nbudget is under the $5 minimum, or the feasibility request it was priced from is not\nanswered yet. Unused budget is refunded when the opportunity is stopped or completes.\n\nA screener is recommended. A draft without `screening_questions` launches only when Terac has\nenabled that for your organization and you send `without_screener: true`. Unless Terac runs its\nAI screening interview on it, everyone who matches its filters then goes straight to the task, so\nthe tasks that deliver the work have to be `review_type: manual_review` and\n`customer_screening_review` has to be `auto_invite`: nobody is paid until you approve their\nsubmission with `POST /submissions/{submissionId}/approve`.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"without_screener":{"type":"boolean","description":"Launch an opportunity that has no screening_questions. Honored only when Terac has enabled it for your organization, and ignored on an opportunity that has a screener. Unless Terac runs its AI screening interview on it, everyone who matches the filters then goes straight to the task, so the tasks that deliver the work have to be review_type manual_review and customer_screening_review auto_invite, and nobody is paid until you approve their submission. A screener is recommended: send this only when none is needed."}}},"example":{}}}},"responses":{"200":{"description":"The launched opportunity, now `active`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]},"example":{"id":"h7k2p9w4c1v6n3x8b5m0q2rt","title":"How finance teams choose spend software","internal_title":null,"description":"Tell us how your team chose and rolled out its spend management software. Eight written questions, about 30 minutes.","status":"active","num_participants":12,"estimated_duration_minutes":30,"filters":[{"multi_select--country":{"$in":["US","CA"]}},{"multi_select--seniority":{"$in":["manager","senior-manager","director"]}}],"screening_questions":[{"key":"chose_tool","text":"Were you involved in choosing your company's current spend management software?","pick":"one","answers":[{"text":"Yes, I led or approved the decision","qualify_logic":"must"},{"text":"Yes, I gave input","qualify_logic":"must"},{"text":"No","qualify_logic":"reject"}]}],"quotas":[],"cross_quotas":[],"tasks":[{"sequence":1,"task_type":"interview","review_type":"manual_review","task_url":"https://research.example.com/studies/spend-software","participant_url_template":"https://research.example.com/studies/spend-software?submissionId={submissionId}&teracSubmissionId={submissionId}&taskId={taskId}","title":"Written interview","description":"Answer the eight questions on the interview page, then submit it.","duration_minutes":30}],"pricing":{"cost_per_participant_cents":6250,"total_cost_cents":75000,"currency":"usd"},"device_types":["desktop","mobile_ios","mobile_android","other"],"business_type":"b2b","candidate_profile":null,"required_accounts":[],"video_required":"none","screen_share_required":"none","customer_screening_review":"auto_invite","pay_frequency":"one_time","screening_option_randomization":"disabled","forwarding_email_enabled":"disabled","max_responses_per_expert":null,"feasibility_request_id":"tz4a98xxat96iws9zmbrgj3a","project_id":"m4q8x2zc7tn1v0bk5r9wh3jd","created_at":"2026-09-28T15:10:05.903Z","updated_at":"2026-09-29T09:30:12.311Z","ending_at":"2026-10-04T09:30:12.311Z","launched_at":"2026-09-29T09:30:12.311Z","links":{"self":"/api/external/v2/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt","launch":null,"submissions":"/api/external/v2/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions","dashboard":{"study":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt","draft_editor":null,"submissions":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions","recruitment":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/recruitment","task":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/task","settings":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/settings"}},"submission_stats":{"total":0,"in_progress":0,"awaiting_review":0,"approved":0,"rejected":0}}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/pause":{"post":{"operationId":"pauseOpportunity","summary":"Pause opportunity","description":"Pauses an active opportunity, so it stops taking new applicants. Participants already working can still finish and submit, and nothing is charged or refunded. Resume it with `POST /opportunities/{opportunityId}/resume`. Only an `active` opportunity can be paused; any other status returns 409.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"responses":{"200":{"description":"The opportunity, now `paused`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}},"requestBody":{"required":true,"description":"This operation takes no body fields, but the request must still carry `Content-Type: application/json` and a JSON body: send `{}`.","content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{}}}}}},"/opportunities/{opportunityId}/resume":{"post":{"operationId":"resumeOpportunity","summary":"Resume opportunity","description":"Resumes a paused opportunity, so it takes new applicants again. Nothing is charged: the budget taken at launch still covers it. Only a `paused` opportunity can be resumed; any other status returns 409.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"responses":{"200":{"description":"The opportunity, `active` again.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}},"requestBody":{"required":true,"description":"This operation takes no body fields, but the request must still carry `Content-Type: application/json` and a JSON body: send `{}`.","content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{}}}}}},"/opportunities/{opportunityId}/stop":{"post":{"operationId":"stopOpportunity","summary":"Stop opportunity","description":"Permanently ends an opportunity and refunds its unused budget to your organization's balance.\n\nPending invitations are withdrawn and recruitment cannot be restarted, so pause instead if you\nmay want to continue. Submissions still awaiting your review block the stop: approve or reject\nthem first. On an hourly or per-task opportunity, the stop itself approves every expert already\npaid on it whose submission is still open. Only an `active`, `paused` or `pre_recruiting`\nopportunity can be stopped; any other status returns 409. Delete a draft instead.","tags":["Opportunities"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":5000,"description":"Why you are stopping it, up to 5,000 characters. Recorded with the stop, the refund and each withdrawn invitation."}}},"example":{"reason":"The audience changed, so a new opportunity replaces this one."}}}},"responses":{"200":{"description":"The opportunity, now `stopped`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"internal_title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"status":{"type":"string","enum":["draft","pre_recruiting","active","fulfilled","paused","stopped","completed","expired","cancelled","failed"],"description":"Decides which calls are allowed:\n- `draft`: edit, launch or delete it. An opportunity Terac is still preparing from a quote reads `draft` too.\n- `pre_recruiting`: taking applicants ahead of a launch set up in the dashboard. Stop only.\n- `active`: pause or stop.\n- `paused`: resume or stop.\n- `fulfilled`: target reached; settles to `completed`.\n- `completed`, `stopped`, `expired`, `cancelled`: ended.\n- `failed`: its launch never finished. Create a new opportunity."},"num_participants":{"type":"integer"},"estimated_duration_minutes":{"type":"integer"},"filters":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"description":"A single hard filter, keyed by a filter slug from GET /filters, mapping to an operator object. Operators are Mongo-style and prefixed with \"$\": e.g. { \"integer--age\": { \"$gte\": 25, \"$lte\": 45 } } or { \"multi_select--country\": { \"$in\": [\"US\", \"CA\"] } }. Use the operators and option values returned by the filters endpoints; an unknown slug is rejected."}},"screening_questions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque."},"text":{"type":"string"},"question_rich_text":{"type":"string","description":"Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth."},"pick":{"type":"string","enum":["one","any","boolean","text","grid"]},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"],"description":"The disposition that is actually stored, which is not always the one that was sent. On a single-select (pick \"one\", which is also how a yes/no question reads back) every qualifying answer reads back as \"must\", because \"may\", \"must\" and \"must_one_of\" collapse to one disposition where only one answer can be picked. So a \"may\" answer reading back as \"must\" is the stored truth, not a mistranslation - rewriting it to \"may\" will not restore a filter. Only \"reject\" and \"review\" screen or flag on a single-select; switch the question to pick \"any\" if you need per-answer dispositions."},"allow_free_text":{"type":"boolean","description":"Selecting this answer reveals a write-in box (\"Other, please specify\")."}},"required":["text","qualify_logic"]}},"grid":{"type":"object","properties":{"pick":{"type":"string","enum":["one","any"]},"rows":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"columns":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"text":{"type":"string"}},"required":["text"]}},"cell_actions":{"type":"array","items":{"type":"object","properties":{"row":{"type":"string"},"column":{"type":"string"},"qualify_logic":{"type":"string","enum":["may","must","must_one_of","reject","review"]}},"required":["row","column","qualify_logic"]}}},"required":["rows","columns"]},"min_qualifying":{"type":"integer","minimum":1},"display_condition":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of an EARLIER question this one depends on"},"answer":{"type":"string","description":"An answer of that earlier question. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"],"default":"and"}},"required":["conditions","join"]},"conditional_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"answer":{"type":"string"},"operator":{"type":"string","enum":["eq","ne"]}},"required":["answer","operator"]}},"join":{"type":"string","enum":["and","or"]},"outcome":{"type":"string","enum":["reject","review"]}},"required":["conditions","join","outcome"]}},"skip_rules":{"type":"array","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the question this rule lives on. A rule tests its OWN answers; use display_condition to gate a question on an earlier one."},"answer":{"type":"string","description":"One of that question's answers (its text; \"Yes\"/\"No\" for pick: \"boolean\")."},"operator":{"type":"string","enum":["eq","ne"],"description":"\"eq\" = answered that value, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26},"join":{"type":"string","enum":["and","or"]},"target_question":{"type":"string","nullable":true,"description":"Key of the question to jump to when the conditions hold; null ends the screener. Must be a LATER question - the screener only walks forward."}},"required":["conditions","join","target_question"]},"description":"Branching authored here or in the dashboard, in the same shape create accepts, so it can be sent straight back on an update. Omitted when the question has none - an absent field means no branching, never that branching was dropped on read."},"allow_paste":{"type":"boolean"}},"required":["key","text","pick"]}},"quotas":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The key of the screening question this quota applies to (must match a screening_questions[].key)"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string","description":"The answer option text (matches a screening answer's text) this quota targets"},"count":{"type":"integer","minimum":1,"description":"How many approved submissions to allow/require for this answer"},"type":{"type":"string","enum":["exact","minimum","maximum"],"description":"How the count is enforced: \"minimum\" = at least this many (a floor; recruiting prioritizes the answer while it is short); \"maximum\" = at most this many (a cap; further applicants who pick this answer are screened out once it is full); \"exact\" = both. Omit for \"minimum\"."}},"required":["value","count"]},"minItems":1,"description":"Per-answer target counts for this question"}},"required":["screening_question","targets"]}},"cross_quotas":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."}},"required":["label","conditions","join","target","quota_type"]}},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string","description":"Any stored task type, so wider than the set you can create: interview, survey, file_upload, activity, scheduled_interview, agreement, join_slack, slack_handoff, e_signature. Only interview, file_upload and activity are accepted on create."},"review_type":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"null when the task type carries no URL."},"participant_url_template":{"type":"string","description":"task_url with the tracking params Terac appends per participant (submissionId, teracSubmissionId, taskId), shown as {submissionId}/{taskId} macros. Omitted when there is no task_url."},"title":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"available_after_sequence":{"type":"integer","description":"This task unlocks only after that task is approved."},"available_after_delay_minutes":{"type":"integer","description":"Wait this long after the gating task before unlocking."},"provider":{"type":"string","description":"Scheduled interview only: the booking provider."},"calendar_owner":{"type":"string","description":"Scheduled interview only: whose calendar is booked (researcher or expert)."},"event_type_url":{"type":"string","description":"Scheduled interview only: the booking link."},"instructions":{"type":"string","description":"File upload only: what to submit."},"accepted_mime_types":{"type":"array","items":{"type":"string"},"description":"\"file_upload\" only, and absent on every other task type. RESOLVED rather than echoed: a task created without this reads back with the default actually enforced, not an empty field."},"min_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_files":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."},"max_file_size_bytes":{"type":"integer","description":"\"file_upload\" only, and absent on every other task type. Resolved to the enforced default when the task did not set one."}},"required":["sequence","task_type","review_type","task_url","duration_minutes"]}},"pricing":{"type":"object","nullable":true,"properties":{"cost_per_participant_cents":{"type":"integer"},"total_cost_cents":{"type":"integer"},"currency":{"type":"string","enum":["usd"]}},"required":["cost_per_participant_cents","total_cost_cents","currency"]},"funding":{"type":"object","properties":{"affordable":{"type":"boolean","description":"Covers the cost, overdraft included. False blocks nothing."},"draws_on_credit":{"type":"boolean","description":"Affordable but goes negative, invoiced later. Say so."},"balance_cents":{"type":"integer","description":"Balance now."},"shortfall_cents":{"type":"integer","description":"Gap after overdraft, 0 when affordable."},"finance_url":{"type":"string","nullable":true,"description":"Where billing adds credit."}},"required":["affordable","draws_on_credit","balance_cents","shortfall_cents","finance_url"]},"device_types":{"type":"array","items":{"type":"string"}},"business_type":{"type":"string","nullable":true},"candidate_profile":{"type":"string","nullable":true,"description":"Free-text profile of the ideal candidate, when set."},"required_accounts":{"type":"array","items":{"type":"string"},"description":"Accounts an expert must have linked to apply (e.g. linkedin)."},"video_required":{"type":"string","description":"Whether the expert's camera is needed: none, requested, required."},"screen_share_required":{"type":"string","description":"Whether a screen share is needed: none, requested, required."},"customer_screening_review":{"type":"string","description":"auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite."},"pay_frequency":{"type":"string","description":"How the incentive is framed: one_time, hourly, or per_task."},"screening_option_randomization":{"type":"string","description":"Whether screening answer options are shown in random order: the string \"enabled\" or \"disabled\", not a boolean."},"forwarding_email_enabled":{"type":"string","description":"Whether each submission gets a Terac forwarding email address: the string \"enabled\" or \"disabled\", not a boolean."},"max_responses_per_expert":{"type":"integer","nullable":true},"feasibility_request_id":{"type":"string","nullable":true},"project_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"ending_at":{"type":"string","nullable":true,"description":"Target completion date for recruitment."},"launched_at":{"type":"string","nullable":true},"links":{"type":"object","properties":{"self":{"type":"string"},"launch":{"type":"string","nullable":true},"submissions":{"type":"string"},"dashboard":{"type":"object","properties":{"study":{"type":"string","nullable":true,"description":"The study's own page. It routes itself by status: an editable draft opens the builder, anything else opens submissions. Never null. Give this when you do not know which tab the customer needs; when `draft_editor` is set, prefer that instead, since it opens the same builder the customer would review in. Null if Terac cannot resolve the organization or project this link belongs to."},"draft_editor":{"type":"string","nullable":true,"description":"Opens this draft in the study builder so a human can review the whole setup (targeting, screener, tasks, price) before anything launches. Set exactly when `submissions` here is null, never both. Give this to the customer after you build a draft, instead of launching. Null if Terac cannot resolve the organization or project this link belongs to."},"submissions":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"recruitment":{"type":"string","nullable":true,"description":"The screener questions and quotas every applicant is filtered against. Set exactly when `draft_editor` here is null. Give this when the customer wants to check who is being screened in or out. Null if Terac cannot resolve the organization or project this link belongs to."},"task":{"type":"string","nullable":true,"description":"What a participant is actually asked to do - task type, duration, and task link. Set exactly when `draft_editor` here is null. Give this when the customer asks what the participants see. Null if Terac cannot resolve the organization or project this link belongs to."},"settings":{"type":"string","nullable":true,"description":"The study's own settings: recruitment window, participant count, and the controls to stop or delete it. Set exactly when `draft_editor` here is null. Give this when the customer wants to change or end a running study rather than look at its results. Pause and resume are not here - they are on the opportunities list. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["study","draft_editor","submissions","recruitment","task","settings"],"description":"Pages a person opens in a browser. `self`, `launch` and `submissions` above are API paths for you, not for them - never give a customer one of those."}},"required":["self","launch","submissions","dashboard"]},"submission_stats":{"type":"object","properties":{"total":{"type":"integer"},"in_progress":{"type":"integer"},"awaiting_review":{"type":"integer"},"approved":{"type":"integer"},"rejected":{"type":"integer"}},"required":["total","in_progress","awaiting_review","approved","rejected"]},"quota_progress":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string"},"targets":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"},"current":{"type":"integer"},"type":{"type":"string","description":"How the count is enforced: exact, minimum or maximum. Tells you whether \"current\" nearing \"count\" means a cap is about to close or a floor is about to be met."}},"required":["value","count","current","type"]}}},"required":["screening_question","targets"]}},"cross_quota_progress":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label for this cell, e.g. \"Brazil x Agency\"."},"conditions":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"Key of a screening_questions[].key this condition tests (one dimension of the cross-tab)."},"answer":{"type":"string","description":"An answer of that question, by its label. For a grid source use \"Row: Column\"."},"operator":{"type":"string","enum":["eq","ne"],"default":"eq","description":"\"eq\" = selected that answer, \"ne\" = did not."}},"required":["screening_question","answer","operator"]},"minItems":1,"maxItems":26,"description":"The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions."},"join":{"type":"string","enum":["and","or"],"default":"and"},"target":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Target participant count for this cell."},"quota_type":{"type":"string","enum":["exact","minimum","maximum"],"default":"minimum","description":"\"exact\" = hit exactly, \"minimum\" = at least, \"maximum\" = no more than."},"dimension":{"type":"string","description":"Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab."},"current":{"type":"integer","description":"Approved participants matching this cell so far."}},"required":["label","conditions","join","target","quota_type","current"]},"description":"Fill progress for each cross-question quota cell (live studies)."},"screening_stats":{"type":"array","items":{"type":"object","properties":{"screening_question":{"type":"string","description":"The `screening_questions[].key`."},"graded":{"type":"integer","description":"Screeners GRADED here, not answers given: a branching screener stops at the first failure."},"rejected":{"type":"integer","description":"Rejected by this question."},"rejection_rate":{"type":"number","description":"`rejected` / `graded`, 0 to 1."},"answers":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The answer as written on the question, not its stored value."},"selected":{"type":"integer"},"share":{"type":"number","description":"`selected` / `graded`, between 0 and 1."}},"required":["text","selected","share"]},"description":"How often each answer was chosen, for spotting a screener that is too narrow."}},"required":["screening_question","graded","rejected","rejection_rate","answers"]},"description":"Per-question screen-out rates and answer breakdown (live studies)."}},"required":["id","title","internal_title","description","status","num_participants","estimated_duration_minutes","filters","screening_questions","quotas","cross_quotas","tasks","pricing","device_types","business_type","candidate_profile","required_accounts","video_required","screen_share_required","customer_screening_review","pay_frequency","screening_option_randomization","forwarding_email_enabled","max_responses_per_expert","feasibility_request_id","project_id","created_at","updated_at","ending_at","launched_at","links"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/submissions":{"get":{"operationId":"listSubmissions","summary":"List submissions for an opportunity","description":"Lists an opportunity's submissions, newest first, each with its status and screening answers. Filter to one `status`; the `awaiting_review` ones wait on you to approve or reject them. Applicants still in the screener, or waiting on your invite decision, are not listed; the latter are in `GET /opportunities/{opportunityId}/applicants`. Returns up to `limit` per page; while `pagination.has_more` is true, pass `pagination.next_cursor` back as `cursor` for the next page. For totals, read `submission_stats` on the opportunity rather than counting pages.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true},{"in":"query","name":"limit","description":"Most items to return in one page, from 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"description":"Most items to return in one page, from 1 to 100. Defaults to 25."}},{"in":"query","name":"cursor","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page.","schema":{"type":"string","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page."}},{"in":"query","name":"status","description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way.","schema":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way."}}],"responses":{"200":{"description":"One page of submissions, with the dashboard page where they are reviewed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"opportunity_id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way."},"participant_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"participant_name":{"type":"string","nullable":true,"description":"The participant's name. Null when your organization hides participant identity in Terac, or when we hold none."},"participant_linkedin_url":{"type":"string","nullable":true,"description":"The participant's LinkedIn profile URL. Null when your organization hides participant identity in Terac, or when they have none on file."},"participant_background":{"type":"object","nullable":true,"properties":{"experiences":{"type":"array","items":{"type":"object","properties":{"company":{"type":"string"},"role":{"type":"string","nullable":true},"started_at":{"type":"string","nullable":true},"ended_at":{"type":"string","nullable":true},"description":{"type":"string","nullable":true,"description":"What they did there, with contact details removed."}},"required":["company","role","started_at","ended_at","description"]},"description":"Their work history."},"educations":{"type":"array","items":{"type":"object","properties":{"school":{"type":"string"},"degree":{"type":"string","nullable":true},"major":{"type":"string","nullable":true}},"required":["school","degree","major"]},"description":"Their education history."}},"required":["experiences","educations"],"description":"The participant's work and education history, with names and contact details removed. Null when we hold no history for them."},"screening_outcome":{"type":"string","nullable":true,"enum":["passed","failed","review",null],"description":"The screener's overall verdict; \"review\" means qualified but flagged for a human. null while it is unfinished or absent."},"screening_answers":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The `screening_questions[].key` this answers. Absent only on legacy panel-backed screenings, which stored answers per insight."},"question":{"type":"string","description":"The prompt as asked, in plain text."},"answer":{"type":"array","items":{"type":"string"}},"outcome":{"type":"string","nullable":true,"enum":["qualify","reject","not_important","review",null],"description":"The verdict this answer received; do not re-derive it from `qualify_logic`. null if a branching screener never graded it."}},"required":["question","answer","outcome"]},"description":"Every answer given, with each verdict. Empty when the screener recorded nothing."}},"required":["id","opportunity_id","status","participant_id","created_at","updated_at","participant_name","participant_linkedin_url","participant_background","screening_outcome","screening_answers"]}},"pagination":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["data","pagination","dashboard_url"]},"example":{"data":[{"id":"q3w8e1r6t4y9u2i7o5p0a3sd","opportunity_id":"h7k2p9w4c1v6n3x8b5m0q2rt","status":"awaiting_review","participant_id":"6f1c2a9e-3b7d-4e58-9a21-c4d8e7f0b3a6","participant_name":"Maya Chen","participant_linkedin_url":"https://www.linkedin.com/in/maya-chen","participant_background":{"experiences":[{"company":"Northwind Logistics","role":"Head of Procurement","started_at":"2021-03","ended_at":null,"description":"Led the evaluation and rollout of a new supplier platform."}],"educations":[{"school":"University of Michigan","degree":"MBA","major":"Operations"}]},"created_at":"2026-09-29T11:42:57.020Z","updated_at":"2026-09-29T13:18:04.655Z","screening_outcome":"passed","screening_answers":[{"key":"chose_tool","question":"Were you involved in choosing your company's current spend management software?","answer":["Yes, I led or approved the decision"],"outcome":"qualify"}]},{"id":"z6x1c8v3b7n2m9k4j0h5g2fd","opportunity_id":"h7k2p9w4c1v6n3x8b5m0q2rt","status":"in_progress","participant_id":"0d9e4b2c-7a1f-4c63-8e5b-2f7a9c1d6e84","participant_name":"Daniel Okafor","participant_linkedin_url":null,"participant_background":null,"created_at":"2026-09-29T11:15:33.748Z","updated_at":"2026-09-29T11:21:09.302Z","screening_outcome":"passed","screening_answers":[{"key":"chose_tool","question":"Were you involved in choosing your company's current spend management software?","answer":["Yes, I gave input"],"outcome":"qualify"}]}],"pagination":{"next_cursor":null,"has_more":false},"dashboard_url":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions"}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/applicants":{"get":{"operationId":"listApplicants","summary":"List applicants awaiting your invite decision","description":"Applicants who passed screening and are waiting on your decision to invite them.\n\nThey are deliberately absent from `GET /opportunities/{opportunityId}/submissions`: an applicant\nhere has not been invited yet, so they hold no submission status, and `GET /submissions/{id}`\nreturns 404 for them. This collection is where they exist until you decide.\n\nAct on one with `POST /submissions/{submissionId}/invite` or `/decline`. Inviting makes them\n`screen_passed` and starts their tasks; declining makes them `screened_out`. Either way they\nleave this collection and appear in the submissions listing from then on.\n\nAn opportunity on `manual_review` routes every qualified applicant here. One on `auto_invite`\nstill routes an individual applicant here when a screening answer you marked `review` flags\nthem, so poll this even when you did not opt into reviewing everyone.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true},{"in":"query","name":"limit","description":"Most items to return in one page, from 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"description":"Most items to return in one page, from 1 to 100. Defaults to 25."}},{"in":"query","name":"cursor","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page.","schema":{"type":"string","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page."}}],"responses":{"200":{"description":"One page of applicants awaiting your decision, each with their screening answers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"opportunity_id":{"type":"string"},"participant_id":{"type":"string"},"screening_outcome":{"type":"string","nullable":true,"enum":["passed","failed","review",null],"description":"The screener's overall verdict; \"review\" means qualified but flagged for a human. null while it is unfinished or absent."},"screening_answers":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The `screening_questions[].key` this answers. Absent only on legacy panel-backed screenings, which stored answers per insight."},"question":{"type":"string","description":"The prompt as asked, in plain text."},"answer":{"type":"array","items":{"type":"string"}},"outcome":{"type":"string","nullable":true,"enum":["qualify","reject","not_important","review",null],"description":"The verdict this answer received; do not re-derive it from `qualify_logic`. null if a branching screener never graded it."}},"required":["question","answer","outcome"]},"description":"Every answer given, with each verdict. Empty when the screener recorded nothing."},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","opportunity_id","participant_id","screening_outcome","screening_answers","created_at","updated_at"]}},"pagination":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["data","pagination","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/submissions/{submissionId}/invite":{"post":{"operationId":"inviteApplicant","summary":"Invite an applicant awaiting your decision","description":"Invites an applicant sitting in your applicant-review queue, which materializes their tasks and notifies them. The submission becomes `screen_passed`. Returns 409 if the applicant is not awaiting your decision, which includes one you have already decided on.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of an applicant awaiting your invite decision: the `id` on their applicant row.","schema":{"type":"string","description":"ID of an applicant awaiting your invite decision: the `id` on their applicant row."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":1000,"description":"Why, recorded on the submission's history for your own team. Optional."}}}}}},"responses":{"200":{"description":"The applicant's submission, now `screen_passed`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"Where the applicant landed: `screen_passed` when invited, `screened_out` when declined."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/submissions/{submissionId}/decline":{"post":{"operationId":"declineApplicant","summary":"Decline an applicant awaiting your decision","description":"Declines an applicant sitting in your applicant-review queue. The submission becomes `screened_out` and they are not invited. Returns 409 if the applicant is not awaiting your decision, which includes one you have already decided on.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of an applicant awaiting your invite decision: the `id` on their applicant row.","schema":{"type":"string","description":"ID of an applicant awaiting your invite decision: the `id` on their applicant row."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":1000,"description":"Why, recorded on the submission's history for your own team. Optional."}}}}}},"responses":{"200":{"description":"The applicant's submission, now `screened_out`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"Where the applicant landed: `screen_passed` when invited, `screened_out` when declined."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/submissions/{submissionId}":{"get":{"operationId":"getSubmission","summary":"Get submission details","description":"Returns one submission with its status, screening answers and per-task progress. An applicant still in the screener, or waiting on your invite decision, returns 404 here; the latter are listed in `GET /opportunities/{opportunityId}/applicants`.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`.","schema":{"type":"string","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`."},"required":true}],"responses":{"200":{"description":"The submission, with its screening answers and per-task progress.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"opportunity_id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way."},"participant_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"participant_name":{"type":"string","nullable":true,"description":"The participant's name. Null when your organization hides participant identity in Terac, or when we hold none."},"participant_linkedin_url":{"type":"string","nullable":true,"description":"The participant's LinkedIn profile URL. Null when your organization hides participant identity in Terac, or when they have none on file."},"participant_background":{"type":"object","nullable":true,"properties":{"experiences":{"type":"array","items":{"type":"object","properties":{"company":{"type":"string"},"role":{"type":"string","nullable":true},"started_at":{"type":"string","nullable":true},"ended_at":{"type":"string","nullable":true},"description":{"type":"string","nullable":true,"description":"What they did there, with contact details removed."}},"required":["company","role","started_at","ended_at","description"]},"description":"Their work history."},"educations":{"type":"array","items":{"type":"object","properties":{"school":{"type":"string"},"degree":{"type":"string","nullable":true},"major":{"type":"string","nullable":true}},"required":["school","degree","major"]},"description":"Their education history."}},"required":["experiences","educations"],"description":"The participant's work and education history, with names and contact details removed. Null when we hold no history for them."},"screening_outcome":{"type":"string","nullable":true,"enum":["passed","failed","review",null],"description":"The screener's overall verdict; \"review\" means qualified but flagged for a human. null while it is unfinished or absent."},"screening_answers":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The `screening_questions[].key` this answers. Absent only on legacy panel-backed screenings, which stored answers per insight."},"question":{"type":"string","description":"The prompt as asked, in plain text."},"answer":{"type":"array","items":{"type":"string"}},"outcome":{"type":"string","nullable":true,"enum":["qualify","reject","not_important","review",null],"description":"The verdict this answer received; do not re-derive it from `qualify_logic`. null if a branching screener never graded it."}},"required":["question","answer","outcome"]},"description":"Every answer given, with each verdict. Empty when the screener recorded nothing."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."},"tasks":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"task_type":{"type":"string"},"status":{"type":"string"}},"required":["sequence","task_type","status"]}}},"required":["id","opportunity_id","status","participant_id","created_at","updated_at","participant_name","participant_linkedin_url","participant_background","screening_outcome","screening_answers","dashboard_url","tasks"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/submissions/{submissionId}/approve":{"post":{"operationId":"approveSubmission","summary":"Approve a submission","description":"Approves a submission that is `awaiting_review`, accepting the reviewed work. The expert is paid when the submission lands in `approved`, once all of its work is accepted. It stays `in_progress` while tasks remain, and on an hourly or per-task opportunity, where approval puts the expert on the project and they are paid through payout runs instead. The response carries the status it lands in. A submission in any other status returns 409.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`.","schema":{"type":"string","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`."},"required":true}],"responses":{"200":{"description":"The submission's status after the approval.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","dashboard_url"]},"example":{"id":"q3w8e1r6t4y9u2i7o5p0a3sd","status":"approved","dashboard_url":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions"}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}},"requestBody":{"required":true,"description":"This operation takes no body fields, but the request must still carry `Content-Type: application/json` and a JSON body: send `{}`.","content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{}}}}}},"/submissions/{submissionId}/reject":{"post":{"operationId":"rejectSubmission","summary":"Reject a submission","description":"Rejects a submission that is `awaiting_review`, so the expert is not paid for that work and the submission becomes `rejected`. `rejection_category` and `rejection_reason` are optional, but they are the only record of why the work went unpaid. A submission in any other status returns 409.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of the submission to reject","schema":{"type":"string","description":"ID of the submission to reject"},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"rejection_category":{"type":"string","enum":["low_quality","failed_instructions","incomplete","suspicious_patterns","other"],"description":"Optional category for the rejection. Strongly recommended so non-payment has an audit trail."},"rejection_reason":{"type":"string","description":"Optional free-text reason, ideally referencing the success/failure criteria the submission missed. Strongly recommended; improves grading and documents why the expert was not paid."}}},"example":{"rejection_category":"failed_instructions","rejection_reason":"Answered four of the eight questions; the brief asked for all eight."}}}},"responses":{"200":{"description":"The submission, now `rejected`.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["screen_passed","screened_out","in_progress","awaiting_review","approved","rejected","abandoned"],"description":"\"screened_out\" never reached the work, \"rejected\" did the work and was not accepted, \"abandoned\" stopped part-way."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","dashboard_url"]},"example":{"id":"q3w8e1r6t4y9u2i7o5p0a3sd","status":"rejected","dashboard_url":"https://terac.com/acme-research/customer-research-mg4q7x2a/opportunities/h7k2p9w4c1v6n3x8b5m0q2rt/submissions"}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/test-submissions":{"post":{"operationId":"createTestSubmission","summary":"Create a test submission in a sandbox","description":"Creates a test submission in a sandbox organization, so your webhook endpoint receives the events a real participant would trigger.\n\nTerac adds a test participant and moves it through the opportunity a few seconds at a time: it answers\nthe screener, starts the task and finishes it. Each step sends the same `submission.status.change`\nwebhook a real participant's step sends, signed and retried the same way. Nobody is contacted and\nnobody is paid. Only a subscription created with a key from the same sandbox organization receives\nthese webhooks.\n\nThe participant stops wherever the next move is yours: inviting or declining an applicant, approving\nor rejecting the work, or your own completion redirect when `task_completion` is `redirect`. Your call\nsends the next webhook. After you invite an applicant, the participant carries on with the task.\n\nThe test submission shows in the sandbox dashboard under the name \"Test Participant\" and counts in the\nopportunity's numbers. It does not count toward a quota, and it cannot be removed.\n\nReturns 403 outside a sandbox organization, and 409 when the opportunity is not active. Returns 422\nwhen Terac cannot create the test submission you asked for on this opportunity, and the `message`\nsays why: for example `failed` with no screener, `passed` with screening questions that no answer\npasses, or `redirect` when the first task has no `task_url`.","tags":["Submissions"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"screening_outcome":{"type":"string","enum":["passed","failed","filtered_out"],"default":"passed","description":"How screening goes for the test participant. \"passed\" (the default) gets through the screener. \"failed\" answers the screener and is screened out. \"filtered_out\" is screened out on its profile before the screener, and is allowed even when the opportunity has no filters."},"task_completion":{"type":"string","enum":["automatic","redirect"],"default":"automatic","description":"Who finishes the task, for a test participant that reaches it. \"automatic\" (the default): Terac finishes it. \"redirect\": Terac starts the task and stops, and you finish it by opening `task_url` and letting your own completion redirect bring the participant back to Terac."}}}}}},"responses":{"200":{"description":"The test submission, which a test participant is now taking through the opportunity.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"ID of the test submission. `GET /submissions/{submissionId}` returns 404 while the test participant is still in screening."},"opportunity_id":{"type":"string"},"task_url":{"type":"string","nullable":true,"description":"The first task's link with this submission's IDs filled in, for you to open once the submission is `in_progress`. Set when `task_completion` is \"redirect\" and `screening_outcome` is \"passed\"; null otherwise."},"dashboard_url":{"type":"string","nullable":true,"description":"Who applied, who is in progress, who is awaiting review. Set exactly when `draft_editor` here is null. Do not read that as the opportunity being live: the `status` field reports several settled states (cancelled, expired, failed) as `draft`, so read status from `status` and treat this only as a link. Give this when the customer asks about progress or has submissions to review. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","opportunity_id","task_url","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/feasibility/requests":{"post":{"operationId":"requestFeasibility","summary":"Submit a feasibility request","description":"Submit a feasibility request: can Terac source a panel for this task, and at what CPI (cost per participant).\n\nThis is an async request-reply. The response comes back immediately with status RECEIVED and no CPI\n(costPerParticipant is null). Terac prices it out of band, either automatically within seconds or by\nrouting it to a person, so poll GET /feasibility/requests/{requestId} rather than assuming a turnaround.\n\nOnly what you send here is priced: taskDescription, panelDescription, and the count and timeline. The\nfilters, screening questions and tasks on the opportunity you build later are not part of the brief that\nwas priced, so a confirmed CPI applies to the scope described here and not to a materially different one.\n\nOnce RESPONDED, create the opportunity with POST /opportunities and pass feasibility_request_id to price\nit from that confirmed CPI (Terac skips the autonomous estimate), then launch it. The request stays\nRESPONDED until that study launches, and is closed as won at launch rather than at create.","tags":["Feasibility"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"taskDescription":{"type":"string","minLength":1,"maxLength":10000,"description":"What participants will do (interview, survey, usability test, ...) and for how long, up to 10,000 characters. Half of the brief that gets priced."},"panelDescription":{"type":"string","minLength":1,"maxLength":10000,"description":"Who the participants must be: role, seniority, industry, market, and anything that rules someone out, up to 10,000 characters. The other half of the priced brief."},"submissionCount":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"How many completed participants you need, if known."},"timelineHours":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"The turnaround you are asking Terac for, in hours."},"payRateFrequency":{"type":"string","enum":["ONE_TIME","HOURLY","PER_TASK"],"description":"How the work is paid: ONE_TIME (per complete), HOURLY, or PER_TASK. Omit when the brief does not say; do not guess."},"loiMinutes":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Minutes one participant spends on the task. Not the turnaround."},"requestorEmail":{"type":"string","format":"email","description":"Who Terac should contact about this request. Defaults to the email of the API key's owner."}},"required":["taskDescription","panelDescription"]},"example":{"taskDescription":"A 30-minute written interview on a page we host: eight open questions about how the participant's team chose and rolled out its spend management software.","panelDescription":"Finance managers, senior managers and directors in the US or Canada who took part in choosing their company's current spend management software.","submissionCount":12,"timelineHours":120,"payRateFrequency":"ONE_TIME","loiMinutes":30}}}},"responses":{"200":{"description":"The new request, `RECEIVED` and not priced yet, with the dashboard page where its price will land.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["RECEIVED","RESPONDED","WON","LOST","NOT_PURSUED"]},"source":{"type":"string"},"taskDescription":{"type":"string"},"panelDescription":{"type":"string"},"submissionCount":{"type":"integer","nullable":true},"timelineHours":{"type":"integer","nullable":true},"payRateFrequency":{"type":"string","nullable":true,"enum":["ONE_TIME","HOURLY","PER_TASK",null],"description":"What costPerParticipant is denominated in once priced: ONE_TIME (per complete), HOURLY, or PER_TASK. Null until the brief states it."},"loiMinutes":{"type":"integer","nullable":true,"description":"Minutes one participant spends on the task. Distinct from timelineHours, which is Terac's turnaround."},"costPerParticipant":{"type":"string","nullable":true},"respondedAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"dashboard_url":{"type":"string","nullable":true,"description":"The organization's feasibility requests and the incentives Terac quoted back. There is no page for a single request, so this list is the only feasibility link; give it after you request feasibility or when the customer asks where a quote went. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","source","taskDescription","panelDescription","submissionCount","timelineHours","payRateFrequency","loiMinutes","costPerParticipant","respondedAt","createdAt","dashboard_url"]},"example":{"id":"tz4a98xxat96iws9zmbrgj3a","status":"RECEIVED","source":"API","taskDescription":"A 30-minute written interview on a page we host: eight open questions about how the participant's team chose and rolled out its spend management software.","panelDescription":"Finance managers, senior managers and directors in the US or Canada who took part in choosing their company's current spend management software.","submissionCount":12,"timelineHours":120,"payRateFrequency":"ONE_TIME","loiMinutes":30,"costPerParticipant":null,"respondedAt":null,"createdAt":"2026-09-28T15:02:11.482Z","dashboard_url":"https://terac.com/acme-research/feasibility"}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"get":{"operationId":"listFeasibilityRequests","summary":"List feasibility requests","description":"List your organization's feasibility requests, newest first. Optionally filter by status\n(RECEIVED / RESPONDED / WON / LOST / NOT_PURSUED) and page through results with limit and offset. Each\nrequest includes its costPerParticipant (the CPI), which is null until the request has been priced\n(status RESPONDED).","tags":["Feasibility"],"security":[{"apiKey":[]}],"parameters":[{"in":"query","name":"status","description":"Return only requests in this status. `GET /feasibility/requests/{requestId}` says what each one means.","schema":{"type":"string","enum":["RECEIVED","RESPONDED","WON","LOST","NOT_PURSUED"],"description":"Return only requests in this status. `GET /feasibility/requests/{requestId}` says what each one means."}},{"in":"query","name":"limit","description":"Most requests to return, from 1 to 100. Defaults to 50.","schema":{"type":"integer","minimum":0,"exclusiveMinimum":true,"maximum":100,"description":"Most requests to return, from 1 to 100. Defaults to 50."}},{"in":"query","name":"offset","description":"How many requests to skip before this page, newest first. Defaults to 0.","schema":{"type":"integer","minimum":0,"description":"How many requests to skip before this page, newest first. Defaults to 0."}}],"responses":{"200":{"description":"One page of feasibility requests, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","description":"How many requests this page holds, not a total. Page with offset until a page comes back shorter than the limit."},"requests":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["RECEIVED","RESPONDED","WON","LOST","NOT_PURSUED"]},"source":{"type":"string"},"taskDescription":{"type":"string"},"panelDescription":{"type":"string"},"submissionCount":{"type":"integer","nullable":true},"timelineHours":{"type":"integer","nullable":true},"payRateFrequency":{"type":"string","nullable":true,"enum":["ONE_TIME","HOURLY","PER_TASK",null],"description":"What costPerParticipant is denominated in once priced: ONE_TIME (per complete), HOURLY, or PER_TASK. Null until the brief states it."},"loiMinutes":{"type":"integer","nullable":true,"description":"Minutes one participant spends on the task. Distinct from timelineHours, which is Terac's turnaround."},"costPerParticipant":{"type":"string","nullable":true},"respondedAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","status","source","taskDescription","panelDescription","submissionCount","timelineHours","payRateFrequency","loiMinutes","costPerParticipant","respondedAt","createdAt"]}},"dashboard_url":{"type":"string","nullable":true,"description":"The organization's feasibility requests and the incentives Terac quoted back. There is no page for a single request, so this list is the only feasibility link; give it after you request feasibility or when the customer asks where a quote went. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["count","requests","dashboard_url"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/feasibility/requests/{requestId}":{"get":{"operationId":"getFeasibilityRequest","summary":"Get a feasibility request","description":"Retrieve one of your feasibility requests by id. Use this to poll for the CPI after submitting.\n\nStatus decides whether there is a price to read:\n- RECEIVED: not priced yet, costPerParticipant is null.\n- RESPONDED: priced. costPerParticipant is the confirmed all-in CPI per participant.\n- WON: already used by a study that launched.\n- LOST or NOT_PURSUED: closed without a usable price. Both are terminal, so stop polling. No reason is\n  returned on this endpoint.\n\nOnce RESPONDED, create the opportunity with POST /opportunities passing feasibility_request_id to price\nit from that confirmed CPI, then launch it.","tags":["Feasibility"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"requestId","description":"ID of the feasibility request, as returned by `POST /feasibility/requests`.","schema":{"type":"string","description":"ID of the feasibility request, as returned by `POST /feasibility/requests`."},"required":true}],"responses":{"200":{"description":"The request, with its CPI once it is priced.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["RECEIVED","RESPONDED","WON","LOST","NOT_PURSUED"]},"source":{"type":"string"},"taskDescription":{"type":"string"},"panelDescription":{"type":"string"},"submissionCount":{"type":"integer","nullable":true},"timelineHours":{"type":"integer","nullable":true},"payRateFrequency":{"type":"string","nullable":true,"enum":["ONE_TIME","HOURLY","PER_TASK",null],"description":"What costPerParticipant is denominated in once priced: ONE_TIME (per complete), HOURLY, or PER_TASK. Null until the brief states it."},"loiMinutes":{"type":"integer","nullable":true,"description":"Minutes one participant spends on the task. Distinct from timelineHours, which is Terac's turnaround."},"costPerParticipant":{"type":"string","nullable":true},"respondedAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"dashboard_url":{"type":"string","nullable":true,"description":"The organization's feasibility requests and the incentives Terac quoted back. There is no page for a single request, so this list is the only feasibility link; give it after you request feasibility or when the customer asks where a quote went. Null if Terac cannot resolve the organization or project this link belongs to."}},"required":["id","status","source","taskDescription","panelDescription","submissionCount","timelineHours","payRateFrequency","loiMinutes","costPerParticipant","respondedAt","createdAt","dashboard_url"]},"example":{"id":"tz4a98xxat96iws9zmbrgj3a","status":"RESPONDED","source":"API","taskDescription":"A 30-minute written interview on a page we host: eight open questions about how the participant's team chose and rolled out its spend management software.","panelDescription":"Finance managers, senior managers and directors in the US or Canada who took part in choosing their company's current spend management software.","submissionCount":12,"timelineHours":120,"payRateFrequency":"ONE_TIME","loiMinutes":30,"costPerParticipant":"62.50","respondedAt":"2026-09-28T15:03:40.117Z","createdAt":"2026-09-28T15:02:11.482Z","dashboard_url":"https://terac.com/acme-research/feasibility"}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/organizations/current/context":{"get":{"operationId":"getOrganizationContext","summary":"Get organization context","description":"Returns a markdown briefing on the organization your API key belongs to. It covers identity, balance and where to add credit, dashboard links, projects, opportunity counts, the operating guide for building an opportunity, and any policies the organization set for task and panel descriptions. Identity, balance and dashboard links also come back as structured fields.","tags":["Organizations"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"The organization's briefing as markdown, with its identity, balance and dashboard links as fields.","content":{"application/json":{"schema":{"type":"object","properties":{"markdown":{"type":"string"},"organizationId":{"type":"string"},"organizationName":{"type":"string"},"organizationSlug":{"type":"string"},"balanceDollars":{"type":"number"},"dashboard":{"type":"object","properties":{"home":{"type":"string","description":"The organization's landing page in the dashboard, with recent activity across every project. Give this when the customer wants somewhere to start rather than a specific study or page."},"opportunities":{"type":"string","description":"Every opportunity in the organization, across all projects, filterable by status. Give this when the customer asks where they can see everything they have running, or when they want to pause or resume something - those two controls live on a list row here, not on the study's own pages."},"drafts":{"type":"string","description":"The same opportunities list, pre-filtered to drafts. Give this when the customer wants to find the drafts you built without hunting through launched studies."},"feasibility":{"type":"string","description":"The organization's feasibility requests and the incentives Terac quoted back. There is no page for a single request, so this list is the only feasibility link; give it after you request feasibility or when the customer asks where a quote went."},"finance":{"type":"string","description":"Where the organization checks its balance and adds credit. Give this when the balance will not cover a launch. Requires billing permission; a member without it gets a 404."},"api_keys":{"type":"string","description":"Where the organization creates and revokes the API keys that authenticate this connection. Give this when the customer needs a new key or wants to revoke one, never for anything about a study."}},"required":["home","opportunities","drafts","feasibility","finance","api_keys"],"description":"The organization's pages in the researcher dashboard, as absolute URLs. Never null - this organization's slug is what builds them. The same URLs appear under `## Dashboard` in the markdown."}},"required":["markdown","organizationId","organizationName","organizationSlug","balanceDollars","dashboard"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/hooks/event-types":{"get":{"operationId":"listWebhookEventTypes","summary":"List subscribable webhook event types","description":"Lists the event types a webhook subscription can receive, each with a description. Read this rather than hardcoding a list: new event types are added without a breaking change, and appear here first.","tags":["Webhooks"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Every event type a subscription can receive.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"event_type":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":{"type":"string"}},"required":["event_type","description"]}}},"required":["data"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/hooks/subscriptions":{"get":{"operationId":"listWebhookSubscriptions","summary":"List webhook subscriptions","description":"Lists every webhook subscription in your organization, one per target URL, with its event types and whether it is confirmed and enabled. Not paginated. Signing secrets are left out; read one from `GET /hooks/subscriptions/{subscriptionId}/secret`.","tags":["Webhooks"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Every webhook subscription in the organization.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"target_url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":"Every event type this endpoint receives. PATCH replaces the whole list."},"is_enabled":{"type":"boolean"},"confirmed_at":{"type":"string","nullable":true,"description":"When this subscription passed the confirmation ping. Null means it receives nothing yet."},"disabled_at":{"type":"string","nullable":true},"disabled_reason":{"type":"string","nullable":true,"description":"Set when Terac disabled the subscription rather than you, after sustained delivery failure."},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","target_url","event_types","is_enabled","confirmed_at","disabled_at","disabled_reason","created_at","updated_at"]}}},"required":["data"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"createWebhookSubscription","summary":"Create a webhook subscription","description":"Creates a webhook subscription for one target URL and returns its signing secret. Created unconfirmed, so it receives nothing until you call the confirm endpoint and Terac's ping to target_url returns 2xx. A URL that already has a subscription returns 409; patch that one's event_types instead.","tags":["Webhooks"],"security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_url":{"type":"string","format":"uri","maxLength":2000,"description":"A public https URL that answers 2xx. Private, loopback, link-local and internal addresses are refused, because confirming makes Terac issue a request to it. One subscription per URL; add event types to the existing one rather than creating a second. Confirm the subscription before it receives anything."},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"minItems":1,"description":"Which events this URL receives. Read GET /hooks/event-types/ for the current list."}},"required":["target_url","event_types"]}}}},"responses":{"200":{"description":"The unconfirmed subscription, with its signing secret in full.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"target_url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":"Every event type this endpoint receives. PATCH replaces the whole list."},"is_enabled":{"type":"boolean"},"confirmed_at":{"type":"string","nullable":true,"description":"When this subscription passed the confirmation ping. Null means it receives nothing yet."},"disabled_at":{"type":"string","nullable":true},"disabled_reason":{"type":"string","nullable":true,"description":"Set when Terac disabled the subscription rather than you, after sustained delivery failure."},"created_at":{"type":"string"},"updated_at":{"type":"string"},"secret":{"type":"string","description":"The signing secret in full, returned here and by the rotate call only. Store it now; you can also read it back from GET /hooks/subscriptions/{id}/secret."}},"required":["id","target_url","event_types","is_enabled","confirmed_at","disabled_at","disabled_reason","created_at","updated_at","secret"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/hooks/subscriptions/{subscriptionId}":{"get":{"operationId":"getWebhookSubscription","summary":"Get a webhook subscription","description":"Returns one webhook subscription: its target URL, event types, and whether it is confirmed and enabled. disabled_reason is set when Terac disabled it after sustained delivery failure; patch is_enabled to true to turn it back on. The signing secret is left out.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"responses":{"200":{"description":"The subscription.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"target_url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":"Every event type this endpoint receives. PATCH replaces the whole list."},"is_enabled":{"type":"boolean"},"confirmed_at":{"type":"string","nullable":true,"description":"When this subscription passed the confirmation ping. Null means it receives nothing yet."},"disabled_at":{"type":"string","nullable":true},"disabled_reason":{"type":"string","nullable":true,"description":"Set when Terac disabled the subscription rather than you, after sustained delivery failure."},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","target_url","event_types","is_enabled","confirmed_at","disabled_at","disabled_reason","created_at","updated_at"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"patch":{"operationId":"patchWebhookSubscription","summary":"Update a webhook subscription","description":"Updates a webhook subscription's target URL, event types or enabled state, and returns it unchanged when the body sets none. event_types replaces the current list rather than adding to it. Changing target_url clears the confirmation, since the new host has not accepted a ping yet. Set is_enabled true to recover a subscription Terac disabled after sustained failure.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_url":{"type":"string","format":"uri","maxLength":2000,"description":"A new public https URL for this subscription. Private, loopback, link-local and internal addresses are refused. Changing it clears the confirmation, so confirm again before it receives events."},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"minItems":1,"description":"Replaces the current list rather than adding to it."},"is_enabled":{"type":"boolean","description":"Set true to re-enable a subscription Terac disabled after sustained failure."}}}}}},"responses":{"200":{"description":"The subscription as stored after the update.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"target_url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":"Every event type this endpoint receives. PATCH replaces the whole list."},"is_enabled":{"type":"boolean"},"confirmed_at":{"type":"string","nullable":true,"description":"When this subscription passed the confirmation ping. Null means it receives nothing yet."},"disabled_at":{"type":"string","nullable":true},"disabled_reason":{"type":"string","nullable":true,"description":"Set when Terac disabled the subscription rather than you, after sustained delivery failure."},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","target_url","event_types","is_enabled","confirmed_at","disabled_at","disabled_reason","created_at","updated_at"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"confirmWebhookSubscription","summary":"Confirm a webhook subscription","description":"Confirms a webhook subscription by POSTing one signed webhook.ping to its target_url. Answer 2xx and the subscription starts receiving events; any other outcome returns 412 with the reason, and it stays unconfirmed. This is also the way to test a receiver end to end, since it exercises the real signature headers.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"responses":{"200":{"description":"The subscription, now confirmed.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"target_url":{"type":"string"},"event_types":{"type":"array","items":{"type":"string","enum":["submission.status.change","submission.approved"]},"description":"Every event type this endpoint receives. PATCH replaces the whole list."},"is_enabled":{"type":"boolean"},"confirmed_at":{"type":"string","nullable":true,"description":"When this subscription passed the confirmation ping. Null means it receives nothing yet."},"disabled_at":{"type":"string","nullable":true},"disabled_reason":{"type":"string","nullable":true,"description":"Set when Terac disabled the subscription rather than you, after sustained delivery failure."},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","target_url","event_types","is_enabled","confirmed_at","disabled_at","disabled_reason","created_at","updated_at"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}},"requestBody":{"required":true,"description":"This operation takes no body fields, but the request must still carry `Content-Type: application/json` and a JSON body: send `{}`.","content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{}}}}},"delete":{"operationId":"deleteWebhookSubscription","summary":"Delete a webhook subscription","description":"Deletes a webhook subscription. Terac stops sending to it at once, including retries already queued, and its past deliveries stay readable in `GET /hooks/events`.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"responses":{"200":{"description":"Confirms the deletion.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}},"required":["id","deleted"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/hooks/subscriptions/{subscriptionId}/secret":{"get":{"operationId":"getWebhookSubscriptionSecret","summary":"Read a subscription's signing secret","description":"Returns a subscription's current signing secret, for recovering one you no longer have. Rotate instead if you believe it leaked, since reading it does not invalidate anything.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"responses":{"200":{"description":"The subscription's current signing secret.","content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string","description":"The signing secret in full. Verify a delivery with base64(HMAC-SHA256(secret, X-Terac-Request-Timestamp + raw body))."}},"required":["secret"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}},"post":{"operationId":"rotateWebhookSubscriptionSecret","summary":"Rotate a subscription's signing secret","description":"Replaces a subscription's signing secret and returns the new one. Takes effect immediately and with no overlap window: the next attempt of every delivery, including one already queued, is signed with the new secret, so deliveries fail your signature check until your receiver has it.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"subscriptionId","description":"ID of the webhook subscription, as returned when it was created or listed.","schema":{"type":"string","description":"ID of the webhook subscription, as returned when it was created or listed."},"required":true}],"responses":{"200":{"description":"The new signing secret, in effect already.","content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string","description":"The signing secret in full. Verify a delivery with base64(HMAC-SHA256(secret, X-Terac-Request-Timestamp + raw body))."}},"required":["secret"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}},"requestBody":{"required":true,"description":"This operation takes no body fields, but the request must still carry `Content-Type: application/json` and a JSON body: send `{}`.","content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{}}}}}},"/hooks/events":{"get":{"operationId":"listWebhookDeliveries","summary":"List webhook deliveries","description":"Lists webhook deliveries to your subscriptions, newest first, optionally for one subscription_id. One row per delivery, updated in place across retries, so id is the X-Event-ID your endpoint saw and attempt_count is how many tries it took. Confirmation pings are not logged here.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"in":"query","name":"limit","description":"Most items to return in one page, from 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"description":"Most items to return in one page, from 1 to 100. Defaults to 25."}},{"in":"query","name":"cursor","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page.","schema":{"type":"string","description":"`pagination.next_cursor` from the previous page, to fetch the page after it. Omit it for the first page."}},{"in":"query","name":"subscription_id","description":"Return only deliveries to this subscription ID.","schema":{"type":"string","description":"Return only deliveries to this subscription ID."}}],"responses":{"200":{"description":"One page of deliveries, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Also the X-Event-ID header, stable across retries of this delivery."},"subscription_id":{"type":"string"},"event_type":{"type":"string"},"resource_id":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempt_count":{"type":"integer"},"response_status":{"type":"integer","nullable":true},"error_message":{"type":"string","nullable":true},"created_at":{"type":"string"},"last_attempt_at":{"type":"string","nullable":true},"delivered_at":{"type":"string","nullable":true}},"required":["id","subscription_id","event_type","resource_id","status","attempt_count","response_status","error_message","created_at","last_attempt_at","delivered_at"]}},"pagination":{"type":"object","properties":{"next_cursor":{"type":"string","nullable":true},"has_more":{"type":"boolean"}},"required":["next_cursor","has_more"]}},"required":["data","pagination"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/opportunities/{opportunityId}/hours":{"get":{"operationId":"getOpportunityHours","summary":"Get tracked hours for an opportunity","description":"Hours each expert worked on this opportunity, one row per person and date. Time spent on your other opportunities, and time clocked in but not spent on work, is not included. The current day is partial and refreshes through the day; `last_updated_at` says when the hours were last refreshed.","tags":["Hours"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"opportunityId","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.","schema":{"type":"string","description":"ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`."},"required":true},{"in":"query","name":"from","description":"Start of the window, inclusive: an ISO 8601 UTC timestamp such as `2026-09-08T00:00:00Z`, or a bare `2026-09-08` read as midnight UTC. A person's day counts when the instant it opened in their own tracker timezone falls in the window, so a day is never returned in halves.","schema":{"type":"string","description":"Start of the window, inclusive: an ISO 8601 UTC timestamp such as `2026-09-08T00:00:00Z`, or a bare `2026-09-08` read as midnight UTC. A person's day counts when the instant it opened in their own tracker timezone falls in the window, so a day is never returned in halves."},"required":true},{"in":"query","name":"to","description":"End of the window, exclusive, in the same UTC form: a day that opened exactly at `to` belongs to the next window, so consecutive requests neither double count nor skip. At most 92 days after `from`.","schema":{"type":"string","description":"End of the window, exclusive, in the same UTC form: a day that opened exactly at `to` belongs to the next window, so consecutive requests neither double count nor skip. At most 92 days after `from`."},"required":true}],"responses":{"200":{"description":"Tracked hours per person and date, with totals for the range.","content":{"application/json":{"schema":{"type":"object","properties":{"last_updated_at":{"type":"string","nullable":true,"description":"When these hours were last refreshed, regardless of the range asked for. Null when no hours have ever been recorded for this opportunity, which tells an empty `data` apart from a range nobody worked in."},"total_seconds":{"type":"integer","description":"Every row's `seconds` added together, for the range you asked for."},"total_hours":{"type":"number","description":"`total_seconds` converted once. Not the rows' `hours` added together, which drifts by minutes over a few thousand rows."},"data":{"type":"array","items":{"type":"object","properties":{"participant_id":{"type":"string","nullable":true,"description":"The Terac participant these hours belong to. Null for a worker we cannot yet match to an account; their hours still count toward the totals."},"submission_id":{"type":"string","nullable":true,"description":"Their submission on this opportunity. Null when they hold no active one."},"email":{"type":"string","nullable":true,"description":"The participant's email. Null when we hold none for them."},"date":{"type":"string","description":"`YYYY-MM-DD`, the day this work is credited to, in `timezone`."},"timezone":{"type":"string","description":"The IANA timezone `date` is expressed in for this person."},"seconds":{"type":"integer","description":"Seconds this participant worked on this opportunity on this date. The authoritative figure: derive your own totals from this rather than from `hours`."},"hours":{"type":"number","description":"`seconds` converted once and rounded to two decimals. Adding rounded hours across many rows drifts; add `seconds` and convert at the end."}},"required":["participant_id","submission_id","email","date","timezone","seconds","hours"]}}},"required":["last_updated_at","total_seconds","total_hours","data"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}},"/submissions/{submissionId}/hours":{"get":{"operationId":"getSubmissionHours","summary":"Get tracked hours for one submission","description":"Hours this submission's participant worked on its opportunity, one row per date. Time spent on your other opportunities, and time clocked in but not spent on work, is not included. The current day is partial and refreshes through the day; `last_updated_at` says when the hours were last refreshed. Returns exactly this submission's participant's rows from the opportunity response for the same range.","tags":["Hours"],"security":[{"apiKey":[]}],"parameters":[{"in":"path","name":"submissionId","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`.","schema":{"type":"string","description":"ID of the submission, as listed by `GET /opportunities/{opportunityId}/submissions`."},"required":true},{"in":"query","name":"from","description":"Start of the window, inclusive: an ISO 8601 UTC timestamp such as `2026-09-08T00:00:00Z`, or a bare `2026-09-08` read as midnight UTC. A person's day counts when the instant it opened in their own tracker timezone falls in the window, so a day is never returned in halves.","schema":{"type":"string","description":"Start of the window, inclusive: an ISO 8601 UTC timestamp such as `2026-09-08T00:00:00Z`, or a bare `2026-09-08` read as midnight UTC. A person's day counts when the instant it opened in their own tracker timezone falls in the window, so a day is never returned in halves."},"required":true},{"in":"query","name":"to","description":"End of the window, exclusive, in the same UTC form: a day that opened exactly at `to` belongs to the next window, so consecutive requests neither double count nor skip. At most 92 days after `from`.","schema":{"type":"string","description":"End of the window, exclusive, in the same UTC form: a day that opened exactly at `to` belongs to the next window, so consecutive requests neither double count nor skip. At most 92 days after `from`."},"required":true}],"responses":{"200":{"description":"This participant's tracked hours per date, with totals for the range.","content":{"application/json":{"schema":{"type":"object","properties":{"last_updated_at":{"type":"string","nullable":true,"description":"When these hours were last refreshed, regardless of the range asked for. Null when no hours have ever been recorded for this opportunity, which tells an empty `data` apart from a range nobody worked in."},"total_seconds":{"type":"integer","description":"Every row's `seconds` added together, for the range you asked for."},"total_hours":{"type":"number","description":"`total_seconds` converted once. Not the rows' `hours` added together, which drifts by minutes over a few thousand rows."},"data":{"type":"array","items":{"type":"object","properties":{"participant_id":{"type":"string","nullable":true,"description":"The Terac participant these hours belong to. Null for a worker we cannot yet match to an account; their hours still count toward the totals."},"submission_id":{"type":"string","nullable":true,"description":"Their submission on this opportunity. Null when they hold no active one."},"email":{"type":"string","nullable":true,"description":"The participant's email. Null when we hold none for them."},"date":{"type":"string","description":"`YYYY-MM-DD`, the day this work is credited to, in `timezone`."},"timezone":{"type":"string","description":"The IANA timezone `date` is expressed in for this person."},"seconds":{"type":"integer","description":"Seconds this participant worked on this opportunity on this date. The authoritative figure: derive your own totals from this rather than from `hours`."},"hours":{"type":"number","description":"`seconds` converted once and rounded to two decimals. Adding rounded hours across many rows drifts; add `seconds` and convert at the end."}},"required":["participant_id","submission_id","email","date","timezone","seconds","hours"]}}},"required":["last_updated_at","total_seconds","total_hours","data"]}}}},"400":{"description":"Invalid input data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.BAD_REQUEST"}}}},"401":{"$ref":"#/components/responses/ApiKeyUnauthorized"},"403":{"$ref":"#/components/responses/ApiKeyForbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.NOT_FOUND"}}}},"429":{"$ref":"#/components/responses/ApiKeyRateLimited"},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/error.INTERNAL_SERVER_ERROR"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"Authorization","description":"API key as Bearer token: Authorization: Bearer <key>"}},"schemas":{"error.BAD_REQUEST":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Invalid input data"},"code":{"type":"string","description":"The error code","example":"BAD_REQUEST"},"issues":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"description":"An array of issues that were responsible for the error","example":[]}},"required":["message","code"],"title":"Invalid input data error (400)","description":"The error information","example":{"code":"BAD_REQUEST","message":"Invalid input data","issues":[]}},"error.FORBIDDEN":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Insufficient access"},"code":{"type":"string","description":"The error code","example":"FORBIDDEN"},"issues":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"description":"An array of issues that were responsible for the error","example":[]}},"required":["message","code"],"title":"Insufficient access error (403)","description":"The error information","example":{"code":"FORBIDDEN","message":"Insufficient access","issues":[]}},"error.NOT_FOUND":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Not found"},"code":{"type":"string","description":"The error code","example":"NOT_FOUND"},"issues":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"description":"An array of issues that were responsible for the error","example":[]}},"required":["message","code"],"title":"Not found error (404)","description":"The error information","example":{"code":"NOT_FOUND","message":"Not found","issues":[]}},"error.INTERNAL_SERVER_ERROR":{"type":"object","properties":{"message":{"type":"string","description":"The error message","example":"Internal server error"},"code":{"type":"string","description":"The error code","example":"INTERNAL_SERVER_ERROR"},"issues":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"description":"An array of issues that were responsible for the error","example":[]}},"required":["message","code"],"title":"Internal server error error (500)","description":"The error information","example":{"code":"INTERNAL_SERVER_ERROR","message":"Internal server error","issues":[]}},"ApiKeyError.UNAUTHORIZED":{"type":"object","title":"API key error (401)","description":"Returned by the API key check that runs before every operation. The fields are nested under `error`, unlike operation errors.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["UNAUTHORIZED"],"description":"The error code."},"message":{"type":"string","description":"What went wrong.","example":"Invalid or expired API key"}}}},"example":{"error":{"code":"UNAUTHORIZED","message":"Invalid or expired API key"}}},"ApiKeyError.FORBIDDEN":{"type":"object","title":"API key error (403)","description":"Returned by the API key check that runs before every operation. The fields are nested under `error`, unlike operation errors.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["FORBIDDEN"],"description":"The error code."},"message":{"type":"string","description":"What went wrong.","example":"This account is not permitted to use the API."}}}},"example":{"error":{"code":"FORBIDDEN","message":"This account is not permitted to use the API."}}},"ApiKeyError.RATE_LIMITED":{"type":"object","title":"API key error (429)","description":"Returned by the API key check that runs before every operation. The fields are nested under `error`, unlike operation errors.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["RATE_LIMITED"],"description":"The error code."},"message":{"type":"string","description":"What went wrong.","example":"Rate limit exceeded. Retry after 61 seconds."}}}},"example":{"error":{"code":"RATE_LIMITED","message":"Rate limit exceeded. Retry after 61 seconds."}}}},"responses":{"ApiKeyUnauthorized":{"description":"The API key is missing, invalid, disabled, expired or revoked, its owner no longer exists, or it is not linked to an organization. The body is nested under `error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyError.UNAUTHORIZED"},"examples":{"missing_key":{"summary":"No Authorization: Bearer header","value":{"error":{"code":"UNAUTHORIZED","message":"API key required. Include Authorization: Bearer <key> header."}}},"invalid_key":{"summary":"Unknown, disabled, expired or revoked key","value":{"error":{"code":"UNAUTHORIZED","message":"Invalid or expired API key"}}},"key_without_organization":{"summary":"Key not linked to an organization","value":{"error":{"code":"UNAUTHORIZED","message":"API key is not scoped to an organization. Please create a new key."}}}}}}},"ApiKeyForbidden":{"description":"Either the account that owns the API key is banned or deleted (from the key check, nested under `error`), or the key's owner lacks the organization permission this operation needs (from the operation, with `code` and `message` at the top level).","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyError.FORBIDDEN"},{"$ref":"#/components/schemas/error.FORBIDDEN"}]},"examples":{"account_not_permitted":{"summary":"Key owner banned or deleted (key check)","value":{"error":{"code":"FORBIDDEN","message":"This account is not permitted to use the API."}}},"missing_scope":{"summary":"Key owner lacks a permission (operation)","value":{"code":"FORBIDDEN","message":"Insufficient permissions: missing required scope(s): viewOrganization"}}}}}},"ApiKeyRateLimited":{"description":"The API key is rate limited. A key accepts 100 requests, then refuses every request until more than 60 seconds pass with no accepted request; refused requests do not extend the wait. Wait the number of seconds in `Retry-After`, then retry. The body is nested under `error`.","headers":{"Retry-After":{"description":"Seconds to wait before the key accepts requests again, counted from its last accepted request.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyError.RATE_LIMITED"}}}}}}}