Skip to content
API v1 preview — endpoints and fields may change before general availability.

Credits and billing

What exactly costs a credit, and how do I avoid a surprise bill?

View .md

1 credit = 1 unique job returned. Duplicates and empty searches are free. You pay per unique job you get back, not per provider asked and not per record a provider returns.

BetterJobs is built so you can know the cost before you spend, cap it while you spend, and prove it after. This page covers all four parts of that system: estimate, cap, receipt and ledger.

Action Cost
A unique job returned by POST /v1/jobs/search or an async search, that you have not paid for before Cost: 1 credit / unique job
The same job returned again, by any request, any strategy, any day Cost: Freealready paid
Duplicate provider records merged into one job Cost: Free
An empty search or empty page Cost: Free
dry_run: true estimate Cost: Free
GET /v1/jobs/{id} for a job you have not paid for Cost: 1 credit / job
GET /v1/companies/{domain} hiring profile Cost: 1 credit / profile
job.opened event delivered by a watch Cost: 1 credit / eventFree if you already paid for that job.
Every other event, retries and replays Cost: Free
Reading async results, events, watches, account, ledger, providers Cost: Free
Sandbox (POST /v1/sandbox/jobs/search) Cost: Freefixed illustrative data

Every operation in the API reference shows its cost. The spec carries it as x-credit-cost on each operation, so the reference, the MCP tools and this table agree.

Once you pay for a job, every later read of the same id is free: another search, a different strategy, GET /v1/jobs/{id}, a page you re-fetch after a crash.

This works because BetterJobs bills canonical jobs, not provider records. Three providers seeing the same opening is one job and one credit. A repost keeps its id, so it is still one credit.

Send the real request with dry_run: true. You get a free estimate and nothing is fetched or charged.

Terminal window
curl https://api.betterjobs.cc/v1/jobs/search \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"title_or": ["Head of RevOps"],
"country_code_or": ["DE", "AT", "CH"],
"posted_within_days": 7
},
"waterfall": { "strategy": "max_coverage" },
"limit": 100,
"dry_run": true
}'

The response (illustrative):

{
"request_id": "req_3Kd8PwZ1uY",
"estimate": {
"expected_unique_jobs_range": { "min": 40, "max": 75 },
"providers_planned": ["betterjobs", "reqbeat", "signalsapi", "theirstack", "jobspipe", "coresignal", "techmap"],
"credits_range": { "min": 40, "max": 75 }
}
}

credits_range is a range, not a quote. Jobs you already paid for are free, so the real charge can come in lower. Agents using the MCP server call the same estimate through the estimate_search tool.

waterfall.max_credits is a hard cap on what one request may charge. Results stop at the cap. The request never charges more, whatever the providers return.

{
"filters": { "title_or": ["Head of RevOps"], "country_code_or": ["DE", "AT", "CH"] },
"waterfall": { "strategy": "max_coverage", "max_credits": 100 }
}

Set it on every production request, including async searches. A good default is the top of the credits_range from your dry run.

Every search response says what it cost:

"metadata": {
"request_id": "req_7Hc2LmQ9xT",
"status": "complete",
"credits_charged": 1,
"jobs_already_paid": 0,
"duplicates_merged": 2,
"credits_remaining": 9841
}
Field Meaning
credits_charged Credits this request charged.
jobs_already_paid Returned jobs you had paid for earlier. Free.
duplicates_merged Provider records merged into canonical jobs. Free.
credits_remaining Credits left on your account.

The same numbers come as headers on every billable response: X-Credits-Charged and X-Credits-Remaining. Quote X-Request-Id to support.

GET /v1/billing/ledger lists every charge, newest first. Filter by job_id to see the full history of one job. It is free.

Terminal window
curl "https://api.betterjobs.cc/v1/billing/ledger?job_id=job_01JC8X4M2Q7RV3T9KD5W6YH0AB" \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01"

One charge, then two free re-reads (illustrative):

{
"data": [
{ "id": "led_0Zc5XvB9nM", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_1Mn4BvC7xZ", "operation": "GET /v1/jobs/{id}", "credits": 0, "reason": "already_paid", "created_at": "2026-10-11T12:40:00Z" },
{ "id": "led_8Yb4WuA3mL", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_6Lk3AzX2wY", "operation": "POST /v1/jobs/search", "credits": 0, "reason": "already_paid", "created_at": "2026-10-11T11:05:00Z" },
{ "id": "led_2Xa3VtZ1kK", "job_id": "job_01JC8X4M2Q7RV3T9KD5W6YH0AB", "request_id": "req_7Hc2LmQ9xT", "operation": "POST /v1/jobs/search", "credits": 1, "reason": "charged", "created_at": "2026-10-11T09:12:00Z" }
],
"next_cursor": null
}

reason is charged or already_paid. Each entry links back to the request that caused it through request_id.

Send an Idempotency-Key header on every POST. A retry with the same key and body returns the first response and never charges again. A 500 internal_error is safe to retry this way. See Idempotency.

Where What happens What to do
Sync search, GET /v1/jobs/{id}, GET /v1/companies/{domain} 402 with error.code: insufficient_credits, plus credits_needed and upgrade_url. Top up or upgrade at upgrade_url, or lower waterfall.max_credits.
Async search The search moves to status: on_hold. Jobs are charged as they are collected, so nothing past your balance is charged. Top up. The search resumes. Poll GET /v1/searches/{id} and branch on status.
{
"error": {
"type": "billing_error",
"code": "insufficient_credits",
"message": "This request needs at least 25 credits; 3 remain.",
"credits_needed": 25,
"upgrade_url": "https://betterjobs.cc/pricing",
"doc_url": "https://docs.betterjobs.cc/platform/errors/#insufficient_credits",
"request_id": "req_6Ek5FuM9wX"
}
}

See insufficient_credits and Async searches. GET /v1/account returns credits_remaining and credits_reset_at, so you can check before a big run.

1 credit = 1 unique job returned. Duplicates and empty searches are free.
PlanPrice / monthCredits / month$ / 1k creditsProvidersIncludes
Free$01,000—Own job sources only1,000 credits to test, No card required
Starter$195,000$3.80Own job sources onlyNo partner providers
Growth$4910,000$4.90Own sources + 2 partner providersAPI, CSV, MCP
Pro$19960,000$3.32Max coverage: all six providersEverything in Growth, plus Webhooks, CRM sync
Scale$599250,000$2.40All six providersEverything in Pro, plus Bring your own provider keys, Priority support
Enterprisefrom $1,500Custom—Contact sales—

Free and Starter route to the BetterJobs index only. Growth adds 2 partner providers. Pro and above add all six. See the waterfall for what each plan can route to. The same plan data is machine-readable at /.well-known/pricing.json.

Enter how many unique jobs you expect per month. Duplicates and empty searches stay free whatever you enter.

Credits charged
Duplicates
Empty searches
Plan credits used
Effective cost