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.
How it works
Section titled “How it works”| 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.
Which requests take it
Section titled “Which requests take it”| 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.
Generate once, reuse on retry
Section titled “Generate once, reuse on retry”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.
# 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"] }'import osimport uuid
API = "https://api.betterjobs.cc/v1"HEADERS = { "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}", "BetterJobs-Version": "2026-10-01",}
key = str(uuid.uuid4()) # once, before any attemptwatch = send( # send() retries 429/5xx with the same headers: see Rate limits "POST", f"{API}/watches", headers={**HEADERS, "Idempotency-Key": key}, json={ "type": "company", "domain": "acme-robotics.example", "webhook_url": "https://hooks.northwind.example/betterjobs", "events": ["job.opened", "job.closed", "company.hiring_started", "company.hiring_stopped"], },).json()const API = "https://api.betterjobs.cc/v1";
const key = crypto.randomUUID(); // once, before any attempt// send() retries 429/5xx with the same init: see Rate limitsconst res = await send(`${API}/watches`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`, "BetterJobs-Version": "2026-10-01", "Content-Type": "application/json", "Idempotency-Key": key, }, body: JSON.stringify({ type: "company", domain: "acme-robotics.example", webhook_url: "https://hooks.northwind.example/betterjobs", events: ["job.opened", "job.closed", "company.hiring_started", "company.hiring_stopped"], }),});const watch = await res.json();send() is the retry helper from Rate limits.
Choosing keys
Section titled “Choosing keys”- 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 returns409 idempotency_conflict. - No secrets in keys. Keys can appear in logs. Do not put API keys, emails or other personal data in them.