Create a transfer
POST/v1/transfers
Submit a transfer of existing value for asynchronous execution. Requires scope
connector:transfers:create. The sending bank is always the calling bank and cannot be
overridden. This endpoint moves existing value only; it does not issue or redeem assets.
Redemptions are created by redemption policies (see GET /v1/redemption-policies).
Transfer kinds
data.attributes.transfer_kind selects the request shape:
transfer_kind | Moves value | You send | Not allowed |
|---|---|---|---|
INTERBANK | To a beneficiary at another bank | source (external_account_id and/or originator), destination (with destination.bank_id, plus external_account_id and/or beneficiary) | receiving_bank_id |
BANK_ROUTED | To another bank, by bank and asset only | receiving_bank_id | source, destination |
For INTERBANK and BANK_ROUTED the platform selects which issuers' tokens settle the
transfer: tokens issued by the receiving bank first, then tokens issued by other banks,
then tokens issued by the sending bank. For BANK_ROUTED it also selects the source and
destination holding accounts. The result is reported as transfer_plan on the transfer.
To check liquidity and limits between your bank and the receiving bank before submitting
an INTERBANK or BANK_ROUTED transfer, use POST /v1/transfers/preflight; it moves no
value. Preflight does not validate accounts or beneficiary details, so an INTERBANK
transfer that preflights as would_succeed: true can still end FAILED if an
external_account_id is not registered.
Account identifiers
external_account_id is the core banking identifier the owning bank registered with
POST /v1/accounts for the same asset_id. It must be 1 to 34 characters and must
satisfy the ISO 20022 Max34Text type. An account that is not registered does not fail
the request synchronously; the transfer ends FAILED (see below).
Each party of an INTERBANK transfer is identified in one of two ways, or both:
| Party | Registered account | Party details |
|---|---|---|
| Sender | source.external_account_id: must be an active registration at your bank | source.originator: passed to the receiving bank for screening |
| Beneficiary | destination.external_account_id: must be an active registration at the receiving bank | destination.beneficiary: passed to the receiving bank to resolve and screen |
A party with neither is rejected with 422 VALIDATION_ERROR. An unregistered
external_account_id ends the transfer FAILED, whether or not details are also sent;
details are never checked against registrations. Either way the value moves between the
two banks on the ledger in the same way: the parties only tell the receiving bank who sent
the payment and whom to credit.
originator and beneficiary share one shape: the party's name and account (an
IBAN, a US routing and account number, or OTHER for a plain account number), and
optionally a bic and a postal_address. The platform checks the format only (IBAN
check digits, 9-digit routing number, BIC format, field lengths), not that the party
exists. The receiving bank reads both from GET /v1/transfers/{transfer_id} (including
while the transfer is AWAITING_SCREENING) and from the transfer.received webhook,
resolves the beneficiary, and screens both parties if it screens transfers.
alias and expected_holder_name are accepted but ignored; use originator and
beneficiary instead.
client_reference (1 to 256 characters) and metadata (a JSON object of at most 4096
bytes when serialised) are stored and returned on the transfer to the sending and the
receiving bank. Use them for information without a dedicated field, such as remittance
details.
bank_id on source is optional and, if sent, must be the calling bank.
destination.bank_id is required for INTERBANK and must be a different bank.
Amounts
amount.value is a positive integer string in the asset's smallest unit (minor units):
digits only, no decimal point, exponent or thousands separator, and not zero. For an asset
with 2 decimals, "150000" is 1,500.00. amount.scale is required by the schema but is
not used or validated: value is always interpreted in the asset's smallest unit, and the
transfer reports amount.scale from the asset definition. Send the asset's number of
decimals as scale.
Idempotency
Idempotency-Key is required. Keys are scoped to the calling principal and this
endpoint, and are kept for 72 hours.
- Same key and same body: returns the original
202response again; no second transfer is created. - Same key and a different body (including a different
transfer_kind):409 IDEMPOTENCY_CONFLICT. - Same key while the first request is still being processed:
409 IDEMPOTENCY_PENDING. - Validation errors raised by the Lyriq Connector itself (the
400,403and422errors in the table below that carry acode) do not consume the key. Errors returned after the key was claimed (the400errors without acode, from the platform stage) leave the key locked for up to 5 minutes, during which a retry with the same key returns409 IDEMPOTENCY_PENDING. Use a new key for a corrected request.
Response and lifecycle
On success the response is 202 Accepted with an operation document:
data.idis theoperation_id; pollGET /v1/operations/{operation_id}or subscribe to webhooks for the outcome.data.relationships.resourceidentifies the transfer; itsidis thetransfer_id(a different UUID from theoperation_id) forGET /v1/transfers/{transfer_id}. The transfer becomes readable shortly after the202; until then that endpoint returns404.
The transfer's state is always the same as its operation's state:
| State | Meaning | Next |
|---|---|---|
ACCEPTED | Request stored. The platform resolves the accounts, selects liquidity and reserves funds and limits. | AWAITING_SCREENING or PROCESSING; FAILED |
AWAITING_SCREENING | Only when beneficiary screening is enabled on the network. The receiving bank has been notified (webhook beneficiary.screening.requested) and must decide with POST /v1/transfers/{transfer_id}/screening-decisions. Nothing has been written to the ledger. | PROCESSING, REJECTED, EXPIRED, FAILED |
PROCESSING | The transfer is being initiated on the ledger. | PENDING_REVIEW; FAILED |
PENDING_REVIEW | Initiated on the ledger and waiting for a checker at the sending bank (maker/checker). | PENDING_COMMITS |
PENDING_COMMITS | The checker decided; the commit (on approval) or the cancellation (on rejection) is being written to the ledger. | SUCCEEDED, REJECTED; FAILED |
SUCCEEDED | Terminal. The transfer is committed on the ledger. | |
REJECTED | Terminal. Declined by the receiving bank during screening or by the sending bank's checker. | |
EXPIRED | Terminal. The screening window closed without a decision. Nothing was written to the ledger. | |
FAILED | Terminal. See result_code and result_message on the operation. |
Maker/checker review. Every transfer that is initiated on the ledger enters
PENDING_REVIEW. The platform creates one review task for the sending bank requiring one
approval from a principal with scope connector:reviews:decide other than the principal
that submitted the transfer (self-approval is refused). The submitter sees the transfer and
operation in PENDING_REVIEW; the review task is listed on the operation and in
GET /v1/reviews. A checker decides with POST /v1/reviews/{review_id}/decisions:
approval leads to SUCCEEDED, rejection leads to REJECTED.
Synchronous errors and FAILED outcomes
Errors returned synchronously (no transfer is created):
| Status | code | When |
|---|---|---|
400 | INVALID_FIELD_FORMAT | amount.value is not a non-negative integer, or is zero (pointer /data/attributes/amount/value); external_account_id is not valid Max34Text. |
400 | (none) | Malformed JSON; missing Idempotency-Key; rejected by the platform stage, with the reason in title: a terminated sending or receiving bank. |
403 | UNAUTHORIZED_SCOPE | The token lacks connector:transfers:create. |
403 | BANK_SCOPE_MISMATCH | A bank_id that must be the calling bank is another bank or not a UUID; destination.bank_id (INTERBANK) or receiving_bank_id (BANK_ROUTED) is the calling bank; destination.bank_id is not a UUID. |
409 | IDEMPOTENCY_CONFLICT, IDEMPOTENCY_PENDING | See Idempotency. |
422 | (none) | The body does not match the schema: unknown or misplaced field, missing required field, wrong type, unknown transfer_kind, wrong data.type (title Invalid request body). |
422 | VALIDATION_ERROR | external_account_id missing or blank, destination.bank_id missing (INTERBANK), an INTERBANK source with neither external_account_id nor originator or destination with neither external_account_id nor beneficiary, receiving_bank_id not a UUID (detail transfer party fields are invalid for this transfer_kind); a transfer_kind that is not currently accepted (pointer /data/attributes/transfer_kind); a malformed source.originator, destination.beneficiary, client_reference or metadata (pointer to the field). |
503 | OUTBOUND_HALTED, READ_ONLY, OPERATIONAL_STATE_UNKNOWN | The network is not accepting new transfers. |
Liquidity, limits, eligibility and account problems are not checked synchronously. They
are reported by the operation ending FAILED (or REJECTED/EXPIRED), with these
result_code values:
result_code | Final state | Meaning |
|---|---|---|
INSUFFICIENT_ELIGIBLE_LIQUIDITY | FAILED | The sending bank does not hold enough eligible liquidity. |
LIMIT_EXCEEDED | FAILED | The sender's mint limit (result_message mint limit exceeded) or the receiving bank's exposure limit (exposure limit exceeded) would be exceeded. |
ASSET_NOT_ELIGIBLE | FAILED | No eligible liquidity position for this asset (no eligible liquidity position for this asset). |
BANK_NOT_ELIGIBLE | FAILED | A participant bank is not eligible for the transfer. |
VALIDATION_ERROR | FAILED | The amount is not valid for liquidity selection. |
SCREENING_REJECTED | REJECTED | The receiving bank rejected the transfer during screening. |
SCREENING_EXPIRED | EXPIRED | The screening window closed without a decision. |
SCREENING_UNDELIVERABLE | FAILED | The screening notification could not be delivered to the receiving bank. |
SCREENING_NOT_STARTED | FAILED | Delivery of the screening notification was never confirmed before the screening ceiling. |
CHECKER_REJECTED | REJECTED | The sending bank's checker rejected the transfer. |
Other failures (for example an unregistered account, with result_message
no active mapping found for the destination account, or a ledger failure with
result_message transfer ledger submission failed) end FAILED with a generic
result_code; rely on result_message for the reason.
Request
Responses
- 202
- 400
- 401
- 403
- 409
- 422
- 429
- 503
Transfer accepted for asynchronous execution. data is the operation that tracks it;
data.relationships.resource identifies the transfer. The response is the same for
every transfer_kind. A replay with the same Idempotency-Key and body returns this
document again.
Malformed request, a field that fails its format rule, a missing Idempotency-Key,
or a rejection by the platform stage (reason in title, no code). See the error
table in the operation description.
The bearer token is missing, malformed, expired, signed by an unknown key, or was not issued by the platform IAM for the Lyriq Connector. Obtain a new token and retry. See the Authentication section.
The token lacks connector:transfers:create (UNAUTHORIZED_SCOPE), or a bank
identifier in the body conflicts with the calling bank (BANK_SCOPE_MISMATCH).
Idempotency-Key reuse: IDEMPOTENCY_CONFLICT (different body) or
IDEMPOTENCY_PENDING (the first request with this key has not completed).
The body parsed but is not valid for its transfer_kind: either it does not match
the schema (title Invalid request body, no code), or a cross-field rule failed
(VALIDATION_ERROR).
The request was refused because a rate limit was reached (code RATE_LIMITED). No
Retry-After header is sent; retry with exponential backoff.
The request could not be served right now. Either the network is not fully operational
(OUTBOUND_HALTED or READ_ONLY: mutations are refused while read endpoints keep
working; OPERATIONAL_STATE_UNKNOWN: the state could not be determined), or a platform
dependency is temporarily unavailable. No Retry-After header is sent; retry later with
backoff. When retrying a mutation, reuse the same Idempotency-Key and body.