Make a payout to an account that paid in

Learn about closed-loop payouts and how to make one.

In a closed-loop payout, a payment is made to a user who has made a previous payment (known as a closed-loop pay-in) to your merchant account.

We identify the user who made the pay-in using two types of id:

  • payment_source_id, which identifies a set of bank account details
  • user id, which identifies a specific user.

These id values ensure that the payout is sent to the correct account, and that you don't need to collect additional user details.

After a closed-loop pay-in, you can can easily make multiple payouts to the same user, and also access other merchant account-related functionality.

After a closed-loop pay-in, you can easily make multiple payouts to the same user, and also access other merchant account-related functionality.

Closed-loop payouts are useful if your customers need to top up and withdraw from an account frequently: for example, in gaming or investment apps.

Closed-loop payout overview

To make a closed-loop payout successfully, there are three key steps:

  1. Authentication: Properly configure authentication before your request. Most of this is the same as the initial pay-in request.
  2. Request configuration: Provide information including the value of the payout, the merchant account that the payout is made from and the payment source that the payout is made to.
    See examples of a complete request.
  3. Monitoring: Set up webhooks for your payout so that you can track its progress and confirm its success.

1. Authenticate a closed-loop payout

Before you can make a closed-loop payout, you must secure your requests. Authentication has two parts:

  • Generating an access token
  • Signing your requests and including an idempotency key.

1.1 Generate an access_token

You must include an access_token with the payments scope in order to make a closed-loop payout. This should be included as a Bearer header along with your payout request.

1.2 Include headers for signing and idempotency

All requests to the Payments API v3, including payouts, should include a valid Tl-Signature header for security purposes. Learn how to sign your requests.

To help with payment signatures, try our signing libraries.

It's also important that your requests include a valid Idempotency-Key header. This should be a unique key, and enables you to run your request multiple times without duplicating it.

2. Configure a closed-loop payout

After setting up your access_token and headers, you're ready to configure and create your closed-loop payout request.

To create a payout, send a POST request to the /payouts endpoint. You must include information about the payment and the beneficiary in this request (see below for the exact details to include, and .

2.1 Request parameters

The table below contains an overview of all the parameters you need to specify to initiate a closed-loop payout.

ParameterDescription
merchant_account_idThe unique id of the merchant account to make the closed-loop payout from.
amount_in_minorThe value of the payout in a minor denomination.

A minor denomination is the smallest unit of the specified currency, so 1 is equivalent to a penny or a cent.
currencyThe currency of the payment, provided as a 3-letter ISO 4217 currency code. For example, GBP for pounds, or EUR for euros.

Note that you cannot make a payment from an account with one currency to an account with a different currency. For example from a GBP to EUR account. Ensure you provide the correct merchant_account_id.
beneficiary.typeThis must be set to payment_source for a closed-loop payout.
beneficiary.referenceA reference for the payout.

The beneficiary sees this reference alongside this payout on their statement or in their banking app.
beneficiary.payment_source_idThe id of the account that made the original deposit.

You can find this id as the value of payment_source.id in the payment_settled webhook for the initial pay-in.

Alternatively, you can find this under Payment Source ID if you click the initial pay-in in the Console payments view.
beneficiary.user_idThe id of the user that made the original deposit.

You can find this id as the value of user_id in the payment_settled webhook for the initial pay-in.

Alternatively, you can find this under User ID if you click the initial pay-in in the Console payments view.
submerchantsThis parameter is only relevant if you are a partner of TrueLayer operating as a collecting PSP. As a partner, you are considered a merchant, and any businesses you process transactions for are 'submerchants'.

If this applies to you, you must provide the details of the submerchant you're processing a transaction for in this parameter, along with the user information for the payout.

Find more information about the required information in the submerchants.ultimate_counterparty parameter in the API reference for the /v3/payouts endpoint.
metadataThis parameter is optional.

You can include up to 10 key-value pairs in this object. This is typically used to attach metadata that helps with reconciliation to your payout.

2.2 Merchant account id

Include the id of the merchant account you want to pay out from in your request. You include it in the merchant_account_id parameter. This ensures that you make the payout from the correct account.

The merchant account that you use must be in the same currency as the payout and beneficiary account. (For example, you cannot make a EUR payment from a GBP merchant account or to a GBP beneficiary account.)

You can get your merchant_account_id through two methods:

This is an example of the merchant_account_id parameter filled out correctly:

{
	"merchant_account_id": "2a485b0a-a29c-4aa2-bcef-b34d0f6f8d51",
//...
}

2.3 Payout amount and currency

When you make a closed-loop payout, you must specify the value and currency of the payout.

To specify the value of your payout, provide a numeric value for the amount_in_minor parameter. This should be the value of your payout in a minor denomination. This means that a value of 1500 corresponds to 15 GBP or 15 EUR.

To specify the currency of your payout, you need to provide a three-letter ISO 4217 currency code for the currency parameter. For example, GBP for pounds sterling and EUR for euros.

This an example of the amount_in_minor and currency parameters filled out correctly:

{
//...
	"amount_in_minor": 1500,
	"currency": "EUR",
//...
}

2.4 Payout scheme selection

You can select a payment scheme for your payout to be made through by using the scheme_selection object. Unlike creating a payment, the scheme_selection object for payouts is not contained within a provider selection object.

There are three different values that you can set for scheme selection:

instant_preferred

Preferably selects a payment scheme that supports instant payments for the currency and geography that you're making the payment in. However, the API will fall back to a non-instant scheme if instant payments are unavailable. The payout_executed webhook will specify the actual scheme used. This is optimal when you don't mind if a payout settles slowly, and can help you avoid fees and payment failures.

instant_only

Automatically selects a payment scheme that supports instant payments for the currency and geography that you're making the payment in. This type is optimal when you need payments to settle quickly.

preselected

Selects a payment scheme compatible with the currency of the payment and geographic region that you are paying in. This is useful if you want full control over the scheme that a payout goes through.

When you use a preselected scheme, you need to specify a scheme_id. Ensure that your scheme ID is compatible with the currency that you are using. Possible scheme IDs are:

  • faster_payments_service
  • sepa_credit_transfer_instant
  • sepa_credit_transfer

See this code block for examples of each scheme selection type.

{
  //[...]
	"scheme_selection": {
		"type" : "instant_preferred"
	}
}
{
  //[...]
	"scheme_selection": {
		"type" : "instant_only"
	}
}
{
  //[...]
	"scheme_selection": {
		"type" : "preselected",
		"scheme_id": "polish_domestic_express"
	}
}

We recommend using instant_preferred for the best user experience.

<€100,000 payouts

Payouts that are €100,000 or over automatically move through the SEPA Credit scheme.

The payout appears as normal in the payments view on Console, with SEPA Credit displayed as the scheme.

Do not use the instant_only scheme selection object for these payments, as they will immediately fail.

2.5 Beneficiary details

Use the beneficiary object in your payout creation request to specify who will receive your payout. For a closed-loop payout, it contains four mandatory parameters, explained below.

2.5.1 type parameter

In a closed-loop payout, you must use a value of payment_source for the type parameter.

You can use a value of external_account or business_account for an open-loop payout or payout to your business account respectively.

2.5.2 payment_source parameter

You must provide the payment source id if the initial pay-in in your payout request. You include this in the payment_source_id parameter. This ensures the payout is sent back to the same account as the initial pay-in.

There are three ways to get the payment source id for a previous pay-in:

2.5.3 user_id parameter

Within your payout request, you must include the user id for the user who made the initial pay-in. You include this in the user_id parameter.

When you accept the initial payment, you can provide a user id or allow us to auto-generate one for you. Whichever approach you choose, you should use this id to represent the same user for future payments.

As with the payment source, there are three ways to get the user id for a previous pay-in:

2.5.4 reference parameter

Include a string for the reference parameter in your payout. The beneficiary sees this reference alongside this payout on their statement or in their banking app.

A common approach to take is to programmatically generate a reference that starts with a string that identifies your business. Then attach a number to it, which you and your user can use to reconcile the payout if needed.

We recommend that you keep your reference under 18 characters, and include no special characters other than - or ..

beneficiary object example

This an example of the beneficiary object filled out correctly, including the four parameters described above:

{
//...
	"beneficiary": {
		"type": "payment_source",
		"user_id": "f61c0ec7-0f83-414e-8e5f-aace86e0ed35",
		"payment_source_id": "c7022d14-3f53-4162-99de-1d615b77b960",
		"reference": "FinancialCo-738940"
	}
}

2.6 Optional metadata

You don't need to include any values for the metadata object. However, if your business needs to include extra metadata in your payout requests (for internal processes for example), you can choose to include up to 10 key value pairs.

To include metadata, provide key value pairs in the object in the format below:

{
//...
  "metadata": {
    "Metadata_key_1": "Metadata_value_1",
    "Metadata_key_2": "Metadata_value_2",
    "Metadata_key_3": "Metadata_value_3"
  }
}

Example closed-loop payout requests

When you specify the mandatory parameters above in your request, you can initiate a closed-loop payout.

The code block below contains two examples of sandbox closed-loop payouts. One for a GBP payout, and one for a EUR payout.

POST /v3/payouts HTTP/1.1
Content-Type: application/json
Idempotency-Key: {RANDOM_UUID}
Tl-Signature: {SIGNATURE}
Authorization: Bearer {ACCESS_TOKEN}
Host: api.truelayer-sandbox.com

{
	"merchant_account_id": "200552da-13da-43c5-a9ba-04ee1502ac57",
	"amount_in_minor": 1,
	"currency": "GBP",
	"beneficiary": {
		"type": "payment_source",
		"user_id": "f61c0ec7-0f83-414e-8e5f-aace86e0ed35",
		"payment_source_id": "41b500a4-5379-4f30-9eda-2fcc032afc37",
		"reference": "PayOutRef"
	}
}
POST /v3/payouts HTTP/1.1
Content-Type: application/json
Idempotency-Key: {RANDOM_UUID}
Tl-Signature: {SIGNATURE}
Authorization: Bearer {ACCESS_TOKEN}
Host: api.truelayer-sandbox.com

{
	"merchant_account_id": "2a485b0a-a29c-4aa2-bcef-b34d0f6f8d51",
	"amount_in_minor": 1,
	"currency": "EUR",
	"beneficiary": {
		"type": "payment_source",
		"user_id": "f61c0ec7-0f83-414e-8e5f-aace86e0ed35",
		"payment_source_id": "c7022d14-3f53-4162-99de-1d615b77b960",
		"reference": "PayOutRef"
	}
}

3. Monitor a closed-loop payout

When you make a successful closed-loop payout, you receive a webhook and also a response that contains the payout id only:

{
	"id": "a7e4e74f-d7da-43e2-af7f-f953724a461c"
}

You can use this webhook and id to confirm whether your payout was successful, and take action if appropriate.

There are two ways that you can check on the progress of your payout:

  • Check the webhooks sent to your webhook URI you have set in Console (recommended)
  • Make a GET request to the /v3/payouts/{id} endpoint.

Payout webhooks

We recommend that you use webhooks to monitor all of the requests you make with the Payments API v3. You receive webhooks for all events in your integration.

Webhooks for your integration are sent to the Webhook URI you have set on the Payments > Settings page in Console. You should ensure that you have set up request signing for your webhook notifications to ensure that they are reliable.

The two webhooks that you receive for a closed-loop payout are payout_executed or payout_failed. You should receive these very soon after your payout request.

This code block contains examples of an executed and a failed webhook for a closed-loop payout:

{
  "type": "payout_executed",
  "event_id": "8712ba64-7864-58d5-1ae8-20ddf61f47d8",
  "event_version": 1,
  "payout_id": "45784e7e-e592-4d9c-b315-3b6a203584bb",
  "executed_at": "2024-01-12T13:53:49.963Z",
  "beneficiary": {
    "type": "payment_source",
    "user_id": "f61c0ec7-0f83-414e-8e5f-aace86e0ed35",
    "payment_source_id": "c7022d14-3f53-4162-99de-1d615b77b960"
  },
  "scheme_id": "internal_transfer"
}
{
  "type": "payout_failed",
  "event_id": "24089aed-4fd4-9e13-f8b2-f458f30c836c",
  "event_version": 1,
  "payout_id": "0a495e9f-2f41-4669-ba33-85407c0b26cb",
  "failed_at": "2024-01-12T14:56:05.117850644Z",
  "failure_reason": "insufficient_funds",
  "beneficiary": {
    "type": "payment_source",
    "user_id": "f61c0ec7-0f83-414e-8e5f-aace86e0ed35",
    "payment_source_id": "c7022d14-3f53-4162-99de-1d615b77b960"
  }
}

GET payouts endpoint

You can use the payout id you received when you made the payout to get information about it.

To do so, make a POST request to the /v3/payouts/{id} endpoint, including the payout id as a path parameter.

You receive a response in this format:

{
  "id": "0cd1b0f7-71bc-4d24-b209-95259dadcc20",
  "merchant_account_id": "AB8FA060-3F1B-4AE8-9692-4AA3131020D0",
  "amount_in_minor": 0,
  "currency": "GBP",
  "beneficiary": {
    "type": "external_account",
    "reference": "string",
    "account_holder_name": "string",
    "account_identifiers": [
      {
        "type": "sort_code_account_number",
        "sort_code": 560029,
        "account_number": 26207729
      },
      {
        "type": "iban",
        "iban": "GB32CLRB04066800012315"
      }
    ]
  },
  "metadata": {
    "prop1": "value1",
    "prop2": "value2"
  },
  "scheme_id": "faster_payments_service",
  "status": "pending",
  "created_at": "string"
}

The status object is especially important, as you can use this to check the progression of your payout through its lifecycle.

Payout lifecycle

After you create a payout, it moves through a series of different states depending on its outcome. See the full list of payout statuses for more information.

This is the journey a payout usually follows:

  1. When a payout is first made, it has a status of pending before it's sent to the payment scheme for authorisation.
  2. Once the payout has been authorised and is with the payment scheme for execution, it has a status of authorized.
    It usually takes just a few seconds for a successful payout to transition through the pending and authorized statuses.
  3. Depending on whether the payment scheme executes the payout, it transitions to one of two statuses:
    1. payout_executed if the payout was successful.
    2. payout_failed if the payout could not be executed.

If a payout fails, the webhook contains a failure_reason that you can use to identify why it failed.

A return occurs when a payout or refund is executed, but rejected by a banking provider. The money moves back to your merchant account.

The reasons for a return are decided by banks. For example, a payout might be returned if the account you're attempting to pay has been closed by the bank or is under investigation.

This occurs in less than 0.01% of payouts, but you must make sure that your system can handle a payout transitioning from executed to failed.