Webhook notifications [PayDirect]

You can follow deposit and withdrawal events for PayDirect through webhook notifications.

A webhook is successfully delivered when we receive a success status code (2xx) from the webhook URI that you specified in your Console settings for PayDirect.

If we receive any other status code (for instance, if your API is temporarily unavailable), we will start retrying. Our retry policy is jittered exponential backoff. We will immediately perform some fast retries and, if those fail, start waiting increasingly longer. We will keep retrying for up to 72 hours.

Validate the received webhook signature

We recommend that you use our signing libraries to verify the Tl-Signature of the received webhooks.

E.g. Java com.truelayer.truelayer-signing

Verifier.verifyWithJwks(jwks)
        .method("POST")
        .path(path)
        .headers(allWebhookHeaders)
        .body(body)
        .verify(webhookSignature);

Handling duplicated webhooks

TrueLayer can’t guarantee that you’ll only receive a single webhook notification for each payment status. As such, your integration should have logic that can handle receiving multiple webhooks for a given payment status.

For example, imagine TrueLayer sends an executed webhook, but doesn’t receive a 200 response due to network issues from the recipient. In this case, TrueLayer sends an extra executed webhook as it cannot confirm the previous one was received – regardless of the current status of the payment. The recipient’s integration should be able to handle such possibilities.

Webhook Structure

All webhooks from the PayDirect API are structured as follows:

Header

FieldTypeDescription
X-TL-Webhook-TimestamptimestampThe time that the webhook was sent to you. This will be in the following format: 2020-05-18T10:17:47Z.
Tl-SignaturestringJSON web signature with a detached payload of the form {HEADER}..{SIGNATURE}
X-Tl-SignaturestringLegacy JWS. Verify using Tl-Signature if at all possible.

Body

FieldTypeDescription
event_typestringDescribes which event is detailed.
event_schema_versionunsigned integerThe version of the event body schema (for example, 1).
event_iduuidThe unique ID of the event.
event_bodyobjectContaining the object relating to the event. The body schema is specified in the following sections for each event type.

Validating the webhook signature

The value of the X-Tl-Signature header contains a JSON Web Signature (JWS) with a detached payload. It takes the form {HEADER}..{SIGNATURE}.

The JWS header contains the following values:

FieldTypeDescription
algstringThe algorithm used to sign the webhook. We currently use the RS512 algorithm.
jkustringThe URI of the JSON Web Key Set (JWKS), a hosted page where a list of public keys owned by TrueLayer can be found.
kidstringThe ID of the key used to sign the webhook, used to retrieve the correct key from the JWKS.
iatintThe time that the JWS was issued, represented in Unix time (number of seconds since 1970-01-01T00:00:00Z).

The steps for verifying the signature are as follows:

  1. Verify the jku field in the token header matches the expected value as listed below. If not, the signature should be rejected as invalid.
    • Sandbox: https://webhooks.truelayer-sandbox.com/.well-known/jwks
    • Production: https://webhooks.truelayer.com/.well-known/jwks
  2. Use the jku value to retrieve the JWKS. This may be cached for some time, but if verification fails, the JWKS must be retrieved again to ensure that all keys are up to date. TrueLayer may rotate or revoke keys at any time.
  3. Retrieve the relevant public key by using the kid to look up the relevant value from the JWKS.
  4. Use a JWT library to verify the JWS headers combined with the payload (the webhook request body) matches the signature provided in the token. The algorithm used to verify the signature must be that declared in the alg header of the JWS.

Deposits

You can find the relevant fields in the event_body for the following event_types:

Event type Field Parameter type Description
  • deposit_initiated
  • deposit_cancelled
  • deposit_auth_failed
  • deposit_expired
  • deposit_executing
  • deposit_rejected
  • deposit_executed
client_id string A unique string identifying a client.
deposit_id uuid Unique ID of the deposit.
user_id uuid Unique ID of the user.

Here is an example deposit_initiated event:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "deposit_initiated",
  "event_id": "714040e3-288e-468d-9575-e5116ff0a9fc",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "deposit_id": "bd780cca-46a6-4f28-a0e4-d36e4e8c57c2",
    "user_id": "8c2f148a-c026-4ce4-8fe4-6afac4f71b6e"
  }
}

For the deposit_settled event_type, the event_body contains:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the transaction.
deposit_iduuidThe unique ID of the deposit.
user_iduuidThe unique ID of the user.
account_iduuidThe unique ID of the user's account.
settled_attimestampThe date and time that the deposit was settled into the account.
amount_in_minorintegerThe amount received in the smallest denomination possible.
currencyISO 4217 Currency Code StringThe currency the top-up was settled in.
remitter_ibanstringThe IBAN of the account that sent the money.
remitter_namestringThe name of the account holder that sent the money.

Here is an example deposit_settled event:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "deposit_settled",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "deposit_id": "88e76e9e-811a-4f02-9624-f808c5925bfb",
    "user_id": "bec96bf3-788b-4266-92dc-77351b680ac5",
    "account_id": "5156e5af-7bf2-49a4-96b3-7fde5ec8ddfe",
    "settled_at": "2019-10-01T17:00:00.0000000Z",
    "amount_in_minor": 10000,
    "currency": "GBP",
    "remitter_iban": "GB33BUKB20201555555555",
    "remitter_name": "Bob Brown"
  }
}

For the external_deposit_received event_type, the event_body contains:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the transaction.
received_attimestampThe date and time that the external deposit was received.
amount_in_minorintegerThe amount received, in the smallest denomination possible.
currencyISO 4217 Currency Code StringThe currency the transaction was settled in.
remitter_ibanstringThe IBAN of the account that sent the money.
remitter_namestringThe name of the account holder that sent the money.
referencestringThe reference which identifies the payment.

Here is an example external_deposit_received event:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "external_deposit_received",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "received_at": "2019-10-01T17:00:00.0000000Z",
    "amount_in_minor": 10000,
    "currency": "GBP",
    "remitter_iban": "GB33BUKB20201555555555",
    "remitter_name": "Bob Brown",
    "reference": "7820IPXNJ1X6QFKRHR"
  }
}

Withdrawals

The withdrawal_authorised event body contains the following fields:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the withdrawal.
authorised_atTimestampThe date and time that the withdrawal was authorised at.

Here's what that looks like:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "withdrawal_authorised",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "authorised_at": "2019-10-01T17:00:00.0000000Z"
  }
}

The withdrawal_submitted event body contains the following fields:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the payout.
submitted_atTimestampThe date and time that the payout was submitted to the scheme.

Here's what that looks like:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "withdrawal_submitted",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "submitted_at": "2019-10-01T17:00:00.0000000Z"
  }
}

The withdrawal_settled event body contains the following fields:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the payout.
settled_atTimestampThe date and time that the payout was settled into the beneficiary account.

Here's what that looks like:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "withdrawal_settled",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "settled_at": "2019-10-01T17:00:00.0000000Z"
  }
}

The withdrawal_rejected event body contains the following fields:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the payout.
rejected_atTimestampThe date and time that the payout was rejected.
rejection_codeStringThe reason the payout was rejected in the form of a machine-parsable enum.
rejection_detailsStringThe reason the payout was rejected in the form of a human-friendly description.

Here's what that looks like:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "withdrawal_rejected",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "rejected_at": "2019-10-01T18:00:00.0000000Z",
    "rejection_code": "insufficient_funds",
    "rejection_details": "There were not enough funds on the account to authorise the payout."
  }
}

The withdrawal_failed event body contains the following fields:

FieldTypeDescription
client_idstringA unique string identifying a client.
transaction_iduuidThe unique ID of the payout.
failed_atTimestampThe date and time that the payout status moved to failed.
failure_codeStringThe reason the payout failed in the form of a machine-parsable enum.
failure_detailsStringThe reason the payout failed in the form of a human-friendly description.

Here's what that looks like:

X-TL-Webhook-Timestamp: 2020-05-18T10:17:52Z
Tl-Signature: "detached..jws"

{
  "event_type": "withdrawal_failed",
  "event_id":  "33c9fc5b-69d7-4de0-83a9-8177f9af79d2",
  "event_schema_version": 1,
  "event_body": {
    "client_id": "client-8806da",
    "transaction_id": "cc328607-e02e-49e2-81c9-5bd044c8f7d7",
    "failed_at": "2019-10-01T18:00:00.0000000Z",
    "failure_code": "server_error",
    "failure_details": "We encountered a technical issue while processing your payment, please try again later."
  }
}