How to implement idempotency in Webhooks
Asaas Webhooks guarantee that events will be delivered at least once, following the "at least once" principle. This means that your endpoint may occasionally receive the same webhook event multiple times in certain situations. For example, when Asaas does not receive a response from your endpoint.
Therefore, the ideal approach is for your application to handle duplicate events using idempotency, and this article aims to explain how idempotency works and how you can protect your application.
What is idempotency?
Idempotency refers to the ability of an operation (function) to consistently return the same result, regardless of how many times it is executed, as long as the parameters remain unchanged.
Bringing this concept into the context of Webhooks, if Asaas occasionally sends the same webhook twice, the ideal behavior is for your application to respond to both requests with HTTP Status 200, always maintaining the same outcome as the first request received.
Why use idempotency?
Before explaining why idempotency is important, let's look at the main HTTP verbs: GET, PUT, DELETE, and POST.
When REST patterns are properly applied, the GET, PUT, and DELETE methods are inherently idempotent:
GETis a retrieval operation that does not modify the resource state.PUT, when executed multiple times with the same parameters, always produces the same result.DELETEchanges the resource state to deleted on the first request, and additionalDELETErequests keep the resource in the same state.
However, POST is the only HTTP verb that is not idempotent by default:
POSTcan create a new unique resource every time the operation is executed.
Since Asaas Webhooks use the POST method, it is important that your application applies the concept of idempotency so that duplicate Webhook deliveries do not interfere with your system's business logic.
Idempotency strategies
1. Using a unique index in the database
Events sent by Asaas Webhooks have unique IDs, and even if they are delivered more than once, you will always receive the same ID.
One strategy is to create an event queue in your database and use this ID as a unique key.
CREATE TABLE asaas_events (
id bigint PRIMARY KEY,
asaas_event_id text UNIQUE NOT NULL,
payload JSON NOT NULL,
status ENUM('PENDING','DONE') NOT NULL
[...]
);The recommended approach is that, when receiving the event from Asaas, you persist the information in a table like the one above and respond with HTTP 200 to Asaas to acknowledge successful receipt.
Remember to return HTTP 200 only after the event has been successfully persisted in your database table, since we do not guarantee that this event will be resent automatically.
After that, create a processing routine, such as Cron Jobs or Workers, to process persisted events with status = PENDING. Once processing is complete, mark them as DONE or simply remove them from the table.
If event order is important to your system, make sure to fetch and process them in ascending order.
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;
const eventType = body.event;
const payload = body; // Persist the entire payload to inspect "event" during processing
const status = "PENDING";
try {
await client.query(
`
INSERT INTO asaas_events (
asaas_event_id,
payload,
status
)
VALUES ($1, $2, $3)
`,
[eventId, payload, status]
);
} catch (e) {
// PostgreSQL code for unique violation
if (e.code === "23505") {
return response.json({ received: true });
}
throw e;
}
// Return a response indicating that the webhook was received
return response.json({ received: true });
}
);
app.listen(8000, () => console.log('Running on port 8000'));If your system receives hundreds of thousands of events per day, the recommendation is to use a more robust queue solution such as Amazon SQS, RabbitMQ, or Kafka.
In addition to solving the idempotency issue, this approach also promotes asynchronous event processing, providing faster responses to Asaas and greater throughput.
2. Storing processed events
Another common strategy is to process Webhooks and store each event ID in a table.
CREATE TABLE asaas_processed_webhooks (
id bigint PRIMARY KEY,
asaas_evt_id text UNIQUE NOT NULL,
[...]
);This way, whenever a new event is received, you can check this table and verify whether the ID has already been processed.
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;
try {
await client.query(
`
INSERT INTO asaas_processed_webhooks (
asaas_evt_id
)
VALUES ($1)
`,
[eventId]
);
} catch (e) {
// PostgreSQL code for unique violation
if (e.code === "23505") {
return response.json({ received: true });
}
throw e;
}
switch (body.event) {
case 'PAYMENT_CREATED':
const payment = body.payment;
createPayment(payment);
break;
// ... handle other events
default:
console.log(`This event is not supported ${body.event}`);
}
// Return a response indicating that the webhook was received
return response.json({ received: true });
}
);
app.listen(8000, () => console.log('Running on port 8000'));In this approach, the table is used as a control mechanism after processing, which still happens within the request timeout limit.
Best practices
In addition to the strategies presented above, we recommend:
- responding quickly with HTTP 200 after persisting the event;
- processing business rules asynchronously;
- using the event identifier (
id) as a unique key; - monitoring pending events and processing failures;
- processing events in chronological order when sequence matters;
- using queue solutions such as RabbitMQ, Amazon SQS, or Kafka for high-volume integrations.
Next steps
After implementing idempotency, we recommend reviewing:
- Receive events from Asaas in your Webhook endpoint;
- Delivery types;
- Webhook events;
- Webhook logs;
- Queue penalties;
- Paused queue;
- Official Asaas IPs.
