Collecting customer card payments

Note

Overview

Duffel Payments provides a PCI-compliant way for you to work with card payments for online bookings from your customers.
This guide will walk you through how to use Duffel Payments to collect payments and book flights using your customers' cards. Duffel Payments adds new steps to the normal search, select and create instant orders workflow:
  • Search for Offers

  • Select Offer

  • Create PaymentIntent

  • Collect Payment

  • Confirm PaymentIntent

  • Create Order

  • Inspect Collected Payments and Markups

Note

Requirements

Duffel Payments currently only works with Managed Content and is built specifically for applications that run in a web browser, have their front-end written in JavaScript, and are backed by a back-end server. If these two requirements are not met you won't be able to use Duffel Payments for now.

Note

This guide assumes that you already have a working integration with the Duffel API. Only the basics of searching and booking are required for this guide. If you could use a refresher, please head over to our Quick Start Guide.

Create PaymentIntent

A PaymentIntent is a resource that will be used to represent and record the process of collecting a payment from your customer. We recommend that you create exactly one PaymentIntent for each offer your customer wants to book.
Once your customer has searched for and selected an offer to book, the first step to use Duffel Payments is to create a PaymentIntent. You should use the Create a Payment Intent endpoint to do this in your back-end server.

Caution

Request

Shell

curl -X POST --compressed "https://api.duffel.com/payments/payment_intents"
-H "Accept-Encoding: gzip"
-H "Accept: application/json"
-H "Content-Type: application/json"
-H "Duffel-Version: v2"
-H "Authorization: Bearer $YOUR_ACCESS_TOKEN"
-d '{
"data": {
"amount": "106.00",
"currency": "GBP"
}
}
In the request above we're creating a PaymentIntent with an amount of £106.00. The amount and currency you specify here is the amount and currency that your customer is going to be charged in. It should be calculated as follows: ((offer and services total_amount + markup) x foreign exchange rate) / (1 - Duffel Payments fee).
  • Offer and services total amount: this is the total cost of the flight plus any extra services without Duffel fees. We always present the offer and service(s) total_amount in your Balance currency.

  • Markup: this is the amount on top of the flight cost that you might charge your customer to cover operational costs and any profits you want to make on the sale of the flight.

  • Foreign exchange rate: this should only be applied if you are charging your customer in a currency different to the currency of your Balance, the currency of the offers returned by the API. You should use the mid-market exchange rate of the day from your Balance currency to the currency you want to charge your customer in. You should use an external source to get this rate (for example, https://fixer.io/), and add a 2% markup on top of the rate in order to cover the Duffel Payments foreign exchange fee of 2% (fx rate x 1.02). We recommend that you add slightly more than 2% to account for the fact the FX you use might be slightly different from the one used by Duffel.

  • Duffel Payments fee: is determined based on the card country, it varies if the card is considered domestic or international, an example would be 2.9% or 0.029.

Diagram illustrating flow of funds all the way through from the customer, to the balance, to Duffel and to the airline

Diagram illustrating flow of funds all the way through from the customer, to the balance, to Duffel and to the airline

For example, say your Balance currency is Euros (EUR), and your customer wants to pay for a flight in Great British Pounds (GBP), the foreign exchange rate between these two currencies is 0.85, they want to book a flight that costs 120.00€ in total, you want to charge them 1.00€ for your booking service, and the Duffel Payments fee is 2.9%. The amount would be calculated as follows ((120.00€ + 1.00€) x 0.85) / (1 - 0.029) ~= £105.92.
It's worth calling out, that at this point, the PaymentIntent being created is not tied to the offer being booked. It's just a resource created to represent and record the collection of a payment.

Response

JSON

{
"data": {
"id": "pit_00009hthhsUZ8W4LxQgkjo",
"live_mode": true,
"status": "requires_payment_method",
"amount": "106.00",
"currency": "GBP",
"net_amount": null,
"net_currency": null,
"fees_amount": null,
"fees_currency": null,
"client_token": "c2hramgzaGVsbG8gd29ybGQgIyMgZ2lyYWY=",
"card_network": null,
"card_last_four_digits": null,
"confirmed_at": null,
"created_at": "2020-04-11T15:48:11.642Z",
"updated_at": "2020-04-11T15:48:11.642Z"
}
}
The response to the request above will contain a PaymentIntent resource. The two fields worth highlighting in this response are the id and the client_token. The client_token will be used in the next step to collect the payment from your customer in the front-end. The id will be used in the confirmation step to confirm the collection of the PaymentIntent, so we recommend you store this information for later use.

Collect payment

Once you have a PaymentIntent created by your back-end server the next step is to use it in your front-end to actually collect the payment.
The first step is to make the PaymentIntent created in the previous step available to the front-end. We recommend that you create an endpoint in your back-end server that the front-end can use to fetch this information.
Card Payment
We offer a component to simplify your integration with Duffel Payments. Card Payment handles validation and localisation, and takes the card payment itself. We recommend using this component in your application.

Note

Using the Duffel Payments component
The payments component is part of the @duffel/components library. You can find up to date documentation on how to use it on github:
Testing the component
The component will collect the payment directly from your customer's card.
You can use the following details in test mode to test the card collection:
  • 4242 4242 4242 4242 as the card number

  • any 3 digits as the CVC

  • any future date as the expiry date

  • and any ZIP code.

If you want to test other scenarios like 3D Secure 2 or failure due to insufficient funds check our documentation.
The 'Pay' button will be enabled when the card details are validated. Any validation errors will be displayed underneath the card input.
  • If the payment is successful, the successfulPaymentHandler will be called without any arguments. Along with displaying a page confirming a successful transaction, this handler should call an endpoint in your back-end server to confirm the PaymentIntent - please see the next section.

  • If the payment is unsuccessful, the errorPaymentHandler will be called with the error as an argument. You'd want to indicate to your user that the transaction hasn't gone through. This error will come directly from Stripe, our underlying payment provider. A full list of errors can be found here.

Confirm PaymentIntent

Once payment collection is successful in the front-end, the next step is to confirm the PaymentIntent with the Duffel API, you should use the Confirm a Payment Intent endpoint to do this in your back-end server.
When you confirm a PaymentIntent, we'll top-up your Balance with the specified amount, minus the Duffel Payments fees. Once topped-up, it'll be available to create an Order using the Create an order endpoint.

Request

Shell

curl -X POST --compressed "https://api.duffel.com/payments/payment_intents/pit_00009hthhsUZ8W4LxQgkjo/actions/confirm"
-H "Accept-Encoding: gzip"
-H "Accept: application/json"
-H "Duffel-Version: v2"
-H "Authorization: Bearer $YOUR_ACCESS_TOKEN"

Response

JSON

{
"data": {
"id": "pit_00009hthhsUZ8W4LxQgkjo",
"live_mode": true,
"status": "succeeded",
"amount": "106.00",
"currency": "GBP",
"net_amount": "121.03",
"net_currency": "EUR",
"fees_amount": "3.62",
"fees_currency": "EUR",
"client_token": "c2hramgzaGVsbG8gd29ybGQgIyMgZ2lyYWY=",
"card_network": "visa",
"card_last_four_digits": "4242",
"confirmed_at": "2020-04-11T15:52:32.686Z",
"created_at": "2020-04-11T15:48:11.642Z",
"updated_at": "2020-04-11T15:48:11.642Z"
}
}
In the response payload above we can see an example Payment Intent representing the collection of £106.00 from a traveller. The £106.00 was converted to 124.65€ (i.e. the balance currency, using 1.176 as an exchange rate), out of which 3.62€ was charged as fees (2.9%) and 121.03€ was used to top up your Balance.

Create Order

Now to actually book the order, with the funds collected from the customer in the previous step, use the Create an order endpoint as normal. You'll still use balance since it will have been topped-up with the payment from your customer.
You can optionally store the Payment Intent's id in your new Order's metadata field, to help with things like reconciliation.

Request

Shell

curl -X POST --compressed "https://api.duffel.com/air/orders"
-H "Accept-Encoding: gzip"
-H "Accept: application/json"
-H "Content-Type: application/json"
-H "Duffel-Version: v2"
-H "Authorization: Bearer $YOUR_ACCESS_TOKEN"
-d '{
"data": {
"type": "instant",
"selected_offers": [
"off_00009htYpSCXrwaB9DnUm0"
],
"payments": [
{
"type": "balance",
"amount": "120.00",
"currency": "EUR"
}
],
"metadata": {
"payment_intent_id": "pit_00009hthhsUZ8W4LxQgkjo"
}
"passengers": [...]
}
}

Response

JSON

{
"data": {
"id": "ord_00009htYpSCXrwaB9DnUm1",
// ...
"metadata": {
"payment_intent_id": "pit_00009hthhsUZ8W4LxQgkjo"
}
}
}
The metadata associated with the order will be visible in the Duffel dashboard too.

Inspect Collected Payments and Markups

After you've collected the payment from your customer and created the order successfully, you can go to the Duffel dashboard's Balance page to inspect the payment intent, how much was spent to create the order and how much you've earned from it (your markup). Your final balance should equal 1.03€ which is your markup. You can request to get this markup paid out to you.
You can also issue refunds for the payments you've collected. If you wish to do this follow this guide.