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
- 01requestCustomerDaloyJS routePOST /checkout/paytabscart + customer (region-bound profile)
- 02requestDaloyJS routePayTabscreatePaymentPage with return + callback URLstran_type 'sale', tran_class 'ecom'
- 03responseDaloyJS routeCustomerReturn redirect_url201 { tranRef, redirectUrl }
- 04asyncCustomerPayTabsPays on the hosted page, returns via the return URLreturn URL is a UX hint, not proof
6. IPN webhook
- 01requestPayTabsDaloyJS routePOST /webhooks/paytabssignature header (HMAC-SHA256) over raw body
- 02noteDaloyJS routeDaloyJS routeHMAC-SHA256 the raw body with the server_keytimingSafeEqual vs the signature header
- 03responseDaloyJS routePayTabs401 when the signature does not match{ error: 'bad signature' }
- 04asyncDaloyJS routePayTabsqueryTransaction(tran_ref), fulfil on status 'A', ack200, re-query before fulfilment
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, 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.