Jobs
Jobs represent content collection requests posted by buyers. Workers browse and accept jobs, then upload files.
Create Job
POST /v1/jobsPost a new content collection job.
Headers:
Authorization: Bearer fh_live_...(required)Idempotency-Key: <unique-key>(required)Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | One of: data_collection, feedback |
description | string | yes | Detailed instructions for workers |
files_needed | integer | yes | Number of files requested |
accepted_formats | string[] | yes | Accepted MIME types (e.g., ["image/jpeg", "image/png"]) |
price_per_file_cents | integer | yes | Price per approved file in cents |
location | object | no | Geographic constraint: { lat, lng, radius_km } |
deadline | string | no | ISO 8601 deadline for job completion |
max_workers | integer | no | Max concurrent workers (default: unlimited) |
metadata | object | no | Arbitrary key-value pairs (max 20 keys) |
Idempotency
All POST requests require an Idempotency-Key header to prevent duplicate job creation.
Use a UUID or any unique string. If you retry a request with the same idempotency key,
you’ll get the original response back instead of creating a duplicate.
Idempotency-Key: your-unique-key-hereExample:
{
"type": "data_collection",
"description": "Photos of residential mailboxes, front view, in daylight",
"files_needed": 100,
"accepted_formats": ["image/jpeg", "image/png"],
"price_per_file_cents": 50,
"location": { "lat": 30.2672, "lng": -97.7431, "radius_km": 15 },
"deadline": "2026-03-25T00:00:00Z"
}Response: 200 OK
{
"object": "job",
"id": "job_01JQ...",
"status": "open",
"type": "data_collection",
"description": "Photos of residential mailboxes, front view, in daylight",
"files_needed": 100,
"files_approved": 0,
"price_per_file_cents": 50,
"created_at": "2026-03-15T10:00:00Z"
}List Jobs
GET /v1/jobsQuery Parameters:
| Param | Type | Description |
|---|---|---|
status | string | Filter by status: open, in_progress, completed, cancelled |
type | string | Filter by type: data_collection, feedback |
limit | integer | Max results (1-100, default 20) |
cursor | string | Pagination cursor |
Get Job
GET /v1/jobs/{job_id}Retrieve a job by ID. Returns the job object with current status, file counts, and submission summary.
Response: 200 OK — Job object with current status and progress.
Long-Polling
To wait for a job to complete without polling repeatedly, add the Prefer header:
curl https://api.firsthandapi.com/v1/jobs/job_01JQ... \
-H "Authorization: Bearer fh_live_..." \
-H "Prefer: wait=30"The server holds the connection for up to 30 seconds and returns immediately when the job status changes. This is more efficient than polling every few seconds.
Cancel Job
POST /v1/jobs/{job_id}/cancelCancel a job that is open or in_progress. Workers are notified and pending submissions are rejected. Approved files are retained and accessible.
Get Job Files
GET /v1/jobs/{job_id}/filesList all approved files for a job. Each file includes a signed download URL (valid for 1 hour), AI star rating, and submission metadata.
Query Parameters:
| Param | Type | Description |
|---|---|---|
min_score | integer | Minimum AI star rating (1-5, default 3) |
limit | integer | Max results (1-100, default 20) |
cursor | string | Pagination cursor |
Response:
{
"object": "list",
"data": [
{
"object": "file",
"id": "file_01JQ...",
"job_id": "job_01JQ...",
"url": "https://files.firsthandapi.com/...",
"content_type": "image/jpeg",
"size_bytes": 2450000,
"ai_star_rating": 4,
"worker_id": "wkr_01JQ...",
"submitted_at": "2026-03-16T14:30:00Z"
}
],
"has_more": true,
"next_cursor": "..."
}List Job Submissions
GET /v1/jobs/{job_id}/submissionsList all submissions for a job, including approved, rejected, and pending submissions.
Query Parameters:
| Param | Type | Description |
|---|---|---|
status | string | Filter: pending_score, approved, rejected, retry_pending |
limit | integer | Max results (1-100, default 20) |
cursor | string | Pagination cursor |
Response:
{
"object": "list",
"data": [
{
"object": "submission",
"id": "sub_01JQ...",
"job_id": "job_01JQ...",
"worker_id": "wkr_01JQ...",
"status": "approved",
"ai_star_rating": 4,
"ai_reasoning": "Clear, well-lit photo that matches the job description. Subject is centered and in focus.",
"file_url": "https://files.firsthandapi.com/...",
"content_type": "image/jpeg",
"size_bytes": 2450000,
"submitted_at": "2026-03-16T14:30:00Z",
"scored_at": "2026-03-16T14:30:05Z"
}
],
"has_more": false,
"next_cursor": null
}Rate Job
POST /v1/jobs/{job_id}/rateRate a completed job with 1-5 stars and optional feedback. Only jobs with status completed can be rated. Each job can be rated once by its buyer.
Headers:
Authorization: Bearer fh_live_...(required)Idempotency-Key: <unique-key>(required)Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
rating | integer | yes | 1-5 star rating |
feedback | string | no | Text feedback about the job quality |
Example:
curl -X POST https://api.firsthandapi.com/v1/jobs/job_01JQ.../rate \
-H "Authorization: Bearer fh_live_..." \
-H "Idempotency-Key: rate-job_01JQ-001" \
-H "Content-Type: application/json" \
-d '{
"rating": 4,
"feedback": "Great quality photos, fast turnaround."
}'Response: 200 OK
{
"object": "job",
"id": "job_01JQ...",
"status": "completed",
"type": "data_collection",
"description": "Photos of residential mailboxes, front view, in daylight",
"files_needed": 100,
"files_approved": 100,
"price_per_file_cents": 50,
"buyer_rating": 4,
"buyer_feedback": "Great quality photos, fast turnaround.",
"created_at": "2026-03-15T10:00:00Z",
"completed_at": "2026-03-17T08:45:00Z"
}