Card Payments

Take card payments through the API

To use this capability add octo/cardPayments to your Octo-Capabilities header.

This capability allows you to accept card payments via the API. It is possible to support multiple gateways, we document 4 in this spec: VivaWallet Adyen Stripe and External

If you want us to add support for any other gateway please contact us at dev@ventrata.com

Adyen Gateway

We add a cardPayment key to the booking object, but if you're using the octo/cart capability then we add the same key to that as well. The value will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "adyen",
  "adyen": {
    "environment": "live",
    "clientKey": "test_870be2...",
    "session": null,
    "paymentMethodsConfiguration": {
      "card": {
        "hasHolderName": true,
        "holderNameRequired": true,
        "billingAddressRequired": true
      }
    }
  }
}

In the response we provide all the values required to initialize Adyen's Drop-in Widget which you can find documented here:

The session field will be null until you have provided a returnUrl field on the order which will be used by the Adyen integration to redirect the guest after the payment is complete.

PATCH /orders/:id or PATCH /bookings/:uuid
{ "returnUrl": "https://www.example.com/redirect" }

Following that the response body will include the session data like so:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "adyen",
  "adyen": {
    "environment": "live",
    "clientKey": "test_870be2...",
    "session": {
      "id": "CSD9CAC3...",
      "sessionData": "Ab02b4c..."
    },
    "paymentMethodsConfiguration": {
      "card": {
        "hasHolderName": true,
        "holderNameRequired": true,
        "billingAddressRequired": true
      }
    }
  }
}

Once complete, and you're ready to confirm the order/booking then you must replay the sessionId back to the API like so:

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

{
  "cardPayment": {
    "gateway": "adyen",
    "adyen": {
      "sessionId": "CSD9CAC3..."
    }
  }
}

If Ventrata hasn't yet received the webhook notification from Adyen then you may receive a PAYMENT_PENDING error code returned, you can just repeat the confirm request until it works. If the payment is refused, you'll get a BAD_REQUEST error with the error message in the errorMessage field and a new session will be generated for you to try again with.

VivaWallet Gateway

We add a cardPayment key to the booking object, but if you're using the octo/cart capability then we add the same key to that as well. The value will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "vivawallet",
  "vivawallet": {
    "orderCode": 1272214778972604
  }
}

In the response we provide orderCode value which you can use to initiate the VivaWallet Smart Checkout as described in the documentation here:

As described in the docs, in production you just redirect the guest to a URL like so:

https://www.vivapayments.com/web2?ref=1272214778972604

Where the value for ref is the orderCode returned in the card payment object. The documentation also describes various ways to customise the checkout including using custom colors and payment methods.

Once the payment is complete, the customer will be redirected back to the Success URL or Failure URL as setup in your gateway account. If directed to the Success URL you will also receive a query parameter t which is a UUID (example t=2b4c6b5b-49ff-4e46-adc5-f53740212361), you must then send this to Ventrata along with the original orderCode using either of the confirmation endpoints (depending if you're using the octo/cart capability):

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

{
  "cardPayment": {
    "gateway": "vivawallet",
    "vivawallet": {
      "orderCode": 1272214778972604,
      "transactionId": "2b4c6b5b-49ff-4e46-adc5-f53740212361"
    }
  }
}

Ventrata will then verify the payment has actually been approved and if so will confirm the order.

Bridgepay Gateway

We add a cardPayment key to the booking object, but if you're using the octo/cart capability then we add the same key to that as well. The value will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "bridgepay",
  "bridgepay": {
    "publicKey": "tokenpay8228api20202615022644676"
  }
}

In the response we provide publicKey value which you can use with Bridgepay's TokenPay.js SDK described in the documentation here:

On the client side, the payment page should look something like this:

<form action="/payment" method="post" id="form">
  <div id="card"></div>
  <div id="error"></div>

  <input type="hidden" id="token">
  <input type="submit" value="Pay Now">
</form>

<script src="https://bridgepaynetsecuretx.com/WebSecurity/TokenPay/plain-js/tokenPay.js"></script>
<script>
  var tokenpay = TokenPay('tokenpay8228api20202615022644676');
  tokenpay.initialize({
    dataElement: 'card',
    errorElement: 'error',
    useACH: false,
    useStyles: false,
    disableZip: false,
    disableCvv: false
  });

  var form = document.getElementById('form');
  form.addEventListener('submit', function(event) {
    event.preventDefault();
    tokenpay.createToken(function (result) {
      var input = document.getElementById('token');
      input.setAttribute('value', result.token);
      form.submit()
    }, function(result) {
      var error = document.getElementById('error');
      error.innerText = result
    })
  })
</script>

The value of result.token which in this example we set on the hidden input field in the form. You must then send this to Ventrata using either of the confirmation endpoints (depending if you're using the octo/cart capability):

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

{
  "cardPayment": {
    "gateway": "bridgepay",
    "bridgepay": {
      "token": "11a3272a-7d16-402f-84a5-1ea29c3a2656"
    }
  }
}

Ventrata will then use the card token to authorise and capture the payment amount on the order and confirm the booking.

Stripe Gateway

We add a cardPayment key to the booking object, but if you're using the octo/cart capability then we add the same key to that as well. The value will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "stripe",
  "stripe": {
    "version": "latest",
    "paymentIntent": {
      "id": "pi_1GG1qyCgIN4MRzgjHrJygury",
      "publishableKey": "pk_test_f2FFE5An5Z0oMyxjlYgFRtdO",
      "clientSecret": "pi_1GG1qyCgIN4MRzgjHrJygury_secret_PW4yD18qsNpU1Gz8etftPCZDM",
      "amount": 9130,
      "currency": "eur"
    }
  }
}

In the response we give some parameters under cardPayment which includes a stripe payment intent. You should follow the Stripe documentation from this section:

https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements

Check the Stripe documentation for more complete examples, but here is an example:

// Change this value cardPayment.stripe.paymentIntent.publishableKey
var stripe = Stripe('pk_test_f2FFE5An5Z0oMyxjlYgFRtdO');

// Change this value cardPayment.stripe.paymentIntent.clientSecret
var elements = stripe.elements({ clientSecret: "pi_1GG1qyCgIN4MRzgjHrJygury_secret_PW4yD18qsNpU1Gz8etftPCZDM" });
var payment = elements.create("payment");
payment.mount("#payment-element");

var form = document.getElementById('payment-form');

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  
  const {error} = await stripe.confirmPayment({
    //`Elements` instance that was used to create the Payment Element
    elements,
    confirmParams: {
      return_url: 'https://my-site.com/order/123/complete',
    },
  });
  
  if (error) {
    // This point will only be reached if there is an immediate error when
    // confirming the payment. Show error to your customer (e.g., payment
    // details incomplete)
    const messageContainer = document.querySelector('#error-message');
    messageContainer.textContent = error.message;
  } else {
    // Send this payment intent back to Ventrata!
  }
});

Once the payment is confirmed you can perform either:

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

The second endpoint is if you're using the octo/cart capability. You then must include the payment intent id in the request body. If you prefer to use a Stripe payment method id instead that's also possible. Both examples are given below:

{
  "cardPayment": {
    "gateway": "stripe",
    "amount": 9130,
    "currency": "EUR",
    "stripe": {
      "paymentIntent": "pi_1GG0bjCgIN4MRzgjMPBIM7SU"
    }
  }
}

Setup Intent

If you do not want to take a payment, but you do want to save an active card against the order which can be charged in future without the customer being present, you can use the setup intent functionality provided by the Stripe gateway.

In the booking/order response, next to the paymentIntent object will be a new field called setupIntent which will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "stripe",
  "stripe": {
    "version": "latest",
      "setupIntent": {
        "id": "seti_1IsSzkFpFSzCJZwK7FZaEFdT",
        "publishableKey": "pk_test_3oDH2WJpi1pHBWwOiR71cmWS",
        "clientSecret": "seti_1IsSzkFpFSzCJZwK7FZaEFdT_secret_JVTx18Jwwt7EFZDSzHsmjNiDkeeNfvX"
      }
    }
  }
}

The response object will look very similar to the paymentIntent object except it won't contain the amount and currency fields because unlike a paymentIntent we're not actually charging the card, we're just collecting the details to be used later. This can be helpful for a "Buy Now, Pay Later" scenario.

To collect the card details from the customer, follow Stripe's guide here: https://stripe.com/docs/payments/save-and-reuse

A simple example using Stripe's card element would be:

var stripe = Stripe('pk_test_f2FFE5An5Z0oMyxjlYgFRtdO');

// Change this value cardPayment.stripe.paymentIntent.clientSecret
var elements = stripe.elements({ clientSecret: "pi_1GG1qyCgIN4MRzgjHrJygury_secret_PW4yD18qsNpU1Gz8etftPCZDM" });
var payment = elements.create("payment");
payment.mount("#payment-element");

var form = document.getElementById('payment-form');

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  
  const {error} = await stripe.confirmSetup({
    //`Elements` instance that was used to create the Payment Element
    elements,
    confirmParams: {
      return_url: 'https://my-site.com/order/123/complete',
    },
  });
  
  if (error) {
    // This point will only be reached if there is an immediate error when
    // confirming the payment. Show error to your customer (e.g., payment
    // details incomplete)
    const messageContainer = document.querySelector('#error-message');
    messageContainer.textContent = error.message;
  } else {
    // Send this payment intent back to Ventrata!
  }
});

Once the card setup is confirmed you can perform either:

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

The second endpoint is if you're using the octo/cart capability. You then must include the setup intent id in the request body. If you prefer to use a Stripe payment method id instead that's also possible. Both examples are given below:

{
  "cardPayment": {
    "gateway": "stripe",
    "stripe": {
      "setupIntent": "seti_1IsSzkFpFSzCJZwK7FZaEFdT"
    }
  }
}

Note if you use the payment method route we include "amount": 0 in the body. This is critical because it tells the system to use set the payment method up to be used in future rather than charging it right now. This isn't necessary for the Setup Intent method.

External Gateway

The external gateway is the simplest and isn't actually a gateway at all. It allows you to register a virtual card payment which might have been taken on another gateway not supported by Ventrata.

We add a cardPayment key to the booking object, but if you're using the octo/cart capability then we add the same key to that as well. The value will look like this:

// ..rest of the booking or order object
"cardPayment": {
  "gateway": "external"
}

Once the payment is confirmed you can perform either:

POST /bookings/:uuid/confirm POST /orders/:orderId/confirm

The second endpoint is if you're using the octo/cart capability. You can then optionally include notes in the external field which will can record an external payment reference which might be useful for backend reconciliation later. For example:

{
  "cardPayment": {
    "gateway": "external",
    "amount": 9130,
    "currency": "EUR",
    "notes": "Ref#25172328"
  }
}

Last updated