Accepting Payments, SCA-ready with 3DS2

Accepting payments online is becoming increasingly complex. Merchants need to optimize the collection of their customers’ card information, they need to do it securely (read: PCI DSS compliance), while also following new regulations happening in their markets.

ProcessOut abstracts this complexity with its suite of tools and SDKs. ProcessOut.js provides an easy to use client-side library allowing you to securely accept card numbers on your website while keeping your PCI DSS compliance requirements minimal.

ProcessOut.js is SCA and 3DS2-ready, meaning that the new requirements mandated by new regulations in Europe will be handled automatically for you. Follow our migration guide ↗ if you already have a ProcessOut integration and wish to support new SCA flows.
ProcessOut.js can be fully customized to match the look of your website and checkout. Preview what the checkout process looks like when using ProcessOut.js and being fully PCI compliant, while staying on top of new regulations like DSP2’s SCA (Strong Customer Authentication, mostly enforced through 3DS2 or 3-D Secure version 2) in Europe.

<div class="container">
  <form action="" method="POST" id="card-form">
    <div class="block-group">
            <input type="text" placeholder="John Smith" class="b75 block" id="cardholdername">
      <input type="text" placeholder="10018" class="b25 block" id="cardholderzip">
        </div>

    <div class="block-group">
      <div class="block" data-processout-input="cc-number" 
      data-processout-placeholder="4242 4242 4242 4242"></div>
      <div class="block" data-processout-input="cc-exp" 
        data-processout-placeholder="MM / YY"></div>
      <div class="block" data-processout-input="cc-cvc" 
        data-processout-placeholder="CVC"></div>
    </div>

    <div>
      <input type="submit" id="paymentBtn">
    </div>
    <div id="errors"></div>
    <div id="success"></div>
  </form>
</div>
/*! PocketGrid 1.1.0
* Copyright 2013 Arnaud Leray
* MIT License
*/
* {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* Clearfix */
.block-group {
  *zoom: 1;
}
.block-group:before, .block-group:after {
  display: table;
  content: "";
  line-height: 0;
}
.block-group:after {
  clear: both;
}

.block-group {
  /* ul/li compatibility */
  list-style-type: none;
  padding: 0;
  margin: 0;
}

/* Nested grid */
.block-group > .block-group {
  clear: none;
  float: left;
  margin: 0 !important;
}

/* Default block */
.block {
  float: left;
  width: 100%;
}
.b75 {
  width: 74%;
}
.b25 {
  width: 25%;
  margin-left: 1%;
}

html, body {
  background: #ECEFF1;
  font-size: 16px;
}

.container {
  width: 100%;
  max-width: 400px;
  background: white;
  margin: 3em auto;
  padding: 1em;
  border-radius: 4px;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
}

[data-processout-input], input {
  border: 1px solid #ECEFF1;
  border-radius: 4px;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
  padding: 0.5em;
  width: 100%;
  margin-bottom: 1em;
  font-size: 14px;
  min-height: 2em;
}
::-webkit-input-placeholder { /* WebKit, Blink, Edge */
  color:    #ECEFF1;
}
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
  color:    #ECEFF1;
  opacity:  1;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
  color:    #ECEFF1;
  opacity:  1;
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
  color:    #ECEFF1;
}
[data-processout-input]:nth-child(1) {
  width: 48%;
  display: inline-block;
}
[data-processout-input]:nth-child(2) {
  width: 25%;
  display: inline-block;
  margin-left: 1%;
}
[data-processout-input]:nth-child(3) {
  width: 25%;
  display: inline-block;
  margin-left: 1%;
}

input[type="submit"] {
  margin: 0;
  box-shadow: 0 5px 7px rgba(50, 50, 93, 0.04), 0 1px 3px rgba(0, 0, 0, 0.03);
  background: #3F51B5;
  color: white;
  border-radius: 4px;
  border: 1px solid #303F9F;
}

#errors, #success {
  margin-top: 1em;
  text-align: center;
  font-size: 0.9em;
  color: #D84315;
}
#success {
  color: #4CAF50;
}
document.addEventListener("DOMContentLoaded", function() {
  var processout = new ProcessOut.ProcessOut("test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x");
  var formElement = document.getElementById("card-form");
  processout.setupForm(formElement, {
    style: {
      fontSize: "14px",
      "::placeholder": {
        color: "#ECEFF1",
      },
    }
  }, function(form) {
    form.getNumberField().on("focus", function(e) {
      document.getElementById("errors").innerHTML = "";
    });
    form.getExpiryField().on("focus", function(e) {
      document.getElementById("errors").innerHTML = "";
    });

    form.addEventListener("submit", function(e) {
      e.preventDefault();
      document.getElementById("paymentBtn").disabled = true;

      // Let's tokenize the card
      processout.tokenize(form, {
        name: document.getElementById("cardholdername").value,
        contact: {
          zip:  document.getElementById("cardholderzip").value
        },

        // Also, if you want to offer the customer a preferred scheme
        // to pay on (for example, if the customer's card supports 
        // co-schemes such as carte bancaire)
        preferred_scheme: "carte bancaire"
      }, function(token) {
        document.getElementById("success").innerHTML = "Success! Your created card token is "+token;
        document.getElementById("paymentBtn").disabled = false;
      }, function(err) {
        document.getElementById("errors").innerHTML = err.message;
        document.getElementById("paymentBtn").disabled = false;
      });

      return false;
    });
  }, function(err) {
    console.log(err);
  });
});


Step 1: Create an invoice

Prior to getting paid, you first need to create an Invoice resource meant to define how much your customer will get charged, and set metadata related to the payment. This Invoice will also be sent back to your frontend to properly authenticate your customer, if required by local regulations.

curl -X POST https://api.processout.com/invoices \
    -u test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x:key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB \
    -d name="Awesome invoice" \
    -d amount="4.99" \
    -d currency=USD
var ProcessOut = require("processout");
var client = new ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x",
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

client.newInvoice().create({
    name:     "Awesome invoice",
    amount:   "4.99",
    currency: "USD",
    device:   client.newInvoiceDevice({
        channel: "web" // optional; can be web, ios or android
    })
}).then(function(invoice) {
    // invoice is our newly created resource

}, function(err) {
    // An error occured

});
import processout
client = processout.ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

invoice = client.new_invoice().create({
    "name":     "Awesome invoice",
    "amount":   "4.99",
    "currency": "USD",
    "device":   client.new_invoice_device({
        "device": "web" # optional; can be web, ios or android
    })
})
require "processout"
client = ProcessOut::Client.new(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

invoice = client.invoice.create(
    name:     "Awesome invoice",
    amount:   "4.99",
    currency: "USD",
    device:   client.invoice_device.new(
        channel: "web" # optional; can be web, ios or android
    )
)
<?php
$client = new \ProcessOut\ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

$invoice = $client->newInvoice()->create(array(
    "name"     => "Awesome invoice",
    "amount"   => "4.99",
    "currency" => "USD",
    "device"   => $client->newInvoiceDevice(array(
        "channel" => "web" // optional; can be web, ios or android
    ))
));
import "gopkg.in/processout.v4"
client := processout.New(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

iv, err := client.NewInvoice().Create(processout.InvoiceCreateParameters{
    Invoice: &processout.Invoice{
        Name:     processout.String("Awesome invoice"),
        Amount:   processout.String("4.99"),
        Currency: processout.String("USD"),
        InvoiceDevice: client.NewInvoiceDevice(processout.InvoiceDevice{
            // optional; can be web, ios or android
            Channel: processout.String("web"),
        }),
    },
})

Note that, although it is optional, you can send the additional InvoiceDevice object within the Invoice to determine which channel will be used to process the transaction. This is especially useful to make use of native 3DS2 SDKs.

Optionally, you may also choose to tokenize the card with a Customer Token to be able to re-use the Card as you please in the future, either for one-click payments, subscriptions, or one-off charges.

Step 2: Create your payment form and tokenize

Let’s set up a payment form. Please note that the credit card fields aren’t actual input boxes, but are only DOM containers. To ensure PCI compliance, ProcessOut hosts the credit card fields on its own servers.

<form action="/your-capture-endpoint" method="POST" id="payment-form">
  <div data-processout-input="cc-number"
        data-processout-placeholder="4242 4242 4242 4242"></div>
  <div data-processout-input="cc-exp-month"
        data-processout-placeholder="Expiration month"></div>
  <div data-processout-input="cc-exp-year"
        data-processout-placeholder="Expiration year"></div>

  <!-- It is also possible to use a unified field for the expiration date: -->
  <div data-processout-input="cc-exp"
        data-processout-placeholder="Expiration date (mm / yy)"></div>


  <div data-processout-input="cc-cvc"
        data-processout-placeholder="CVC"></div>

  <input type="submit" class="submit" value="Submit Payment">
</form>

For the payment form to be interactive, you will need to load ProcessOut.js. It is important to always load it from our CDN like shown below. Please note that ProcessOut.js has no external dependencies, meaning it can work without jQuery.

<script src="https://js.processout.com/processout.js"></script>

Now that ProcessOut.js is loaded, we can start using it. Let’s first instanciate a new ProcessOut instance in a different script tag as soon as the DOM is fully loaded, and set up our form:

document.addEventListener("DOMContentLoaded", function() {
  var client = new ProcessOut.ProcessOut(
    "proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x");
  var formElement = document.getElementById("payment-form");
  client.setupForm(formElement, processoutReadyHandler, function(err) {
    console.log("Woops, couldn’t setup the form: "+err);
  });
});

Note: Remember to replace the project ID in the example with your own. When testing, prepend your project ID with test- like so: test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x.

We also still have to add the handler, which we’ll do just now:

Step 3: Bind the form and tokenize the card

As soon as the form is fully loaded, setupForm will call processoutReadyHandler with the created CardForm object. We’ll use this object to handle the tokenization transparently.

Let’s bind the form submission and tokenize the card:

function processoutReadyHandler(form) {
  // The form is now fully loaded!
  formElement.addEventListener("submit", function(e) {
    // Cancel any default action
    e.preventDefault();
    // Blocking the form while performing the tokenization may also
    // prevent race conditions from happening, such as when
    // the user double-clicks on the button
    document.getElementById("paymentBtn").disabled = true;

    // Let’s tokenize the card
    client.tokenize(form, {
      // It’s possible to send cardholder information (optional)
      name: document.getElementById("cardholdername").value,
      contact: {
        zip: document.getElementById("cardholderzip").value,
        // Available contact fields:
        // address1, address2, city, state, country_code, zip
      },

      // Also, if you want to offer the customer a preferred scheme
      // to pay on (for example, if the customer's card supports 
      // co-schemes such as carte bancaire)
      preferred_scheme: "carte bancaire"
    }, processoutCardTokenized, function(err) {

      // Card validation errors or network issues are returned
      // as well. Find the full list of errors below
      alert(err.message);

      // Enable back the button
      document.getElementById("paymentBtn").disabled = false;
    });

    return false;
  });
}

We have bound the form but we also want to implement the processoutCardTokenized handler which will receive the ID of the tokenized card. This is the ID which you can use to charge your customers, either directly, or by creating a Customer Token from it.

Step 4: Make payment on the Invoice

For now, we’ll simply get this card ID and use it to place our payment. If you choose to create a Customer Token to be able to re-use the card details for future payments, you can also use that token directly instead of the card token in the makeCardPayment call.

If you’re also working on mobile applications, you can see our mobile SDK reference for 3DS2 here ↗

function processoutCardTokenized(token) {
    // make sure `invoiceID` generated from
    // your backend is available in this scope
    client.makeCardPayment(invoiceID, token, {
        authorize_only: false, // set to true if you don’t want to capture directly
    }, function(iv) {
        var field   = document.createElement("input");
        field.type  = "hidden";
        field.name  = "invoice_id";
        field.value = iv;

        // Enable back the button
        document.getElementById("paymentBtn").disabled = false;

        // We add the invoice_id input so that it’s sent back to the server.
        // The only thing left to do is to submit the form
        formElement.appendChild(field);
        formElement.submit();
    }, function(err) {
        document.getElementById("errors").innerHTML = err.message;
    });
}

Step 5: Confirm the payment in your backend (synchronous)

The final step in processing the transaction is confirming the payment from your backend. To do so, you should make an authorization or capture call on the Invoice object. Even if the capture was performed from the frontend earlier, this call will let your code verify the status of the transaction.

curl -X POST https://api.processout.com/invoices/iv_lEZFFcQg5WwpKcolBrlzioeeFnnuPmyE/capture \
    -u test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x:key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB \
    -d source=card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ
var ProcessOut = require("processout");
var client = new ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x",
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

invoice.capture("card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ").then(
    function(transaction) {
        // 

    }, function(err) {
        // The invoice could not be captured
    });
import processout
client = processout.ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

transaction = invoice.capture("card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ")
require "processout"
client = ProcessOut::Client.new(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

transaction = invoice.capture("card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ")
<?php
$client = new \ProcessOut\ProcessOut(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB");

$transaction = $invoice->capture("card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ");
import "gopkg.in/processout.v4"
client := processout.New(
    "test-proj_gAO1Uu0ysZJvDuUpOGPkUBeE3pGalk3x", 
    "key_sandbox_mah31RDFqcDxmaS7MvhDbJfDJvjtsFTB")

tr, _ := iv.Capture("card_1jSEVrx7oaRta1KEdxoMWbiGkK2MijrZ")

If the capture was successful, a Transaction object is returned. You may then check that its status attribute is set to completed in order to confirm the payment. Learn more about the possible statuses for a transaction here ↗.

Step 6: Listen to webhooks for payment updates

Although ProcessOut.js supports a synchronous flow, allowing you to give your users a better experience, we strongly recommend that you also set up an endpoint to receive the webhooks we send with payment updates.

This can be particularly important when the user drops from your checkout flow after the payment, which could lead to a transaction not being logged in your systems. Webhooks will ensure that your backend is always up-to-date with the correct status for your transactions.