consume moves credits, so the same request must never charge twice — no matter how badly the network behaves. That is what the idempotency key is for. This page explains why it exists, how to use it, and — just as important — where its protection ends.
consume requires a key. events accepts one. check takes none, because it changes nothing.
Why idempotency exists
Charging a customer and telling you about it are two separate events. Usually they arrive together. Sometimes they don’t:- The charge commits on our side, and the response is lost in transit — a dropped connection, a rebalancing load balancer, your own client timeout firing first.
- Your worker dies mid-flight and the queue redelivers the job, maybe minutes later, maybe to a different machine.
- Your deploy restarts while a request is in the air.
Same key + same payload = the same answer, charged at most once.If the first attempt went through, the retry replays the recorded answer without charging again. If it never landed, the retry performs it and charges once. Either way, the customer is charged exactly once for the thing they got. Think of the key as the receipt number for one billable intention. Present the same receipt number twice and the cashier hands back the receipt from the first purchase; they don’t ring you up twice.
Pick a stable key
Use an identifier from your own job or command — something that already exists and already means “this unit of work”:- Decide the key before the first attempt. Not after a failure — by then you’re choosing under pressure.
- Persist it before the first attempt. Write it down where the job lives (same row, same document). A key that only lives in memory dies with the process, and the retry after a crash invents a new one — which is exactly the double-charge bug.
Replay behavior
The first request claims the key. QuotaMint stores the completed response and returns the same response on later calls with the same project, operation, environment, and payload. Forconsume, this means:
- One credit deduction
- One usage event
- One ledger entry
- Byte-identical responses for every replay
- A retry may reformat
metadata(whitespace, key order) and still replay — the content is what counts. creditsRemainingin a replayed response is the balance as it was at the first charge. Correct for accounting; do not use it to refresh a live balance display.
The limits — read this part twice
The key is powerful but not magic. Four boundaries, in order of how often they bite: 1. The replay window is 24 hours. Same key, same payload, within 24 hours: replays forever (for the window). After 24 hours the record is gone, and the same key becomes a new operation that charges again. So keep your own job records — and the keys inside them — for at least 24 hours. If your system redelivers jobs after a longer retention window, deduplicate on your side too; the key will not save you past the window. 2. A denial is an answer, and it occupies the key. If a consume is denied (say,insufficient_credits), that denial is recorded just like a success. Retrying the same key after the customer tops up returns the old denial, not a fresh decision — for the full 24 hours. The rule that keeps this straight: a new attempt is a new key. Name it job_8472:attempt_2 and move on. (A 400 is different: it is rejected before anything is recorded, so the key is untouched — fix the payload and retry with the same key.)
3. Same key with different content is a conflict, not a correction. Reusing a key with a changed payload, a different endpoint, or the other environment returns 409 idempotency_key_reused and charges nothing. This is QuotaMint telling you two different intentions share one receipt number — fix the key derivation. Retrying a 409 is always pointless.
4. The key covers the charge, not your side effects. QuotaMint guarantees the credits move once per key. It cannot make your side effects idempotent: if a retried job re-sends the email, re-renders the image, or re-posts the webhook, that’s your application logic running twice behind one successful charge. Keep the key with your job record so any retry picks up the same intention, and make the rest of the job naturally re-runnable or guarded on your side.
One more scoping fact: keys are independent across environments and endpoints. The same key used with a qm_test_ key and a qm_live_ key are two separate operations — each may charge in its own environment. Namespace your key store if it is shared across environments.
What to retry
Retry with the same key — always the same key — for transient failures:500 internal_errorand503 service_unavailable, with backoff429 rate_limited, after waitingRetry-Afterseconds- Network failures and client timeouts: the outcome is unknown, and the same key is what makes the retry safe
400, 401, 403, 404, or 409 without correcting the request first. A denial (allowed: false, HTTP 200) is not a failure to retry at all — it’s an answer; see the limits above.
Full status-by-status guidance is in errors and denial reasons.