TchokoPay
Examples

Node.js — full example

Charge a customer and handle the webhook, end to end.

A complete, minimal integration — creating a payment with an Express route, and confirming it with a verified webhook.

const express = require('express');
const crypto = require('crypto');

const app = express();
const TCHOKO_KEY = process.env.TCHOKOPAY_API_KEY;
const WEBHOOK_SECRET = process.env.TCHOKOPAY_WEBHOOK_SECRET;

async function chargeCustomer(orderId, amountXaf) {
  const res = await fetch('https://connect.tchokopay.com/v1/payments', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TCHOKO_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `charge-${orderId}`,
    },
    body: JSON.stringify({
      amount: amountXaf,
      currency: 'XAF',
      description: `Order ${orderId}`,
      reference: orderId,
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`TchokoPay error: ${err.message}`);
  }

  const payment = await res.json();
  return payment.checkoutUrl; // redirect your customer here
}

function verifyWebhook(rawBody, signatureHeader, timestampHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestampHeader}.${rawBody}`)
    .digest('hex');

  const provided = signatureHeader.replace('sha256=', '');
  const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));

  const age = Math.abs(Date.now() / 1000 - Number(timestampHeader));
  if (age > 300) return false; // stale — reject even if the signature matches

  return valid;
}

app.get('/checkout/:orderId', async (req, res) => {
  const checkoutUrl = await chargeCustomer(req.params.orderId, 5000);
  res.redirect(checkoutUrl);
});

app.post('/webhooks/tchokopay', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-tchokopay-signature'];
  const timestamp = req.headers['x-tchokopay-timestamp'];

  if (!verifyWebhook(req.body.toString(), signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(400).send('invalid signature');
  }

  const event = JSON.parse(req.body.toString());

  if (event.status === 'SUCCESS') {
    markOrderPaid(event.invoiceReference, event.settledAmount, event.settledCurrency);
  } else if (event.status === 'FAILED') {
    markOrderFailed(event.invoiceReference, event.failureReason);
  }

  res.sendStatus(200);
});

app.listen(3000);

The webhook route uses express.raw(), not express.json() — signature verification needs the exact raw bytes that were sent, before any parsing. See Webhooks for why this matters.