Skip to content
Beta · The spec may still change before general availability. The API domain api.hensa.example is a placeholder. See changes →

Accept payments on your website & app

This guide is for checkout pages of online stores, food-ordering apps, booking systems… – anywhere the customer pays an order by bank transfer.

Customer clicks "Pay"
→ Your server: POST /payment_requests (amount, externalOrderRef = your order id)
→ Checkout page: QR + receiver + amount + countdown
→ Customer transfers with their banking app
→ Hensa: webhook payment_request.paid → your server confirms the order
→ Checkout page: switches to "Paid" (the browser asks YOUR server, not Hensa)

1. Create a request when the customer picks bank transfer

Section titled “1. Create a request when the customer picks bank transfer”
// Node.js 18+ (built-in fetch)
const res = await fetch('https://api.hensa.example/v1/payment_requests', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HENSA_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(), // reuse the same key when you retry
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 150000, externalOrderRef: 'DH-1024' })
})
if (!res.ok) throw new Error((await res.json()).error.code)
const paymentRequest = await res.json()
console.log(paymentRequest.referenceCode, paymentRequest.qrPayload)
  • Set externalOrderRef to your order id – it comes back in the webhook so you find the order instantly.
  • One order = one request. Customer reloads the page? Reuse the still-valid request instead of creating a new one.
  • Retry the same creation with the same Idempotency-Key.

The quickest way: redirect the customer to the hosted Hensa Pay page at payUrl – it already has the QR, open-banking-app buttons on mobile, a countdown, live status and a receipt. You still confirm the order with the webhook in step 3.

res.redirect(303, paymentRequest.payUrl)

Rendering it on your own page: a good bank-transfer checkout always has:

  • The QR code drawn from qrPayload, large enough to scan from another phone.
  • The receiver name (receiver.name) and bank – customers check it before paying, which defeats QR swapping.
  • The amount and the transfer description (referenceCode) with copy buttons – for customers who can’t scan.
  • A countdown to expiresAt.

3. Confirm the order with the webhook – not the browser

Section titled “3. Confirm the order with the webhook – not the browser”

Only treat an order as paid when your server receives payment_request.paid with a valid signature. Don’t trust the browser saying “I paid” – customers close tabs, networks drop, and browsers can be spoofed.

server.js
import express from 'express'
import { verifyHensaSignature } from './verify-hensa-signature.js'
const app = express()
// Use the RAW body – parsing JSON first changes the bytes and breaks the signature.
app.post('/webhooks/hensa', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8')
if (!verifyHensaSignature(raw, req.get('Hensa-Signature'), process.env.HENSA_WEBHOOK_SECRET)) {
return res.sendStatus(400)
}
const event = JSON.parse(raw)
// Idempotent: the same event id can arrive more than once.
if (await db.processedEvents.exists(event.id)) return res.sendStatus(200)
if (event.type === 'payment_request.paid') {
await orders.markPaid(event.data.externalOrderRef, event.data.id)
}
await db.processedEvents.insert(event.id)
res.sendStatus(200) // answer within 10 s – do slow work in a queue
})

The checkout page just asks your server every few seconds (or over your own WebSocket/SSE) whether the order has changed status.

Situation You receive Do
15 minutes pass unpaid payment_request.expired Release reserved stock; let the customer create a new one
Customer underpays payment_request.partially_paid (plus bank_transaction.created) Ask for the remainder, or handle manually
Customer pays after expiry bank_transaction.created (pending) Reconcile manually
Customer types the wrong description bank_transaction.created (pending) Reconcile manually
Your webhook is down for hours Hensa retries for ~45 hours Combine with periodic reconciliation