Accept payments with Tap Payments in DaloyJS
Tap Payments is the default acquirer for the GCC and wider MENA region — it's how you accept KNET (Kuwait), Mada (Saudi), Benefit / BenefitPay (Bahrain), STC Pay, plus cards, Apple Pay, Google Pay, and BNPL methods like Tabby and Tamara. There's no first-party Node SDK; you integrate against the REST API with fetch.
What you should know up front
- Bearer auth, secret key in the backend only.
Authorization: Bearer sk_test_...orsk_live_.... Public keys (pk_*) are for the frontend SDKs; never send a secret key from the browser. - It's a redirect flow. You create a charge, Tap returns
transaction.url, you redirect the customer. They come back via yourredirect.urlwith?tap_id=chg_xxx— that's a UX hint, not proof of payment. - Webhooks come with a
hashstring. Tap sends an HMAC-SHA256 over a specific concatenation of fields, base64-encoded, in thehashstringheader. Verify it on every request; never trust the body alone. - Amounts are decimals. Tap takes
{ amount: 10, currency: "KWD" }as a number — but KWD has 3 decimals (fils), SAR/AED/QAR/BHD have 2/3, USD 2. Use the right precision per currency or you'll under/overcharge. - Always re-fetch the charge. On both webhook and redirect return, GET
/v2/charges/{id}before marking anything paid. The status you want isCAPTURED(orAUTHORIZEDfor the auth-only flow).
1. Provision
- Create an account at tap.company and sign in to the dashboard.
- Accounts → Operators → MERCHANT to grab your Merchant ID, plus Test/Live Secret Keys (
sk_*) and Public Keys (pk_*). - Enable the payment methods you need (KNET, Mada, Benefit, etc.) — some require contacting Tap support for activation.
- Configure a webhook URL on your account so Tap can POST events to your DaloyJS server.
2. Environment variables
3. Plugin (no SDK — fetch-based)
The field order inside buildHashString is the part Tap is strict about — keep it pinned to their webhook docs. If they add a new field to the hash, every webhook will fail until you update the function.
4. Create a hosted charge
The simplest integration: source.id: "src_all"gets you Tap's hosted checkout page with every method you've enabled. Use src_card, src_kw.knet, src_sa.mada, etc. to pin a specific method.
5. Webhook
Tap POSTs JSON for every charge state change. Verify the hashstring header, then refetch the charge before doing anything irreversible:
6. Refunds
Authorize + capture
For an auth-then-capture flow (useful for hotels, marketplaces, anything where the final amount is decided after the customer's session), use /v2/authorize instead of /v2/charges, then POST /v2/authorize/{id} with { status: "VOID" } or a capture body. Not every method supports it — confirm with Tap support per scheme before relying on it.
Runtimes
Everything here is plain fetch and node:crypto. Swap createHmac for crypto.subtleif you're targeting Cloudflare Workers or Vercel Edge — Tap itself has no runtime requirements beyond a TLS-capable HTTP client.
Errors
Tap returns JSON like { "errors": [{ "code": "1101", "description": "..." }] } with an HTTP error status. Map them through problem+json; the most common ones are 400 (bad body), 401 (wrong key or test/live mismatch), and 404 (asking for a charge that belongs to a different account).
Modernisation notes
- Use
src_allfor the hosted page unless you need to pin a method. Saves you from maintaining a method-picker UI and lets Tap roll out new payment options without you redeploying. - Always re-fetch on webhook. The body is signed but webhooks get retried; treating GET
/charges/{id}as the source of truth means out-of-order delivery can't flip a paid order back to pending. - Stop using
charge_idfrom the redirect to fulfil orders. The?tap_id=on the return URL is for showing the customer "thanks" — never for marking the order paid. Fulfillment belongs in the webhook handler. - Keep
buildHashStringin one place. If Tap changes the hash inputs you want to update one function, not eight call sites.
See also the payments overview, Adyen guide, Mollie guide, and problem+json errors.