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_datapointsminus every datapoint already in a status — i.e. datapoints not yet queued for annotation.failed_datapointsandblocked_datapointseach count only their own bucket. Because those states are tracked separately, the buckets don't necessarily add up tototal_datapoints.total_responsesis the capped, billable response count — capped atmax_responses_per_datapointper datapoint. The figure can move down as well as up, so treat it as a live count rather than a monotonic counter. It's whatcost_creditsbills against.cost_creditsis computed fromtotal_responsesat the job's per-response rate — the actual work collected so far, not the full reservation.refundable_creditsis 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
- Authentication: API key management and security
- Task types: rating, ranking, and multiple-choice surveys
- Errors: full status-code reference and error envelopes
- API Reference: Jobs: every job endpoint with request/response schemas