Skip to content

Sending / Idempotency

Idempotency

Retry a send safely without notifying anyone twice.

Networks fail after a request has already arrived. Send an Idempotency-Key header with POST /v1/notifications and a retry of the same request never notifies anyone twice. The key is any string of 1 to 255 characters; use something tied to the event, like an order id.

⫻

curl

curl https://api.meslzy.com/push/v1/notifications \  -H "Authorization: Bearer $PUSH_API_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: order-48213-shipped" \  -d '{ "recipient": "me", "title": "Order #48213 shipped" }'

What Push does with the key#

SituationResponse
First accepted request with this key202 Accepted. The notification is sent and the key is stored with it.
Same key, same body200 OK with the original id, status, attempts and mode. Nothing new is sent or charged.
Same key, different body409 IDEMPOTENCY_CONFLICT. Nothing is sent.
Same key while the first request is still runningThe second request waits for the first to finish. If the first was accepted, the second gets its 200 OK replay, or 409 IDEMPOTENCY_CONFLICT if the body differs. If the first failed, the second is handled as a new request.
Key shorter than 1 or longer than 255 characters400 VALIDATION_ERROR.

A replayed response looks like this:

⫻

200 OK

HTTP/1.1 200 OKContent-Type: application/jsonX-Request-Id: req_0199b1c27a4e7c3d9f1e2b6a8d4c5e11
{  "id": "0199b1c2-7a4e-7c3d-9f1e-2b6a8d4c5e10",  "status": "queued",  "attempts": 2,  "mode": "live"}

What counts as the same body#

Push compares the validated request, after defaults are filled in. Key order, whitespace and spelling out a default (for example "urgency": "normal") do not make a body different. Changing any value does.

Scope and lifetime#

  • Keys belong to the project, not to the API key: every key of a project, live or test, shares the same keys.
  • Only an accepted send stores the key. A request that failed with 400, 402, 422 or 429 did not use it up, so you can fix the cause and retry with the same key.
  • A key is remembered for as long as its notification exists, which is up to 90 days.
NoteA 500 INTERNAL_ERROR response says it in its message: try again with the same Idempotency-Key. If the first request did go through, the retry gets the original response instead of a second notification.

A safe retry loop#

Retry network errors, 429 and 5xx with the same key, and wait for Retry-After on 429. Do not retry other 4xx blindly: the request itself is wrong and will fail the same way. A 409 means the key was already used with a different body: a bug to fix, not something to retry.

⫻

JavaScript

const sendOnce = async (body, idempotencyKey) => {  for (let attempt = 1; attempt <= 5; attempt += 1) {    let response;
    try {      response = await fetch("https://api.meslzy.com/push/v1/notifications", {        method: "POST",        headers: {          Authorization: `Bearer ${process.env.PUSH_API_KEY}`,          "Content-Type": "application/json",          "Idempotency-Key": idempotencyKey,        },        body: JSON.stringify(body),      });    } catch {      await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));      continue;    }
    if (response.ok) {      return await response.json();    }
    if (response.status === 429) {      const seconds = Number(response.headers.get("Retry-After") ?? "1");      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));      continue;    }
    if (response.status >= 500) {      await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));      continue;    }
    throw new Error(`push failed: ${response.status} ${JSON.stringify(await response.json())}`);  }
  throw new Error("push failed after retries");};
await sendOnce({ recipient: "ops", title: "Order #48213 shipped" }, "order-48213-shipped");
WarningGenerate the key once per event and reuse it on every retry. A fresh key on each try turns every retry into a new notification.