---
url: /docs/payable-evm.md
description: >-
  Pay a Brazilian boleto or PIX code from an external EVM wallet by quoting the
  bill, approving the ERC-20 pull, then committing.
---

This tutorial pays a Brazilian bill from an external EVM wallet on Ethereum, Base, Polygon or Arbitrum. All stablecoins on EVM chains are ERC-20 tokens, so authorization means calling `approve` on the token contract to let BlindPay pull the quoted amount.

If the funds live in a BlindPay-custodied wallet instead, skip the approval entirely: see [Payable with managed wallet](/payable-managed-wallet).

The examples below use `base_sepolia` and `USDB` since they're the development network and test token. Swap in the matching production network and token (`USDC` or `USDT`) when you go live.

Payables are **EVM only** today. A non-EVM network is refused at quote time with `payable_network_not_supported`.

## Prerequisites

You also need:

1. A [customer](/learn/customers) with `kyc_status: "approved"`. There is no bank account to register: the bill names its own beneficiary
2. A real boleto linha digitável or PIX copia e cola code to pay
3. [An RPC provider URL for Base Sepolia](https://chainlist.org/?search=base\&testnets=true)
4. [A wallet funded with testnet ether](https://www.alchemy.com/faucets/base-sepolia) to pay gas
5. [The private key, or any way to instantiate the wallet with ethers.js](https://docs.ethers.org/v6/api/wallet/#BaseWallet_new)

### Quote the bill and approve the tokens

The quote resolves the code and prices it. Read `amount` (what is owed today, in BRL cents) and `sender_amount` (what it costs in stablecoin) before going further, and show your user the beneficiary the rail returned rather than the one they expected.

The `contract` object in the response carries everything the `approve` call needs, including `amount` already adjusted for the token's decimals.

```js [index.js]
import { ethers } from 'ethers'

const quoteResponse = await fetch(
  'https://api.blindpay.com/v1/instances/in_000000000000/payable-quotes',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      receiver_id: 're_000000000000',
      type: 'boleto',
      payload: '34191790010104351004791020150008191070026000',
      network: 'base_sepolia',
      token: 'USDB',
    }),
  }
)

const quote = await quoteResponse.json()

// What the bill actually costs, resolved from the rail. Show these before
// asking anyone to sign anything.
console.log(quote.amount, quote.beneficiary_name, quote.scheduled_date)

const provider = new ethers.JsonRpcProvider('YOUR_RPC_URL')
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider)

const token = new ethers.Contract(
  quote.contract.address,
  quote.contract.abi,
  wallet
)

const approval = await token.approve(
  quote.contract.blindpayContractAddress,
  quote.contract.amount
)
await approval.wait()
```

The quote expires 5 minutes after creation. If the approval transaction is slow to mine, quote again rather than committing an expired one.

### Commit the quote

Committing creates the payable and starts the payment. `sender_wallet_address` is the wallet that just approved.

```js [index.js]
const response = await fetch(
  'https://api.blindpay.com/v1/instances/in_000000000000/payables',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      payable_quote_id: quote.id,
      sender_wallet_address: wallet.address,
    }),
  }
)

const payable = await response.json()
```

The payable is born in `processing`. If the quote flagged `duplicate_of_payable_id` and you still mean to pay it, add `force: true`; that is accepted for boletos only.

If the wallet has not approved enough, this call fails immediately with `erc20_allowance_insufficient` rather than returning a payable that dies minutes later. Approve at least `contract.amount` and retry.

### Track it to settlement

Stablecoins are collected first, then the bill is paid. A PIX code usually completes in under a minute. A boleto settles on a banking day, so one quoted on a Saturday sits in `processing` until Monday, which is not a stuck payment.

Subscribe to `payable.complete` to hear about every payable that finishes, whether it completed, failed or was refunded. `payable.update` covers changes while it is still in flight. See [Payables](/payables) for the full webhook set.

```js [index.js]
const detail = await fetch(
  `https://api.blindpay.com/v1/instances/in_000000000000/payables/${payable.id}`,
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } }
).then(r => r.json())

console.log(detail.status, detail.scheduled_date)
```

## Related

* [Payable quotes](/payable-quotes): what the quote resolves and why it can refuse a code
* [Payable with managed wallet](/payable-managed-wallet): the same flow with no approval step
* [Payables](/payables): lifecycle, webhooks and the banking calendar
