Crowd Jobs
Creating and managing crowdsourcing jobs — define tasks, publish to workers, and control the job lifecycle.
A crowd job is a self-contained crowdsourcing campaign scoped to a project. It ties a published survey template version to an optional input data set, then makes structured tasks available to Crowdee's worker pool. Workers claim individual task slots, complete the structured questionnaire, and submit answers. You review each submission and accept or reject it — accepted answers release the reserved credits to the worker.
Crowd jobs are independent of verification pipelines. While crowd stages in pipelines are triggered automatically by the pipeline runner, crowd jobs give you direct control: you define the task, set the availability window and reward, and manage the lifecycle through the API or the platform UI.
Job Fields
When creating or updating a crowd job, the following fields are accepted.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Internal label visible only to organisation members. |
title | string | — | Headline displayed to workers when they browse available jobs. |
description | string | — | Full job description shown to workers before they accept a task. |
category | "survey" | "qualification_test" | "gathering" | "survey" | Survey jobs collect data; qualification tests screen workers before they access higher-value campaigns; gathering jobs ask workers to find and submit new content — see Content Gathering. |
surveyTemplateVersionId | CUID | — | Required. Links a specific published survey template version. The version is frozen at creation time — updating the underlying template does not affect running jobs. |
inputDataSetId | CUID | null | Attaches an input data set. Each record in the set becomes a separate task variant, so workers see different content while answering the same questionnaire. When omitted, all workers answer a single identical task. |
maxAssignments | integer | 1 | Reserved field for capping total task slot assignments across all variants. |
maxRepetitionsPerVariant | integer | 1 | How many independent workers can complete each variant. Combined with the variant count this determines maxSlots — the total number of task completions the job accepts. Frozen after creation — see Extending a Job to raise it later. |
rewardCredits | integer | 10 | Credits paid to a worker per accepted answer. Credits are blocked from your organisation balance at job creation and released to the worker when you accept their answer. Must be at least maxWorkTimeMinutes × 20 — see Maximum Work Time. |
maxWorkTimeMinutes | integer | null | How long, in minutes, a worker has to complete a task once claimed — from 1 to 1440 (24 hours). Required for every category, including qualification_test jobs. See Maximum Work Time. |
validFrom | ISO 8601 timestamp | now | When the job becomes visible and assignable to workers. Defaults to the creation timestamp. |
validUntil | ISO 8601 timestamp | now + 30 days | When the job stops accepting new assignments. After this point the computed status transitions to not_assignable. |
qualificationRequirements | JSONB | null | One or more qualification-test jobs a worker must pass before this job becomes available to them. Enforced by the matching engine — see Qualification Tests. |
forceUniqueTasks | boolean | true | When true, the same worker cannot be assigned the same input data variant twice. Recommended for most jobs to avoid contaminating results with repeat exposures. |
isDryRun | boolean | false | Test mode. No credits are blocked, and access is restricted to workers in dryRunAllowedEmails or those holding the magic token generated at creation. |
dryRunAllowedEmails | string[] | [] | Email allowlist for dry-run jobs. Workers whose registered email appears here can access the job without the magic token. |
verificationCode | string | null | Optional password workers must enter before claiming a task. Useful for invite-only or gated campaigns. |
creditsBlocked is a read-only field returned by the API. It records the credits currently reserved against your organisation balance and is managed automatically — do not include it in create or update requests.
Job Status Lifecycle
A job moves through the following statuses as conditions change. The computedStatus field returned by GET /v2/crowd-jobs/:jobId reflects the live state recalculated on every fetch; the stored status field is updated lazily to match.
assignable — The job is live. validFrom has passed, validUntil has not yet been reached, and available task slots remain. Workers can discover and claim tasks.
not_assignable — The job is paused. Either validUntil has passed or you have manually set the status to not_assignable via a PUT request. No new assignments are accepted, but in-progress tasks can still be submitted.
no_tasks — All task slots are occupied by workers who have claimed but not yet submitted, or by answers awaiting your review. The slot ceiling has been reached. The job returns to assignable if any slots are freed — for example, when you reject an answer the variant slot is returned to the pool.
finished — The total number of accepted (and pending-review) answers has reached maxSlots. The job is complete and no further assignments are accepted. You can still review and act on any pending answers.
archived — The job has been permanently closed via a DELETE request. All outstanding assigned tasks are cancelled, remaining blocked credits are returned to your organisation balance, and the job cannot be reactivated. Archived jobs are excluded from list responses unless you pass includeArchived=true.
maxSlots is derived at runtime as variantCount × maxRepetitionsPerVariant, where variantCount is the number of records in the attached input data set (or 1 when no input data set is attached). It is returned alongside computedStatus in the GET /v2/crowd-jobs/:jobId response.
Creating a Job
Jobs are created under a project. The minimum required fields are name, title, description, and surveyTemplateVersionId.
curl -X POST https://api.crowdee.ai/v2/projects/{projectId}/crowd-jobs \
-H "X-API-Key: crw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 Image Authenticity Review",
"title": "Assess whether this image has been digitally manipulated",
"description": "You will be shown an image and asked to provide your expert assessment. Please review all visible evidence carefully before submitting.",
"category": "survey",
"surveyTemplateVersionId": "tmplv_abc123def456ghi789jkl01",
"inputDataSetId": "idata_xyz987stu654rqp321onm09",
"maxRepetitionsPerVariant": 3,
"rewardCredits": 25,
"validFrom": "2026-07-01T08:00:00Z",
"validUntil": "2026-07-31T18:00:00Z",
"forceUniqueTasks": true
}'A 201 Created response returns the full job object including the system-assigned id, the computed creditsBlocked, and a dryRunMagicToken if isDryRun was true.
The organisation must have sufficient balance to cover creditsBlocked at the time of job creation. If the balance is insufficient the API returns 403 Forbidden and the job is not created. Top up credits via the billing section of the platform before publishing large campaigns.
Dry Runs
A dry run lets you test the full worker experience — task rendering, survey display, answer submission — without blocking credits or exposing the job to the full worker pool.
Set isDryRun: true when creating the job. The platform will:
- Block zero credits from your organisation balance.
- Generate a unique
dryRunMagicToken(a short random string returned in the job response). - Restrict task access to workers whose registered email is in
dryRunAllowedEmails, or to anyone who accesses the job via the magic token link in the worker UI.
A magic token link takes the form https://workers.crowdee.ai/jobs/{jobId}?token={dryRunMagicToken}. Share it directly with internal testers who need to preview the worker experience without a Crowdee account.
{
"name": "Q3 Image Authenticity Review — Test",
"title": "Assess whether this image has been digitally manipulated",
"description": "Internal preview only. Please complete as if this were a real task.",
"category": "survey",
"surveyTemplateVersionId": "tmplv_abc123def456ghi789jkl01",
"isDryRun": true,
"dryRunAllowedEmails": ["tester@yourorg.com", "qa@yourorg.com"]
}Dry-run answers follow the same accept/reject flow as live answers, but no credits change hands at any point — not even on acceptance. Use dry runs to validate template rendering, input data mapping, and your review workflow before committing credits to a live campaign. Once you are satisfied, clone the dry-run job without isDryRun to publish the live version.
Qualification Tests
A qualification test is a job created with category: "qualification_test". It uses the same survey template and worker-facing task flow as any other job, but with two differences enforced automatically: it pays no reward (rewardCredits: 0), and each worker gets exactly one attempt. It still requires maxWorkTimeMinutes like every other job category — since there's no reward, it's exempt from the reward-per-minute floor, but the value still drives the worker-app timer and how long a claimed-but-abandoned attempt is held before its slot times out and returns to the pool.
Gate access to a real job by setting qualificationRequirements on it:
{
"qualificationRequirements": {
"requiredQualificationJobs": [
{
"qualificationJobId": "job_qual123abc456def789ghi",
"checks": [
{ "questionName": "language_check", "operator": "EQUALS", "value": "fluent" },
{ "questionName": "score", "operator": "GREATER_THAN", "value": 7 }
]
}
]
}
}- Each entry in
requiredQualificationJobsmust reference an existingqualification_testjob in the same project. - A worker passes a required test when all of its
checksevaluate totrueagainst their submitted answers. Supported operators:EQUALS,GREATER_THAN,LESS_THAN,EXISTS,NOT_EXISTS. - If a job requires more than one qualification test, a worker must pass all of them before the job becomes available.
- A
qualification_testjob cannot itself carryqualificationRequirements— qualification chaining is not supported.
Because qualification tests allow only one attempt, a worker who does not pass is permanently ineligible for every job that requires it — there is no retry. Conversely, passing is a one-time investment: the worker stays qualified for that test, and every job that requires it, indefinitely. Workers who have not yet attempted a required test see a prompt to start it in place of the job's normal task flow.
Credits and Billing
When a non-dry-run job is created, Crowdee immediately reserves credits from your organisation balance. The amount blocked is:
creditsBlocked = variantCount × maxRepetitionsPerVariant × rewardCreditsWhere variantCount is the number of records in the attached input data set, or 1 if no input data set is used.
Credits move through the following states across the job lifecycle:
| State | Event |
|---|---|
| Blocked | Reserved on your organisation balance when the job is created. Not yet paid to anyone. |
| Earned (pending) | When a worker submits an answer, rewardCredits are moved into the worker's pending balance. The credits remain held until you review the answer. |
| Released | When you accept an answer, the credits move from the worker's pending balance into their available balance. The job's creditsBlocked decreases by rewardCredits. |
| Returned to job | When you reject an answer, the credits are returned to the job's blocked pool and the variant slot is reopened so another worker can attempt it. |
| Refunded to org | When you archive a job (DELETE), all remaining creditsBlocked are returned to your organisation balance in full. |
If you update a job in a way that increases the required reservation — for example by raising rewardCredits or maxRepetitionsPerVariant — the additional credits are deducted from your balance immediately. Decreases release the surplus back to your balance immediately.
Your organisation must have sufficient balance to cover creditsBlocked at the time of job creation. If the balance is insufficient, the API returns 403 Forbidden and the job is not created. Top up your balance via the billing section of the platform before retrying.
Maximum Work Time
maxWorkTimeMinutes sets how long a worker has to complete a task once they claim it — any value from 1 to 1440 (24 hours). It's required for every job category, including qualification_test.
To keep pay proportional to time, the platform enforces a minimum reward rate on every category except qualification_test (which is always unpaid and so exempt):
rewardCredits ≥ maxWorkTimeMinutes × 20A 10-minute task, for example, must offer at least 200 rewardCredits. Creating or updating a job with a rewardCredits value below this floor for the given maxWorkTimeMinutes returns 400 Bad Request.
In the worker app, a live countdown starts the moment a task is claimed. As the deadline approaches, workers see a short grace-period warning; if the grace period also elapses without a submission, the task's status is flipped to timed_out by a recurring backend sweep (running every minute) and its slot returns to the pool for another worker to claim.
Cloning a Job
Cloning creates a new job that inherits all configuration from the source — template version, input data set, reward settings, availability window, and access controls — while resetting task and answer history. The clone starts with status: assignable.
curl -X POST https://api.crowdee.ai/v2/crowd-jobs/{jobId}/clone \
-H "X-API-Key: crw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"isDryRun": false,
"name": "Q4 Image Authenticity Review"
}'Both isDryRun and name are optional. When isDryRun is omitted it defaults to false. When name is omitted the clone is named {original name} (copy).
Common use cases for cloning:
- Test before launch — Clone an existing production job with
isDryRun: trueto verify template or input data changes before committing credits. - Repeat a campaign — Clone a
finishedjob to rerun it against the same template and input data set, optionally adjusting the availability window or reward. - Staging mirrors — Maintain a dry-run copy of each production job to onboard new internal reviewers without touching live data.
Cloning a job as a non-dry-run immediately blocks credits for the new job. Ensure your organisation balance is sufficient before calling the clone endpoint without isDryRun: true.
Extending a Job
maxRepetitionsPerVariant (or "target items" for gathering jobs) is frozen after creation — see the note in Managing Jobs. To accept more completions than the job originally allowed — for example after a finished job proves the template works well and you want more responses — extend it instead:
curl -X POST https://api.crowdee.ai/v2/crowd-jobs/{jobId}/extend \
-H "X-API-Key: crw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"additionalRepetitions": 5
}'additionalRepetitions is always an increase — there is no way to lower maxRepetitionsPerVariant through this endpoint. For a non-dry-run job with a non-zero rewardCredits, the additional credits (additionalRepetitions × variantCount × rewardCredits, plus its share of the platform fee) are blocked from your organisation balance immediately, the same way they are at job creation. Dry-run jobs and jobs with rewardCredits: 0 never block credits, so extending them is free.
You can preview the cost of an extension before committing to it:
curl "https://api.crowdee.ai/v2/crowd-jobs/{jobId}/extend/preview?additionalRepetitions=5" \
-H "X-API-Key: crw_YOUR_API_KEY"The preview response includes additionalCost, platformFeeAmount, totalCost, your current organizationBalance, and a sufficientBalance flag — check sufficientBalance before calling the extend endpoint to avoid a 403 Forbidden.
If your organisation balance is insufficient to cover the additional credits, POST .../extend returns 403 Forbidden and maxRepetitionsPerVariant is left unchanged.
Managing Jobs
List jobs for a project — returns all non-archived jobs by default; pass includeArchived=true to include archived ones, or status to filter by lifecycle state:
GET https://api.crowdee.ai/v2/projects/{projectId}/crowd-jobs?status=assignable
X-API-Key: crw_YOUR_API_KEYGet a single job — includes the live-computed computedStatus and the derived maxSlots:
GET https://api.crowdee.ai/v2/crowd-jobs/{jobId}
X-API-Key: crw_YOUR_API_KEYcomputedStatus reflects the actual current state — for example not_assignable if validUntil has already passed even though the stored status field still says assignable. maxSlots is variantCount × maxRepetitionsPerVariant.
Update a job — most fields can be changed while the job is not archived, for example validUntil, maxWorkTimeMinutes, or qualificationRequirements:
curl -X PUT https://api.crowdee.ai/v2/crowd-jobs/{jobId} \
-H "X-API-Key: crw_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"validUntil": "2026-08-15T18:00:00Z"
}'rewardCredits, maxRepetitionsPerVariant, isDryRun, and inputDataSetId are frozen once a job is created — a PUT request that tries to change any of them is rejected with 400 Bad Request. Already-blocked credits and already-accepted answers were committed against these values, so letting them drift after the fact would silently change the job's committed cost. To accept more completions than the job originally allowed, use the dedicated extend endpoint below instead of trying to raise maxRepetitionsPerVariant via PUT.
Archive a job — equivalent to a soft delete. All currently assigned tasks are cancelled, remaining blocked credits are returned to your organisation balance, and the job transitions to archived. This action is irreversible:
curl -X DELETE https://api.crowdee.ai/v2/crowd-jobs/{jobId} \
-H "X-API-Key: crw_YOUR_API_KEY"Archiving a job cancels all in-progress tasks immediately. Workers who were mid-task will lose their work. If you want to stop new assignments without disrupting workers already in progress, set status to not_assignable via a PUT request first, then archive only after all in-flight tasks have been submitted or expired.
Job Statistics
The stats endpoint gives you a real-time breakdown of all task and answer activity for a job.
GET https://api.crowdee.ai/v2/crowd-jobs/{jobId}/stats
X-API-Key: crw_YOUR_API_KEYThe response contains aggregate counts and per-worker detail arrays:
| Field | Description |
|---|---|
maxSlots | Total task completions this job will accept (variantCount × maxRepetitionsPerVariant). |
currentlyWorking | Workers with an assigned task that has not yet been submitted or expired. |
submitted | Total answers submitted across all workers, regardless of review status. |
accepted | Answers you have accepted; credits have been released to those workers. |
rejected | Answers you have rejected; those variant slots have been returned to the pool. |
pending | Submitted answers not yet reviewed. |
timedOut | Tasks that were assigned but expired before the worker submitted. |
cancelled | Tasks cancelled by the worker or by a job archive operation. |
returned | Task slots reopened after answer rejection — available for reassignment. |
The workers object contains four arrays, each identifying individual workers by workerId (authenticated workers) or guestWorkerId (guest / magic-token workers):
| Array | Contents |
|---|---|
workers.started | Currently assigned workers: workerId, guestWorkerId, taskId, assignedAt. |
workers.submitted | All workers who submitted: workerId, guestWorkerId, answerId, createdAt. |
workers.cancelled | Workers whose tasks were cancelled: workerId, guestWorkerId, taskId, updatedAt. |
workers.timedOut | Workers whose tasks expired: workerId, guestWorkerId, taskId, expiresAt. |
An example response for a job with 60 total slots partway through a campaign:
{
"maxSlots": 60,
"currentlyWorking": 4,
"submitted": 38,
"accepted": 31,
"rejected": 3,
"pending": 4,
"timedOut": 2,
"cancelled": 1,
"returned": 0,
"workers": {
"started": [
{ "workerId": "usr_abc", "guestWorkerId": null, "taskId": "tsk_001", "assignedAt": "2026-07-14T09:12:00Z" }
],
"submitted": [...],
"cancelled": [...],
"timedOut": [...]
}
}How is this guide?