Integrate Payment Gateway

Setup

In order to be able to accept payments through your own gateway, you need to follow these steps.

  1. Create a Gateway
  2. Get the secret of the gateway
  3. Integrate your Payment Gateway as described here
  4. Activate your Gateway

Overview

External payment gateway flow

If you have an active payment gateway, the customer will see your gateway as a possible payment method during the checkout process. When the customer decides to proceed with this payment gateway he will be redirect to the URL which was defined during creation. When paying online with a saved method, that URL includes a customerPaymentMethodToken query parameter. Use the stored method and do not collect new payment details.

From there you are responsible to accept a payment, send a request to the vivenu API when the payment either fails or succeeds and to redirect the customer to according return URL.

If the customer already paid and the /confirm request fails please remember to refund the amount.

Example Payment Gateway

The following code demonstrates a very naive implementation of an external payment gateway.

When a saved payment method is used online, the pay endpoint also receives a customerPaymentMethodToken query parameter. See Example Charge Endpoint for setup handling, token matching against customerId, and signature verification.

const express = require('express')
const { nanoid } = require('nanoid')
const fetch = require('node-fetch')
const app = express()
const port = 7000

const VIVENU_URL = 'https://vivenu.dev'
const API_KEY = 'key_'
const GATEWAY_SECRET = 'pm_secret_'

const getPaymentRequest = async (paymentId) => {
  const response = await fetch(
    VIVENU_URL + '/api/payments/requests/' + paymentId,
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: 'Bearer ' + API_KEY,
      },
    },
  )

  const json = await response.json()
  return json
}

const completePaymentRequest = async (paymentId) => {
  const response = await fetch(
    VIVENU_URL + '/api/payments/requests/' + paymentId + '/confirm',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: 'Bearer ' + API_KEY,
      },
      body: JSON.stringify({
        gatewaySecret: GATEWAY_SECRET,
        reference: nanoid(),
      }),
    },
  )

  const json = await response.json()
  return json
}

app.get('/payment/gateway', async (req, res) => {
  const paymentId = req.query.paymentId
  const paymentRequest = await getPaymentRequest(paymentId)

  console.log(paymentRequest)

  if (paymentRequest.status !== 'NEW') {
    console.error('payment request is already processed')
    return res.status(403).end()
  }

  const completedPaymentRequest = await completePaymentRequest(paymentId)
  res.redirect(completedPaymentRequest.successReturnUrl)
  res.end()
})

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`)
})

Refunds

Payment gateway refund flow

In order to accept refunds through you custom payment gateway you need to expose a POST route and set it up in the payment gateway.

Whenever a refund is requested we will POST to your endpoint and send a refund request. We will add a x-vivenu-signature header in order to enable you to verify that the request is authentic and signed with your gateway secret.

Always verify the signature to prevent malicious users from sending refund requests.

The refund request

Required attributes

  • Name
    id
    Type
    string
    Description

    A unique ID for the request

  • Name
    time
    Type
    string date-time
    Description

    An ISO timestamp of the request. Can be used to prevent old requests from being processed

  • Name
    mode
    Type
    enum(dev, prod)
    Description

    Server mode of the API

  • Name
    type
    Type
    enum(payment.refund)
    Description

    The type of the action

  • Name
    data
    Type
    object
    Description
    Required nested attributes (5)
    • Name
      transactionId
      Type
      string
      Description

      The ID of the transaction to refund

    • Name
      sellerId
      Type
      string
      Description

      The ID of the seller

    • Name
      psp
      Type
      string
      Description

      The reference of the payment

    • Name
      amount
      Type
      number float
      Description

      The amount to refund

    • Name
      currency
      Type
      enum(EUR, USD, GBP, AUD, CHF, THB, ILS, COP, MXN, DKK, NOK, SEK, QAR, CAD, ISK, GTQ, INR, DOP, SGD, PLN, SAR, TTD, ZAR, KYD, HKD, CZK, KRW, JPY, NZD, AED, MAD, TWD, BRL, BWP, NAD, KES, SCR, TRY, SZL, LSL, TZS, UGX, ZMW, ZWG, GHS, NGN, SLE, LRD, XOF, XAF, GEL, IDR, ARS, CRC, HUF, EGP, MYR, VND, PHP, MWK)
      Description

      An ISO 4217 3-character code of the currency

Example

{
  "id": "507f191e810c19729de860ea",
  "time": "2030-01-23T23:00:00.123Z",
  "mode": "dev",
  "type": "payment.refund",
  "data": {
    "transactionId": "507f191e810c19729de860ea",
    "sellerId": "507f191e810c19729de860ea",
    "psp": "string",
    "amount": 19.15,
    "currency": "EUR"
  }
}

The refund response

Optional attributes

  • Name
    reference
    Type
    string
    Description

    A reference for the refund, e.g. a PSP transaction ID. Defaults to original refund request ID

  • Name
    error
    Type
    enum(amount_too_high, amount_too_low, payment_already_refunded, payment_not_refundable, insufficient_account_balance, payment_disputed, partial_refunds_not_supported, payment_too_old)
    Description

    The error code. If present, the refund is considered failed.

Example

{
  "reference": "string",
  "error": "amount_too_high"
}

Charge payment method

In order to allow users to save payment methods and use them for recurrent payments such as subscriptions or payment plans, you need to support setup requests for your payment interface, and expose an additional POST route and set it up in the payment gateway. Make sure to tick "Recurrent payments" in your gateway settings as well.

The following changes are required from your gateway:

  1. Save a payment method (setup)

    The customer adds a payment method in their account and chooses your custom gateway. They are redirected to your pay endpoint with a payment request whose origin is "setup". Collect the payment method details, then confirm the request via POST /api/payments/requests/{id}/confirm with paymentMethodIdentifier and optional paymentMethodExpiration. The response includes a customerPaymentMethodToken — store it together with the payment credentials — and a successReturnUrl to redirect the customer back.

  2. Charge a saved payment method

    When the customer pays with a saved method, we POST a charge request to your charge endpoint. The payload includes the same customerPaymentMethodToken. Use the x-vivenu-signature header to verify that the request is authentic and signed with your gateway secret, look up the stored credentials, and charge the payment method.

  3. Online payments with saved payment method

    The pay endpoint needs to accept a customerPaymentMethodToken query parameter. When provided, the payment page should not allow the user to enter their payment details, but instead should use previously saved payment method details associated with the token.

    This flow can be used if re-authorization is needed, for example if the issuer requires 3DS or similar online re-authorization for a payment method.

The charge request

Required attributes

  • Name
    id
    Type
    string
    Description

    A unique ID for the request

  • Name
    time
    Type
    string date-time
    Description

    An ISO timestamp of the request. Can be used to prevent old requests from being processed

  • Name
    mode
    Type
    enum(dev, prod)
    Description

    Server mode of the API

  • Name
    paymentId
    Type
    string
    Description

    The ID of the payment request to charge

  • Name
    customerPaymentMethodToken
    Type
    string
    Description

    The token of the customer payment method to charge

Example

{
  "id": "507f191e810c19729de860ea",
  "time": "2030-01-23T23:00:00.123Z",
  "mode": "dev",
  "paymentId": "507f191e810c19729de860ea",
  "customerPaymentMethodToken": "string"
}

The charge response

Optional attributes

  • Name
    reference
    Type
    string
    Description

    A reference for the charge, e.g. a PSP transaction ID. Defaults to original charge request ID

  • Name
    error
    Type
    enum(authentication_required, authorization_expired, authorization_revoked, insufficient_funds, card_blocked, card_expired, suspected_fraud, invalid_amount, purchase_type_unsupported)
    Description

    The error code. If present, the payment is considered failed.

Example

{
  "reference": "string",
  "error": "authentication_required"
}

Verify signature

The signature can be verified by calculating the HMAC of the raw json string with key=gateway.secret, alg=sha256 and comparing it to the x-vivenu-signature header of the request. Always verify this header on every refund and charge request your gateway receives.

const GATEWAY_SECRET = 'pm_secret_55a54...'
const signature = crypto
  .createHmac('sha256', GATEWAY_SECRET)
  .update(req.rawPayload)
  .digest('hex')

const requestSignature = req.headers['x-vivenu-signature']
const isValid = signature.toLowerCase() === requestSignature.toLowerCase()

Example Refund Endpoint

The following code adds a very naive implementation of a refund endpoint to our example gateway.

In a real world application your endpoint should also check if the id of the refund request has already been processed and that the difference from now to time is not greater than 60 seconds.

app.post('/payment/gateway/refund', async (req, res) => {
  const payload = req.body
  if (payload.type !== 'payment.refund') {
    return res.status(400).send(JSON.stringify({ error: 'unsupported type' }))
  }

  const signature = crypto
    .createHmac('sha256', GATEWAY_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex')

  const isValid =
    signature.toLowerCase() === req.headers['x-vivenu-signature'].toLowerCase()
  if (!isValid) {
    return res.status(400).send(JSON.stringify({ error: 'invalid signature' }))
  }

  // do some refund logic delegation

  res.send(JSON.stringify({ reference: '...' }))
})
{
  "reference": "refund_3470c03290dc5b0bd631ab34afc982fe"
}

Example Charge Endpoint

The following code extends our example gateway with setup handling on the payment endpoint, support for the customerPaymentMethodToken query parameter, and a naive charge endpoint.

When the payment request origin is "setup", confirm the request with paymentMethodIdentifier and optional paymentMethodExpiration, store the returned customerPaymentMethodToken with the payment credentials, and redirect to successReturnUrl.

When the pay endpoint receives customerPaymentMethodToken as a query parameter, use the stored payment method associated with that token instead of collecting new payment details. Still verify that the token matches the customerId on the payment request.

In a real world application your charge endpoint should also check if the id of the charge request has already been processed and that the difference from now to time is not greater than 60 seconds.

const completePaymentRequest = async (paymentId, extra = {}) => {
  // ...

  body: JSON.stringify({
    gatewaySecret: GATEWAY_SECRET,
    reference: nanoid(),
    ...extra,
  })

  // ...
}

app.get('/payment/gateway', async (req, res) => {
  const customerPaymentMethodToken = req.query.customerPaymentMethodToken

  // ...

  if (customerPaymentMethodToken) {
    const stored = database.getPaymentMethod(customerPaymentMethodToken)
    if (!stored || stored.customerId !== paymentRequest.customerId) {
      return res.status(403).end()
    }

    // use previously saved payment method details associated with the token
    // do not allow the user to enter payment details
  }

  if (paymentRequest.origin === 'setup') {
    // collect payment method details (naive)
    const creditCardNumber = '123456789012'
    const paymentMethodIdentifier = creditCardNumber.slice(-4)
    const paymentMethodExpiration = '2030-12'

    const completedPaymentRequest = await completePaymentRequest(paymentId, {
      paymentMethodIdentifier,
      paymentMethodExpiration,
    })

    database.savePaymentMethod(
      completedPaymentRequest.customerPaymentMethodToken,
      creditCardNumber,
    )

    res.redirect(completedPaymentRequest.successReturnUrl)
    res.end()
    return
  }

  // ...
})

app.post('/payment/gateway/charge', async (req, res) => {
  const payload = req.body

  const signature = crypto
    .createHmac('sha256', GATEWAY_SECRET)
    .update(JSON.stringify(payload))
    .digest('hex')

  const isValid =
    signature.toLowerCase() === req.headers['x-vivenu-signature'].toLowerCase()
  if (!isValid) {
    return res.status(400).send(JSON.stringify({ error: 'invalid signature' }))
  }

  // lookup payment credentials by customerPaymentMethodToken,
  // verify they belong to the customerId on the payment request,
  // and charge the saved payment method
  const creditCardNumber = database.getPaymentMethod(
    payload.customerPaymentMethodToken,
  )

  res.send(JSON.stringify({ reference: '...' }))
})
{
  "reference": "charge_3470c03290dc5b0bd631ab34afc982fe"
}

Was this page helpful?