Accept payments with PayTabs in DaloyJS
PayTabs is a MENA-region payment gateway with strong coverage of Mada, KNET, BenefitPay, STC Pay, OmanNet, cards, and Apple Pay. This guide uses the official paytabs_pt2 npm package from a DaloyJS server, wrapped to feel like a normal async API.
What you should know up front
- The official SDK is callback-based with positional array arguments. It works, but it's noisy. We'll wrap
createPaymentPageonce in a Promise-returning function with named object arguments — every route handler stays clean after that. - Regions are not interchangeable. Your profile lives in one of
ARE,SAU,OMN,JOR,EGY,IRQ,PSE, orGLOBAL. Passing the wrong region results in "Invalid credentials" even when the key is right. - It's a redirect flow. You call
createPaymentPage, PayTabs returns aredirect_url, the customer pays there and comes back to your return URL. The callbackURL is the server-side IPN — that's the only signal you should mark an order paid on. - IPN signature is HMAC-SHA256. The raw POST body is signed with your
server_keyand sent in thesignatureheader. Verify before trusting anything in the payload. - Use
tran_type: "sale"for direct capture,"auth"for an authorisation you'll capture later, andtran_class: "ecom"for normal online checkouts.
1. Provision
- Sign in to the PayTabs merchant dashboard.
- Developers → Profile to grab your Profile ID and Server Key. Note your Region.
- Enable the payment methods you need (Mada, KNET, BenefitPay, STC Pay all require explicit activation — some need extra paperwork).
- In Developers → IPN, point the IPN URL at your DaloyJS webhook endpoint.
2. Install
3. Environment variables
4. Plugin
We wrap two things here: createPaymentPage's positional-array+callback shape into a Promise with named args, and the IPN signature check into a one-call helper. setConfig is module-global, so we only call it once at register time.
The signature check uses the rawrequest body — if you JSON.parse and re-stringify, the byte order changes and the HMAC won't match.
5. Create a payment page
6. IPN webhook
PayTabs POSTs the same payload as a successful queryTransactioncall to your IPN URL on every transaction state change. Always verify the signature, ack 200 fast, and re-query before fulfilment if you don't fully trust the body:
7. Refunds and capture
Runtimes
paytabs_pt2 uses Node's https module and runs on Node 18+. It is notedge-runtime compatible. If you're deploying to Cloudflare Workers or Vercel Edge, call the PayTabs PT2 REST endpoints directly with fetch (Bearer auth on Authorization using the server key, endpoints like POST /payment/request and POST /payment/query against your regional base URL).
Errors
On failure, response.payment_result.response_code tells you the gateway outcome (e.g. 200 success, 481 3-D Secure failed, 500 generic decline). Surface declines through problem+json with the PayTabs response_message in the detailfield — don't pass it verbatim to end users; declines often include scheme-specific text the customer can't act on.
Modernisation notes
- Wrap the SDK once, then forget it. The positional-array signatures are easy to typo and harder to grep for than named-object arguments. The plugin above pays that cost in one place.
- Verify the IPN signature, always.Don't fall back to "the IP is from PayTabs" — IPs change, and HMAC over the raw body is the only verification PayTabs actually publishes a contract for.
- Fulfil on IPN, not on return URL. The customer can close the tab mid-3DS. The IPN is the source of truth; the return URL just renders a confirmation page.
- Consider the REST API directly if you need edge. The Node SDK predates the wide adoption of edge runtimes. A 30-line
fetchwrapper gives you the same shape and works on Workers/Edge — at the cost of maintaining the request shape yourself.
See also the payments overview, Tap Payments guide, Adyen guide, and problem+json errors.