API Keys and Environment
- No secret key client-side: search your rendered page source and all browser network traffic for
sk_— the Secret Key must never appear in the frontend. Snippets and the HPP use only the Public Key (pk_...) and merchant code - Secret Key for all backend calls: every server-side request (session creation, payments, captures, refunds, webhooks) is authorized with the Secret Key — the Public Key belongs to the frontend snippets only
- All calls use the base URL matching the merchant’s region — see Base URLs
- Capture, refund and close use the
/api/v2/endpoints (v1 is deprecated)
On-Site Messaging
- Product and Cart snippets and pop-ups are present in accordance with the:
- custom integration documentation
- or the SDK documentation used
- Product snippets are shown for all products, there is no amount limitation on displaying snippets
- Cart snippet is shown for all amounts, there is no amount limitation on displaying snippets
- Cart snippet amount is updated successfully when changes are performed with the items in the Cart: addition / removal / deletion of the items
- If the store has both Arabic and English languages - snippets should be displayed correctly for both of them
- Website: snippets should fit the width of a Mobile Web screen and have suitable width for a Desktop Web as well
- In case your store has several countries: Tabby snippets should be displayed only for countries you have already registered with Tabby
- If our code is not compatible with yours or you have a non-standard plan: kindly use one of the following custom snippets after the confirmation from your assigned business manager is received
Tabby as a Payment Method
- Payment method name is present in accordance with the documentation
- Tabby logo is present near the payment method name
- Checkout snippet is displayed under the selected Tabby payment method (recommended), or the payment method description matches the approved copy — see Tabby on Checkout
- There should be no restrictions on displaying Tabby payment method from your side - this behaviour should be handled by background pre-scoring process
- If the store has both Arabic and English languages - Payment method should be displayed correctly for both of them
- In case your store has several countries: Tabby payment method should be displayed only for countries you have already registered with Tabby
Checkout
- Background Pre-scoring check is present and working in accordance with the documentation
- Website: when a customer decides to place an order with Tabby - Tabby Checkout is opened in the same browser window
- Mobile apps: no control buttons (e.g., X, close, back, etc.) from your app are present on Tabby Checkout
- Total amount on Checkout = amount shown on Tabby Checkout
- If the store has both Arabic and English languages - language marker is sent correctly in a session creation request: object
“lang”, enum“ar”/“en” - Session creation request contains all the required parameters from Tabby API
- Session is created only when the customer clicks “Place order” — check your backend logs: the full-payload
/api/v2/checkoutcall is triggered by the Place-order action only, not by opening the checkout page (the background pre-scoring call is separate), and one click produces exactly one session - Sessions are disposable: a fresh session is created for every payment attempt — the HPP URL is never cached or reused, and your own checkout timeout is shorter than the HPP session lifetime
- Price parity: the order total with Tabby selected equals the total with any other payment method — no Tabby-specific surcharges or reduced discounts
- A
rejectedsession is a business outcome, not an error — don’t log it as a failure or fire alerts. On a pre-scoring reject, hide Tabby or mark it unavailable with the rejection message (per Background pre-scoring); on a reject at session creation, show the rejection message instead of redirecting (there is noweb_urlto redirect to) - Cart behaviour: the cart is kept after cancellation/failure and cleared after a successful payment
- If the store has both Arabic and English languages — your own redirect/result pages (success, cancel, failure messages) are localized too, not only the snippets
- Payload data quality: place a second order with the same registered account and confirm
buyer_history/order_historycarry real values (actual registration date, real past orders, ISO-8601 dates) — omit optional fields instead of sending empty strings or placeholders - Success scenario is working
- Cancellation scenario is working
- Failure scenario is working
- Corner case is supported — complete the OTP, close the tab before the redirect, and verify the payment is still captured via the webhook path
Payment Verification and Processing
- Webhooks are registered for each
merchant_code+ secret key pair (up to 4 webhooks per pair). To receive webhooks for test payments, register them with your test key (sk_test_...) - After a payment is placed successfully with Tabby you receive a webhook to your registered url with status
“authorized” - On receiving it you should trigger a getPayment request to verify the status of the payment
- If a status is
“AUTHORIZED”- a capture request should be triggered from your side- It is an expected behaviour that webhooks return
“authorized”in lower case while getPayment - in upper case:“AUTHORIZED”.
- It is an expected behaviour that webhooks return
- A full amount must be captured
- Your webhook endpoint answers
200fast and tolerates duplicates — the same event can be delivered twice, and delivery order is not guaranteed (see Handling Edge Cases) - For a test payment you observe the full webhook sequence:
authorized→authorizedwith your capture incaptures[]→closed - A capture timeout is not a failure: if the Capture Request times out, your system retrieves the payment and retries the capture with the same
reference_id— verify no duplicate capture is created and no order is left uncaptured - Every capture and refund carries a unique
reference_idderived from your order (see Idempotent Requests)
Refunds and Cancellations
- Refunds validate against the captured amount, not the authorized one — a refund on an uncaptured payment fails. To release an uncaptured amount use Close instead, including the leftover after a partial capture (it is not auto-closed)
- Reusing a
reference_idreplays the first refund instead of creating a new one — a second, genuine refund needs a newreference_id, and the cumulative refunded total cannot exceed the captured amount (see refund test scenario) - Your code reads
captures[]andrefunds[]entries bycreated_at, never by array position — the order is not guaranteed