Callbacks for Payment Request Notifications
Flywire supports notifications via callbacks (also known as webhooks). These allow your system to receive real-time notifications about important events.
Payment Request callbacks deliver notifications for key events, for example when a Payment Request has been viewed.
Payment Request callbacks are different from payment callbacks:
-
The content of payment callbacks is different, they provide you with information about the individual payment.
-
To receive payment callbacks for payments that are part of a Payment Request, your portal needs to have a static callback URL in place.
What is static callback URL?
There are two different URLs for receiving callbacks:
Static URL
A static callback URL is a fixed URL defined in your portal. All payments for this portal will send callbacks to this URL.
The static callback URL has been defined when your portal was set up. If you don't use a static callback URL yet and want to start using it, please reach out to your Flywire contact.
Dynamic URL
A dynamic callback URL is a URL that you define when you create the payment. Since this URL can be different for every payment you create, it is called dynamic.
For
Pay-By-Link and
Checkout , you define the URL via the callbackUrl parameter.
If you want to receive callbacks, you must provide a callback URL and a callback ID, otherwise no callbacks will be triggered. You also must set the callback version to 2.
You cannot provide a dynamic callback URL for payments that are part of a Payment Request. To receive callbacks about those payments, you have to use the static URL. Alternatively, you can use callback notifications for Payment Request status updates, but note that the content of those callbacks are different from payment status callbacks.
How defining static and dynamic URLs affect callbacks
= not set
= set
Static
URLDynamic
URLResult You won't receive callbacks . You'll receive callbacks to your static URL.
You'll also receive callbacks for payments that are part of Payment Requests.
You'll receive callbacks to both URLs.
You'll also receive callbacks for payments that are part of Payment Requests, but only to the static URL.
You'll receive callbacks to your dynamic URL
You will not receive callbacks for payments that are part of Payment Requests since there is no static URL.
How to set up Payment Request Notifications
Callbacks for Payment Requests are set up in Client Dashboard.
-
Go to Settings > Webhooks.
-
Click on Add new Webhook. Enter the callback URL you want to use to receive notifications (must be a publicly accessible HTTPS endpoint) and choose which event notifications you want to receive.
You can use one callback URL for several or all events, or you can define different callback URLs for specific events.
See Events for Payment Requests for a detailed description of the events.
-
Handle callbacks by parsing the event objects.
-
Return a 2xx response status code after receiving the callbacks.
Why is this necessary?
To confirm receipt of an event, your callback URL must return a 2xx HTTP status code to Flywire. If Flywire does not receive a 2xx HTTP status code, the callback will be considered failed, see Failed callbacks: What happens if a callback can't get delivered?
Note:
-
Any response outside the 2xx range, including 3xx codes, signals Flywire that the event was not received.
-
Return the response quickly. Avoid delays by returning the 2xx code before running complex logic that could cause a timeout.
-
Rate Limits for Payment Request Callbacks
Callbacks for Payment Request events are limited to maximal 3500 per day. This daily quota resets at 00:00 UTC.
If you exceed the limit, you'll receive an error message to your callback URL informing you that the rate limit has been exceeded. The error message is sent with the usual X-Flywire-Digest header to enable you to validate its authenticity, see Validating Payment Request Notifications.
All callbacks scheduled for delivery within the same UTC day will be withheld once the rate limit is reached. Normal service will resume when your daily quota is reset.
Failed callbacks: What happens if a callback can't get delivered?
If Flywire does not receive a 2xx HTTP status code, the notification attempt is repeated a further 5 times (6 in total):
-
Initial attempt.
-
After a delay of 10 minutes.
-
After a delay of a further 20 minutes.
-
After a delay of a further 30 minutes.
-
After a delay of a further 50 minutes.
-
After a delay of a further 80 minutes.
If the notification is not acknowledged by any of these attempts, Flywire marks the event as failed and stops trying to send it to your callback URL.
Events for Payment Requests
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |
Content of Payment Request Notifications
- Viewed
- Payment Guaranteed
- Fully Paid
- Installment Paid
- Installment Failed
- Payment Method Changed

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
{
"type": "payment_request.viewed",
"payment_request_type": "SUBSCRIPTION",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "unpaid",
"status": "active"
}

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The payment reference.

The payment reference is an ID generated by Flywire to identify a payment.
Format:
Either: ABC123456789
3-letter portal/recipient ID 9 numbers
Or: 1AB12CD452ABC1D
number 8 alphanums number 5-alphanum portal/recipient ID
With the payment reference, the payment can be tracked through the different stages of the payment process.
The payment reference is also important in other situations, for example:
-
When a payer is using bank transfer as payment method, they usually must provide the payment reference when sending the funds.
-
The payment reference helps Flywire to identify the payment if you or your payer needs support.

The payer currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.


The payment amount in the payer currency.
The amount is specified in the smallest unit of the currency, called subunits. For example, in USD, the subunit is cents, and 100 cents equal 1 USD. So, an amount of 12025 (cents) is equivalent to 120.25 USD.
Note that the subunit-to-unit ratio varies by currency, it is not always 100. See Currencies for the subunits of each currency.

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The payment amount in the billing currency.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the
The amount is specified in the smallest unit of the currency, called subunits. For example, in USD, the subunit is cents, and 100 cents equal 1 USD. So, an amount of 12025 (cents) is equivalent to 120.25 USD.
Note that the subunit-to-unit ratio varies by currency, it is not always 100. See Currencies for the subunits of each currency.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
payer object
The payer information is not automatically included. Returning payer information is disabled by default and needs to be enabled by Flywire.
Please reach out to your Flywire contact if you require payer information to be returned.

The payer's first name.

The payer's last name.

The payer's middle name.

The payer's first line of address.

The payer's second line of address.

The payer's city.

The payer's zip code.

The ISO2 code of the payer country (the country the money was sent from).

The payer's state.

The payer's phone number.

The payer's email address.
{
"type": "payment_request.payment_guaranteed",
"payment_request_type": "SCHEDULED",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_id": "PFU958007137",
"payment_currency_from": "EUR",
"payment_amount_from": 773,
"payment_currency_to": "USD",
"payment_amount_to": 1000,
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "paid",
"status": "paid"
"payer": {
"first_name": "Peter",
"last_name": "Payer",
"middle_name": null,
"address1": "789 Calle Mayor",
"address2": null,
"city": "Madrid",
"country": "ES",
"state": null,
"zip": "28013",
"phone": "0034912345678",
"email": "[email protected]"
},
}

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
{
"type": "payment_request.fully_paid",
"payment_request_type": "SIMPLE",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "paid",
"status": "paid"
}

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The payment reference.

The payment reference is an ID generated by Flywire to identify a payment.
Format:
Either: ABC123456789
3-letter portal/recipient ID 9 numbers
Or: 1AB12CD452ABC1D
number 8 alphanums number 5-alphanum portal/recipient ID
With the payment reference, the payment can be tracked through the different stages of the payment process.
The payment reference is also important in other situations, for example:
-
When a payer is using bank transfer as payment method, they usually must provide the payment reference when sending the funds.
-
The payment reference helps Flywire to identify the payment if you or your payer needs support.

The payer currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.


The payment amount in the payer currency.
The amount is specified in the smallest unit of the currency, called subunits. For example, in USD, the subunit is cents, and 100 cents equal 1 USD. So, an amount of 12025 (cents) is equivalent to 120.25 USD.
Note that the subunit-to-unit ratio varies by currency, it is not always 100. See Currencies for the subunits of each currency.

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The payment amount in the billing currency.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the
The amount is specified in the smallest unit of the currency, called subunits. For example, in USD, the subunit is cents, and 100 cents equal 1 USD. So, an amount of 12025 (cents) is equivalent to 120.25 USD.
Note that the subunit-to-unit ratio varies by currency, it is not always 100. See Currencies for the subunits of each currency.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
payer object
The payer information is not automatically included. Returning payer information is disabled by default and needs to be enabled by Flywire.
Please reach out to your Flywire contact if you require payer information to be returned.

The payer's first name.

The payer's last name.

The payer's middle name.

The payer's first line of address.

The payer's second line of address.

The payer's city.

The payer's zip code.

The ISO2 code of the payer country (the country the money was sent from).

The payer's state.

The payer's phone number.

The payer's email address.
{
"type": "payment_request.installment_paid",
"payment_request_type": "SUBSCRIPTION",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_id": "PFU958007137",
"payment_currency_from": "GBP",
"payment_amount_from": 773,
"payment_currency_to": "USD",
"payment_amount_to": 1000,
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "paid",
"status": "paid"
"payer": {
"first_name": "Peter",
"last_name": "Payer",
"middle_name": null,
"address1": "789 Calle Mayor",
"address2": null,
"city": "Madrid",
"country": "ES",
"state": null,
"zip": "28013",
"phone": "0034912345678",
"email": "[email protected]"
},
}

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
{
"type": "payment_request.installment_failed",
"payment_request_type": "SUBSCRIPTION",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "partially_paid",
"status": "failed"
}

The type of the Payment Request event.
Possible values:
payment_request.viewed | Triggered when the Payment Request is viewed for the first time |
payment_request.payment_guaranteed |
Triggered when any payment for a Payment Request moves to the ![]() |
payment_request.fully_paid |
Triggered when a Payment Request balance reaches zero and is fully paid (final ![]() |
payment_request.installment_paid |
Triggered when a Payment Request installment is paid (![]() |
payment_request.installment_failed |
Triggered when a Payment Request installment payment fails |
payment_request.cancelled_by_payer |
Triggered when a Payment Request is cancelled by the payer (subscriptions) |
payment_request.payment_method_by_payer |
Triggered when a payer changes the payment method on a Payment Request with auto-pay enabled (installments / subscription) |

The meaning of the type values depend on how you created the Payment Request or subscription:


SIMPLE |
A Payment Request of any type (single payment or multiple installments, with our without a due date). |
SUBSCRIPTION |
A subscription. |


SIMPLE |
A single payment for a specific amount that is part of a Payment Request. Giving the payment a due date is optional. |
SCHEDULED |
Scheduled payments allow you to collect future payments with variable amounts on specific dates. All payments that belong together are bundled into one Payment Request. |
SUBSCRIPTION |
Subscription payments allow you to collect future payments with a fixed amount on regular intervals.For subscriptions, you cannot create or edit installments yourself - they are automatically generated by Flywire based on the rules that have been defined for the subscription. In Dashboard, you can find subscriptions under "Payment Requests". |

The billing currency.
Format:
Three-letter ISO 4217 currency code, for example EUR.

The billing currency is the currency in which the recipient of the payment is billing their payer. The billing currency depends on the

The total amount of all payments of this Payment Request.

The recipient ID (also called portal code) of the recipient for this Payment Request.
The portal code identifies the portal. The portal code has been assigned by Flywire when the portal has been set up.
Format:
Either: 3 letters (ABC)
Or: 5 alphanumeric characters, always starting with a letter (ABC1D)
custom_fields object

Custom fields are fields that are specific to your portal.
Fields are defined when the recipient (also called portal) is set up by Flywire. They are additional fields that the payer has to fill out when they make their payment (additional to the standard payer fields that are the same for all recipients).

Identifier of the field.

The value for this field.

The date and time the Payment Request (or subscription) was created.
Timestamps use ISO 8601 format with UTC (YYYY-MM-DDTHH:MM:SSZ, e.g., 2025-03-31T13:21:27Z).

The last part of a timestamp indicates the time zone:
-
Z → Means UTC (Coordinated Universal Time).
Example: 2024-09-20T14:30:00Z = 2:30 PM in UTC.
-
+hh:mm or -hh:mm → Offset from UTC.
Example: 2024-09-20T14:30:00+02:00 = 2:30 PM in a time zone 2 hours ahead of UTC (e.g., Central European Summer Time).

The status of the Payment Request. Possible values:
unpaid | No payment of the Payment Request has been paid yet. |
partially_paid | At least one of the installments of the Payment Request has been paid. |
paid | All installments of the Payment Request have been paid. |

The current status of the Payment Request.
ACTIVE | The Payment Request is active - this is the normal state without any issues. |
CANCELLED | The Payment Request has been cancelled via the API or Dashboard. |
PAID | All installments of the Payment Request have been fully paid. |
FAILED | The Payment Request has at least one FAILED installment. |
In the context of Payment Request callbacks, this value is in lowercase.
{
"type": "payment_request.payment_method_by_user",
"payment_request_type": "SUBSCRIPTION",
"payment_request_currency": "USD",
"payment_request_total_amount": 1000,
"receiving_account": "PFU",
"custom_fields": {
"invoice_number": "INV1234"
},
"payment_request_created_date": "2021-11-15T15:08:10.513Z",
"payment_request_status": "partially_paid",
"status": "active"
}
Validating Payment Request Notifications
Validating notifications is optional, as it’s on your server side, but for security reasons it is recommended to validate all notifications.
How to validate a notification
To validate a notification, check its X-Flywire-Digest header. This value is generated by Flywire using your Shared Secret to encrypt the notification body. To verify the notification, generate the digest using the same method and compare it to the X-Flywire-Digest value in the header. If they match, the notification is legitimate and hasn't been tampered with.
- Step-by-step Guide
- Full Node.js Example
- PHP Example
-
Retrieve the raw HTTP body of the notification you received.
Make sure you use the raw HTTP body of the notification. You must generate the X-Flywire-Digest value using the exact payload you received in the notification. If you change the body in any way the values won't match later.
-
Encrypt the received notification twice:
1) Encrypt the raw HTTP body of the received notification with your Shared Secret using the SHA-256 algorithm.
2) Take the result and encrypt it in Base64.
The examples show you how to do this in different programming languages. In each example, exchange the shared_key with your Shared Secret and message_body with the raw HTTP body of the notification.
What is my Shared Secret?
A Shared Secret is a string of characters that is used to authenticate requests and validate callbacks. Note that each portal might have a different shared secret. If you have access to multiple portals within your company, make sure you use the correct shared secret for each portal. Please reach out to your Flywire contact to get the shared secret for a portal.
- Ruby
- .NET
- JavaScript
digest = OpenSSL::Digest.new('sha256') encrypted_payload = OpenSSL::HMAC.digest(digest, shared_secret, notification_body) Base64.encode64(encrypted_payload).strip # Step 1: Define the hashing algorithm to use for the HMAC. # This initializes the SHA-256 hashing mechanism. hash_algorithm = OpenSSL::Digest.new('sha256') # Step 2: Compute the HMAC in binary format. # This uses the shared secret and the message body to produce the HMAC signature. hmac_binary = OpenSSL::HMAC.digest(hash_algorithm, shared_secret, message_body) # Step 3: Encode the HMAC in Base64 for use in the X-Flywire-Digest header. x_flywire_digest = Base64.encode64(hmac_binary).strip
public static string Digest(string shared_secret, string message_body) { // Step 1: Initialize the HMACSHA256 object with the shared secret. // This sets up the hashing algorithm (SHA-256) and the secret key for HMAC. using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(shared_secret))) { // Step 2: Convert the message body into a byte array. // The message body is the message payload you want to hash var bytes = Encoding.UTF8.GetBytes(message_body); // Step 3: Compute the HMAC hash of the message body. // This generates the HMAC using the shared secret and the message body. var hashedBytes = hmacsha256.ComputeHash(bytes); // Step 4: Convert the hashed byte array into a Base64 string. return Convert.ToBase64String(hashedBytes); } }
const crypto = require('crypto'); function createDigest(shared_secret, message_body) { // Step 1: Initialize the HMAC with the SHA-256 algorithm and the shared secret. const hmac = crypto.createHmac('sha256', shared_secret); // Step 2: Update the HMAC with the message body. // The message body is the message payload you want to hash hmac.update(message_body); // Step 3: Get the HMAC digest and encode it in Base64. const digestHeader = hmac.digest('base64'); return digestHeader; // The `digestHeader` is the X-Flywire-Digest header. }
-
Compare your Base64 string to the value in the X-Flywire-Digest parameter of the notification you received.
If the values match, the notification came from Flywire and hasn't been changed by a third party.
If the values don't match, you shouldn't trust the notification.
// Import the Express framework for setting up the server and the crypto library for hashing
const express = require("express");
const crypto = require("crypto");
const app = express(); // Create an Express app
const port = 3000; // Define the port the server will listen on
// PART 1: Server Setup
// Listen for incoming requests on the specified port
app.listen(port, () => {
console.log(`Example Flywire Webhook App listening on port ${port}`);
});
// PART 2: Middleware and Routing
// Use express.raw middleware to treat incoming JSON data as raw data (needed for HMAC verification)
app.use(express.raw({ type: "application/json" }));
// Define a route that will handle POST requests sent to /flywire-notification
app.post("/flywire-notification", (req, res) => {
// Extract the raw request body and the digest header from the incoming request
const payload = JSON.stringify(req.body); // Convert raw request body to JSON string
const requestDigest = req.header("X-Flywire-Digest"); // Retrieve the digest header from Flywire
const sharedSecret = "YOUR SHARED SECRET"; // Define the shared secret for HMAC hashing
// PART 3: Verification
// Generate a digest using the payload and shared secret, matching Flywire's HMAC-SHA256 and Base64 encoding
const serverDigest = digest(payload, sharedSecret);
// Compare the digests to verify if the request is from Flywire
if (requestDigest === serverDigest) {
// Parse the payload and process data if verification is successful
const data = JSON.parse(payload).data;
console.log(`Processing notification: ${data.payment_id} - ${data.status}`);
}
// Send a 200 OK response back to Flywire indicating the request was processed
res.sendStatus(200);
});
// Helper function to create a digest (HMAC-SHA256 + Base64) from the data and shared secret
function digest(data, sharedSecret) {
const hmac = crypto.createHmac("sha256", sharedSecret); // Create HMAC using SHA256
hmac.update(data); // Update with the data (payload)
return hmac.digest("base64"); // Return the digest in Base64 format
}
<?php
// Read the raw incoming data from the request body
$data = file_get_contents('php://input');
// Define the shared secret for verifying the request authenticity
$sharedSecret = "YOUR SHARED SECRET";
// Retrieve the 'X-Flywire-Digest' header from the incoming request
$requestDigest = getHeader("X-Flywire-Digest");
// Generate the server's own digest from the incoming data and shared secret
$serverDigest = digest(trim($data), $sharedSecret);
// Compare the request digest with the server's digest to verify authenticity
if ($requestDigest == $serverDigest) {
// Process notification (only if the verification passed)
}
// Function to create an HMAC-SHA256 digest and encode it in Base64
function digest($data, $secret) {
$digest = hex2bin(hash_hmac('sha256', $data, $secret)); // Create HMAC hash
return trim(base64_encode($digest)); // Encode the hash in Base64
}
// Helper function to retrieve a specific header from the request
function getHeader($header) {
foreach (getallheaders() as $name => $value) { // Loop through all headers
if ($name == $header) { // Check if the header matches
return $value; // Return the header value if matched
}
}
}
?>