The Moov Transfers API moves money between Moov accounts across ACH, RTP and card rails, and its most important design decision sits in one optional header. Call POST /accounts/{accountID}/transfers without x-wait-for and you get the asynchronous response by default. Send x-wait-for: rail-response and you get full rail-specific details, with a timeout at 15 seconds. An integrator who treats a success code here as “the money moved” has misread the contract.
Moov scores 74.2 on the Kin Score (exemplar) and 56.8 on Agent Readiness (agent-native). Its page lists 42 API contracts, though its header counts 3 published, and this one has 16 operations.
What the API covers
| Resource | Operations | Methods |
|---|---|---|
| Transfers | 4 | create, list, batch get, get |
| Transfer metadata | 1 | PATCH |
| Transfer options | 1 | POST |
| Cancellations | 3 | create, list, get |
| Refunds | 3 | initiate, list, get |
| Reversals | 1 | POST |
| Transfer configuration | 3 | create, get, update |
The API page carries an OpenAPI contract, documentation, an AsyncAPI definition of Moov’s webhooks, a JSON-LD context and two JSON Schemas. The AsyncAPI file names transfer.created and transfer.updated, and it describes the first as the start of the transfer lifecycle. That is the other half of the async design: you start a transfer over REST, then follow its status through events or by polling.
Three successes, one of them a warning
The create operation documents three different success codes, and they are not interchangeable:
- 200 returns a
CreatedTransfer, the synchronous result. - 201 means the transfer was created, but an error occurred while generating the synchronous response, so you get the asynchronous object instead.
- 202 means the transfer was created, but rail details may not be available within the 15-second window.
So 201 and 202 are both “the transfer exists, and you do not have the full details yet.” Code that branches only on “2xx” loses that distinction, and code that treats a slow response as a failure can retry a transfer that exists.
The contract guards against that retry. x-idempotency-key is a required header on transfer creation, and a duplicate key returns a 409 whose documented body is a Transfer. The status code reads as an error, but the payload is the transfer you already created. Reversals and refund creation also take the key. Cancellation creation does not declare it, so check that path before automating retries.
X-Moov-Version is optional, and omitting it defaults you to v2024.01.00, while this document describes v2026.01.00. The refund operation’s own description also points you to the reversal endpoint for more complete cancel and refund options.
What is ours to fix
The three transfer-config operations do not match the other thirteen. They declare bearer auth where the others declare Basic, they have no descriptions, and each documents only a 200. Our contract is assembled from three of Moov’s published definitions, and these three paths carry a different server entry from the rest, which points to a different source. We have not verified them against Moov’s live API, so treat them as lower confidence. The four refund and reversal operations also appear again under the separate Moov Refunds API, which inflates any operation count that adds the two together.
Takeaway
Build on this API as an event-driven system, pin X-Moov-Version, store your idempotency key before the first attempt, and treat 201, 202 and 409 as states to handle, not outcomes to log. See the contract at apis.io/apis/moov/moov-transfers-api/.