Jobs

Jobs represent content collection requests posted by buyers. Workers browse and accept jobs, then upload files.

Create Job

POST /v1/jobs

Post a new content collection job.

Headers:

  • Authorization: Bearer fh_live_... (required)
  • Idempotency-Key: <unique-key> (required)
  • Content-Type: application/json

Request Body:

FieldTypeRequiredDescription
typestringyesOne of: data_collection, feedback
descriptionstringyesDetailed instructions for workers
files_neededintegeryesNumber of files requested
accepted_formatsstring[]yesAccepted MIME types (e.g., ["image/jpeg", "image/png"])
price_per_file_centsintegeryesPrice per approved file in cents
locationobjectnoGeographic constraint: { lat, lng, radius_km }
deadlinestringnoISO 8601 deadline for job completion
max_workersintegernoMax concurrent workers (default: unlimited)
metadataobjectnoArbitrary 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-here

Example:

{
  "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/jobs

Query Parameters:

ParamTypeDescription
statusstringFilter by status: open, in_progress, completed, cancelled
typestringFilter by type: data_collection, feedback
limitintegerMax results (1-100, default 20)
cursorstringPagination 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}/cancel

Cancel 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}/files

List 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:

ParamTypeDescription
min_scoreintegerMinimum AI star rating (1-5, default 3)
limitintegerMax results (1-100, default 20)
cursorstringPagination 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}/submissions

List all submissions for a job, including approved, rejected, and pending submissions.

Query Parameters:

ParamTypeDescription
statusstringFilter: pending_score, approved, rejected, retry_pending
limitintegerMax results (1-100, default 20)
cursorstringPagination 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}/rate

Rate 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:

FieldTypeRequiredDescription
ratingintegeryes1-5 star rating
feedbackstringnoText 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"
}