Quick start

From docker run to a webhook on your machine and a month skipped. Every call below runs as written against a fresh container.

  1. Run it

    Pin the API key, or a fresh volume prints a new one at boot (in a shell, single quotes keep the $). Start the clock in a virtual mode, or it follows the machine and refuses to skip.

    docker run --rm -p 45445:45445 \
      -e 'ASAAS_API_KEY=$aact_hmlg_local' \
      -e CLOCK_MODE=VIRTUAL_FLOWING \
      -e WEBHOOK_LOCALHOST_REWRITE=host.docker.internal \
      --add-host host.docker.internal:host-gateway \
      ghcr.io/instituto-ama/asaas-mock:latest

    WEBHOOK_LOCALHOST_REWRITE lets a webhook registered as http://localhost:4000/… reach your machine from inside the container. The panel is at http://localhost:45445.

  2. Point your application at it

    Switching from the sandbox to the mock is switching the base URL. The paths, bodies, headers and errors are Asaas's.

    - ASAAS_BASE_URL=https://api-sandbox.asaas.com/v3
    + ASAAS_BASE_URL=http://localhost:45445/v3
      ASAAS_API_KEY=$aact_hmlg_local
  3. Create a customer

    curl localhost:45445/v3/customers \
      -H 'access_token: $aact_hmlg_local' -H 'content-type: application/json' \
      -d '{"name":"Maria Souza","cpfCnpj":"24971563792","email":"maria@example.com","mobilePhone":"11987654321"}'
    # → {"object":"customer","id":"cus_526128665490",…}
  4. Register your webhook

    Through the real /v3/webhooks. The rules are Asaas's: the authToken needs 32 to 255 characters, and your endpoint must answer HTTP 200.

    curl localhost:45445/v3/webhooks \
      -H 'access_token: $aact_hmlg_local' -H 'content-type: application/json' \
      -d '{"name":"my-app","url":"http://localhost:4000/webhooks/asaas","email":"dev@example.com",
           "enabled":true,"interrupted":false,"apiVersion":3,"sendType":"SEQUENTIALLY",
           "authToken":"whsec_local_demo_token_0123456789abcdef",
           "events":["PAYMENT_CREATED","PAYMENT_CONFIRMED","PAYMENT_RECEIVED","PAYMENT_OVERDUE"]}'

    Each delivery carries the asaas-access-token header with your token. GET /_admin/webhooks/deliveries shows every attempt with your endpoint's answer.

  5. Charge and pay

    A Pix charge goes straight to RECEIVED when paid. Pay it like the payer would, on the page at invoiceUrl (a real QR code), or with the sandbox's own call:

    curl localhost:45445/v3/payments \
      -H 'access_token: $aact_hmlg_local' -H 'content-type: application/json' \
      -d '{"customer":"cus_526128665490","billingType":"PIX","value":89.9,"dueDate":"2026-11-10"}'
    
    curl -X POST localhost:45445/v3/sandbox/payment/pay_…/confirm -H 'access_token: $aact_hmlg_local'
    # → your endpoint receives PAYMENT_CREATED, then PAYMENT_RECEIVED
  6. Skip time

    Every simulated day runs a full tick: subscriptions charge, unpaid charges go overdue, cards credit, webhooks fire.

    curl localhost:45445/_admin/clock                     # where the clock is
    curl -X POST localhost:45445/_admin/clock/advance \
      -H 'content-type: application/json' -d '{"days": 32}'
    curl -X POST localhost:45445/_admin/clock/set \
      -H 'content-type: application/json' -d '{"date": "2027-01-01"}'

    The use cases put this to work: a card that credits at D+32, a boleto paid late, a subscription a year ahead.

  7. In your compose

    services:
      asaas:
        image: ghcr.io/instituto-ama/asaas-mock:0.11.0   # pin a version, or :latest
        ports:
          - '45445:45445'
        environment:
          # `$$` is a literal `$` in compose.
          ASAAS_API_KEY: '$$aact_hmlg_local'
          WEBHOOK_LOCALHOST_REWRITE: host.docker.internal
          CLOCK_MODE: VIRTUAL_FLOWING
        extra_hosts:
          - 'host.docker.internal:host-gateway'   # Linux
        volumes:
          - asaas-data:/data
    volumes:
      asaas-data:

    From a service in the same network, the mock is http://asaas:45445/v3. Put a whatsapp, a mailpit and an sms service next to it and it finds them by itself, as on the Asaas page.

  8. In CI

    Use CLOCK_MODE=VIRTUAL_FROZEN and no volume: the clock moves only when the test says so, and every run starts from the same state. A test then reads like the business rule it checks:

    # create the charge → assert PENDING
    # advance 1 day after the due date → assert your order is marked overdue
    # confirm the payment → assert the webhook handler settled the order