Creating a payment

POST /v1/outbound-payments sends money to a payee. This guide covers the fields you have to get right, the ones that trip people up, and what to do with the response.

Before you start you need the payments:write scope, a payee (see Creating payees), an account with an available balance in the currency you are sending, and a signing key. Payments are money-moving, so the request must carry an RFC 9421 signature on top of the usual token and DPoP proof. See Authentication & Request Signing.

The smallest request that works

{
  "clientId": "a1000000-0000-0000-0000-000000000001",
  "payeeId": "b2000000-0000-0000-0000-000000000002",
  "accountId": "c3000000-0000-0000-0000-000000000003",
  "amount": 50000,
  "currency": "GBP",
  "reference": "Invoice INV-2026-0042",
  "idempotencyKey": "payment-2026-08-24-001"
}

That is all seven required fields. Everything else is optional and most of it is resolved for you.

⚠️

amount is an integer in minor units

50000 is 500.00 GBP, not 50,000. See Amounts, currencies and dates.

Every field

FieldRequiredMaxNotes
clientIdYesThe paying client. Must be a client your credential may act for. This may be a sub account Client Id.
payeeIdYesThe payee to pay.
accountIdYesThe account to debit. It must hold a balance in currency.
amountYesInteger, minor units, greater than zero.
currencyYes3ISO 4217, upper case. An empty value returns 400 with Currency must not be empty.
referenceYes140Travels to the beneficiary.
idempotencyKeyYes100Your key. Makes the request retry-safe.
internalReferenceNo140Your own reference. Never leaves our system.
purposeNo1000Purpose of payment. Some corridors require a code. See below.
paymentMethodNoFps, Chaps, Swift or Sepa. Blank auto-resolves.
chargeTypeNoSwift only. Blank defaults to OUR. Sending it on any other rail is rejected.
valueDateNoYYYY-MM-DD. Blank resolves to the earliest available date for the resolved rail. See Value dates.
copyNumberNoDefaults to 1. Increment only when you deliberately want to send the same payment twice.
ultimateDebtor*NoNine fields describing the party the money is ultimately from. See below.

Three reference fields, three jobs

This is the most common source of confusion:

  • reference is what the beneficiary sees on their statement. It is required, up to 140 characters, and in the sandbox it also selects the simulated outcome.
  • internalReference is yours. It is stored against the payment and returned on reads, but never sent to the beneficiary or the rail. Use it to carry your own order or invoice id.
  • purpose describes why the payment is being made, for regulatory reporting. It is not a free-text note for the beneficiary.

Purpose codes

Some destination countries require a purpose code rather than free text. GET /v1/reference/countries/{countryCode}/payment-purpose-codes returns the codes for a destination, as a plain array of {code, description}. Note that this endpoint is not paginated.

GET /v1/reference/countries/AE/payment-purpose-codes

It returns the country-specific list where one exists, and the generic list otherwise. 400 means the country code is not a valid ISO 3166-1 alpha-2 code; 404 means the country is not supported. Call it with the payee's bankCountry and cache the result.

Ultimate debtor

When you are paying on behalf of somebody else, such as a marketplace paying a seller's supplier or a platform paying out for a merchant, the rails need to know who the money is really from. Nine flat fields carry it:

FieldMax
ultimateDebtorName150
ultimateDebtorIdentificationNumber100
ultimateDebtorCountry3
ultimateDebtorBuildingName100
ultimateDebtorBuildingNumber100
ultimateDebtorStreetAddress250
ultimateDebtorCity100
ultimateDebtorCounty100
ultimateDebtorPostCode20

They travel as a set. ultimateDebtorName is the anchor. On the payment you read back they come back as a nested ultimateDebtor object in which name is the only mandatory member. Supplying a partial set is a documented cause of 422, so send the complete address you hold or send none of them at all.

If you are paying from your own funds, leave all nine out.

Choosing the rail, or not choosing it

Leave paymentMethod blank and the rail is resolved from what the payee and the account support. That is the right default for almost every integration, because the resolver knows which rails are enabled for you and which the payee's bank details actually permit.

Set it explicitly only when you have a reason, for example forcing Chaps for a same-day high-value payment. An explicit value is rejected when the rail is disabled for your client, unsupported for that payee or account, or when the payment is really an internal IBANQ-to-IBANQ movement. For that last case use POST /v1/internal-payments instead. The payee's supportedPaymentTypes tells you what is available before you try.

chargeType only applies to Swift, and only OUR is documented as the default. Sending it on Fps, Chaps or Sepa is rejected.

Value dates

valueDate is a UK business date, not a timestamp. Leave it blank and it resolves to the earliest available date for the resolved rail: today, if today is a working day for the rail and IFX's release cut-off for that rail has not passed, otherwise the rail's next working day. A date you supply must be a working day for the rail, on or after the earliest available date and no later than the latest allowed date. Fps has no non-working days. A date outside that window, or one that is not a working day, is rejected with 422, and the non-working-day error names the next working day. A payment with a future value date parks at Scheduled until that date arrives.

Finding available value dates

GET /v1/outbound-payments/value-dates returns the value dates available on each rail before you create a payment. It needs the payments:read scope and no request signing. It takes the payee's bank country instead of a payee id, so you can call it before the payee exists.

Query parameters:

  • accountId (required): the account the payment would be made from, as returned by GET /v1/accounts.
  • payeeBankCountry (required): ISO 3166 alpha-2 code of the payee's bank country, as in a payee's bankCountry. Case-insensitive.
  • currency (required): ISO 4217 code, for example GBP. Case-insensitive. INR is not available through the Client API.
  • paymentMethod (optional): Fps, Chaps, Swift or Sepa. Leave it out to get every eligible rail.

The response gives latestValueDate, the latest value date create accepts, and one entry per eligible rail with its earliestValueDate and cutOffAt. The values below are illustrative.

{"currency": "GBP", "latestValueDate": "2025-01-07", "paymentMethods": [{"paymentMethod": "Fps", "earliestValueDate": "2024-01-08", "cutOffAt": null}, {"paymentMethod": "Chaps", "earliestValueDate": "2024-01-08", "cutOffAt": "2024-01-08T16:40:00Z"}, {"paymentMethod": "Swift", "earliestValueDate": "2024-01-08", "cutOffAt": "2024-01-08T17:00:00Z"}]}

earliestValueDate is today if today is a working day for the rail and IFX's release cut-off has not passed, otherwise the rail's next working day. cutOffAt is IFX's cut-off for releasing payments on that date on that rail, in UTC. It is not a cut-off for you. It is null for Fps, which has no cut-off.

Create accepts any working day for the rail from earliestValueDate to latestValueDate. The endpoint returns only these bounds, not every working day. Weekends of the payee's bank country and bank holidays in the UK and the payee's bank country are not working days (German holidays also apply to Sepa). Fps runs every day.

A rail is left out when the currency, your account and the payee bank country do not allow it, when it is disabled, when Sepa is restricted for your client, or when no cut-off is configured for it. If none remain the endpoint returns 422. Because no payee is named, the payee's bank details are not checked. Send the same paymentMethod when you create the payment, as create can still refuse a rail the payee's details do not support.

Errors: 400 for a missing or invalid parameter, 403 without payments:read, 404 when the account is not visible to your credential, and 422 when no rail or value date is available for the request.

What you get back

201 returns the payment's id and its URL in the Location header. 200 means your request was an identical retry and the original payment's id is being replayed. Treat both as success.

Store the id immediately. If you lose the response, do not create the payment again: retry the identical request with the same idempotencyKey and you will get the original id back. See Idempotency.

Reading the payment

GET /v1/outbound-payments/{id} returns the payment with everything the platform added:

Note that in the response, amount is returned as an Amount object (amountInMinorUnits, value, currency) rather than the flat amount/currency pair you sent in the request. See Amounts, currencies and dates.

FieldWhat it tells you
status, statusReasonWhere the payment is and, on a return or cancellation, the reason.
paymentMethodThe rail that was actually resolved, even if you left it blank.
feeAny fee charged for the payment, returned as an Amount object (amountInMinorUnits, value, currency). Only present when the payment is billed on demand.
fxTradeIdSet when the payment was funded by an FX trade.
massPaymentIdSet when the payment came from a mass payment.
approvalsWho approved it and when. API payments skip manual approval, so expect this to be empty or system-populated.
createdAt, createdBy, channelProvenance: when, by whom, and from where.
codeThe payment's human-readable code.
ultimateDebtorThe nested form of the nine flat fields you sent.
eventsEvery event raised for this payment so far, in the same shape as a row from GET /v1/events.

Polling and the meaning of each status are covered in Payment lifecycle and statuses.

Cancelling

POST /v1/outbound-payments/{id}/cancel cancels a payment that has not yet been sent for processing and returns 204. reason is required and must be incorrect_amount, incorrect_payee or payment_not_required. A payment already with the processor or the bank returns 422; one already cancelled returns 409. Status transitions are asynchronous, so whether a payment is cancellable can change between one call and the next. In particular a payment that has just been created may sit briefly in a non-cancellable processing step before the sandbox records a hold or a schedule.

Errors worth handling

CodeMeaningWhat to do
400Malformed request, or the RFC 9421 signature failed. Signature failures read Request signature verification failed: <reason>.Fix the request. Signature problems are almost always a re-serialised body, a charset suffix on Content-Type, or a stale created.
403Payments are not enabled for this client, or clientId is not one you may act for.Not a token problem. Check the client.
404The payee, account or client does not exist, or belongs to someone else.Check the ids.
409The idempotencyKey was reused with different details, or this is a byte-identical duplicate under a new key without copyNumber incremented.The body carries existingPaymentId where one exists. Read it before retrying.
422A business rule rejected it: insufficient balance, an unsupported rail for the payee, an out-of-window valueDate, or incomplete ultimate debtor details. A valueDate that is not a working day for the rail is also rejected.Read errors, fix the underlying condition, then retry. Retrying unchanged fails the same way.

Testing it

In the sandbox the reference you send selects the outcome, so you can drive a payment to Rejected, Returned or a stuck Processing on demand. Fund the account first with POST /v1/simulations/inbound-payments. See Sandbox Outcome Triggers for the trigger vocabulary, and strip those prefixes from your fixtures before you go live.


Did this page help you?