Asaas, with a clock you move.

The Asaas API v3 on localhost, with the business rules captured from the real sandbox. Skip days and watch billing happen: subscriptions charge the saved card on the due date and retry when it is refused, boletos go overdue with their fine and interest, cards credit at D+32, and every webhook reaches your machine. Debug your payment state machine end to end, not by guessing.

Run it on your machine Use cases
ASAAS MOCK
replay of a run
api-sandbox.asaas.com/v3 localhost:45445/v3
  1. 27/10/2026
  2. POST /v3/creditCard/tokenizeCreditCard 200
    MASTERCARD 8829, token 4e0459fd…
  3. PUT /_admin/credit-cards/4e0459fd… 200
    {"approvalsLeft":3}
  4. POST /v3/subscriptions 200
    CREDIT_CARD R$ 129,90 MONTHLY
    first due 05/11
  5. webhook PAYMENT_CREATED 200
  6. 05/11/2026
  7. webhook PAYMENT_CONFIRMED 200
  8. 05/12/2026
  9. webhook PAYMENT_CONFIRMED 200
  10. 05/01/2027
  11. webhook PAYMENT_CONFIRMED 200
  12. 05/02/2027
  13. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
  14. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
  15. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
    charge still PENDING
  16. 06/02/2027
  17. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
  18. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
  19. webhook PAYMENT_CREDIT_CARD_CAPTURE_REFUSED
  20. webhook PAYMENT_OVERDUE 200
  21. 3 paid, 6 attempts refused, 1 overdue

A real run of the image: a saved card set to approve three charges pays three invoices on their due dates, then the fourth is refused at 08:00, 14:00 and 20:00 and three more times the next day, as Asaas retries, and goes overdue. Only the paper is drawn.

Run it on your machine.

Four ways, from the fastest to the most complete. The image is public: no login.

Ready image

The fastest way: the image straight from GHCR.

docker pull ghcr.io/instituto-ama/asaas-mock:latest
docker run -d -p 45445:45445 -e CLOCK_MODE=VIRTUAL_FLOWING \
  ghcr.io/instituto-ama/asaas-mock:latest

Panel and API at localhost:45445, with the virtual clock on. With the WhatsApp mock (port 3010), a Mailpit (port 8025) and the SMS mock (port 8899) up, it finds all three by itself and every charge already tells the payer.

In your docker-compose

To run next to your application, in its compose. Nothing to wire: Asaas finds WhatsApp, Mailpit and the SMS mock by service name.

services:
  asaas:
    image: ghcr.io/instituto-ama/asaas-mock:0.11.0
    ports:
      - '45445:45445'
    environment:
      ASAAS_API_KEY: '$$aact_hmlg_local'   # `$$` is a literal `$` in compose
      CLOCK_MODE: VIRTUAL_FLOWING
    depends_on: [whatsapp, mailpit, sms]
    volumes:
      - asaas-data:/data

  whatsapp:
    image: ghcr.io/instituto-ama/whatsapp-mock:0.5.0
    ports:
      - '3010:3010'

  mailpit:
    image: axllent/mailpit:v1.31.4
    ports:
      - '8025:8025'

  sms:
    image: ghcr.io/instituto-ama/sms-mock:0.2.0
    ports:
      - '8899:8899'

volumes:
  asaas-data:

From your application: http://asaas:45445/v3. The payer notices at localhost:8025 (e-mail), localhost:3010 (WhatsApp) and localhost:8899 (SMS).

Build from source

To run from the source, or after changing the mock.

git clone https://github.com/Instituto-AMA/ama4devs.git
cd ama4devs
docker build -t asaas-mock mocks/asaas
docker run -d -p 45445:45445 -e CLOCK_MODE=VIRTUAL_FLOWING asaas-mock

Panel and API at localhost:45445; WhatsApp, Mailpit and the SMS mock, when up, it finds by itself

Run everything

With the WhatsApp mock, Mailpit and the SMS mock, wired together: every charge tells the payer by e-mail, by WhatsApp and by SMS.

git clone https://github.com/Instituto-AMA/ama4devs.git
cd ama4devs
make setup

Asaas at localhost:45445, WhatsApp at localhost:3010, e-mails at localhost:8025, SMS at localhost:8899

The clock is the difference.

On the real sandbox, time is real: a card payment credits in 32 days and an overdue boleto takes a day to show up. Here every simulated day runs the jobs Asaas runs, in order, so you see your system react to months of billing in seconds.

Each day, a tick

  • Subscriptions create their next charges, 40 days ahead.
  • A saved card is charged on the due date at 08:00, 14:00 and 20:00, and retried the next day.
  • Unpaid charges go OVERDUE; the fine once, interest per day.
  • Boleto credits at D+1 business day, card at D+32, debit at D+3.
  • Webhooks fire and retry on Asaas's own schedule.
  • Payer reminders and notices go out by e-mail and WhatsApp.
  • API keys with an expiry date expire.
curl -X POST localhost:45445/_admin/clock/advance \
  -H 'content-type: application/json' -d '{"days": 32}'

Three modes

  • REAL: the machine's time, like the sandbox; the clock refuses to skip.
  • VIRTUAL_FLOWING: time flows, and you skip ahead when you want. The default in the repository's compose.
  • VIRTUAL_FROZEN: the clock moves only when a test says so. Made for CI: the same run gives the same result.
The virtual clock in the simulator's panel: its time and mode, and the buttons to advance one, seven or thirty-two days, run a tick or go back to the present.

Use cases.

Scenarios your payment code has to survive, each one a few calls away. They assume the quick start's container: key $aact_hmlg_local, clock in VIRTUAL_FLOWING.

A saved card that pays three months and fails the fourth

Proves your subscription state machine end to end, instead of by guessing: invoices paid by the saved card on their due dates, then a refused renewal. Asaas tries the card at 08:00, 14:00 and 20:00 on the due date and three more times the next day; each refusal is a PAYMENT_CREDIT_CARD_CAPTURE_REFUSED while the charge stays PENDING, then PAYMENT_OVERDUE and no more attempts. Does your app warn the customer on the first refusal, keep access during the retries, and suspend only when the charge goes overdue?

# a saved card that approves three charges, then declines
curl -X PUT localhost:45445/_admin/credit-cards/<creditCardToken> \
  -H 'content-type: application/json' -d '{"approvalsLeft":3}'
curl localhost:45445/v3/subscriptions -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"CREDIT_CARD",
  "value":129.9,"nextDueDate":"2026-11-05","cycle":"MONTHLY","creditCardToken":"<creditCardToken>",
  "creditCardHolderInfo":{…},"remoteIp":"127.0.0.1"}'
curl -X POST localhost:45445/_admin/clock/advance -d '{"days":102}' -H 'content-type: application/json'
# from 27/10 to 06/02, the day after the fourth due date:
# → 3 × PAYMENT_CONFIRMED, then 6 × PAYMENT_CREDIT_CARD_CAPTURE_REFUSED and PAYMENT_OVERDUE

Swap the card with PUT /v3/subscriptions/{id}/creditCard during the retries and the next attempt captures with the new one. The schedule is configuration: CARD_CAPTURE_SCHEDULE or the panel's test cards page.

A card charge that credits 32 days later

Proves your order flow tells PAYMENT_CONFIRMED (the customer paid) from PAYMENT_RECEIVED (the money is yours), and that your balance view waits for the second.

curl localhost:45445/v3/payments -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"CREDIT_CARD",
  "value":350,"dueDate":"2026-10-27","creditCardToken":"…","remoteIp":"127.0.0.1"}'
# → CONFIRMED, netValue 339.04, estimatedCreditDate 2026-11-30
curl -X POST localhost:45445/_admin/clock/advance -d '{"days":34}' -H 'content-type: application/json'
# → webhook PAYMENT_RECEIVED; balance 339.04

A boleto paid late, with fine and interest

Proves you charge what Asaas charges: the 2% fine once on the first overdue day, the 1% a month interest pro rata over a 30-day commercial month. R$ 120,00 paid six days late is R$ 122,64.

curl localhost:45445/v3/payments -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"BOLETO",
  "value":120,"dueDate":"2026-12-02","fine":{"value":2},"interest":{"value":1}}'
curl -X POST localhost:45445/_admin/clock/advance -d '{"days":6}' -H 'content-type: application/json'
# → webhook PAYMENT_OVERDUE on 03/12
curl -X POST localhost:45445/v3/sandbox/payment/pay_…/confirm -H 'access_token: $aact_hmlg_local'
# → CONFIRMED, value 122.64; one day later PAYMENT_RECEIVED

Your webhook answers 204

Proves your endpoint answers what Asaas accepts. Only HTTP 200 counts: 201 and 204 are failures, the real rule. A failed delivery retries 15 times on Asaas's table (now, +30 s, +1 min, +3 min 30 s, +5 min, +15 min, +25 min, +1 h five times, +2 h twice, +3 h). With SEQUENTIALLY, the event behind it waits; after the 15th the webhook is interrupted and the whole queue stops.

curl localhost:45445/_admin/webhooks/deliveries   # every attempt, with your endpoint's status and body

A subscription, one year ahead

Proves your recurrence logic over twelve cycles in seconds. The first charge is born with the subscription; the next ones are born 40 days before their due date, as the real sandbox does.

curl localhost:45445/v3/subscriptions -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"BOLETO",
  "value":99.9,"nextDueDate":"2026-11-10","cycle":"MONTHLY"}'
curl -X POST localhost:45445/_admin/clock/advance -d '{"days":365}' -H 'content-type: application/json'
curl localhost:45445/v3/subscriptions/sub_…/payments -H 'access_token: $aact_hmlg_local'

A saved card that stops working

On the real sandbox a card that declines never becomes a token, so a saved card always approves. Here the test card 4000000000000341 tokenizes and then declines every charge made with its token, with the real error (invalid_action, same sentence): the renewal that fails months later, before it happens in production.

curl localhost:45445/v3/payments -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"CREDIT_CARD",
  "value":99.9,"dueDate":"2026-11-10","creditCardToken":"<token of 4000000000000341>"}'
# → 400 {"errors":[{"code":"invalid_action","description":"Transação não autorizada. …"}]}

Installments that add up

Proves your split of a total matches Asaas's: the rounding remainder goes to the last installment, the card fee truncates (R$ 350 in 12x is R$ 1,20 of fee per installment, not R$ 1,21), and on a card installment n credits at D+32×n.

curl localhost:45445/v3/payments -H 'access_token: $aact_hmlg_local' \
  -H 'content-type: application/json' -d '{"customer":"cus_…","billingType":"BOLETO",
  "totalValue":350,"installmentCount":12,"dueDate":"2026-11-10"}'

What the payer receives

Every charge event tells the payer by e-mail (in Mailpit), by a WhatsApp template (in the WhatsApp mock) and by SMS (in the SMS mock). An installment plan is told once. Run the repository's compose and open the screens side by side.

A charge with its invoice link, the payment buttons and its payer notifications: the e-mail sent to Mailpit and the WhatsApp template sent to the WhatsApp mock.

The panel: see what the real API hides.

Open http://localhost:45445. Manage customers, invoices, credit card payments, webhooks, notifications, and simulate time progression with the virtual clock.

Proven against the real sandbox.

The rules were captured from the Asaas sandbox and a parity suite replays them field by field, down to the exact set of keys of each object. The capture overturned 20 rules the documentation suggests. Every operation of the official specification answers: 135 of 213 with a business engine, the rest with 501 naming the operation, never fake data.