Billing, Pix and boleto
WhatsApp with the Asaas webhook: notify when a Pix is paid or a charge goes overdue
Asaas sends a POST to your URL on every charge change, with the event name in event and a payment object. That payload carries the customer id (payment.customer) but not the customer's mobile number. So the link to WhatsApp goes through a step of yours: receive the event, look up the customer in Asaas and call the ZapZap API. This page shows that full path, with code, and says when it is not worth it.
Updated on
Why connect
If you bill by Pix, boleto or card on Asaas, you lose time on two repeated problems: the customer says they paid and you do not see it, or you forget a charge went overdue. With the webhook on, a confirmed payment produces a receipt message within seconds and an overdue charge produces a reminder the same day, with nobody opening the dashboard. WhatsApp is for notifying people who are already customers: message only people who have a relationship with you and make it clear how to stop receiving messages. ZapZap connects numbers through an unofficial, managed channel, so pace and consent matter. It costs R$15 per active number per month, prepaid, and you start with a welcome credit when you create the account, subject to the current campaign.
Step by step
- 1
Create the webhook in Asaas with an auth token
In Asaas, create a webhook in the web app or through the API (up to 10 per account) pointing to your HTTPS endpoint, and tick only the events you will use, for example PAYMENT_RECEIVED and PAYMENT_OVERDUE. Set the authToken: Asaas sends it on every notification in the asaas-access-token header. It must be 32 to 255 characters long, with no spaces, and it cannot be an Asaas API Key. Test in the Sandbox first: Sandbox and Production settings are independent.
- 2
Validate the token and store the event before answering
In the endpoint, compare the asaas-access-token header with your token and reject (401) if it does not match. Then store the event id in a table with a uniqueness constraint and only then answer HTTP 200. Asaas delivers each event at least once, so the same id can arrive again, and the id is your idempotency key. A slow answer causes a timeout, so the WhatsApp send goes after the 200.
- 3
Look up the customer's mobile number
The payload has payment.customer (something like cus_000005913252), not the phone. Call GET https://api.asaas.com/v3/customers/{id} with the access_token header and read mobilePhone (mobile) or phone (landline or general). If both are empty there is nobody to send to: log it and move on. Normalize to digits and add Brazil's 55 if it is missing.
- 4
Send the message through the ZapZap API
Call POST https://api.zapzapapi.com/api/v1/{instanceId}/send/text with x-api-key, x-api-secret and a JSON body with number and text. Build the text with the name, the amount (payment.value) and the charge link, if you store one. Retry 429 and 5xx with spacing, and treat 400, 401 and 403 as configuration errors, without retrying.
- 5
Reactivate the queue if it pauses
After 15 consecutive delivery failures Asaas stops that webhook's queue, keeps the pending events for up to 14 days and warns you by email. Fix the endpoint, reactivate the queue in the Asaas dashboard and check the HTTP code and attempts under Integrations > Webhook Logs. Set up your own alert for 5xx errors on the endpoint, because nobody reads the Asaas email every day.
Example
// Express: receives the Asaas event and notifies the customer on WhatsApp
import express from 'express';
const app = express();
app.post('/asaas', express.json(), async (req, res) => {
if (req.get('asaas-access-token') !== process.env.ASAAS_WEBHOOK_TOKEN) return res.sendStatus(401);
const { id, event, payment } = req.body;
// Idempotency: INSERT with UNIQUE on the event id. If it already exists, just acknowledge.
const isNew = await saveEventOnce(id, req.body); // your function
res.sendStatus(200); // answer 200 right after storing
if (!isNew || event !== 'PAYMENT_RECEIVED') return;
const cust = await fetch('https://api.asaas.com/v3/customers/' + payment.customer, {
headers: { access_token: process.env.ASAAS_API_KEY },
}).then((r) => r.json());
const phone = String(cust.mobilePhone || cust.phone || '').replace(/\D/g, '');
if (!phone) return;
await fetch('https://api.zapzapapi.com/api/v1/' + process.env.ZAPZAP_INSTANCE + '/send/text', {
method: 'POST',
headers: {
'x-api-key': process.env.ZAPZAP_API_KEY,
'x-api-secret': process.env.ZAPZAP_API_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
number: phone.startsWith('55') ? phone : '55' + phone,
text: 'Hi, ' + cust.name + '! We received your payment of R$ ' + payment.value + '. Thank you!',
}),
});
});Illustrative. The exact fields and endpoints are in the documentation.
Which Asaas events are worth a message
Asaas has a long list of charge events (refunds, chargeback, Split, collections). For day-to-day billing, four are enough, and what matters is picking one per goal so you do not send two messages about the same fact.
| Event | When it happens (according to Asaas) | Use with WhatsApp |
|---|---|---|
| PAYMENT_CREATED | A new charge is generated. | New charge notice, if the customer expects one. |
| PAYMENT_CONFIRMED | Payment made, but the balance is not available yet. | Card: first sign of payment. Use this or RECEIVED, not both. |
| PAYMENT_RECEIVED | Charge received, with the amount available in the Asaas account. | Receipt and access release. |
| PAYMENT_OVERDUE | Charge overdue. | Due date reminder, with a helpful tone. |
Security, idempotency and what Asaas asks of you
- Authentication: Asaas does not sign the body with HMAC. What exists is the authToken, sent in the asaas-access-token header. Always validate it, and if your infrastructure filters by origin, use the official Asaas IP list.
- Idempotency: the event id is unique and repeats on redeliveries. Store it with a uniqueness constraint before processing, and also use payment.id so you do not notify the same payment twice.
- Response: return HTTP 200. The paused queue page says only 200 counts as success, while the penalty page mentions any 2xx; use 200 and stay on the safe side.
- Retries: the first failure retries after 30 seconds, then 1 minute, 3 min 30 s and so on, with intervals reaching 3 hours. On the 15th consecutive failure the queue pauses.
- Reuse: one Asaas webhook, one endpoint of yours. Give it an internal queue and process the WhatsApp send in the background.
When NOT to use this path
If your goal is recovering abandoned carts for a digital product, Asaas is not the place: it bills, it does not monitor checkout. In that case, look at Kiwify, Hotmart or Eduzz, which have a cart event and already work with the ZapZap Sales Recovery assistant with no code. If you have nobody to maintain an endpoint, use n8n with a Webhook node and an HTTP Request node, which follows the same path without your own server. And do not use WhatsApp to bill people who never talked to you: without a prior relationship and consent, the risk of reports and of the number being blocked is high, and LGPD (Brazil's data protection law) requires a legal basis for contact.
Sources and date checked
Events, payload structure, idempotency, the asaas-access-token header, token rules, retries and the paused queue were checked on 29 Oct 2026 in the official Asaas documentation (docs.asaas.com), on the pages Charge events, Receive Asaas events at your webhook endpoint, How to implement idempotency in webhooks, Paused queue and Queue penalties. The mobilePhone field comes from the Retrieve a single customer reference. Documentation changes: check the pages before going to production.
Frequently asked questions
Does Asaas send the customer's phone in the webhook?
No. The payment object has the customer id in payment.customer. You look up the mobile number with GET /v3/customers/{id}, in the mobilePhone or phone field. That is why the link to ZapZap goes through an endpoint or an n8n flow, and not through ZapZap's direct webhook trigger.
How do I know the webhook really came from Asaas?
Set the authToken on the webhook and compare it with the asaas-access-token header on every request. It is not an HMAC signature of the body, it is a fixed token, so keep it as a secret and never use an Asaas API Key in its place. If you restrict by origin, use the official IPs published by Asaas.
I received the same event twice. Is that a bug?
No. Delivery is at least once, and the same id can arrive again. Store the event id with a uniqueness constraint and ignore what was already processed. Without that, the customer gets the same receipt twice.
What happens if my server is down?
Asaas retries with growing intervals. After 15 consecutive failures it pauses that webhook's queue and keeps the events for up to 14 days. Fix the endpoint and reactivate the queue in the dashboard; after that period, the oldest events are deleted.
Can I use the Sandbox to test?
You can and should. Sandbox and Production settings are independent, so repeat the webhook setup in production after validating the flow. On the ZapZap side, test with your own number before messaging customers.
Which event do I use to say the Pix was paid?
PAYMENT_RECEIVED means the charge was received with the amount available in the account. PAYMENT_CONFIRMED means payment was made but the balance is not available yet. Pick one of the two for the message and use payment.id so you do not notify twice.
Can I send billing messages on WhatsApp to any customer?
Only to people who have a relationship with you and expect this contact. Use a reminder tone, without embarrassment, and offer a way to stop receiving messages. ZapZap is an unofficial channel and does not remove the risk of blocking; consent, a human pace and a clean list remain on you.
Start now
Create your account, connect a number in minutes and test the API with warm-up and anti-ban. R$15 per active number/month, prepaid, no lock-in.
