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

Idempotency

How do I retry a POST safely without being charged twice?

View .md

Networks fail. A request can time out after BetterJobs already did the work. Send an Idempotency-Key header on every POST, and reuse it when you retry. The retry returns the first response instead of doing the work again.

You send You get
A new key The request runs normally.
The same key and the same body The first response, replayed. Nothing runs again. Nothing is charged again.
The same key and a different body 409 idempotency_conflict. Nothing runs.

The key is any string up to 255 characters. A UUID v4 is the simplest choice.

Endpoint Without a key, a retry could…
POST /v1/jobs/search Run the search again. Jobs you already paid for are free, so the main cost is time and rate limit.
POST /v1/searches Start a second async search.
POST /v1/watches Create a second watch, which then delivers every event twice.
POST /v1/webhooks/replay Queue the same event twice.

GET and DELETE are already safe to repeat. They take no key. If a retried DELETE /v1/watches/{id} returns 404 not_found, the first attempt worked.

Create the key before the first attempt, outside your retry loop. A key generated inside the loop is a new key on every attempt and protects nothing.

Terminal window
# Pick one key per logical request. Reuse it verbatim on retry.
curl https://api.betterjobs.cc/v1/watches \
--retry 5 \
-H "Authorization: Bearer $BETTERJOBS_API_KEY" \
-H "BetterJobs-Version: 2026-10-01" \
-H "Idempotency-Key: 7b8f2c4e-1a3d-4f5b-9c6e-0d2a4b6c8e1f" \
-H "Content-Type: application/json" \
-d '{
"type": "company",
"domain": "acme-robotics.example",
"webhook_url": "https://hooks.northwind.example/betterjobs",
"events": ["job.opened", "job.closed", "company.hiring_started", "company.hiring_stopped"]
}'

send() is the retry helper from Rate limits.

  • One key per logical operation. “Create the watch for acme-robotics.example” is one operation, however many attempts it takes.
  • Derive it when the operation already has an id. If a job in your own queue triggers the request, a key such as watch-acme-robotics.example-<your job id> survives a process restart. A random key held in memory does not.
  • New body, new key. Each page of a paged search has a different cursor, so each page needs its own key. Reusing a key with a changed body returns 409 idempotency_conflict.
  • No secrets in keys. Keys can appear in logs. Do not put API keys, emails or other personal data in them.