Why idempotency keys matter when you push registrations into a CRM
An idempotency key is a unique client-generated value sent with a write request so the server recognises a retry of that same request and replays the original response instead of performing the work twice. On a registration push into a CRM, it converts an ambiguous timeout into a safe retry.
The push job that moves registrations into the CRM ran at 03:12 and logged a gateway timeout on batch 41 of 62. The runner did what runners do, retried the batch, finished clean, and reported success. On Monday the audience acquisition lead opens a campaign segment and finds the same buyer twice, two record IDs, both stamped 03:12.
Idempotency keys are the control that stops this, and the reason they are worth understanding rather than copying from a code sample is that the failure they prevent is invisible at the moment it happens. Your job did not error. It succeeded twice.
What a retry does to a CRM that was already listening
A timeout tells you that you stopped waiting. It says nothing about what the other system did.
There are three states hiding behind one gateway 504. The request never arrived, so nothing was written. The request arrived, was processed in full, and the response was lost on the way back. Or the request arrived and was halfway through a batch when the connection dropped, so some rows exist and some do not. From the client's side these look identical, and the only honest description of your CRM's state after a timeout is that you do not know.
Retrying is still the right instinct. The alternative, treating every timeout as a failure and skipping the batch, loses registrations quietly, which is worse than duplicating them loudly. The problem is that a plain retry resolves the ambiguity in the most expensive direction: it assumes nothing was written, and it is wrong roughly whenever the write had already landed.
What does an idempotency key actually guarantee?
The mechanism is simple enough to hold in your head. The client generates a unique value, sends it as a header with the write, and the server records the outcome against that value. Any later request carrying the same value gets the recorded outcome back instead of doing the work again.
Stripe's documentation is the clearest published description of the contract, and it is worth quoting because the detail people miss is in the second clause. Stripe's idempotency "works by saving the resulting status code and body of the first request made for any given idempotency key, regardless of whether it succeeds or fails. Subsequent requests with the same key return the same result, including 500 errors" (Stripe, 2026). Failures are stored too. That is deliberate, and it means a retry cannot quietly turn a rejected write into an accepted one.
Two other details from the same page shape how you use it. Keys are pruned once they are at least 24 hours old, and a key reused after pruning generates a new request, which sets a hard ceiling on how long a resumable job can pause. And the idempotency layer compares incoming parameters against the original request and errors when they differ, so you cannot reuse a key for a modified payload and expect the vendor to sort it out.
Stripe also draws a line that matters for backfills: results are saved only after execution of an endpoint begins, so a request rejected at validation, or one that collides with a concurrent request on the same key, leaves nothing stored and can be retried cleanly (Stripe, 2026).
The arithmetic of one bad batch
Put numbers on the Monday morning above, because the size of the mess decides whether anyone notices it.
Say the show has 12,400 registrations to push and the job batches them 200 at a time, so 62 requests. Batch 41 times out at the gateway's 30 second limit. The CRM had already committed 173 of the 200 rows when the connection dropped. The runner retries the whole batch without a key, and the CRM, having no way to recognise the repeat, creates 173 new records and 27 first-time records.
You now hold 12,573 records for 12,400 people. As a percentage that is 1.4 per cent duplication, which sounds survivable and is not, because the duplicates are not spread evenly. Every one of them sits in the block of registrations that arrived in the same window, which is usually a single day, often a single campaign send. The channel report for that day overstates by 173. Any deduplication you run later merges records that already have activity history attached to both copies.
Run the same batch with an idempotency key on each request and the retry returns the stored response for the 173 committed rows and completes the remaining 27. Total records written, 200. Total records in the CRM, 12,400. The job still logged a timeout, which is useful, because the timeout was real and someone should look at why batch 41 took more than 30 seconds.
How should you build the key?
The key has to identify the request, not the moment you sent it. A timestamp or a random value generated fresh at retry time defeats the whole mechanism, and this is the single most common implementation error, because the retry usually lives in a wrapper that has no idea it is repeating anything.
Generate the key once, when the request is first constructed, and store it alongside the payload so the retry path reads the same value. Stripe suggests V4 UUIDs or another random string with enough entropy to avoid collisions, allows up to 255 characters, and warns against putting sensitive data such as email addresses into the key (Stripe, 2026). That last point is easy to trip over in an event context, because a natural key like the registration email plus the show code is both readable and exactly the thing you should not send in a header that gets logged.
A composite of internal identifiers works well. Show instance, batch number and a hash of the payload gives you a value that is stable across retries, unique across batches, and meaningless to anyone reading your access logs. Storing it in your own job table is the part people skip, and it is what lets a job that died overnight resume the next morning without re-sending anything.
What the draft standard adds that vendor documentation does not
The IETF httpapi working group has a draft specification for this header, "The Idempotency-Key HTTP Header Field" by Jena and Dalal, whose seventh revision was published in October 2025 and has since expired without becoming an RFC. Treat it as a well-argued design document rather than a standard you can hold a vendor to.
Its contribution is a defined error vocabulary, which vendor documentation tends to leave vague. The draft says a resource should reply with 400 when the header is missing for a documented idempotent operation that requires it, 422 when a key is reused with a different request payload, and 409 when a retry arrives while the original request is still being processed (Jena and Dalal, 2025). That third case is the interesting one for a registration push, because a client that fires a retry on a short timeout while the first request is still running needs to know the difference between "already done" and "still running". A 409 tells you to wait and ask again. A 422 tells you your client has a bug.
The draft also describes an idempotency fingerprint, a checksum or digest of the request payload, used alongside the key to decide whether two requests really are the same. That is what Stripe's parameter comparison is doing under a different name. If you are building the receiving end of an integration, copy it, because a fingerprint is what lets you tell a genuine retry from a client that reused a key by accident.
When your event platform is the one calling you
Most of this post assumes you are the client. Increasingly you are also the server, because the registration platform posts to your endpoint and your endpoint writes to the warehouse.
The same discipline applies in reverse, and the honest position is that you should implement it even though almost no event vendor sends an idempotency key. What they do send, reliably, is an event ID. Log the IDs you have processed, check before you write, and you have built the receiving half of the contract yourself. This is the same habit that stops a redelivered webhook from double-counting a registration, and the wider question of what to do when two systems have already drifted apart belongs with reconciling two systems that disagree about the same record.
Deciding how long to wait before the retry, and how to spread retries so a whole fleet of jobs does not return at the same instant, is a separate design problem covered in a backoff strategy for the busiest registration day. So is the prior question of whether a given failure should be retried at all, which depends on what the vendor's error response actually means.
Where this stops
An idempotency key protects one request. It does nothing for a job that is not idempotent as a whole.
If your push builds its batch by querying "registrations created since the last successful run" and the run marker is updated before the push completes, a rerun skips rows and no key anywhere will bring them back. If the CRM applies a workflow on record creation that sends an email, the stored response protects the record and not the email, because the side effect happened inside the first request and the vendor's replay does not undo it.
The other limit is a plainer one. Vendors that document idempotency properly are mostly payment processors and infrastructure providers. Registration platforms, exhibitor portals and lead retrieval systems largely do not, and where the documentation is silent you should assume the header is ignored rather than honoured. Testing that takes ten minutes: send the same create request twice with the same key and count the records. If you get two, you are building the deduplication yourself, and the sooner that is a known fact in the integration design the less it costs.
This week, take your registration push job and find the line where the retry happens. If the key, or any request identifier, is generated inside that retry rather than passed into it, you have the bug described here, and it will show up on the next slow night.
Questions people ask about idempotency keys
- What is an idempotency key in an API integration?
- It is a unique value the client generates and sends with a write request. The server stores the outcome of the first request carrying that value and returns the stored outcome for any later request carrying it again. Stripe saves the status code and body of the first request and replays them, including a 500 error, so a retry cannot create a second object.
- How long do idempotency keys stay valid?
- That depends on the vendor and it is worth reading before you design a backfill. Stripe removes keys automatically once they are at least 24 hours old and treats a reused key after pruning as a new request. The IETF working group draft leaves expiry to the implementer and says the resource should define an expiration policy rather than assume one.
- Can you send the same idempotency key with different parameters?
- No, and a well-built API will stop you. Stripe compares the incoming parameters against the original request and returns an error when they differ. The IETF draft recommends a 422 response for a key reused with a different payload and a 409 when a retry arrives while the original request is still being processed.