> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beinfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing in the sandbox

> What the sandbox really is: Asaas in homologation, real pix, and where the loop closes.

The sandbox is **not a mock**. Your charges go out through **Infi**, a provider
of ours, which underneath talks to an **Asaas homologation account** — the same
adapter, the same webhook formats, the same failure modes as production. What
changes is the account, not the code.

In practice: the `provider` field comes back `"infi"`, and everything below
describes Asaas behaviour, because Asaas is what sits underneath.

That is great for fidelity, and its annoying part is over: **you close the loop
yourself**, without anybody's key. The QR we return in sandbox is a confirmation
page of ours — scan it with your phone, press a button, the invoice turns paid.

## What comes back on a pix charge

```json theme={null}
{
  "provider": "infi",
  "method": "pix",
  "status": "pending",
  "pixPayload": "https://api-sandbox.beinfi.com/pay/{slug}/sandbox/{paymentId}",
  "pixQrImage": "iVBORw0KGgoAAAANSUhEUg…",
  "sandboxConfirmUrl": "https://api-sandbox.beinfi.com/pay/{slug}/sandbox/{paymentId}",
  "providerPixPayload": "00020101021226820014br.gov.bcb.pix2560pix-h.asaas.com/qr/…",
  "pixExpiresAt": "2026-08-20T16:30:00Z"
}
```

* `pixPayload` — **render it as a QR, exactly as in production.** In sandbox it
  is the URL of our confirmation page; in production it is the copy-and-paste
  EMV. Your rendering code is the same either way: it is a QR.
* `pixQrImage` — a ready base64 PNG, generated from that same `pixPayload`. When
  it comes, use it instead of generating the QR yourself.
* `sandboxConfirmUrl` — **exists only in sandbox**, and it is the explicit
  marker. If you want a "confirm as if I had paid" button on your test screen,
  check **this field**. Never by sniffing whether `pixPayload` looks like a URL:
  in production it is EMV, and that kind of check is how a test-only thing leaks
  into the real checkout.
* `providerPixPayload` — Asaas's EMV, so you can see what production would
  return. It is not payable in a banking app: it is a homologation account.
* If `pixPayload` comes back **empty**, the charge exists but cannot be paid. Do
  not invent a QR from `id` or `providerId`: the payer's bank refuses it, and
  the screen lies to them. Show an error and offer another method.

<Warning>
  **Pix requires a tax id.** Asaas refuses to create the payer without a CPF/CNPJ — without it the charge
  stops at `422 customer_tax_id_required`. Collect and pass `taxId` with the
  customer.
</Warning>

## Closing the loop: confirming the payment

Two ways, and neither needs a provider key.

**By scanning**, which is the test that counts — it is your real buyer's flow:
render the `pixPayload` as a QR, point your phone's camera at it, open it. You
land on a page of ours with the amount and a button.

**Or over HTTP**, for your CI:

```bash theme={null}
curl -X POST "$SANDBOX_CONFIRM_URL"
# -> 200  { "status": "confirmed" }
```

`$SANDBOX_CONFIRM_URL` is the `sandboxConfirmUrl` the charge returned. In the
SDK:

```ts theme={null}
const pay = await infi.pay.charge({ slug, invoiceId, method: "pix" });
if (pay.sandboxConfirmUrl) await fetch(pay.sandboxConfirmUrl, { method: "POST" });
await infi.pay.waitForPaid({ slug, invoiceId });
```

Confirming twice is safe: the second answers `200` and does nothing — it is a
page people reload.

Measured end to end: confirmation → `paid` in about 3s, with the download grant
issued and delivery triggered.

<Info>
  **Underneath it is Asaas confirming, not us making it up.** Our route does not write "paid" into the database. It calls the sandbox
  `confirm` of the provider that created the charge, and that provider's real
  webhook comes back through the same path production uses. That is why you see
  `pixTransaction`, a deducted fee and a credit date — the charge is real, only
  the confirmation is yours.
</Info>

<Warning>
  **This does not exist in production.** The confirmation route is **not mounted** on a live deployment — it is not
  protected, it is absent. And `sandboxConfirmUrl` does not come in the response.
  If your code calls it, there is nothing to call in production; if your code
  checks the field, it behaves correctly in both. That is why the marker is a
  field and not the shape of `pixPayload`.
</Warning>

## How to know they paid

Two ways, and in sandbox only one works today:

|                                 | sandbox             | production |
| ------------------------------- | ------------------- | ---------- |
| **Polling** the invoice         | ✅ works             | ✅ works    |
| **Webhook** `payment.confirmed` | ❌ `503` on register | ✅          |

Registering a webhook in sandbox answers `503 secret_store_unavailable` — the
secret store is not available to a sandbox tenant. So in sandbox, **polling is
the way**:

```ts theme={null}
const paid = await infi.pay.waitForPaid({ slug, invoiceId });
// or, server-side:
const inv = await infi.invoices.get(invoiceId);   // status: "open" | "paid"
```

The events, signature verification and what to do in production are in
[webhooks](https://beinfi.com/en/webhooks).

## What does NOT exist in the sandbox

Things that answer with an error and are not your bug:

* **`providers.*` → `404`.** Connecting a PSP (BYOP) is a production-only
  surface. In sandbox the processor is **Infi** — a provider of ours, and what
  shows up in `provider` on your charges. It runs on Asaas underneath, which
  explains the formats and errors you see, but the account is ours and there is
  nothing to connect.
* **`webhooks.create` → `503`**, as above.
* **Card may come disabled.** The link's public response carries `cardEnabled`;
  when it is `false`, only pix and boleto are available on that tenant.
