Billing
How credits are reserved, charged, topped up, and audited.
Datapoint bills per response collected, not per job created. Credits are purchased in advance and spent as jobs run.
How cost is computed
A standalone job's maximum cost is:
max_responses × datapoints × num_dimensions × per-response rate
datapoints: length of thedatapointsarray in your job.max_responses: the job'smax_responses_per_datapoint(default 10).num_dimensions: 1 for single-instruction jobs, N for multi-dimension jobs. This factor applies to standalone jobs only and is mutually exclusive withsteps— a job is either dimensional or a chain, never both.
A chain job (a job with steps) is priced instead by:
max_responses × steps × datapoints × per-response rate
where steps is the number of chain steps (1–5). Each datapoint fans out into one task per step, and every step is capped at max_responses.
Either way the total is maximal at submission time — it's what you'd pay if every task collected its full response cap. Partial chains (walks abandoned mid-way) and jobs that settle with unused capacity consume less; the unspent portion is released back to your available balance (see below).
Targeting surcharges
Jobs that target a specific audience pay a per-response surcharge on top of your account's base rate:
- Geography targeting (
annotator_filterwithcountry,country_name,region,city,postal, ormedian_household_income): the surcharge depends on the countries targeted — some countries carry a premium. When multiple countries are listed, the highest applicable surcharge is used. Targetingregion,city, orpostalwithout naming the countries is priced at the highest country premium — list the countries (bycountrycode orcountry_name) to get their actual rates. Income targeting (median_household_income) is a geographic attribute and is priced as a geo surcharge, not a demographic one. - Demographic targeting (
annotator_filterwithgender,age_range, or a custom dimension, or a chain with screening steps — demographic questions or early-exit rules): a flat surcharge, applied once regardless of how many demographic dimensions you combine.
The effective rate — base plus surcharges — is fixed when the job is created and returned as credits_per_response (with a pricing breakdown) by POST /jobs. Later pricing changes never affect a running job: reservation, the running cost_credits, and final settlement all use the job's own rate. Untargeted jobs pay the base rate.
Surcharge amounts are part of the platform's pricing configuration and can change over time — any credit values in the examples on this page are illustrative. To see the exact rate a targeted job would be priced at, use the quote endpoint below.
Credit lifecycle
Every job's credit moves through these ledger entries:
- Reserve: at job creation, the full estimated cost is moved from
availabletoreserved. Ifavailableisn't enough, the API rejects with402 Payment Requiredand returnsneeded_creditsandavailable_credits. - Release (settlement): when the job reaches a terminal state — it completes, is cancelled, or every datapoint fails or is blocked during ingestion — the job is billed for the responses actually collected (capped at the reservation) and any unspent portion is returned to
available. Settlement happens once, at the job level; a single datapoint failing mid-run does not release credits on its own. - Release (dispatch failure): if tasks can't be dispatched (a transient
503fromPOST /jobs), the entire reservation is released immediately and the failed job is renamed, so you can resubmit under the samename.
A job that stalls without ever completing is settled automatically by a background sweep after roughly 14 days, releasing its unused reservation the same way.
Your reserved_credits grows when you submit jobs and shrinks as they settle. Your available_credits is the amount you could safely reserve for a new job.
Other ledger entries can appear on your account:
- Purchase: a top-up adds to
available. - Grant: promotional or signup credits added to
available(also reflected ingranted_credits). New accounts receive a one-time signup grant. - Refund: a reversed purchase. It removes the credits the original purchase granted, so it lowers
available. - Planner fee: a successful
POST /surveys/plancosts a small flat fee (see the survey planner). - Message fee: a signed charge for a dashboard-chat turn. Any account that used the chat builder sees these entries; if a turn fails after being charged, a matching positive
message_feeentry nets it back out.
Check balance
Returns your account's current credit balance.
curl https://api.trydatapoint.com/data-labelling/v1/billing/balance \
-H "X-API-Key: $DATAPOINT_KEY"Response (200 OK)
{
"available_credits": 4725,
"reserved_credits": 275,
"total_purchased_credits": 5000,
"granted_credits": 100
}| Field | Type | Description |
|---|---|---|
available_credits | int | Credits you can reserve for a new job right now. |
reserved_credits | int | Credits held against in-flight jobs. Released when a job settles. |
total_purchased_credits | int | Lifetime purchased credits for the account. |
granted_credits | int | Promotional or signup credits granted to the account. Spendable, tracked separately from purchased credits. |
Quote a per-response rate
Returns the effective per-response rate a job with the given targeting would be priced at — before any job exists. Use it to show an accurate cost preview while the user is still choosing their audience. Read-only and free to call.
curl -X POST https://api.trydatapoint.com/data-labelling/v1/billing/pricing/quote \
-H "X-API-Key: $DATAPOINT_KEY" \
-H "Content-Type: application/json" \
-d '{"annotator_filter": {"country": ["US", "GB"]}}'Request body
| Field | Type | Description |
|---|---|---|
annotator_filter | object | The audience filter you would pass to POST /jobs, restricted to allow-list columns (e.g. `{"country": ["US"]}`). Omit for an untargeted quote. |
has_screening_steps | booleandefault: false | Set true when the prospective job is a chain with screening steps (demographic questions or early-exit rules) — they price like a demographic filter. |
Response (200 OK)
{
"base_credits_per_response": 5,
"demographic_surcharge_credits": 0,
"geo_surcharge_credits": 3,
"credits_per_response": 8
}credits_per_response (base plus the surcharges) is the rate POST /jobs will reserve at for the same filter — the example values above are illustrative; the endpoint returns your account's current rates. Allow-list keys the filter validator would ultimately reject still return a quote — that validation happens at job creation, not here.
Top up
Starts a hosted checkout session. Redirect the user to the returned checkout_url. On success the payment provider notifies Datapoint and your available_credits updates within a few seconds.
curl -X POST https://api.trydatapoint.com/data-labelling/v1/billing/checkout \
-H "X-API-Key: $DATAPOINT_KEY" \
-H "Content-Type: application/json" \
-d '{}'Request body
| Field | Type | Description |
|---|---|---|
product_id | string | Optional. A specific purchasable product to check out. When omitted, the response opens a product picker chosen from your billing state rather than a single fixed product. |
metadata | object | Optional string-keyed map forwarded with the checkout, echoed back so post-payment side-effects can be routed. |
Empty body ({}) is valid — it opens the default picker.
Response (200 OK)
{
"checkout_url": "https://buy.polar.sh/s/...",
"session_id": "cs_abc..."
}Error cases
| Status | Cause |
|---|---|
400 | Unknown or disallowed product_id. |
403 | Credit packs are not available (code: "credit_packs_disabled"). |
429 | Rate limit exceeded. |
500 | Checkout products are not configured for this environment. |
View history
Paginated transaction history: every purchase, grant, reserve, release, refund, and fee.
Query parameters
| Field | Type | Description |
|---|---|---|
page | integerdefault: 1 | 1-indexed page (min 1). |
per_page | integerdefault: 50 | Entries per page (min 1, max 200). |
curl "https://api.trydatapoint.com/data-labelling/v1/billing/history?page=1&per_page=50" \
-H "X-API-Key: $DATAPOINT_KEY"Response (200 OK)
{
"entries": [
{
"entry_type": "purchase",
"amount_credits": 5000,
"job_id": null,
"description": "Credit purchase (5000 credits, $50.00 paid, order=ord_...)",
"created_at": "2026-04-15 08:21:04.123456+00:00"
},
{
"entry_type": "reserve",
"amount_credits": -150,
"job_id": "f3a7...",
"description": "Reserved for job",
"created_at": "2026-04-15 09:10:22.123456+00:00"
},
{
"entry_type": "release",
"amount_credits": 15,
"job_id": "f3a7...",
"description": "Released unused credits (15 credits)",
"created_at": "2026-04-15 11:42:17.123456+00:00"
},
{
"entry_type": "planner_fee",
"amount_credits": -50,
"job_id": null,
"description": "Survey planner fee",
"created_at": "2026-04-15 12:01:09.123456+00:00"
}
],
"total": 4,
"page": 1,
"per_page": 50
}Ledger entry
| Field | Type | Description |
|---|---|---|
entry_type | "purchase" | "grant" | "reserve" | "release" | "refund" | "planner_fee" | "message_fee" | Kind of credit movement. `purchase`, `grant`, and `release` add to `available`; `reserve` and `refund` are debits (a `refund` reverses the credits of the original purchase, so its `amount_credits` is negative); `planner_fee` and `message_fee` are signed (a charge is negative, and a same-type refund is positive). |
amount_credits | int | Signed integer credits. Credits to your balance are positive; debits are negative. |
job_id | string | null | A stable job reference shared by that job's ledger entries — not the `job_id` returned by `POST /jobs`. Null for purchases, grants, and fees. |
description | string | null | Human-readable note — a provider order reference or a fixed system string such as `Reserved for job` or `Released unused credits`. |
created_at | string | Datetime string: `'YYYY-MM-DD HH:MM:SS.ffffff+00:00'`. |
Handling insufficient balance
When POST /jobs responds with 402, parse detail to show a clear top-up prompt. The global error envelope mirrors detail into a top-level message field; see the errors page for the full shape.
{
"detail": {
"message": "Insufficient balance",
"needed_credits": 150,
"available_credits": 75
}
}Direct the user through POST /billing/checkout, wait for the balance to update, and retry the job with the same request body. Job creation is idempotent on name: if the original request was already received, the retry returns that job's original fields (and omits the pricing breakdown, which is only present on a fresh creation) rather than reserving again.