submitBulkOperation returns immediately with a jobId. Everything after that is polling: getBulkOperationJob for the job’s counters, getBulkOperationItems for the per-target outcomes.
Completion is polling-only — there are no bulk webhooks.
Requirements
Authorization: Basic <API_KEY>(your application API key)- The bulk API capability on your application
- The job must belong to your application — another app’s job id returns not-found, identical to an unknown id
Which one to call
getBulkOperationJob reads only the job row — no per-item scan — so it’s the one to poll on an interval. Reach for getBulkOperationItems once the job is terminal, or when you specifically need per-target detail.
Poll the job
Job fields
skipped and failed are not the same thing. Skipped items never ran — they were rejected at submission and consumed no downstream call. Failed items ran and something went wrong. Reconciling a batch means reading both.
The counters relate as requestedTargetCount = acceptedItemCount + skippedItemCount, and once terminal, acceptedItemCount = succeededItemCount + failedItemCount.
Job status
Terminal statuses are
COMPLETED, COMPLETED_WITH_ERRORS, FAILED and CANCELLED. Stop polling on any of them.
UPDATE_TRANSACTION_METADATA executes synchronously. Its job is already terminal when submitBulkOperation returns, so there is nothing to poll — go straight to getBulkOperationItems for the per-edit results.Read per-target results
Arguments
Item fields
totalCount is the job’s unfiltered item total, read from the job’s own counters — not a count of the filtered page. Filtering to FAILED on a 240-item job still reports totalCount: 240. Page with hasNextPage/nextCursor; don’t infer the end from totalCount.
itemIndex is the stable join key back to your submission: item n corresponds to the n-th entry in the items array you submitted, so you can map a failure to the exact transfer or deposit you asked for even when externalReferenceId is absent.
Reconciling a batch
Metadata edit results
UPDATE_TRANSACTION_METADATA reports per edit, not just per target — one item per target user, each carrying a metadataResults entry for every transaction it tried to change:
SUCCEEDED while individual edits inside it failed, so check metadataResults[].success rather than the item status alone.
Results file
For read exports (GET_TRANSACTIONS_EXPORT, GET_BALANCES_EXPORT) the output is an NDJSON file. For the write operations (CREATE_TRANSFER, DEPOSIT_CASH_BALANCE) it’s a per-target results ledger — one row per target with its status, resultResourceId and any error.
resultUrlis minted only when you select it. Pollingstatuswithout it stays cheap; the URL is signed on request.- It’s
nulluntil the job is terminal, and staysnullfor jobs that produced no file — for example when every target was denied. - It expires shortly after issue. Check
resultUrlExpiresAt; to get a fresh URL, just selectresultUrlagain. - Neither Fluz’s API nor your integration is in the download path — the URL points straight at storage.
Cancelling
Errors
An id that belongs to another application returns the same not-found response as an id that doesn’t exist — job ids are not enumerable across applications.
Branch on
extensions.code, not on message text.
Polling guidance
PollgetBulkOperationJob — it reads one row and never scans items. A few seconds between polls is reasonable for small jobs; back off for large ones, since a 10,000-item job is paced by queue throughput, not by how often you ask.
Stop on any terminal status. Then:
COMPLETED— nothing to reconcile.COMPLETED_WITH_ERRORS— readstatus: FAILEDitems, andSKIPPEDtoo ifskippedItemCount > 0.FAILEDorCANCELLED— read the items to see how far it got before stopping.
resultUrl only once the status is terminal; before that it’s always null.