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

Quickstart

Goal: from zero to your first payment_request.paid webhook in 5 minutes, entirely in test mode – no one to contact, no real transfer.

  1. Go to Dashboard › Settings › Developer › Create key and choose Test. The key looks like hk_test_… and is shown once – put it in an environment variable:

    export HENSA_API_KEY="hk_test_..."
  2. One request per order. Send an Idempotency-Key so a flaky-network retry never creates a duplicate.

    curl https://api.hensa.example/v1/payment_requests \
    -H "Authorization: Bearer $HENSA_API_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{ "amount": 150000, "externalOrderRef": "DH-1024" }'

    Response (201 Created):

    {
    "id": "prq_01JD7Q8M4K2X9V3T6B5N0R1W8Y",
    "amount": 150000,
    "currency": "VND",
    "referenceCode": "LK7F3K9",
    "status": "pending",
    "qrPayload": "00020101021238…",
    "receiver": { "name": "CA PHE MOC", "bankName": "MB Bank", "accountMasked": "•••• 6789" },
    "externalOrderRef": "DH-1024",
    "expiresAt": "2026-11-03T08:15:00Z",
    "createdAt": "2026-11-03T08:00:00Z"
    }
  3. Render qrPayload as a QR code with any QR library, together with the receiver name (receiver.name) and the amount so the customer can check before paying. It expires at expiresAt (15 minutes).

  4. Dashboard › Developer › Add endpoint → enter your HTTPS URL and pick payment_request.paid. Store the signing secret whsec_… (shown once).

    Working locally? Use an HTTPS tunnel (e.g. ngrok, Cloudflare Tunnel) to get a public URL pointing at localhost.

  5. 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
    })

    verifyHensaSignature is in Verify signatures.

  6. In test mode you “transfer” with one command – put the reference code in the description so Hensa matches it:

    curl https://api.hensa.example/v1/test/bank_transfers \
    -H "Authorization: Bearer $HENSA_TEST_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{ "amount": 150000, "description": "LK7F3K9 thanh toan don hang", "counterpartyName": "NGUYEN VAN AN" }'

    Within seconds your server receives bank_transaction.created and then payment_request.paid. Order DH-1024 is now paid.

  • Switch to an hk_live_… key (separate environment variable for production)
  • Create a production webhook endpoint with your real URL and store the new secret
  • Handle duplicates by event.id – see Webhooks
  • Run periodic reconciliation as a safety net
  • Handle payment_request.expired (release reserved stock)