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.
amountis an integer in minor units
50000is 500.00 GBP, not 50,000. See Amounts, currencies and dates.
Every field
| Field | Required | Max | Notes |
|---|---|---|---|
clientId | Yes | The paying client. Must be a client your credential may act for. This may be a sub account Client Id. | |
payeeId | Yes | The payee to pay. | |
accountId | Yes | The account to debit. It must hold a balance in currency. | |
amount | Yes | Integer, minor units, greater than zero. | |
currency | Yes | 3 | ISO 4217, upper case. An empty value returns 400 with Currency must not be empty. |
reference | Yes | 140 | Travels to the beneficiary. |
idempotencyKey | Yes | 100 | Your key. Makes the request retry-safe. |
internalReference | No | 140 | Your own reference. Never leaves our system. |
purpose | No | 1000 | Purpose of payment. Some corridors require a code. See below. |
paymentMethod | No | Fps, Chaps, Swift or Sepa. Blank auto-resolves. | |
chargeType | No | Swift only. Blank defaults to OUR. Sending it on any other rail is rejected. | |
valueDate | No | YYYY-MM-DD. Blank resolves to the earliest available date for the resolved rail. See Value dates. | |
copyNumber | No | Defaults to 1. Increment only when you deliberately want to send the same payment twice. | |
ultimateDebtor* | No | Nine fields describing the party the money is ultimately from. See below. |
Three reference fields, three jobs
This is the most common source of confusion:
referenceis what the beneficiary sees on their statement. It is required, up to 140 characters, and in the sandbox it also selects the simulated outcome.internalReferenceis 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.purposedescribes 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-codesIt 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:
| Field | Max |
|---|---|
ultimateDebtorName | 150 |
ultimateDebtorIdentificationNumber | 100 |
ultimateDebtorCountry | 3 |
ultimateDebtorBuildingName | 100 |
ultimateDebtorBuildingNumber | 100 |
ultimateDebtorStreetAddress | 250 |
ultimateDebtorCity | 100 |
ultimateDebtorCounty | 100 |
ultimateDebtorPostCode | 20 |
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 byGET /v1/accounts.payeeBankCountry(required): ISO 3166 alpha-2 code of the payee's bank country, as in a payee'sbankCountry. 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.
| Field | What it tells you |
|---|---|
status, statusReason | Where the payment is and, on a return or cancellation, the reason. |
paymentMethod | The rail that was actually resolved, even if you left it blank. |
fee | Any fee charged for the payment, returned as an Amount object (amountInMinorUnits, value, currency). Only present when the payment is billed on demand. |
fxTradeId | Set when the payment was funded by an FX trade. |
massPaymentId | Set when the payment came from a mass payment. |
approvals | Who approved it and when. API payments skip manual approval, so expect this to be empty or system-populated. |
createdAt, createdBy, channel | Provenance: when, by whom, and from where. |
code | The payment's human-readable code. |
ultimateDebtor | The nested form of the nine flat fields you sent. |
events | Every 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
| Code | Meaning | What to do |
|---|---|---|
400 | Malformed 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. |
403 | Payments are not enabled for this client, or clientId is not one you may act for. | Not a token problem. Check the client. |
404 | The payee, account or client does not exist, or belongs to someone else. | Check the ids. |
409 | The 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. |
422 | A 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.
Updated 10 days ago