Skip to main content
Paying fifty people with fifty API calls means fifty chances to half-fail. A bulk transfer replaces that with one CSV file: you validate it, you upload it, and Spotflow processes the rows. All calls are on the accounts service:

What you’ll need

  • Your secret key. Every call in this guide authenticates with it.
  • A funded account in the payout currency.
  • A CSV file of recipients, in the exact template below.
Bulk transfers are a multipart file upload, not a JSON body. You send -F "file=@payouts.csv", not -d '{...}'. This is the single most common mistake with these endpoints.
These endpoints are on /accounts/api/v1, including /transfers/bulk/categories. The gateway service returns 404 No static resource.

The shape of the flow

1

Build the CSV

The column names are fixed. Getting them wrong fails the whole file.
2

Check your categories

Categories group a batch for reporting.
3

Validate the file

A dry run. Every bad row is reported, and no money moves.
4

Upload the batch

The same file, now for real.
5

Track the batch

Read the batch back and watch the rows settle.

Step 1: Build the CSV

The header row must be exactly this, in snake_case:
The column names are snake_case and the API will not guess. A header row of accountNumber,accountName,bankCode,amount,narration — the camelCase spelling used everywhere else in the API — is rejected outright:
That message names no column, so it tells you nothing about which one is wrong. If you see it, check the spelling of the whole header row first.

Step 2: Check your categories

A category labels the batch. List the ones available to you:
An empty list is normal on a new merchant — it means no categories have been set up yet. The category query parameter in Step 4 is still required, so you must send a value whether or not it appears here.

Step 3: Validate the file

This is a dry run. It parses every row, applies the same validation as the real upload, and moves no money. Always do this before Step 4.
A clean file returns 200:
A file with problems returns 400, and names every one of them:
Read this carefully:
  • totalRequests is every row, not every failure. Here 3 rows were parsed and 2 failed. The count of good rows is totalRequests - failedRequests.length.
  • Each failure echoes the whole parsed row back in request. That is how you tell which line of your CSV it came from, since there is no line number.
  • The second message spells out the three valid ways to name a destination: an internal accountId, an external accountNumber + bankCode, or a stablecoin walletId + chain + toAddress.

Step 4: Upload the batch

The same file, with the batch settings as query parameters:
Omitting category returns 400:
This endpoint is currently returning 500 on the development environment. Uploading a file that Step 3 validated cleanly returns:
We reproduced it with one-row and two-row files, with skipErrors both true and false, with different category values, and with both a secret key and a dashboard token. Parameter validation passes first (omitting category still gives the 400 above), so the failure is downstream of the request itself. If you hit this, quote the traceId from your own response to support.Because no batch could be created, the response body of a successful upload is not documented here — we will not guess at it. The tracking calls in Step 5 are listed for completeness but are likewise unverified.

Step 5: Track the batch

The calls in this step could not be exercised, because Step 4 does not currently return a batch id on dev. Shapes are from the API specification, not from a live run — check them against your own response before depending on them.
List your batches:
This call is verified — it returns 200 and an empty page on a merchant with no batches. It accepts page, size, query, status, category, currency, from and to. The remaining three take a batch id:
  • GET /transfers/bulk/{id} — the batch itself
  • GET /transfers/bulk/{id}/stats — counts by status
  • GET /transfers/bulk/{id}/transfers — the individual payouts in the batch

When things go wrong

What to do next