List opportunities
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}.
Authorization
apiKey API key as Bearer token: Authorization: Bearer
In: header
Query Parameters
Most items to return in one page, from 1 to 100. Defaults to 25.
251 <= value <= 100pagination.next_cursor from the previous page, to fetch the page after it. Omit it for the first page.
Return only opportunities in this status: draft, active, fulfilled, paused, stopped or completed. draft includes opportunities Terac is still preparing.
Return only opportunities filed under this project ID.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://terac.com/api/external/v2/opportunities"{
"data": [
{
"id": "string",
"title": "string",
"status": "draft",
"num_participants": 0,
"pricing": {
"cost_per_participant_cents": 0,
"total_cost_cents": 0,
"currency": "usd"
},
"created_at": "string",
"dashboard_url": "string"
}
],
"pagination": {
"next_cursor": "string",
"has_more": true
}
}{
"code": "BAD_REQUEST",
"message": "Invalid input data",
"issues": []
}{
"error": {
"code": "UNAUTHORIZED",
"message": "API key required. Include Authorization: Bearer <key> header."
}
}{
"error": {
"code": "FORBIDDEN",
"message": "This account is not permitted to use the API."
}
}{
"code": "NOT_FOUND",
"message": "Not found",
"issues": []
}{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 61 seconds."
}
}{
"code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"issues": []
}Create a draft opportunity POST
Creates the opportunity as a DRAFT. Nothing is charged and no recruitment starts until you call `POST /opportunities/{opportunityId}/launch`. **The incentive is derived, not sent.** There is no field for participant pay, and the amount is fixed when the draft is created. Two paths decide which number you get: - **Omit `feasibility_request_id`** and the incentive is an automatic estimate made during this call. It is priced from the whole brief, not just its size: the participant count, the task duration, the audience your `filters` and `screening_questions` describe, and the recruitment window from `expected_days_to_complete`. Changing any of those changes the price, so read the result back from `pricing` on the response; do not quote a price to anyone before you have. - **Pass a `feasibility_request_id`** and that request's confirmed CPI is honored exactly, with no re-estimate. Submit the brief to `POST /feasibility/requests` first and poll `GET /feasibility/requests/{requestId}` until it reads `RESPONDED`, which is when a price exists. This is the way to control what participants are paid. The platform fee follows the same split. Priced by feasibility, the confirmed recruitment fee becomes this opportunity's fee, as a flat per-participant amount, so the all-in CPI you agreed is the one you are charged. Priced automatically, it comes from the organization's configuration instead. Either way it is not a per-opportunity input, and neither is the currency (always USD) or the pay cadence (always `one_time`). `pricing` on the response carries the all-in cost per participant and the total, and `funding` says whether the balance covers a launch. Editing after this call is draft-only: `PATCH /opportunities/{opportunityId}` returns 409 once the opportunity is launched, and a new recruitment window moves the deadline without re-pricing.
Get opportunity details GET
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.