Why not just the sandbox
The Asaas sandbox does not deliver webhooks to localhost. That forces a tunnel, a staging deploy or hand-forged payloads, and none of them tests what matters: the credit delay, the overdue interest, the split, the webhook retry. And on the sandbox, a card payment credits in 32 real days.
This simulator delivers webhooks to your machine and lets you move time. Unlike a mock of static responses, it keeps state: balances, statements, queues and schedules.
The rules it follows
- A state machine per billing type. Pix goes straight to
RECEIVED; boleto credits at D+1 business day; credit card at D+32; debit at D+3. An unpaid charge passes throughOVERDUEfirst. - Real money.
netValue = value − fee, the fee is debited in the ledger, and the statement reconciles with the balance (a tested invariant). - Interest and fine. The fine once, on the first day overdue; interest pro rata die over a 30-day commercial month.
- Split across accounts. Every API key is an account with its own
walletIdand balance. A split moves money and posts entries to both statements. - Webhooks the way Asaas sends them. The
asaas-access-tokenheader; only HTTP 200 counts; 15 attempts on Asaas's table;SEQUENTIALLYwith real head-of-line blocking;interruptedat the end. - Card handling. Tokenization (the card number is never stored), test cards, pre-authorization and capture, chargeback. Card installments charge the card.
- Recurring card charges. On the due date the subscription's saved card is tried at 08:00, 14:00 and 20:00, and three more times the next day (Asaas's own account of its schedule, blog.asaas.com, 2024). Each refusal is a
PAYMENT_CREDIT_CARD_CAPTURE_REFUSEDwhile the charge staysPENDING; thenOVERDUE, never tried again. A new card on the subscription is used by its pending charges. A saved card can be set to approve N more charges and then decline. - Recurrence. A subscription creates its first charge on creation; the next ones are born 40 days before their due date. In an installment plan the rounding remainder goes to the last installment, and on a card installment
ncredits at D+32×n. - Pix that pays.
GET /v3/payments/{id}/pixQrCodereturns a real EMV BR Code with its CRC16 and the QR image: a phone camera reads it. - Subaccount onboarding. Create the subaccount, list the pending documents, upload a file, approve it.
Payer notifications
What Asaas tells the person who pays, delivered for real on your machine.
- Every charge event (created, updated, due soon, overdue, received) sends the payer an e-mail to Mailpit, a WhatsApp template through the WhatsApp mock's Cloud API and an SMS to the SMS mock (sender
ASAAS). - An installment plan is told once: the first installment's creation tells the whole plan (R$ 3.000,00 em 5x de R$ 600,00); reminders and payment notices stay per installment.
- Nothing to wire, however it runs (bun,
docker run, your compose,make setup): Mailpit, the WhatsApp mock and the SMS mock are found by themselves, by compose service name (mailpit,whatsapporwhatsapp-mock,smsorsms-mock), on this machine or on the Docker host, on boot and every 10 s, the closest one first. The address the WhatsApp mock calls back on is found the same way: the first one it verifies is kept. - The generator is found the same way (service
generatororgenerator-mock, port 45450, on boot and every 10 s): while reachable, the panel's Integrations › Generator tab lets you open it or populate the selected account with customers, charges, and confirmed payments. - Everything comes up configured: on boot and on every tick, the sender number gets this server's webhook and the payer templates are created in the WhatsApp mock, through Meta's own calls.
- Every customer is born with Asaas's eight default notification settings (
GET /v3/customers/{id}/notifications, changed withPUT /v3/notifications/batch), with e-mail, SMS and WhatsApp all on here (Asaas starts WhatsApp off). The due-date warning goes out N days before (10 by default) and on the due day; an overdue charge is told again every N days (7 by default) while unpaid; a subscription's charge is not told on creation. - The customer's
notificationDisabledand each setting are honored (the SMS needs a mobile phone on the customer), a failing channel never blocks the charge, and the panel resends on demand.
The panel
Open http://localhost:45445. It is not the Asaas dashboard: it is the simulator's, and it shows what Asaas does not let you see.
Zoom
Overview
Charges by status with their totals, test cards and the account's wallet and key.
Zoom
Customers
Manage payer profiles, CPF/CNPJ, contacts and billing history.
Zoom
Create Customer
Instant customer registration with complete input validation.
Zoom
Charges & Invoices
Single charges, installments and subscriptions; real-time status tracker.
Zoom
Create Charge
Configure customer, amount, due date, billing type (Pix, Boleto, Card).
Zoom
Invoice Page
What the payer opens at invoiceUrl: live payment simulation options.
Zoom
Pix QR Code
Scannable EMV BR Code with copy-and-paste payload.
Zoom
Bank Slip (Boleto)
Printable layout with standard 47-digit line and barcode.
Zoom
Webhooks
Endpoint URLs, authentication token, and queue status controls.
Zoom
Create Webhook
Subscribe to payment, transfer, and customer events.
Zoom
Webhook Logs
History of delivered events, request payloads and endpoint responses.
Zoom
Request Logs
Full inspection of calls to /v3, parameters and status codes.
Zoom
Virtual Clock
Advance time by days or months to observe automatic charge state transitions.
Zoom
Test Cards
Pre-set numbers for approved, declined, stolen or insufficient limit flows.
Zoom
Financial Statement
Ledger entries, fee deductions, and reconciled account balance.
- Customers: create, edit, remove and restore; open one to see everything billed to it.
- A charge: its invoice link, simulate the payment, confirm a cash receipt, receive, overdue, refund, delete, restore, and its notifications.
- Integrations, per account: API keys, webhooks with the event checklist, request logs (every
/v3call with what went in and what came back, errors included), webhook logs with your endpoint's answer and the events no webhook took, the e-mail and WhatsApp channels, and the generator (Gerador). - Test cards: accept any well-formed card or only the listed ones; add a card with the outcome it forces.
- Notas fiscais (NFS-e): schedule a note for a charge, issue it, cancel it, and register the account's fiscal information. The city hall answers on the next tick (
SYNCHRONIZED→AUTHORIZED, orERRORwithout fiscal information), or now with the simulator's buttons; an authorized note'spdfUrlandxmlUrlopen its document. A subscription withinvoiceSettingsgets a note for each charge. - Profiles: one root account per tenant of your application, created and deleted without a restart. Reset empties one and keeps its id and keys: everything, or only the charges, notes and logs with customers and subaccounts kept; cancelling first sends
PAYMENT_DELETED,SUBSCRIPTION_DELETEDandINVOICE_CANCELEDto your webhooks before the delete.
Configuration
| Variable | Default | Meaning |
|---|---|---|
PORT | 45445 | HTTP port |
ASAAS_API_KEY | generated, printed at boot | The main account's key |
WEBHOOK_LOCALHOST_REWRITE | (empty) | Host that replaces localhost in webhook URLs; use host.docker.internal |
CLOCK_MODE | REAL | REAL (follows the machine; the clock refuses to skip), VIRTUAL_FLOWING or VIRTUAL_FROZEN |
ADMIN_ENABLED | true | The /_admin/* routes and the panel |
INVOICE_BASE_URL | http://localhost:<PORT> | Where invoiceUrl and bankSlipUrl point, as the browser reaches this server |
MAILPIT_URL | (unset: found by itself) | Mailpit for the payer e-mails; set empty, e-mail is off |
WHATSAPP_MOCK_URL | (unset: found by itself) | The WhatsApp mock for the payer templates; set empty, WhatsApp is off |
SMS_MOCK_URL | (unset: found by itself) | The SMS mock for the payer SMS; set empty, SMS is off |
SMS_UI_URL | http://localhost:8899 | Where the browser opens the SMS mock |
GENERATOR_URL | (unset: found by itself) | The generator mock for Brazilian fake data and account population; set empty, generator is off |
GENERATOR_UI_URL | http://localhost:45450 | The generator screen, as the browser opens it from Integrations |
WHATSAPP_PHONE_NUMBER_ID | 5511400000000 | The sender number in the WhatsApp mock |
WHATSAPP_WABA_ID | 100000000000001 | The WABA whose library holds the payer templates |
NOTIFY_CALLBACK_BASE_URL | (unset: found by itself) | This server as the WhatsApp mock reaches it |
MAILPIT_UI_URL, WHATSAPP_UI_URL | http://localhost:8025, http://localhost:3010 | The two screens, as the browser opens them from Integrations |
NOTIFY_FROM_EMAIL | notificacoes@asaas.localhost | Sender of the e-mails |
CARD_CAPTURE_SCHEDULE | 0@08:00,0@14:00,0@20:00,1@08:00,1@14:00,1@20:00 | When a recurring card charge is tried: days after the due date @ local time. The panel overrides it at runtime |
DATABASE_PATH | /data/asaas.db | Mount a volume at /data to persist |
TZ | America/Sao_Paulo | Time zone |
Fidelity: proven, not promised
The business rules were captured from the real Asaas sandbox and are checked field by field by a parity suite. tools/capture.ts runs the scenarios against Asaas and records the answers in tests/golden/; the suite replays them against the simulator and compares everything that is not legitimately volatile, down to the exact set of keys of each object.
The capture overturned 20 rules deduced from the documentation, among them: a subscription creates its first charge at once; the card installment fee comes off the total and the division truncates; installment n credits at D+32×n; the discount field is limitDate, which the specification does not declare.
bun run test:parity
ASAAS_SANDBOX_API_KEY='$aact_hmlg_…' bun run capture # capture again with your sandbox keyThe script refuses any key that does not start with $aact_hmlg_. Details in progress.md.
Development
bun install
bun run codegen # OpenAPI spec → TypeBox and the manifest of operations
bun run db:generate # migrations from the Drizzle schema
bun run dev
bun test
bun run e2e # boots the server, pays a Pix charge, waits for PAYMENT_RECEIVED
bun run spec:diff # did Asaas change the API?Read AGENTS.md before contributing: pure domain, no Date.now(), money in integer cents.
License
MIT, see LICENSE.
Not affiliated with Asaas. An independent simulator of their public API, for local development and testing. "Asaas" is a trademark of its owners. Do not use it to move real money: no payment here is real.