Docs

What the simulator does behind each call, the panel that shows it, and every setting.

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 through OVERDUE first.
  • 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 walletId and balance. A split moves money and posts entries to both statements.
  • Webhooks the way Asaas sends them. The asaas-access-token header; only HTTP 200 counts; 15 attempts on Asaas's table; SEQUENTIALLY with real head-of-line blocking; interrupted at 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_REFUSED while the charge stays PENDING; then OVERDUE, 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 n credits at D+32×n.
  • Pix that pays. GET /v3/payments/{id}/pixQrCode returns 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, whatsapp or whatsapp-mock, sms or sms-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 generator or generator-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 with PUT /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 notificationDisabled and 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.

  • 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 /v3 call 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, or ERROR without fiscal information), or now with the simulator's buttons; an authorized note's pdfUrl and xmlUrl open its document. A subscription with invoiceSettings gets 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_DELETED and INVOICE_CANCELED to your webhooks before the delete.

Configuration

VariableDefaultMeaning
PORT45445HTTP port
ASAAS_API_KEYgenerated, printed at bootThe main account's key
WEBHOOK_LOCALHOST_REWRITE(empty)Host that replaces localhost in webhook URLs; use host.docker.internal
CLOCK_MODEREALREAL (follows the machine; the clock refuses to skip), VIRTUAL_FLOWING or VIRTUAL_FROZEN
ADMIN_ENABLEDtrueThe /_admin/* routes and the panel
INVOICE_BASE_URLhttp://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_URLhttp://localhost:8899Where 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_URLhttp://localhost:45450The generator screen, as the browser opens it from Integrations
WHATSAPP_PHONE_NUMBER_ID5511400000000The sender number in the WhatsApp mock
WHATSAPP_WABA_ID100000000000001The 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_URLhttp://localhost:8025, http://localhost:3010The two screens, as the browser opens them from Integrations
NOTIFY_FROM_EMAILnotificacoes@asaas.localhostSender of the e-mails
CARD_CAPTURE_SCHEDULE0@08:00,0@14:00,0@20:00,1@08:00,1@14:00,1@20:00When a recurring card charge is tried: days after the due date @ local time. The panel overrides it at runtime
DATABASE_PATH/data/asaas.dbMount a volume at /data to persist
TZAmerica/Sao_PauloTime 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 key

The 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.