# Stop opportunity (https://terac.com/docs/developers/reference/stopOpportunity)

`POST https://terac.com/api/external/v2/opportunities/{opportunityId}/stop`

Permanently ends an opportunity and refunds its unused budget to your organization's balance.

Pending invitations are withdrawn and recruitment cannot be restarted, so pause instead if you
may want to continue. Submissions still awaiting your review block the stop: approve or reject
them first. On an hourly or per-task opportunity, the stop itself approves every expert already
paid on it whose submission is still open. Only an `active` or `paused` opportunity can be
stopped; a `fulfilled`, `completed` or `stopped` one returns 409. Delete a draft instead.

## Authorization

Requires a Bearer token via the `Authorization` header.

```
Authorization: Bearer <token>
```

## Path Parameters

- `opportunityId` (string) **(required)**: ID of the opportunity, as returned by `POST /opportunities` or `GET /opportunities`.

## Request Body (required)

`Content-Type: application/json`

- `reason` (string): Why you are stopping it, up to 5,000 characters. Recorded with the stop, the refund and each withdrawn invitation.

## Responses

### 200: The opportunity, now `stopped`.

- `id` (string) **(required)**
- `title` (string) **(required)**
- `internal_title` (string, nullable) **(required)**
- `description` (string, nullable) **(required)**
- `status` (string) **(required)**: one of: `draft`, `active`, `fulfilled`, `paused`, `stopped`, `completed`
- `num_participants` (integer) **(required)**
- `estimated_duration_minutes` (integer) **(required)**
- `filters` (array) **(required)**
- `screening_questions` (array) **(required)**
  - `key` (string) **(required)**: Identifier for this question, yours or assigned. Comes back as `screening_answers[].key`, so answers join to their question. Opaque.
  - `text` (string) **(required)**
  - `question_rich_text` (string): Markdown version of the prompt, when the author wrote one. `text` stays the plaintext source of truth.
  - `pick` (string) **(required)**: one of: `one`, `any`, `boolean`, `text`, `grid`
  - `answers` (array)
    - `text` (string) **(required)**
    - `qualify_logic` (string) **(required)**: one of: `may`, `must`, `must_one_of`, `reject`, `review`: 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` (boolean): Selecting this answer reveals a write-in box ("Other, please specify").
  - `grid` (object)
    - `pick` (string): one of: `one`, `any`
    - `rows` (array) **(required)**
      - `key` (string)
      - `text` (string) **(required)**
    - `columns` (array) **(required)**
      - `value` (string)
      - `text` (string) **(required)**
    - `cell_actions` (array)
      - `row` (string) **(required)**
      - `column` (string) **(required)**
      - `qualify_logic` (string) **(required)**: one of: `may`, `must`, `must_one_of`, `reject`, `review`
  - `min_qualifying` (integer)
  - `display_condition` (object)
    - `conditions` (array) **(required)**
      - `screening_question` (string) **(required)**: The key of an EARLIER question this one depends on
      - `answer` (string) **(required)**: An answer of that earlier question. For a grid source use "Row: Column".
      - `operator` (string) **(required)**: one of: `eq`, `ne`: "eq" = answered that value, "ne" = did not.
    - `join` (string) **(required)**: one of: `and`, `or`
  - `conditional_rules` (array)
    - `conditions` (array) **(required)**
      - `screening_question` (string)
      - `answer` (string) **(required)**
      - `operator` (string) **(required)**: one of: `eq`, `ne`
    - `join` (string) **(required)**: one of: `and`, `or`
    - `outcome` (string) **(required)**: one of: `reject`, `review`
  - `skip_rules` (array): 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.
    - `conditions` (array) **(required)**
      - `screening_question` (string) **(required)**: 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` (string) **(required)**: One of that question's answers (its text; "Yes"/"No" for pick: "boolean").
      - `operator` (string) **(required)**: one of: `eq`, `ne`: "eq" = answered that value, "ne" = did not.
    - `join` (string) **(required)**: one of: `and`, `or`
    - `target_question` (string, nullable) **(required)**: 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.
  - `allow_paste` (boolean)
- `quotas` (array) **(required)**
  - `screening_question` (string) **(required)**: The key of the screening question this quota applies to (must match a screening_questions[].key)
  - `targets` (array) **(required)**: Per-answer target counts for this question
    - `value` (string) **(required)**: The answer option text (matches a screening answer's text) this quota targets
    - `count` (integer) **(required)**: How many approved submissions to allow/require for this answer
    - `type` (string): one of: `exact`, `minimum`, `maximum`: 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".
- `cross_quotas` (array) **(required)**
  - `label` (string) **(required)**: Human label for this cell, e.g. "Brazil x Agency".
  - `conditions` (array) **(required)**: The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions.
    - `screening_question` (string) **(required)**: Key of a screening_questions[].key this condition tests (one dimension of the cross-tab).
    - `answer` (string) **(required)**: An answer of that question, by its label. For a grid source use "Row: Column".
    - `operator` (string) **(required)**: one of: `eq`, `ne`: "eq" = selected that answer, "ne" = did not.
  - `join` (string) **(required)**: one of: `and`, `or`
  - `target` (integer) **(required)**: Target participant count for this cell.
  - `quota_type` (string) **(required)**: one of: `exact`, `minimum`, `maximum`: "exact" = hit exactly, "minimum" = at least, "maximum" = no more than.
  - `dimension` (string): Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab.
- `tasks` (array) **(required)**
  - `sequence` (integer) **(required)**
  - `task_type` (string) **(required)**: 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` (string) **(required)**
  - `task_url` (string, nullable) **(required)**: null when the task type carries no URL.
  - `participant_url_template` (string): 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` (string)
  - `description` (string)
  - `duration_minutes` (integer) **(required)**
  - `available_after_sequence` (integer): This task unlocks only after that task is approved.
  - `available_after_delay_minutes` (integer): Wait this long after the gating task before unlocking.
  - `provider` (string): Scheduled interview only: the booking provider.
  - `calendar_owner` (string): Scheduled interview only: whose calendar is booked (researcher or expert).
  - `event_type_url` (string): Scheduled interview only: the booking link.
  - `instructions` (string): File upload only: what to submit.
  - `accepted_mime_types` (array): "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` (integer): "file_upload" only, and absent on every other task type. Resolved to the enforced default when the task did not set one.
  - `max_files` (integer): "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` (integer): "file_upload" only, and absent on every other task type. Resolved to the enforced default when the task did not set one.
- `pricing` (object, nullable) **(required)**
  - `cost_per_participant_cents` (integer) **(required)**
  - `total_cost_cents` (integer) **(required)**
  - `currency` (string) **(required)**: one of: `usd`
- `funding` (object)
  - `affordable` (boolean) **(required)**: Covers the cost, overdraft included. False blocks nothing.
  - `draws_on_credit` (boolean) **(required)**: Affordable but goes negative, invoiced later. Say so.
  - `balance_cents` (integer) **(required)**: Balance now.
  - `shortfall_cents` (integer) **(required)**: Gap after overdraft, 0 when affordable.
  - `finance_url` (string, nullable) **(required)**: Where billing adds credit.
- `device_types` (array) **(required)**
- `business_type` (string, nullable) **(required)**
- `candidate_profile` (string, nullable) **(required)**: Free-text profile of the ideal candidate, when set.
- `required_accounts` (array) **(required)**: Accounts an expert must have linked to apply (e.g. linkedin).
- `video_required` (string) **(required)**: Whether the expert's camera is needed: none, requested, required.
- `screen_share_required` (string) **(required)**: Whether a screen share is needed: none, requested, required.
- `customer_screening_review` (string) **(required)**: auto_invite = qualified applicants are invited automatically; manual_review = you pick who to invite.
- `pay_frequency` (string) **(required)**: How the incentive is framed: one_time, hourly, or per_task.
- `screening_option_randomization` (string) **(required)**: Whether screening answer options are shown in random order: the string "enabled" or "disabled", not a boolean.
- `forwarding_email_enabled` (string) **(required)**: Whether each submission gets a Terac forwarding email address: the string "enabled" or "disabled", not a boolean.
- `max_responses_per_expert` (integer, nullable) **(required)**
- `feasibility_request_id` (string, nullable) **(required)**
- `project_id` (string) **(required)**
- `created_at` (string) **(required)**
- `updated_at` (string) **(required)**
- `ending_at` (string, nullable) **(required)**: Target completion date for recruitment.
- `launched_at` (string, nullable) **(required)**
- `links` (object) **(required)**
  - `self` (string) **(required)**
  - `launch` (string, nullable) **(required)**
  - `submissions` (string) **(required)**
  - `dashboard` (object) **(required)**: 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.
    - `study` (string, nullable) **(required)**: 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` (string, nullable) **(required)**: 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` (string, nullable) **(required)**: 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` (string, nullable) **(required)**: 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` (string, nullable) **(required)**: 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` (string, nullable) **(required)**: 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.
- `submission_stats` (object)
  - `total` (integer) **(required)**
  - `in_progress` (integer) **(required)**
  - `awaiting_review` (integer) **(required)**
  - `approved` (integer) **(required)**
  - `rejected` (integer) **(required)**
- `quota_progress` (array)
  - `screening_question` (string) **(required)**
  - `targets` (array) **(required)**
    - `value` (string) **(required)**
    - `count` (integer) **(required)**
    - `current` (integer) **(required)**
    - `type` (string) **(required)**: 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.
- `cross_quota_progress` (array): Fill progress for each cross-question quota cell (live studies).
  - `label` (string) **(required)**: Human label for this cell, e.g. "Brazil x Agency".
  - `conditions` (array) **(required)**: The conditions, one per dimension: one for a simple per-answer quota, two+ to interlock a cross-tab across distinct questions.
    - `screening_question` (string) **(required)**: Key of a screening_questions[].key this condition tests (one dimension of the cross-tab).
    - `answer` (string) **(required)**: An answer of that question, by its label. For a grid source use "Row: Column".
    - `operator` (string) **(required)**: one of: `eq`, `ne`: "eq" = selected that answer, "ne" = did not.
  - `join` (string) **(required)**: one of: `and`, `or`
  - `target` (integer) **(required)**: Target participant count for this cell.
  - `quota_type` (string) **(required)**: one of: `exact`, `minimum`, `maximum`: "exact" = hit exactly, "minimum" = at least, "maximum" = no more than.
  - `dimension` (string): Cells sharing a dimension are summed as one interlocked cross-tab quota; use one token per cross-tab.
  - `current` (integer) **(required)**: Approved participants matching this cell so far.
- `screening_stats` (array): Per-question screen-out rates and answer breakdown (live studies).
  - `screening_question` (string) **(required)**: The `screening_questions[].key`.
  - `graded` (integer) **(required)**: Screeners GRADED here, not answers given: a branching screener stops at the first failure.
  - `rejected` (integer) **(required)**: Rejected by this question.
  - `rejection_rate` (number) **(required)**: `rejected` / `graded`, 0 to 1.
  - `answers` (array) **(required)**: How often each answer was chosen, for spotting a screener that is too narrow.
    - `text` (string) **(required)**: The answer as written on the question, not its stored value.
    - `selected` (integer) **(required)**
    - `share` (number) **(required)**: `selected` / `graded`, between 0 and 1.
### 400: Invalid input data

- `message` (string) **(required)**: The error message
- `code` (string) **(required)**: The error code
- `issues` (array): An array of issues that were responsible for the error
  - `message` (string) **(required)**
### 401: 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`.

- `error` (object) **(required)**
  - `code` (string) **(required)**: one of: `UNAUTHORIZED`: The error code.
  - `message` (string) **(required)**: What went wrong.
### 403: 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).

- One of: API key error (403)
  - `error` (object) **(required)**
    - `code` (string) **(required)**: one of: `FORBIDDEN`: The error code.
    - `message` (string) **(required)**: What went wrong.
- One of: Insufficient access error (403)
  - `message` (string) **(required)**: The error message
  - `code` (string) **(required)**: The error code
  - `issues` (array): An array of issues that were responsible for the error
    - `message` (string) **(required)**
### 404: Not found

- `message` (string) **(required)**: The error message
- `code` (string) **(required)**: The error code
- `issues` (array): An array of issues that were responsible for the error
  - `message` (string) **(required)**
### 429: 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`.

- `error` (object) **(required)**
  - `code` (string) **(required)**: one of: `RATE_LIMITED`: The error code.
  - `message` (string) **(required)**: What went wrong.
### 500: Internal server error

- `message` (string) **(required)**: The error message
- `code` (string) **(required)**: The error code
- `issues` (array): An array of issues that were responsible for the error
  - `message` (string) **(required)**