# Divit Pay Now — LLM Integration Guide Welcome! This document is optimized for LLMs, AI coding assistants (like Gemini, Cursor, Copilot, ChatGPT, and Cline) and automated code generators. It contains the complete reference needed to implement a secure, end-to-end checkout integration with Divit Pay Now. --- ## Prerequisites & Environment Variables Ensure the LLM sets up or reads the following configuration variables inside the target application's environment configuration (e.g., `.env` file): * `DIVIT_ENV`: Current environment mode (`sandbox` or `production`). * `DIVIT_API_BASE_URL`: Base API URL (`https://sandbox-api.divit.dev` or `https://api.divit.com.hk`). * `DIVIT_API_KEY`: Secret merchant credential starting with `dvt_`. * `DIVIT_SIGNATURE_KEY`: Signature secret configured in the Divit Merchant Admin Portal for webhook validation. --- ## Core Gateway Base URLs Divit Pay Now is a direct bank-to-bank FPS (Faster Payment System) payment gateway in Hong Kong. | Environment | Base API URL | Merchant Portal URL | | :--- | :--- | :--- | | **Sandbox** | `https://sandbox-api.divit.dev` | `https://sandbox-admin.divit.dev` | | **Production** | `https://api.divit.com.hk` | `https://admin.divit.com.hk` | --- ## End-to-End Integration Steps (Phase-by-Phase) ### Phase 1: Client Initializer When initializing the integration client or service, ensure every outgoing HTTP request to Divit includes your secret API key and specifies JSON content-type in the headers: ```http api-key: Content-Type: application/json ``` *Pitfall Warning: Never expose `DIVIT_API_KEY` in frontend templates, client-side Javascript, or mobile apps. Access it strictly on the backend/server-to-server.* --- ### Phase 2: Create Payment Order (POST `/directpay/o/order`) When a user clicks "Checkout" or "Pay with Divit" on your store, make a secure server-to-server POST call to initiate a Divit order. * **Endpoint**: `POST /directpay/o/order` * **Sandbox URL**: `https://sandbox-api.divit.dev/directpay/o/order` * **Production URL**: `https://api.divit.com.hk/directpay/o/order` #### Amount Format Rule: All currency amounts must be positive integers representing the **smallest currency subunit** (cents). For example, `HKD $120.50` must be multiplied by `100` and passed as `12050`. #### JSON Request Payload Structure: ```json { "order": { "totalAmount": { "amount": 12050, "currency": "HKD" }, "webhookSuccess": "https://your-site.com/checkout/success", "webhookFailure": "https://your-site.com/checkout/cancel", "webhookEvents": "https://your-api.com/api/webhooks/divit", "merchantRef": "ORDER-10024A", "expiredAt": 1799999999, "language": "en" }, "customer": { "firstName": "John", "lastName": "Doe", "email": "customer@example.com", "tel": "+85291234567", "language": "en" } } ``` #### JSON Response Payload Structure: ```json { "code": 0, "message": "OK", "data": { "redirectURI": "https://sandbox-pay.divit.dev/request/fps/956db0a3-68b1-420c-9df9-3ec5d77136b9?signature=a1b2c3d4e5f6g7h8", "orderID": "956db0a3-68b1-420c-9df9-3ec5d77136b9", "token": "" } } ``` --- ### Phase 3: Client Redirection Immediately extract `data.redirectURI` from the response payload and redirect the customer's browser session or mobile webview to it. * **Client-side Redirect (Web)**: ```javascript window.location.replace(redirectURI); ``` * **Mobile Webview**: Load the `redirectURI` into a standard webview. It automatically optimizes for mobile viewports and supports deep-linking into native Hong Kong retail banking applications. --- ### Phase 4: Secure Webhook Event Handler (HMAC-SHA256) Since payment transfers occur asynchronously, **never rely on client-side redirection to fulfill orders**. Your backend server must listen for asynchronous HTTP POST callbacks from Divit. #### Event Status Codes: * **`2001`** (payment success): Settle the payment, release inventory, and mark the order as paid. * **`4001`** (payment expired): Release the inventory or close checkout. #### Webhook Payload Example: ```json { "event": { "eventId": 2001, "eventDescription": "Order payment completed successfully" }, "eventData": { "OrderID": "87418689-8f26-4200-8d6e-8c4430b41759", "OrderAmount": { "amount": 12050, "currency": "HKD" }, "MerchantRef": "ORDER-10024A" } } ``` #### Cryptographic Signature Verification: To prevent spoofing, **you must cryptographically validate the signature** of every incoming webhook request. 1. **Extract Header**: Capture `X-DIVIT-SIGNATURE` from incoming headers (format: `t=timestamp,s1=signature_hash`). 2. **Capture Raw Request Body (CRITICAL)**: Always use the raw, unparsed request body string for HMAC calculation. Re-stringifying a parsed JSON object will change spacing or ordering, breaking the signature hash. 3. **Reconstruct Content String**: Concatenate the timestamp `t`, a period `.`, and the raw string body: `signatureContent = t + "." + rawRequestBodyString` 4. **Verify Hash**: Compute HMAC-SHA256 hash of `signatureContent` using `DIVIT_SIGNATURE_KEY` as key. Base64-encode and perform a constant-time comparison against `s1`. --- ## Webhook Verification Snippets ### Node.js (Express) ```javascript const crypto = require('crypto'); function verifyDivitSignature(headerVal, rawBody, signatureKey) { if (!headerVal || !rawBody || !signatureKey) return false; const params = {}; headerVal.split(',').forEach(item => { const [key, value] = item.trim().split('='); params[key] = value; }); const timestamp = params['t']; const expectedSignature = params['s1']; if (!timestamp || !expectedSignature) return false; const signatureContent = `${timestamp}.${rawBody}`; const calculatedSignature = crypto .createHmac('sha256', signatureKey) .update(signatureContent) .digest('base64'); try { return crypto.timingSafeEqual( Buffer.from(calculatedSignature), Buffer.from(expectedSignature) ); } catch (e) { return calculatedSignature === expectedSignature; } } ``` ### Python (FastAPI / Flask) ```python import hmac import hashlib import base64 def verify_divit_signature(header_val: str, raw_body_bytes: bytes, signature_key: str) -> bool: if not header_val or not raw_body_bytes or not signature_key: return False try: parts = {item.split('=')[0].strip(): item.split('=')[1].strip() for item in header_val.split(',')} timestamp = parts.get('t') expected_sig = parts.get('s1') if not timestamp or not expected_sig: return False sig_content = f"{timestamp}.".encode('utf-8') + raw_body_bytes calculated_sig = base64.b64encode( hmac.new(signature_key.encode('utf-8'), sig_content, hashlib.sha256).digest() ).decode('utf-8') return hmac.compare_digest(calculated_sig, expected_sig) except Exception: return False ``` ### Go ```go package main import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "strings" ) func VerifyDivitSignature(headerVal string, rawBody []byte, signatureKey string) bool { if headerVal == "" || len(rawBody) == 0 || signatureKey == "" { return false } var timestamp, expectedSignature string parts := strings.Split(headerVal, ",") for _, part := range parts { kv := strings.Split(strings.TrimSpace(part), "=") if len(kv) == 2 { if kv[0] == "t" { timestamp = kv[1] } else if kv[0] == "s1" { expectedSignature = kv[1] } } } if timestamp == "" || expectedSignature == "" { return false } signatureContent := timestamp + "." + string(rawBody) mac := hmac.New(sha256.New, []byte(signatureKey)) mac.Write([]byte(signatureContent)) calculatedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(calculatedSignature), []byte(expectedSignature)) } ``` --- ### Phase 5: Manual Order Status Query (GET status) As a self-healing fallback when a customer returns to your storefront before webhooks are dispatched, manually query the transaction state. * **Endpoint**: `GET /paynow/orders/{orderID}/status` * **Sandbox URL**: `https://sandbox-api.divit.dev/paynow/orders/{orderID}/status` * **Production URL**: `https://api.divit.com.hk/paynow/orders/{orderID}/status` #### Response Payload (JSON): ```json { "code": 0, "message": "OK", "data": { "orderID": "c3222a2f-2908-468d-b0a8-7c64f3a21b4c", "merchantRef": "ORDER-10024A", "status": "completed", "orderAmount": { "amount": 40000, "currency": "HKD" } } } ``` *Status values*: `new`, `active`, `completed`, `expired`, `cancelled`. --- ## Sandbox Verification Guide To test and simulate the payment journey end-to-end without real currency: 1. Initiate an order in sandbox mode (`DIVIT_ENV=sandbox`). 2. Redirect to the generated `redirectURI`. 3. Open the **FPS Payment Simulator** at [bank.divit.dev](https://bank.divit.dev/). 4. Input the reference ID or scan the sandbox payment QR code on the payment screen to initiate a mockup payment. 5. Confirm that your callback server receives the webhook event `2001` (Payment Success) and processes the transaction correctly.