How to implement idempotence in Webhooks
Implement idempotence in Webhooks
Asaas Webhooks use the at least once delivery model. Therefore, the same event may be sent more than once.
Use the event id field to identify duplicates and prevent a new delivery from running the same business rule again.
How it works
When Asaas resends the same event, the id stays the same.
Example:
{
"id": "evt_05b708f961d739ea7eba7e4db318f621&368604920",
"event": "PAYMENT_RECEIVED",
"payment": {
"id": "pay_080225913252"
}
}Your application must treat this identifier as a unique key.
%%{init: {"flowchart": {"nodeSpacing": 28,"rankSpacing": 32,"diagramPadding": 8,"padding": 10}}}%%
flowchart TD
A["Receive the event"] --> B{"Has the ID already been persisted?"}
B --> BSim(("Yes"))
B --> BNao(("No"))
BSim --> C["Respond HTTP 200"]
BNao --> D["Persist the event"]
D --> E["Respond HTTP 200"]
E --> F["Process in the background"]
F --> G["Mark as processed"]
classDef inicio fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A,stroke-width:3px,font-size:17px
classDef decisao fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:3px,font-size:17px
classDef validacao fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E,stroke-width:2px,font-size:17px
classDef sucesso fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:3px,font-size:17px
classDef respostaSim fill:#22C55E,stroke:#15803D,color:#FFFFFF,stroke-width:3px,font-size:16px
classDef respostaNao fill:#EF4444,stroke:#B91C1C,color:#FFFFFF,stroke-width:3px,font-size:16px
class A inicio
class B decisao
class C,D,E,F validacao
class G sucesso
class BSim respostaSim
class BNao respostaNao
linkStyle default stroke:#94A3B8,stroke-width:2px
linkStyle 1 stroke:#22C55E,stroke-width:4px
linkStyle 2 stroke:#EF4444,stroke-width:4px
Recommended strategy: persist before processing
Create a structure that records the received id and apply a uniqueness constraint to this field.
PostgreSQL example:
CREATE TABLE asaas_events (
id BIGSERIAL PRIMARY KEY,
asaas_event_id TEXT UNIQUE NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(16) NOT NULL
CHECK (status IN ('PENDING', 'DONE'))
);When receiving an event:
- try to persist the
idand the payload; - if the
idalready exists, treat the request as a duplicate; - after confirming persistence, respond
HTTP 200; - process the records with
status = 'PENDING'in the background; - after completing the business rule, change the status to
DONE.
AttentionRespond
HTTP 200only after confirming that the event has been persisted.Once the delivery is confirmed, your application must not depend on a new delivery to recover an event that was not stored.
Node.js example
const express = require('express');
const app = express();
app.post(
'/asaas/webhooks/payments',
express.json({ type: 'application/json' }),
async (request, response) => {
const body = request.body;
const eventId = body.id;
await client.query(
`
INSERT INTO asaas_events (
asaas_event_id,
payload,
status
)
VALUES ($1, $2, 'PENDING')
ON CONFLICT (asaas_event_id) DO NOTHING
`,
[eventId, body]
);
return response.status(200).json({
received: true
});
}
);
app.listen(8000, () => console.log('Running on port 8000'));With the UNIQUE constraint, a new delivery of the same id does not create another record.
The business rule must be executed by a Worker, Cron Job or another process responsible for consuming pending events.
Avoid processing before persisting
Another approach is to run the business rule during the request itself and record the event as processed afterwards.
This flow increases the risk of:
- a timeout before the response;
- simultaneous processing of the same event;
- running the business rule without a reliable record that the event was handled.
Prefer to reserve or persist the id atomically before running the business rule.
High-volume processing
For high-volume integrations, use a queue or broker suited to your application's architecture, such as:
- Amazon SQS;
- RabbitMQ;
- Kafka.
The principle remains the same: the event id must prevent a new delivery from triggering another processing of the same operation.
When event order is required
Idempotence prevents duplicates, but it does not define the delivery order.
If your application logic depends on the sequence in which events occurred, use the appropriate send type and also preserve that order during internal processing.
Validate the implementation
Test the endpoint by sending or triggering two deliveries with the same id.
The expected result is:
- only one record for the
id; - both requests receive
HTTP 200; - the business rule is executed only once.
Use the Webhook Logs to track delivery attempts.
Next steps
Updated 20 days ago
