Quickstart

Make your first Datapoint API call in under 5 minutes.

This guide walks the full lifecycle: create an API key, upload media, create a comparison job, and fetch aggregated human votes. By the end you'll have a real survey result back from real annotators.

1. Create an API key

In the dashboard, open API keys and click Create key. Copy the key immediately. We only display it once.

Every client-facing endpoint accepts the key as an X-API-Key header:

export DATAPOINT_KEY="dp_live_abc123..."
curl https://api.trydatapoint.com/data-labelling/v1/jobs \
  -H "X-API-Key: $DATAPOINT_KEY"

2. Upload media

Datapoint references uploaded assets by opaque dp:// URIs. Upload with POST /media:

curl -X POST https://api.trydatapoint.com/data-labelling/v1/media \
  -H "X-API-Key: $DATAPOINT_KEY" \
  -F "files=@video_a.mp4" \
  -F "files=@video_b.mp4"

The response returns one entry per file, each with a media_ref you'll use when creating jobs. Each item also carries a url: a signed, uploader-scoped preview path (valid ~1 hour) you can fetch to render the file straight away. It's a relative path — prepend the API base — and it expires, so re-fetch rather than storing it.

{
  "media": [
    {
      "filename": "video_a.mp4",
      "media_id": "b6a1cd3f-1234-4abc-9def-0123456789ab",
      "media_ref": "dp://b6a1cd3f1234/video_a.mp4",
      "type": "video",
      "size_bytes": 2457623,
      "url": "/media/v2/b6a1cd3f-1234-4abc-9def-0123456789ab?cid=…&exp=…&sig=…"
    },
    {
      "filename": "video_b.mp4",
      "media_id": "c8f3ae9b-5678-4def-9012-3456789abcde",
      "media_ref": "dp://c8f3ae9b5678/video_b.mp4",
      "type": "video",
      "size_bytes": 2310044,
      "url": "/media/v2/c8f3ae9b-5678-4def-9012-3456789abcde?cid=…&exp=…&sig=…"
    }
  ]
}

3. Create a comparison job

A comparison job asks annotators to choose between two candidates. Under media.candidates, pass a list of two media objects, each with a url (dp://… or a public https URL) and a type (video, image, or audio).

curl -X POST https://api.trydatapoint.com/data-labelling/v1/jobs \
  -H "X-API-Key: $DATAPOINT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hello-datapoint",
    "instruction": "Which video looks more realistic?",
    "task_type": "comparison",
    "max_responses_per_datapoint": 10,
    "datapoints": [
      {
        "media": {
          "candidates": [
            { "url": "dp://b6a1cd3f1234/video_a.mp4", "type": "video" },
            { "url": "dp://c8f3ae9b5678/video_b.mp4", "type": "video" }
          ]
        }
      }
    ]
  }'

Creating a job reserves its estimated cost up front. The response echoes estimated_cost_credits, the snapshotted credits_per_response, and a pricing breakdown of how that rate was built (base rate plus any targeting surcharges — see targeting surcharges):

{
  "job_id": "job_abc123def456",
  "status": "processing",
  "total_datapoints": 1,
  "estimated_cost_credits": 50,
  "credits_per_response": 5,
  "pricing": {
    "base_credits_per_response": 5,
    "demographic_surcharge_credits": 0,
    "geo_surcharge_credits": 0,
    "credits_per_response": 5
  },
  "created_at": "None"
}

The initial status is processing while the job is being queued; it becomes active once the first datapoint is ready to serve, then completed when every datapoint reaches its response cap. The terminal statuses are completed, failed, blocked, and cancelled — poll until the status is one of those, not just completed, since a job can also finish in one of the other terminal states.

4. Poll for completion

Check progress by polling GET /jobs/{job_id}:

curl https://api.trydatapoint.com/data-labelling/v1/jobs/$JOB_ID \
  -H "X-API-Key: $DATAPOINT_KEY"

Responses arrive as annotators pick the job up — poll until the status is terminal, and watch completed_datapoints catch up to total_datapoints:

{
  "job_id": "job_abc123def456",
  "name": "hello-datapoint",
  "status": "active",
  "task_type": "comparison",
  "serving_environment": "prod",
  "total_datapoints": 1,
  "processing_datapoints": 0,
  "ready_datapoints": 1,
  "completed_datapoints": 0,
  "failed_datapoints": 0,
  "blocked_datapoints": 0,
  "total_responses": 3,
  "max_responses_per_datapoint": 10,
  "cost_credits": 15,
  "credits_per_response": 5,
  "refundable_credits": 35,
  "created_at": "2026-04-21 12:34:56.123456+00:00",
  "errors": [],
  "response_options": null,
  "annotator_filter": null,
  "is_paused": false
}

Every status response includes blocked_datapoints, credits_per_response, refundable_credits, response_options, annotator_filter, and is_paused (the last three echo the job's configuration, and are null/false when unset). A chain_progress block appears only on chain jobs.

How to read the counters:

  • ready_datapoints = active + completed — datapoints that are being served or already have all the responses they need. A datapoint moves here as soon as it's been dispatched for annotation.
  • processing_datapoints = total_datapoints minus every datapoint already in a status — i.e. datapoints not yet queued for annotation.
  • failed_datapoints and blocked_datapoints each count only their own bucket. Because those states are tracked separately, the buckets don't necessarily add up to total_datapoints.
  • total_responses is the capped, billable response count — capped at max_responses_per_datapoint per datapoint. The figure can move down as well as up, so treat it as a live count rather than a monotonic counter. It's what cost_credits bills against.
  • cost_credits is computed from total_responses at the job's per-response rate — the actual work collected so far, not the full reservation. refundable_credits is what you'd get back if you cancelled the job right now.

5. Fetch the results

When all datapoints have reached consensus, pull aggregated results:

curl https://api.trydatapoint.com/data-labelling/v1/jobs/$JOB_ID/results \
  -H "X-API-Key: $DATAPOINT_KEY"

For a comparison job, each result includes vote counts, the winning option, and a confidence score. Annotators see the two candidates in a randomised display order per task; vote key A corresponds to the first candidate you submitted and B to the second. Each result also carries a media array describing the underlying assets — media_id, type, role, and a signed url and thumbnail_url. Match each media_id against the candidates you submitted to map votes back to assets:

{
  "job_id": "job_abc123def456",
  "status": "completed",
  "task_type": "comparison",
  "page": 1,
  "per_page": 100,
  "total_results": 1,
  "results": [
    {
      "datapoint_index": 0,
      "votes": { "A": 7, "B": 3 },
      "total_responses": 10,
      "consensus": "A",
      "confidence": 0.7,
      "agreement_rate": 0.7,
      "media": [
        {
          "media_id": "b6a1cd3f-1234-4abc-9def-0123456789ab",
          "type": "video",
          "role": "candidates",
          "label": null,
          "url": "/media/v2/b6a1cd3f-1234-4abc-9def-0123456789ab?cid=…&exp=…&sig=…",
          "thumbnail_url": "/media/v2/b6a1cd3f-1234-4abc-9def-0123456789ab?cid=…&exp=…&sig=…&preview=1"
        },
        {
          "media_id": "c8f3ae9b-5678-4def-9012-3456789abcde",
          "type": "video",
          "role": "candidates",
          "label": null,
          "url": "/media/v2/c8f3ae9b-5678-4def-9012-3456789abcde?cid=…&exp=…&sig=…",
          "thumbnail_url": "/media/v2/c8f3ae9b-5678-4def-9012-3456789abcde?cid=…&exp=…&sig=…&preview=1"
        }
      ]
    }
  ]
}

The url and thumbnail_url on each media item are signed, client-scoped relative paths (valid ~1 hour). Prepend the API base to fetch them, and re-fetch rather than storing them — they expire.

That's a full round-trip. For per-annotator detail behind these aggregates, pull GET /jobs/{id}/responses.

Next steps