EVM mass payouts
ERC-20 mass payouts on Polygon and more.
EVM mass payouts distribute ERC-20 tokens (USDC, USDT) to many recipients via an on-chain escrow contract. The merchant deposits tokens into the contract; Payzum's operator wallet pays gas to trigger each distribution batch.
EVM mass payouts use the same CSV format and quote concepts as UTXO payouts. See Mass payouts overview for the CSV format, quote field reference, and idempotency details.
How EVM payouts differ from UTXO
Unlike UTXO (where Payzum holds a derived signing key), EVM uses an on-chain escrow contract. Funds never leave the merchant's control until distributeBatch is called:
- Merchant deposits tokens into the
PayzumMassPayoutcontract via an ERC-20approve+deposit. - Payzum's operator wallet calls
distributeBatchfor each batch, paying only gas. - Recipients receive tokens directly from the contract.
The quote fee covers Payzum's service fee (basis points on the token amount) plus an estimated gas cost in the same token denomination.
Supported chains and tokens
The create endpoint accepts the following chain values: polygon, arbitrum, optimism, base, bnb, avalanche. Call GET /v1/currencies for the current live token set — do not hardcode contract addresses.
On the dashboard wizard, select Testnet to use Polygon Amoy for integration testing before going live on mainnet.
CSV format
Use the standard CSV format (header row with address, amount, optional label) plus the tokenContract field in the request body to specify the ERC-20 token (with optional tokenSymbol and tokenDecimals).
address,amount,label
0xAbCd1234abcd1234AbCd1234abcd1234AbCd1234,10.00,user-001
0x5678EfGh5678EfGh5678EfGh5678EfGh5678EfGh,25.50,user-002Amounts are decimal strings in whole token units (e.g. 10.00 for 10 USDC).
Endpoints
All merchant endpoints require the x-api-key header. Pagination, filters, the error envelope, and the Idempotency-Key semantics follow the shared conventions in the Mass payouts overview.
| Method | Path | Description |
|--------|------|-------------|
| POST | /v1/evm-mass-payout | Create order (multipart form with chain, mode, tokenContract, csv) |
| GET | /v1/evm-mass-payout | List orders (paginated) |
| GET | /v1/evm-mass-payout/:id | Fetch order with batch detail |
| GET | /v1/evm-mass-payout/:id/recipients | Paginated recipient rows |
| GET | /v1/evm-mass-payout/:id/deposit-instructions | Where and how to deposit tokens into the contract |
| POST | /v1/evm-mass-payout/:id/confirm | Confirm quote; advance to pending_deposit |
| POST | /v1/evm-mass-payout/:id/cancel | Cancel order |
| POST | /v1/evm-mass-payout/:id/refresh-quote | Refresh the gas/fee estimate |
POST /v1/evm-mass-payout/:id/refund and POST /v1/evm-mass-payout/:id/retry-failed also exist but are operator-only: they are authenticated with the X-Operator-Key header and used internally by payzum operations. They are not merchant endpoints — to request a refund or a retry, contact support.
In the fetch-by-id response, order and batches are siblings: { "ok": true, "order": { ... }, "batches": [ ... ] }. This differs from UTXO, where batches is nested inside order.
There is no /report.csv endpoint on EVM: the settlement CSV is delivered via the mass_payout.completed webhook's reportR2DownloadUrl field.
Create order — request fields
POST /v1/evm-mass-payout accepts multipart/form-data.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| chain | string | yes | polygon | arbitrum | optimism | base | bnb | avalanche |
| mode | string | yes | mainnet | testnet |
| tokenContract | string | yes | ERC-20 contract address of the payout token (0x…) |
| tokenSymbol | string | no | Token symbol (default USDC) |
| tokenDecimals | number | no | Token decimals (default 6) |
| csv | file | yes | CSV file with header row; columns address, amount, optional label |
Deposit instructions
After confirming the order, call GET /v1/evm-mass-payout/:id/deposit-instructions to get pre-built calldata for the two on-chain steps:
{
"approve": { "to": "...", "value": "...", "data": "0x..." },
"deposit": { "to": "...", "value": "...", "data": "0x..." },
"totalToDepositRaw": "...",
"payoutRoot": "0x...",
"chain": "polygon",
"mode": "mainnet"
}approve and deposit are ready-to-send transaction payloads (ERC-20 approve followed by the contract deposit), totalToDepositRaw is the exact token amount in base units, and payoutRoot is the merkle root committing to the recipient set. The dashboard wizard handles this step with a Web3 wallet (MetaMask or similar).
Order lifecycle
pending_quote → pending_deposit → deposit_pending → deposit_confirmed → executing
→ completed | partial_failed | expired | cancelled | refunded
The lifecycles differ per chain family. EVM has no underfunded or funded state; instead it adds deposit_pending (deposit seen on-chain, awaiting confirmations) and deposit_confirmed (required confirmation count reached) between pending_deposit and executing. The deposit-watcher detects the on-chain Deposited event and advances the order. The mass_payout.underfunded webhook event applies to UTXO orders only.
cURL example — create order
curl -X POST "$PAYZUM_BASE/v1/evm-mass-payout" \
-H "x-api-key: $PAYZUM_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "chain=polygon" \
-F "mode=testnet" \
-F "tokenContract=0xYourTokenContractAddress" \
-F "tokenSymbol=USDC" \
-F "tokenDecimals=6" \
-F "csv=@payouts.csv"The response includes the order object with the quote and deposit instructions reference. Call confirm, then follow the deposit-instructions endpoint to fund the contract. Reusing the same Idempotency-Key returns 409 with kind: idempotency_conflict (see the overview).
Error catalog
Errors use the { "ok": false, "error": { "kind": ... } } envelope (auth and rate-limit failures use the canonical {statusCode, code, message} envelope instead — see the overview).
| Error kind | HTTP | Description |
|------------|------|-------------|
| invalid_csv | 400 | Malformed form data, missing field, or CSV parse failure |
| invalid_address | 400 | A recipient address failed EVM validation |
| invalid_amount | 400 | A recipient amount could not be parsed |
| over_max_decimals | 400 | Amount precision exceeds the token's decimals |
| amount_below_minimum | 400 | A recipient amount is below the per-recipient minimum |
| order_too_small | 400 | Total payout sum below the minimum order size |
| token_not_allowed | 400 | The token contract is not allowed on this chain |
| chain_disabled | 409 | Chain is disabled via the operator kill-switch (note: on UTXO this same kind is returned as 400) |
| unprofitable_chain | 409 | The chain is currently unprofitable for payouts |
| idempotency_conflict | 409 | Duplicate (merchant, Idempotency-Key). The original order is not replayed — fetch it with GET /v1/evm-mass-payout/:id |
| invalid_transition | 409 | The order is not in a state that allows the requested action |
| conflict | 409 | Concurrent-update conflict |
| not_requotable | 409 | The order can no longer be re-quoted |
| no_recipients | 409 | The order has no recipients to act on |
| network_error | 503 | Upstream RPC/network failure |
| unknown | 500 | Unexpected server error |
Quote expiry and refresh
EVM quotes have a 15-minute TTL. If the quote expires before you confirm and fund, call POST /v1/evm-mass-payout/:id/refresh-quote to renew it.
Refunds
If an order is cancelled, expires, or ends in partial_failed, the deposited funds can be returned via the contract's emergencyRefund to the original depositor address on-chain. This is triggered by the operator-only POST /v1/evm-mass-payout/:id/refund endpoint (internal, X-Operator-Key) — contact support to request it.