Create jobs safely with idempotency
A request can time out after WorkWing has already created the job. If you retry blindly you get a second job, a second visit and, if your plan bills per job, a second charge. The Idempotency-Key header is how you retry without that risk. This recipe shows how to choose keys and what each answer means.
Getting started with the API has the basic rule: every POST, PATCH, PUT and DELETE carries an Idempotency-Key, 1 to 256 printable ASCII characters.
The guarantee
Section titled “The guarantee”Within 24 hours, a write sent with a given key is applied at most once. If you never received a response, retrying with the same key and the same request is always safe. A key is consumed only when the write is actually applied, because the key and the write are saved together or not at all.
What a key means
Section titled “What a key means”A key names one attempt at one operation. WorkWing remembers the first answer it gave for that key, and for the next 24 hours any identical request carrying the same key gets that answer instead of running again.
Keys belong to your organisation and are shared across every endpoint. The same key sent to POST /v1/jobs and then to POST /v1/customers is refused with E_AP_6010, never treated as a new operation. The same key sent by two different API keys in your organisation names one operation, so two integrations that both count up from 1 will trip over each other. Use UUIDs, or prefix keys with the name of the integration.
Choosing the key
Section titled “Choosing the key”There are two good choices.
Random, held for the life of the operation. Generate a UUID when you decide to create the job, and reuse it for every retry of that one request. This is enough when the retries happen in the same process.
Derived from your own record. If the job comes from an order in your system, build the key from the order: shop-order-88213-create-job. Now a retry still matches after your process crashes and restarts, or when a queue redelivers the same message, because the key comes from the order rather than from memory.
What you must not do is generate a fresh key on each attempt. Then every retry is a new operation and idempotency does nothing for you.
Retrying a create
Section titled “Retrying a create”import { randomUUID } from "node:crypto";
async function createJob(body, idempotencyKey) { for (let attempt = 1; attempt <= 5; attempt++) { let res; try { res = await fetch("https://api.workwing.io/v1/jobs", { method: "POST", headers: { Authorization: `Bearer ${process.env.WORKWING_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify(body), }); } catch { await sleep(1000 * 2 ** attempt); continue; }
const payload = await res.json().catch(() => null); if (res.ok) return payload.data;
const code = payload?.error?.code; if (code === "E_AP_6013") return fetchExistingJob(body); const retryable = res.status >= 500 || res.status === 429 || code === "E_AP_8003"; if (!retryable) throw new Error(`${code}: ${payload?.error?.message}`);
const retryAfter = Number(res.headers.get("Retry-After")); await sleep(retryAfter > 0 ? retryAfter * 1000 : 1000 * 2 ** attempt); } throw new Error("Gave up creating the job");}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const job = await createJob( { customer_id: "0199a1b2-c3d4-7000-8000-00000000c001", template_id: "0199a1b2-c3d4-7000-8000-00000000a001", title: "Annual boiler service", }, `shop-order-88213-create-job`,);fetchExistingJob is your own lookup, for example by the external reference described below. The key is built once, outside the loop, and every attempt sends it. The body is built once too, so every attempt sends exactly the same thing. Handling errors has a general version of this loop for every endpoint.
What comes back
Section titled “What comes back”The first time. The job is created and you get 201 with the job, as usual.
The same key and the same body again. You get the same 201 and the same job, with an extra header: Idempotency-Replayed: true. Nothing new is created. This is the answer you’d see after a timeout where the first request had in fact gone through.
The same key while the first request is still running. WorkWing waits for it to finish (up to five seconds) and replays the result. If it is still running after that, you get 409 with E_AP_8003 and a Retry-After: 1 header. Wait that long and send the same request again with the same key: you’ll get the replay once the first one is done.
The job was created but the answer is gone. If the server stopped after saving the job but before recording its reply, or the reply was larger than 1 MB, you get 409 with E_AP_6013. The job exists, and retrying won’t create another: fetch it with a GET instead of sending the write again.
The same key with a different request. You get 422 with E_AP_6010, and nothing is created. Two different operations are sharing a key, which is almost always a bug in how the key is built. “Different” covers the method, path, query string and body. In the body, a changed value, a changed array order or an added or removed field counts; the order of fields within an object does not. Build the body once and resend that.
The same key after a failure. A key is consumed only when the write is applied. If the first attempt failed with a 5xx, or was refused with any 4xx (a missing field, an unknown customer, a rate limit, a missing permission), the key is still free. Send the same key again and the request runs again, with the same body or a corrected one.
The database being unreachable. If WorkWing can’t check the key at that moment you get 503 with E_AP_9002. Nothing was written. Retry with the same key.
Beyond 24 hours
Section titled “Beyond 24 hours”The replay window is 24 hours. If your system can redeliver an order later than that, the key alone won’t stop a second job. Write your own reference onto the job straight after creating it, and check for it before you create one:
curl -X POST https://api.workwing.io/v1/jobs/0199a1b2-c3d4-7000-8000-00000000j001/external-references \ -H "Authorization: Bearer ww_sk_live_abc123.xxxxxxxx" \ -H "Idempotency-Key: shop-order-88213-external-ref" \ -H "Content-Type: application/json" \ -d '{"provider": "shop", "type": "order", "ref": "88213"}'curl -G https://api.workwing.io/v1/jobs \ -H "Authorization: Bearer ww_sk_live_abc123.xxxxxxxx" \ --data-urlencode 'external_references=cs.[{"provider":"shop","type":"order","ref":"88213"}]' \ --data-urlencode 'select=id,job_number'An empty data array means no job carries that reference yet. Writing the reference needs update:jobs. A job holds 20 references at most; a 21st with a new provider and type is refused with E_AP_4007, while sending the same provider and type again replaces the existing one.
What’s next
Section titled “What’s next”- Create and schedule a job: the full booking sequence this fits into
- Handling errors: which answers to retry, and how long to wait