Finalize an existing v2 draft transfer
Finalizes a transfer created by `POST /v2/transfers/create`. The client may send the full `CreatePaymentRequestV2` again, or omit the body and let the server reuse the finalize-shaped payload stored on the draft. V2 request metadata uses `metadata`, not legacy `meta`. Cross-currency transfers require a locked top-level `quote_id` from `POST /v2/transfers/quotes`. Repeated and concurrent calls are deduplicated by the draft `transfer_id`: the finalized transfer uses a server-derived `draft-finalize:` idempotency key, while the draft superseded link is updated under a row lock and preserved on retries.
Authorization
BearerAuth JWT authentication token
In: header
Path Parameters
Transfer UUID
transfer_id path parameter
Header Parameters
Language preference for the response
Optional idempotency key for finalize retries; when a body is present it must match body idempotency_key. Mismatches return 400 common.invalid_input with details[0].field=idempotency_key and details[0].rule=header_body_mismatch. For requests with a body, omitting both key locations returns 400 common.invalid_input with details[0].field=idempotency_key and details[0].rule=required; empty-body requests may omit both because the stored draft payload is reused. This key governs the draft lookup only. The resulting finalized transfer is deduplicated by a server-derived key bound to the draft transfer_id, so repeated finalize calls on the same draft return the same finalized transfer regardless of the client key sent.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Canonical v2 finalize/draft payment body. Same as v1 CreatePaymentRequest, but client metadata is sent as metadata instead of legacy meta.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v2/transfers/string/finalize" \ -H "Content-Type: application/json" \ -d '{ "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e", "channel": "string", "amount": { "amount_minor": "string", "currency": "string" }, "source": { "counterparty_id": "fd38dae9-b300-4017-a630-101c4279eafd", "cp_account_id": "eb064395-4075-439f-a7b1-d660c0883425" }, "destination": { "counterparty_id": "fd38dae9-b300-4017-a630-101c4279eafd", "cp_account_id": "eb064395-4075-439f-a7b1-d660c0883425" }, "fee_bearer": "DEBT" }'{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"transfer_id": "d4a2d8dd-7def-4545-a062-761683b9aa05",
"status": "draft",
"active": true,
"product_id": "0d012afa-f885-4e65-aeca-37e27701e2d1",
"product_code": "string",
"customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",
"direction": "outbound",
"source": {
"counterparty_id": "fd38dae9-b300-4017-a630-101c4279eafd",
"cp_account_id": "eb064395-4075-439f-a7b1-d660c0883425"
},
"destination": {
"counterparty_id": "fd38dae9-b300-4017-a630-101c4279eafd",
"cp_account_id": "eb064395-4075-439f-a7b1-d660c0883425"
},
"sender": {
"name": "Muster Handels GmbH",
"iban": "CH9300762011623852957",
"bic": "UBSWCHZH80A",
"wallet_address": "0x0000000000000000000000000000000000000001",
"account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
"customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e"
},
"beneficiary": {
"name": "Muster Handels GmbH",
"iban": "CH9300762011623852957",
"bic": "UBSWCHZH80A",
"wallet_address": "0x0000000000000000000000000000000000000001",
"account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
"customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e"
},
"channel": "string",
"fee_bearer": "DEBT",
"quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",
"reference": "string",
"amount": {
"amount": "string",
"currency": "string",
"precision": 0
},
"fee_amount": {
"amount": "string",
"currency": "string",
"precision": 0
},
"amount_net": {
"amount": "string",
"currency": "string",
"precision": 0
},
"recipient_amount": {
"amount": "string",
"currency": "string",
"precision": 0
},
"metadata": {},
"created_at": "2019-08-24T14:15:22Z",
"updated_at": "2019-08-24T14:15:22Z"
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"status": 422,
"message": "This payee's account cannot accept payments at the moment. Choose another payee, or ask the payee for up-to-date details.",
"code": "transfers_m.beneficiary_account_not_active",
"class": "validation"
}{
"status": 429,
"message": "Rate limit exceeded",
"code": "rate_limits_m.exceeded",
"class": "temporary",
"retryable": true
}{
"status": 0,
"message": "string",
"code": "transfers_m.account_not_found",
"class": "validation",
"retryable": false,
"details": [
{
"field": "amount",
"rule": "gt",
"param": "0",
"message": "amount must be greater than 0"
}
]
}{
"overall_status": "unhealthy",
"message": "Service is shutting down",
"timestamp": "2026-08-27T15:04:05Z"
}Creates a transfer in `draft` status from the modern v2 finalize payload. The request body is `DraftPaymentRequestV2` (the draft-relaxed variant of `CreatePaymentRequestV2`) and uses `metadata` for client metadata. Only `source` (and its owning `customer_id`) is required to save a draft; every other field is optional. The server resolves the source (and the destination when supplied) and stores the finalize-shaped payload on the draft; contract validation (product, destination, amount, FX/quote, product pre-flight) is deferred to finalize. First successful saves return 201. A retry with the same key for the same `customer_id` while the stored row is still `draft` and not claimed by finalize returns the existing draft with 200 and `Idempotency-Replayed: true`; the request body is not stored again. Replay grants are re-derived from the saved draft payload when it still resolves: the originator grant stays pinned to the saved `customer_id`, and a beneficiary grant is retried when the saved destination resolves to an internal beneficiary. If the saved destination can no longer be resolved after the draft exists, replay may fall back to persisted beneficiary columns only when they still match an internal account, IBAN, or wallet. Derived participant customer columns are not used by themselves to widen access on retry. Reusing the key after the stored row has left `draft` returns 409 (`transfers_m.draft_idempotency_key_reused`); replaying while finalize has already claimed the draft returns 409 (`transfers_m.draft_finalize_in_progress`). If the same key already belongs to another customer's draft-create row, create returns 409 (`transfers_m.idempotency_key_conflict`) instead of returning that row. Use `PUT /v2/transfers/{transfer_id}` to edit the draft and `POST /v2/transfers/{transfer_id}/finalize` to execute it.
Canonical modern finalize endpoint. Creates a transfer after pre-flight discovery and returns `TransferPaymentResponse`. Request body is `CreatePaymentRequestV2`: same selector/amount semantics as v1 finalize, but client metadata is sent as `metadata` (not `meta`). The server maps to the same product matrix filter, resolves `source` to the real debit `source_account_id = accounts.accounts.id`, normalizes `destination` to exact universal `destination_account_id = counterparties.cp_accounts.id`, runs `ValidatePreFlightRequest`, then `PreFlightForFinalize`; on deny returns 422. Cross-currency requires a locked top-level `quote_id` from `POST /v2/transfers/quotes`.