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

# Testing in the sandbox

> Run the Tap on Phone SDK against the sandbox with the sandbox artifact and a real contactless card, choose the outcome with the amount, and verify each transaction on the API.

The sandbox runs the real card reader on a real phone with a real card, against a simulated acquirer. Nothing moves money, and the outcome of every tap is yours to choose through the amount.

## Set up for the sandbox

* **Artifact.** Use `rinne-sdk-checkout-sandbox`. It pairs the card reader's test kernel with Rinne's test environments.
* **Environment.** Always pass `environment = Environment.SANDBOX` to `Payments.getReady`. The sandbox artifact defaults to a Rinne-internal environment, and a token minted with your sandbox key is refused there with `ACCESS_TOKEN_REJECTED`.
* **Token.** Your backend mints it at `https://api-sandbox.rinne.com.br/core/v1/terminal/token` with your sandbox API key, exactly as in production.
* **Device.** A physical Android phone with NFC and Google Play services. Emulators fail attestation by design.
* **Card.** Any contactless card, phone or watch. The sandbox reads it like production does and never charges it.

```kotlin theme={null}
Payments.getReady(
    context = this,
    accessTokenProvider = { backend.fetchTapOnPhoneToken() },
    config = CheckoutConfig(merchantName = "Padaria Bela Vista"),
    environment = Environment.SANDBOX,
) { result -> /* ReadyResult.Ready | ReadyResult.Failed */ }
```

## Choose the outcome with the amount

The sandbox acquirer decides from the last two digits of the amount in cents. Anything not listed approves.

| Amount ends in             | Outcome                      | `PaymentResult`                | Transaction status |
| -------------------------- | ---------------------------- | ------------------------------ | ------------------ |
| `,00` or anything unlisted | Approved                     | `Approved`                     | `APPROVED`         |
| `,51`                      | Declined, insufficient funds | `Declined(DECLINED_BY_ISSUER)` | `REFUSED`          |
| `,05`                      | Declined, do not honor       | `Declined(DECLINED_BY_ISSUER)` | `REFUSED`          |
| `,54`                      | Declined, expired card       | `Declined(DECLINED_BY_ISSUER)` | `REFUSED`          |
| `,14`                      | Declined, invalid card       | `Declined(DECLINED_BY_ISSUER)` | `REFUSED`          |
| `,96`                      | Provider error               | `Failed`                       | `FAILED`           |

Worked examples: R$ 10,00 (`amountCents = 1000`) approves; R$ 10,51 (`1051`) is declined for insufficient funds; R\$ 10,96 (`1096`) fails. The card never overrides the amount.

The [simulator](/tap-on-phone/android/simulator) applies the same table to the amount in its Sale section, so you can rehearse a test plan before touching a phone.

## What a sandbox transaction looks like

A sandbox transaction appears on the API like any other, with the `requestId` and `metadata` you sent and the payment method the operator chose. Two differences from production:

* **No card digits.** Simulated approvals return no card data, so the record carries no masked last four digits or brand.
* **No settlement.** Nothing is sent to the acquirer for settlement, so ledger entries and cashouts do not follow.

Whether the certified PIN pad appears depends on the card and the amount, exactly as in production: cards ask for a PIN above their contactless limit. To exercise the pad, tap with an amount above that limit, or with a card that always asks.

## Verify each test

Look the transaction up with the `requestId` you sent. The transactions list requires the `transaction.list` permission, which a key scoped only to `tap_on_phone.*` does not hold, so use a key that carries both permissions:

```bash theme={null}
curl "https://api-sandbox.rinne.com.br/core/v1/transactions?request_id=order-2026-000418" \
  -H "x-api-key: $RINNE_API_KEY"
```

<Check>
  The response holds one transaction with the amount you charged, `payment_method` matching what the operator chose, your `metadata`, and the status from the table above. A `transaction.created` webhook fires at the same time if you have one configured, and `transaction.status-changed` follows a refund.
</Check>

## A test plan that covers the integration

| Test                       | How                                                                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Approved sale, no PIN      | Small amount ending in `,00`                                                                                  |
| Approved sale with PIN     | Amount above the card's contactless limit                                                                     |
| Declined sale              | Amount ending in `,51`; confirm your app offers another payment method and does not retry the same card       |
| Technical failure          | Amount ending in `,96`; confirm your app logs `code` and `message` and offers a retry                         |
| Cancel from the tap screen | Tap the close button; confirm `Cancelled` and no transaction on the API                                       |
| Refund                     | Refund an approved sale with a card re-tap; confirm the sale's status changes                                 |
| Process death              | Kill the app from the tap screen and confirm the restored screen delivers `Failed` with the reconcile message |
| Idempotency                | Launch the same `requestId` twice; confirm one transaction                                                    |

## What the sandbox cannot prove

* **Attestation on your production build.** The sandbox artifact relaxes the integrity check; production enforces it and also checks the install source. See [Going to production](/tap-on-phone/android/production).
* **Sensory branding.** The approval sound, animation and haptic come from the card reader with a real production card.
* **Your production credentials.** The production artifact, keys and merchant enablement are provisioned separately by Rinne.

## Troubleshooting

| Symptom                               | Likely cause                                                                                                                                                           |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_REJECTED` at `getReady` | The token was minted on the wrong environment, expired, or your key lacks `tap_on_phone.*`. Check `environment = Environment.SANDBOX` and the host your backend calls. |
| `TAP_ON_PHONE_NOT_ENABLED`            | The sandbox merchant is not enabled for contactless capture. Contact Rinne.                                                                                            |
| `EMV_CONFIG_UNAVAILABLE`              | No card configuration is published for the SDK version you pinned. Pin the version named in your beta access, or contact Rinne.                                        |
| Every tap approves                    | The amount does not end in a listed suffix. Check `amountCents`, not the displayed amount.                                                                             |
| `ATTESTATION_FAILED`                  | Emulator, rooted device or no Play services. Use a physical, unmodified phone.                                                                                         |
