Why POST Needs Idempotency Keys
You send a request. The server processes it. But the response is lost in the network — a timeout, a dropped connection, a flaky mobile network. So your client retries. The server processes it again. Now you have two short links, or two charges, or two emails.
This is the classic duplicate-problem, and the fix is an idempotency key: a unique string your client sends with the request so the server can recognize and deduplicate retries. This guide explains how they work, when you need them, and how to implement them on the create-links pattern.
The problem: retries are inevitable
Networks drop things. It's not a bug — it's physics. A request can succeed at the server and still "fail" for the client because the response never arrived. The client's only sane behavior is to retry. But a retry of a non-idempotent operation creates a duplicate.
- Retrying a create → duplicate resource.
- Retrying a charge → double charge.
- Retrying a send → double email.
Without a way to tell "this is the same request as before," the server can't know it should return the original result rather than create another one.
What an idempotency key is
An idempotency key is a unique, client-generated string sent as a header (or body field) on a request:
POST /api/v1/links
Idempotency-Key: 4f8a9c2e-...-unique
The contract:
- The client generates a fresh key for each new logical operation.
- The same key is reused on every retry of that operation.
- The server stores the key with the operation's result.
- If a request with an already-seen key arrives, the server returns the stored result instead of running the operation again.
The key only needs to be unique per client; the server treats it as opaque.
How the server handles it
A minimal implementation:
key = request.header("Idempotency-Key")
if key is present:
existing = store.lookup(key)
if existing:
return existing.result # deduplicate — no second create
result = create_link(payload) # do the real work
store.save(key, result) # remember for retries
return result
Two things matter:
- Check the key before doing the work. The lookup must happen before the side effect, so a retry never triggers a second create.
- Store the result atomically with the work, so a race between two identical requests can't both create.
A real example: creating a link
Say a mobile app shortens a URL. The user taps once, the network blips, the app retries. Without a key:
- Request 1 creates
ab3x7. - Request 2 creates
cd5y9. - The user now has two links for the same destination.
With an idempotency key:
- Request 1 (
Idempotency-Key: abc) createsab3x7and stores the result underabc. - Request 2 (
Idempotency-Key: abc) finds the stored result and returnsab3x7. - The user gets one link, and the client gets a clean success.
This is exactly how a robust create-links API should behave — and why a good
shortener API supports idempotency keys on its POST /links endpoint.
When you need it vs when you don't
Need it for any non-safe operation with a side effect you don't want duplicated: creating resources, charging payments, sending emails, registering, uploading.
Don't strictly need it for GET (already idempotent) or for operations you're
willing to let run twice. But even "harmless" duplicates create confusion and
data-quality problems, so the key pattern is cheap insurance.
Common mistakes
- Checking the key after the work. If you create first and check later, retries still duplicate.
- Reusing one key for everything. The key identifies a specific operation; reuse breaks dedup.
- Ignoring the key on some clients. If only some clients send keys, retries from the others still duplicate. Enforce it on the critical paths.
- Not storing enough to return the original result. If you don't persist the result, you can't replay it.
The takeaway
Idempotency keys turn an unsafe retry into a safe one. Generate a unique key per operation, reuse it on retries, and have the server check it before doing work and store the result. It's a small header that prevents the most embarrassing class of bugs — duplicate links, duplicate charges, duplicate emails.
When you're building on yas.sh's API, send an Idempotency-Key on link creation to
guarantee your retries never create duplicates.
Why POST is not naturally idempotent
The problem idempotency keys solve comes from a property of HTTP itself: GET is naturally safe and repeatable, but POST is not. A POST that creates a resource will create a new one every time it is sent — so if a network failure causes the client to retry, the server can receive two create requests and produce two resources. This is a real problem for link creation, checkout, and any write where duplicates are expensive. Idempotency keys make a POST safe to retry by letting the server recognize that a repeated request is the same logical operation and return the original result instead of creating a duplicate.
How an idempotency key works
The pattern is straightforward:
- The client generates a unique key for the operation — a UUID or a hash of the request content — and sends it in the request.
- The server records the key along with the result of the first request that used it.
- If another request arrives with the same key, the server returns the stored result instead of performing the action again.
The key is scoped to the resource and identifies the logical operation, not the physical request. The result is that retries become safe: a request that succeeded but whose response was lost, or that failed mid-flight, can be safely re-sent with the same key and will not duplicate work.
Using idempotency in link automation
Idempotency is the backbone of reliable link automation and bulk operations. In the bulk creation workflow, attaching an idempotency key per row means that if the batch is interrupted and restarted, already-created links are recognized and returned rather than created a second time. The same applies to webhook handling, where legitimate re-deliveries must not duplicate actions. Wherever you have a write that could be retried — and in automation, everything can be retried — an idempotency key turns a potentially duplicate operation into a safe one.
Choosing and storing keys
Two design details matter. First, generate the key deterministically where possible: a content hash makes the same logical request produce the same key even across retries, while a random UUID guarantees uniqueness but requires the client to keep the key for the whole retry sequence. Second, decide how long to retain keys and results — long enough to cover legitimate retry windows and reconciliation, but not so long that storage grows unboundedly. With a deterministic key and a sensible retention period, idempotency becomes an invisible guarantee that your writes stay correct under failures.
