# Guide to Asaas Webhooks: Features and Best Practices

Asaas is a Brazilian payments platform for charges by Pix, boleto and card, subscriptions, invoices and transfers. Its webhooks tell your application when a charge is paid, a subscription renews, a transfer settles or your account status changes, so your systems can react straight away instead of polling the API.

This guide covers how Asaas webhook configurations work, how to check the `asaas-access-token` header, the events and payloads you will receive, and how Asaas's retry penalty and paused queues affect you in production.

## What are Asaas webhooks?

An Asaas webhook is a configuration with a URL, a list of events, a send type and an authentication token. When a selected event occurs, Asaas sends a `POST` with a JSON body describing the event and the resource it relates to. Each account can have up to 10 webhooks, with different URLs and events.

Asaas doesn't sign payloads. Instead, you choose an `authToken` when you create the webhook, and Asaas sends it in the `asaas-access-token` header of every notification. Your endpoint compares the header with the stored value.

Each webhook also has a send type. `SEQUENTIALLY` delivers events in the order they occurred, and `NON_SEQUENTIALLY` delivers them in parallel with no ordering guarantee.

## Asaas webhook features

| Feature | Details |
| --- | --- |
| Configuration | Web app (**Integrations** > **Webhooks**) or `POST /v3/webhooks` |
| Limit | 10 webhooks per account |
| Environments | Sandbox and production are configured separately |
| Authentication | `authToken` you choose, sent in `asaas-access-token` |
| Token rules | 32 to 255 characters, no whitespace, no simple sequences, not an API key |
| Signature | None |
| Event field | `event` in the body |
| Send types | `SEQUENTIALLY` (ordered) or `NON_SEQUENTIALLY` (parallel) |
| Delivery guarantee | At least once |
| Retries | 15 attempts with increasing intervals, then the queue is interrupted |
| Retention | Undelivered events are kept for 14 days |
| Redirects | Not followed |
| Logs | **Webhook Logs**, with payload, status code, attempts and errors |

## Common events

Asaas groups events by resource. Each webhook subscribes to the individual events it needs:

| Resource | Example events |
| --- | --- |
| Charges | `PAYMENT_CREATED`, `PAYMENT_CONFIRMED`, `PAYMENT_RECEIVED`, `PAYMENT_OVERDUE`, `PAYMENT_REFUNDED` |
| Subscriptions | `SUBSCRIPTION_CREATED` |
| Invoices | `INVOICE_CREATED` |
| Transfers | `TRANSFER_CREATED` |
| Bill payments | `BILL_PAID` |
| Anticipations | `RECEIVABLE_ANTICIPATION_CREDITED` |
| Account status | `ACCOUNT_STATUS_COMMERCIAL_INFO_APPROVED` |
| Checkout | `CHECKOUT_CREATED` |
| Balance blocks | `BALANCE_VALUE_BLOCKED` |
| Internal movements | `INTERNAL_TRANSFER_DEBIT` |
| API keys | `ACCESS_TOKEN_CREATED` |
| Automatic Pix | `PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED`, `PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CANCELLED` |

Mobile phone recharges have their own events too. Each resource's page in Asaas's docs lists its full set.

Every event shares an envelope of `id`, `event` and `dateCreated`, plus an object for the resource. Here is Asaas's `PAYMENT_RECEIVED` example:

```json
{
  "id": "evt_123456789",
  "event": "PAYMENT_RECEIVED",
  "dateCreated": "2026-06-19 14:30:00",
  "payment": {
    "object": "payment",
    "id": "pay_080225913252",
    "customer": "cus_000005913252",
    "value": 150.00,
    "netValue": 148.35,
    "billingType": "PIX",
    "status": "RECEIVED"
  }
}

```

The resource key changes with the event: `payment`, `subscription`, `invoice`, `transfer` and so on. Some events, such as transfers, also include an `account` object. `id` identifies the event and stays the same on redelivery, so use it to deduplicate. `dateCreated` has no time zone.

## Setting up Asaas webhooks

1. Generate a token of 32 to 255 characters with no whitespace, for example a long random string. Don't reuse an Asaas API key.
2. Create the webhook in the web app under **Integrations** > **Webhooks**, or through the API:
  
  
  ```json
  POST /v3/webhooks
  
  {
    "name": "Payments Webhook",
    "url": "https://your-app.com/webhooks/asaas",
    "email": "integrations@example.com",
    "enabled": true,
    "interrupted": false,
    "apiVersion": 3,
    "authToken": "a-random-token-of-at-least-32-characters",
    "sendType": "SEQUENTIALLY",
    "events": ["PAYMENT_RECEIVED", "PAYMENT_OVERDUE"]
  }
  
  ```
  
  
3. Store the token, for example as `ASAAS_WEBHOOK_TOKEN`. Asaas returns it only in the creation response, and later queries don't include it. Store the webhook's `id` too, so you can update or reset it later.
4. Test in the sandbox: trigger an event, such as creating a charge, and confirm your endpoint receives it. Then repeat the configuration in production, because the two environments are independent.
5. Check **Webhook Logs** for the status code and response of each attempt.

## Securing Asaas webhooks

Compare `asaas-access-token` with your stored token in constant time:

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyAsaasToken(tokenHeader, expectedToken) {
  if (!tokenHeader || !expectedToken) return false;
  const received = Buffer.from(tokenHeader, 'utf8');
  const expected = Buffer.from(expectedToken, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/asaas', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.ASAAS_WEBHOOK_TOKEN;
  if (!token) return res.sendStatus(500); // fail closed

  if (!verifyAsaasToken(req.get('asaas-access-token'), token)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Persist event.id first, then respond and process event.event asynchronously.
  res.status(200).json({ status: 'SUCCESS' });
});

```

The same check in Python:

```python
import hmac
import os

def verify_asaas_token(token_header, expected_token: str) -> bool:
    if not token_header or not expected_token:
        return False
    return hmac.compare_digest(token_header.encode("utf-8"), expected_token.encode("utf-8"))

# In FastAPI:
# verify_asaas_token(request.headers.get("asaas-access-token"), os.environ["ASAAS_WEBHOOK_TOKEN"])

```

Return exactly `200`. Asaas's queue penalty page counts any `2xx` as success, but its paused queue page says only `200` counts and that `201` and `204` are failures. Webhooks configured with an `apiVersion` below 3 also need `SUCCESS` in the response body, which is why the example returns `{"status": "SUCCESS"}`.

> **Make Asaas webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `asaas-access-token` header, answers Asaas with a `200` straight away, and holds every payment event for retry and replay so your queue never gets paused.

In Hookdeck, create an Asaas source and enter the same token. Hookdeck compares it with `asaas-access-token` on each request. Verification is optional on the source, so leave verification off if you didn't set an `authToken`. Hookdeck's default response is a `200` with a JSON body that includes `"status": "SUCCESS"`.

## Asaas webhook limitations and pain points

### Failures pause the whole queue

**The Problem:** A few hours of errors stops all deliveries for that webhook, and events older than 14 days are deleted.

**Why It Happens:** Asaas retries with growing intervals: immediately, then after 30 seconds, 1, 3.5, 5, 15 and 25 minutes, then hourly, then two and three hours. After 15 consecutive failures the queue is interrupted. New events keep being generated and stored, but nothing is sent until you reactivate the queue.

**Workarounds:**

* Act on the alert emails Asaas sends at attempts 5, 10 and 15.
* Fix the cause, then use **Remove penalty** (or `POST /v3/webhooks/{id}/removeBackoff`) to skip the remaining wait. An interrupted queue needs to be reactivated separately.
* Don't automate penalty removal. Asaas rate-limits that endpoint to discourage it.

**How Hookdeck Can Help:** Hookdeck answers Asaas with a `200` as soon as it stores the event, so your service's errors don't count against Asaas's queue. Hookdeck then retries delivery to your service on its own schedule and lets you replay anything that failed.

### Sequential delivery blocks on one bad event

**The Problem:** One event that your endpoint keeps rejecting holds up every event after it.

**Why It Happens:** In `SEQUENTIALLY` mode, events wait for the previous one to be delivered. If `PAYMENT_CONFIRMED` is penalised, `PAYMENT_RECEIVED` waits behind it.

**Workarounds:**

* Use `NON_SEQUENTIALLY` when your logic doesn't depend on order, and use `dateCreated` and the resource's status to resolve conflicts.
* Never return an error for an event you can't process yet. Store it and return `200`.

**How Hookdeck Can Help:** Hookdeck accepts each event in order, so Asaas's queue keeps moving. Failed deliveries to your service are retried individually rather than blocking the stream.

### A shared token, not a signature

**The Problem:** The token proves who sent the request, but doesn't protect the body.

**Why It Happens:** Asaas sends the same static header with every notification and computes nothing over the payload.

**Workarounds:**

* Use a long random token and HTTPS.
* Allowlist Asaas's official IPs, remembering that the sandbox may use extra addresses.
* For high-value actions, fetch the resource from the Asaas API before acting.

**How Hookdeck Can Help:** Only Hookdeck needs the Asaas token. Your services receive events from Hookdeck and don't need to store or compare it.

### Redirects and firewalls cause silent failures

**The Problem:** Deliveries fail even though the endpoint works in a browser.

**Why It Happens:** Asaas doesn't follow redirects, so a `301` from HTTP to HTTPS or a trailing-slash redirect fails. A WAF that challenges unknown clients returns `403`.

**Workarounds:**

* Register the final HTTPS URL.
* Add allow rules for Asaas's IPs in your firewall or WAF.

**How Hookdeck Can Help:** Hookdeck gives Asaas a stable HTTPS URL that doesn't redirect, and its request log shows exactly what arrived.

## Best practices

### Persist, respond, then process

Asaas's own guidance is to store the event, return `200`, and run business logic asynchronously. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Deduplicate on the event `id`

Delivery is at least once in both send types. Keep a record of processed `id` values and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Subscribe only to what you use

Asaas recommends configuring only the events your integration needs. With 10 webhooks per account, you can also split payment events and account events across separate URLs.

### Monitor Webhook Logs

Check the logs for rising error counts before a queue gets interrupted, and resolve any paused queue within 14 days.

## Conclusion

Asaas webhooks deliver payment, subscription, transfer and account events at least once, authenticated by a token in the `asaas-access-token` header. Compare the token in constant time, deduplicate on `id`, return `200` quickly, and watch your queue: 15 consecutive failures pause it, and events are deleted after 14 days.

[Hookdeck Event Gateway](https://hookdeck.com) keeps Asaas's queue moving by acknowledging every event, then retries delivery to your service until it succeeds.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Asaas webhooks reliably in minutes.