Collecting customer card payments

note

Duffel Payments is available for customers in Australia, Europe, United Kingdom and the United States. If Duffel Payments is not available in your country you can register your interest by sending an email to help@duffel.com.


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

Although this guide is focused on the flow of booking instant orders, Duffel Payments can also be used to hold orders and order changes.


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

Support for mobile applications is coming soon. Interested in getting early access? Drop us a message at help@duffel.com.

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

It's very important that you do this in your back-end server. It will prevent malicious users from changing the payment amount and will prevent you from exposing your Duffel API token to the public internet.

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: v1"
-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

Our mission is to make selling travel effortless. If this component doesn't meet your needs, tell us what's missing at help@duffel.com so we can improve it.

There are two ways you can integrate Card Payment into your application.
  • Import the Duffel Components library into your project using a package manager

  • Use the UMD module (for non-React projects)

Payments component

Payments component

Importing Duffel Components library using a package manager
To get started, import the package using your favourite package manager.

Shell (using yarn)

yarn add @duffel/components
Then import Card Payment into your application.

JavaScript

import { CardPayment } from '@duffel/components'
import '@duffel/components/dist/CardPayment.min.css'
You'll embed the component into your front-end application's checkout page, passing in the client_token from the PaymentIntent you created in the previous step, and handlers for both successful and errored transactions.

JavaScript

const successfulPaymentHandlerFn = () => {
// Show 'successful payment' page and confirm Duffel PaymentIntent
}
const errorPaymentHandlerFn = (error: StripeError) => {
// Show error page
}
const Example = () => (
<CardPayment
duffelPaymentIntentClientToken={duffelPaymentIntentClientToken}
successfulPaymentHandler={successfulPaymentHandlerFn}
errorPaymentHandler={errorPaymentHandlerFn}
/>
)
Using Duffel Components library UMD module (for non-React projects)
The Payments component can also be used in non-React projects. You'll use the Duffel Components UMD module, which bundles React, ReactDOM and other dependencies required for Card Payment to be rendered in your application.
First, you'll import the Duffel Components UMD module and CSS file and then create a placeholder node in your HTML for the component.
You'll then use DuffelComponents.renderCardPaymentComponent to render the component into your application, passing in the id of the placeholder node you created, as well as the required props for CardPayment as in the example above.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<title>
Duffel Components - example usage of CardPayment in non-React checkout
page
</title>
<link
rel="stylesheet"
href="https://unpkg.com/@duffel/components@latest/dist/CardPayment.min.css"
/>
</head>
<body class="container">
<h1>Example CardPayment checkout page</h1>
<div id="target"></div>
<script
type="text/javascript"
src="https://unpkg.com/@duffel/components@latest/dist/CardPayment.umd.min.js"
></script>
<script>
DuffelComponents.renderCardPaymentComponent('target', {
duffelPaymentIntentClientToken: duffelPaymentIntentClientToken
successfulPaymentHandler: successfulPaymentHandlerFn, // Show 'successful payment' page and confirm Duffel PaymentIntent
errorPaymentHandler: errorPaymentHandlerFn // Show error page
})
</script>
</body>
</html>
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: v1"
-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: v1"
-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.