
APIs & Automation / For developers building product integrations
Imagine a customer completes a booking. The payment provider sends your application an event. Your server saves the booking, but its response is lost before the provider receives it. A little later, the same event arrives again.
Nothing unusual has happened from the provider's point of view. It still needs to know whether the event was received. Your application now has to decide whether this delivery should create anything new.
This is one of the details I care about in API and workflow work. Booking systems and connected SaaS products need a plan for uncertain outcomes. A successful request in a local test only covers part of the job.
Read the delivery contract first
Providers have different rules for retries, timeouts, signatures, and event retention. Read those rules before designing the endpoint around an assumption.
For example, Stripe's webhook documentation says that events can be delivered more than once and are not guaranteed to arrive in the order they were generated. It also describes verifying signatures with the raw request body and returning a successful response promptly.
Those details shape the application. Signature verification belongs before processing. Event ordering cannot be inferred from arrival time. Long-running work should not keep the provider waiting for everything to finish.
In the rest of this article, consider an illustrative booking integration. It is an architecture example, not a claim that every provider or every project uses the same delivery rules.
Separate receipt from processing
I want receipt to be a small, durable operation. Verify the request, validate the event envelope, and record enough information to process it safely. Then acknowledge successful receipt.
One option is a database-backed inbox. It stores the provider, account or endpoint context where needed, event identifier, type, receipt time, and a processing status. A worker reads pending entries and performs the application work.
If you use a separate queue, be explicit about the boundary. Saving an inbox row and publishing a queue message are two operations unless something coordinates them. A crash between those steps can leave a recorded event that no worker knows to process. A worker that polls the inbox, or an appropriate transactional outbox design, can close that gap.
Do not send a success response before the event is durably accepted. Otherwise, a crash can lose work the provider believes you received. At the same time, acceptance should not wait for a slow report or email to finish.
Give each operation a stable identity
For delivery deduplication, a provider's event identifier is a useful starting point. Scope it correctly if several provider accounts share the same inbox. Enforce uniqueness in the database so two concurrent requests cannot both create the same entry.
A check followed by an insert is not sufficient on its own: both requests can pass the check before either inserts. PostgreSQL's constraint documentation explains how a unique constraint enforces uniqueness across rows. The application still needs to handle the resulting conflict deliberately.
There is a second identity to think about: the business operation. Two different events may refer to the same booking or the same state transition. Deduplicating the delivery does not automatically stop the application from issuing the same ticket twice through two different paths.
For our example, the booking transition and ticket record should have their own constraints. An event can ask the application to confirm a booking, but the database should also know that a given booking must not acquire duplicate confirmation records.
Make retries safe around the actual side effect
Marking an event as processed before the work finishes can lose work. Marking it only after an external action finishes can leave another problem: the action may succeed, the worker may crash, and the next attempt may repeat it.
Within one database, a transaction can commit the state change and the processing record together. An external API call introduces another boundary. It may need a stable idempotency key, a stored result, or a reconciliation step, depending on what the external service supports.
The AWS Builders' Library discusses this uncertainty in Making retries safe with idempotent APIs. The useful idea is to identify a logical operation so a retry can be recognised without treating every similar request as the same intent.
Keep a retry's identity stable. If the application generates a new key for every attempt, the receiver has no reason to recognise the request as a repeat. If the user intentionally starts a new operation, that operation needs its own identity.
Also read the provider's limits. Stripe's idempotent-request documentation, for example, defines how keys and repeated requests are handled. A provider's idempotency feature should not be assumed to cover every side effect in your own application.
Keep state changes deliberate
A delayed event should not casually move a record backwards. In the booking example, an older update about an incomplete payment should not overwrite a more recent confirmed state simply because it arrived later.
Define allowed transitions and consider how to obtain the current authoritative state when an event is ambiguous. The right approach depends on the provider. Timestamp comparisons alone can be too weak when several events describe different parts of the same operation.
I would also distinguish a retryable processing failure from an invalid event that needs investigation. Keep attempt counts, a useful error category, and a next-action field. After a bounded number of attempts, someone should be able to see and inspect the problem.
Test the awkward sequence
A useful integration review should include more than a successful delivery. I would exercise these cases with test events and controlled failures:
- The same event arrives twice, including at the same time.
- An older event arrives after a newer state change.
- The database is unavailable when receipt is attempted.
- A worker stops after updating local data.
- An external action succeeds but its response is lost.
- An operator retries a previously failed event.
The review should ask what the customer sees as well as what the logs contain. A pending booking needs an understandable status, and support needs a way to find the relevant operation.
When those behaviours are planned, retries become something the product can handle. That is the difference between an endpoint that receives data and an integration a team can operate with confidence.
Iām Amminadab Elias, an AI Product Engineer based in Addis Ababa, Ethiopia. Explore my APIs & Workflow Automation work, or tell me what you are building.

