Skip to main content

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_kindMoves valueYou sendNot allowed
INTERBANKTo a beneficiary at another banksource (external_account_id and/or originator), destination (with destination.bank_id, plus external_account_id and/or beneficiary)receiving_bank_id
BANK_ROUTEDTo another bank, by bank and asset onlyreceiving_bank_idsource, 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:

PartyRegistered accountParty details
Sendersource.external_account_id: must be an active registration at your banksource.originator: passed to the receiving bank for screening
Beneficiarydestination.external_account_id: must be an active registration at the receiving bankdestination.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 202 response 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, 403 and 422 errors in the table below that carry a code) do not consume the key. Errors returned after the key was claimed (the 400 errors without a code, from the platform stage) leave the key locked for up to 5 minutes, during which a retry with the same key returns 409 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.id is the operation_id; poll GET /v1/operations/{operation_id} or subscribe to webhooks for the outcome.
  • data.relationships.resource identifies the transfer; its id is the transfer_id (a different UUID from the operation_id) for GET /v1/transfers/{transfer_id}. The transfer becomes readable shortly after the 202; until then that endpoint returns 404.

The transfer's state is always the same as its operation's state:

StateMeaningNext
ACCEPTEDRequest stored. The platform resolves the accounts, selects liquidity and reserves funds and limits.AWAITING_SCREENING or PROCESSING; FAILED
AWAITING_SCREENINGOnly 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
PROCESSINGThe transfer is being initiated on the ledger.PENDING_REVIEW; FAILED
PENDING_REVIEWInitiated on the ledger and waiting for a checker at the sending bank (maker/checker).PENDING_COMMITS
PENDING_COMMITSThe checker decided; the commit (on approval) or the cancellation (on rejection) is being written to the ledger.SUCCEEDED, REJECTED; FAILED
SUCCEEDEDTerminal. The transfer is committed on the ledger.
REJECTEDTerminal. Declined by the receiving bank during screening or by the sending bank's checker.
EXPIREDTerminal. The screening window closed without a decision. Nothing was written to the ledger.
FAILEDTerminal. 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):

StatuscodeWhen
400INVALID_FIELD_FORMATamount.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.
403UNAUTHORIZED_SCOPEThe token lacks connector:transfers:create.
403BANK_SCOPE_MISMATCHA 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.
409IDEMPOTENCY_CONFLICT, IDEMPOTENCY_PENDINGSee 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).
422VALIDATION_ERRORexternal_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).
503OUTBOUND_HALTED, READ_ONLY, OPERATIONAL_STATE_UNKNOWNThe 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_codeFinal stateMeaning
INSUFFICIENT_ELIGIBLE_LIQUIDITYFAILEDThe sending bank does not hold enough eligible liquidity.
LIMIT_EXCEEDEDFAILEDThe sender's mint limit (result_message mint limit exceeded) or the receiving bank's exposure limit (exposure limit exceeded) would be exceeded.
ASSET_NOT_ELIGIBLEFAILEDNo eligible liquidity position for this asset (no eligible liquidity position for this asset).
BANK_NOT_ELIGIBLEFAILEDA participant bank is not eligible for the transfer.
VALIDATION_ERRORFAILEDThe amount is not valid for liquidity selection.
SCREENING_REJECTEDREJECTEDThe receiving bank rejected the transfer during screening.
SCREENING_EXPIREDEXPIREDThe screening window closed without a decision.
SCREENING_UNDELIVERABLEFAILEDThe screening notification could not be delivered to the receiving bank.
SCREENING_NOT_STARTEDFAILEDDelivery of the screening notification was never confirmed before the screening ceiling.
CHECKER_REJECTEDREJECTEDThe 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​

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.