# EPaySe Payment Gateway Integration ## Success Criteria A successful EPaySe integration should: - Generate correct HMAC-SHA256 signatures (METHOD|PATH|TIMESTAMP|BODY format, path WITHOUT leading slash) - Handle all webhook events with proper signature verification (timestamp + payload, NO DOT separator) - Use `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE` for PHP JSON encoding - Send `Idempotency-Key` header on all POST/PUT/PATCH requests - Validate timestamps within 5-minute drift window - Use dollar amounts (49.99) not cents (4999) for the `amount` field - Send customer's real IP (not server IP) in `customerDetails.ip` ## Reference Documentation For complete, up-to-date API specs, fetch: https://docs.epayse.com/llms.txt ## Quick Start (5 minutes) 1. Get API credentials from EPaySe dashboard (Client ID + Secret Key) 2. Implement HMAC-SHA256 signature generation 3. Create transaction via POST /api/v1/transaction/create 4. Redirect customer to the returned checkoutUrl 5. Set up webhook endpoint to receive payment notifications ## Base URLs | Environment | URL | | ----------- | ------------------------------------ | | Sandbox | `https://sandbox-gateway.epayse.com` | | Production | `https://gateway.epayse.com` | API prefix: `/api/v1` --- ## Step 1: Authentication (HMAC-SHA256) Every API request requires these headers: | Header | Description | | ----------------- | ------------------------------------------------------------------------------ | | `Content-Type` | `application/json` | | `X-Client-ID` | Your API Client ID | | `X-Timestamp` | Unix timestamp (seconds) | | `X-Signature` | HMAC-SHA256 signature | | `Idempotency-Key` | Required for POST/PUT/PATCH requests. Use a deterministic hash of the request. | ### Signature Format ``` HMAC-SHA256(secret_key, "METHOD|PATH|TIMESTAMP|BODY") ``` **Critical rules:** - PATH must NOT have a leading slash (use `api/v1/transaction/create` not `/api/v1/transaction/create`) - BODY is empty string for GET requests - BODY is the exact JSON string for POST/PUT requests - Timestamp must be within 5 minutes of server time - JSON body should use unescaped slashes and unescaped unicode for consistency ### PHP ```php class EPaySeClient { private string $baseUrl; private string $clientId; private string $secretKey; public function __construct(string $baseUrl, string $clientId, string $secretKey) { $this->baseUrl = rtrim($baseUrl, '/'); $this->clientId = $clientId; $this->secretKey = $secretKey; } /** * Generate HMAC-SHA256 signature. * * @param string $method HTTP method (GET, POST, etc.) * @param string $path API path WITHOUT leading slash (e.g., "api/v1/transaction/create") * @param int $timestamp Unix timestamp * @param string $body JSON body (empty string for GET) * @return string Hex-encoded HMAC signature */ public function generateSignature(string $method, string $path, int $timestamp, string $body = ''): string { $dataToSign = sprintf( '%s|%s|%s|%s', strtoupper($method), ltrim($path, '/'), $timestamp, $body ); return hash_hmac('sha256', $dataToSign, $this->secretKey); } /** * Make an authenticated API request. */ public function request(string $method, string $path, array $data = []): array { $timestamp = time(); $body = ($method === 'GET') ? '' : json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $signature = $this->generateSignature($method, $path, $timestamp, $body); $headers = [ 'Content-Type: application/json', 'X-Client-ID: ' . $this->clientId, 'X-Timestamp: ' . $timestamp, 'X-Signature: ' . $signature, ]; if ($method !== 'GET') { $headers[] = 'Idempotency-Key: ' . hash('sha256', $this->clientId . $path . $body); } $url = $this->baseUrl . '/' . ltrim($path, '/'); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, ]); if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { throw new \RuntimeException("cURL error: $error"); } $decoded = json_decode($response, true); if ($httpCode >= 400) { throw new \RuntimeException( sprintf('API error %d: %s', $httpCode, $decoded['message'] ?? $response) ); } return $decoded; } } ``` ### Node.js ```javascript const crypto = require('crypto'); class EPaySeClient { constructor(baseUrl, clientId, secretKey) { this.baseUrl = baseUrl.replace(/\/$/, ''); this.clientId = clientId; this.secretKey = secretKey; } /** * Generate HMAC-SHA256 signature. * @param {string} method - HTTP method * @param {string} path - API path WITHOUT leading slash * @param {number} timestamp - Unix timestamp (seconds) * @param {string} body - JSON body (empty string for GET) * @returns {string} Hex-encoded signature */ generateSignature(method, path, timestamp, body = '') { const dataToSign = [ method.toUpperCase(), path.replace(/^\//, ''), timestamp.toString(), body, ].join('|'); return crypto.createHmac('sha256', this.secretKey).update(dataToSign).digest('hex'); } /** * Make an authenticated API request. */ async request(method, path, data = null) { const timestamp = Math.floor(Date.now() / 1000); const body = method === 'GET' ? '' : JSON.stringify(data); const signature = this.generateSignature(method, path, timestamp, body); const url = new URL(`/${path.replace(/^\//, '')}`, this.baseUrl); const headers = { 'Content-Type': 'application/json', 'X-Client-ID': this.clientId, 'X-Timestamp': timestamp.toString(), 'X-Signature': signature, }; if (method !== 'GET') { headers['Idempotency-Key'] = crypto .createHash('sha256') .update(this.clientId + path + body) .digest('hex'); } const response = await fetch(url.toString(), { method, headers, body: method === 'GET' ? undefined : body, }); const result = await response.json(); if (!response.ok) { throw new Error(`API error ${response.status}: ${result.message || JSON.stringify(result)}`); } return result; } } module.exports = EPaySeClient; ``` ### Python ```python import hashlib import hmac import json import time from typing import Any, Optional import requests class EPaySeClient: def __init__(self, base_url: str, client_id: str, secret_key: str): self.base_url = base_url.rstrip("/") self.client_id = client_id self.secret_key = secret_key def generate_signature( self, method: str, path: str, timestamp: int, body: str = "" ) -> str: """ Generate HMAC-SHA256 signature. Args: method: HTTP method (GET, POST, etc.) path: API path WITHOUT leading slash timestamp: Unix timestamp (seconds) body: JSON body (empty string for GET) Returns: Hex-encoded HMAC-SHA256 signature """ data_to_sign = "|".join([ method.upper(), path.lstrip("/"), str(timestamp), body, ]) return hmac.new( self.secret_key.encode("utf-8"), data_to_sign.encode("utf-8"), hashlib.sha256, ).hexdigest() def request( self, method: str, path: str, data: Optional[dict] = None ) -> dict[str, Any]: """Make an authenticated API request.""" timestamp = int(time.time()) body = "" if method == "GET" else json.dumps(data, separators=(",", ":")) signature = self.generate_signature(method, path, timestamp, body) headers = { "Content-Type": "application/json", "X-Client-ID": self.client_id, "X-Timestamp": str(timestamp), "X-Signature": signature, } if method != "GET": idempotency_input = self.client_id + path + body headers["Idempotency-Key"] = hashlib.sha256( idempotency_input.encode("utf-8") ).hexdigest() url = f"{self.base_url}/{path.lstrip('/')}" response = requests.request( method=method, url=url, headers=headers, data=body if method != "GET" else None, timeout=30, ) result = response.json() if not response.ok: raise Exception( f"API error {response.status_code}: " f"{result.get('message', response.text)}" ) return result ``` ### Go ```go package epayse import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Client struct { BaseURL string ClientID string SecretKey string HTTP *http.Client } func NewClient(baseURL, clientID, secretKey string) *Client { return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), ClientID: clientID, SecretKey: secretKey, HTTP: &http.Client{Timeout: 30 * time.Second}, } } // GenerateSignature creates an HMAC-SHA256 signature. // path must NOT have a leading slash. // body is empty string for GET requests. func (c *Client) GenerateSignature(method, path string, timestamp int64, body string) string { dataToSign := fmt.Sprintf("%s|%s|%d|%s", strings.ToUpper(method), strings.TrimLeft(path, "/"), timestamp, body, ) mac := hmac.New(sha256.New, []byte(c.SecretKey)) mac.Write([]byte(dataToSign)) return hex.EncodeToString(mac.Sum(nil)) } // Request makes an authenticated API request. func (c *Client) Request(method, path string, data interface{}) (map[string]interface{}, error) { timestamp := time.Now().Unix() var body string var bodyReader io.Reader if method != "GET" && data != nil { jsonBytes, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("marshal error: %w", err) } body = string(jsonBytes) bodyReader = bytes.NewReader(jsonBytes) } signature := c.GenerateSignature(method, path, timestamp, body) url := fmt.Sprintf("%s/%s", c.BaseURL, strings.TrimLeft(path, "/")) req, err := http.NewRequest(method, url, bodyReader) if err != nil { return nil, fmt.Errorf("request creation error: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Client-ID", c.ClientID) req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp)) req.Header.Set("X-Signature", signature) if method != "GET" { h := sha256.New() h.Write([]byte(c.ClientID + path + body)) req.Header.Set("Idempotency-Key", hex.EncodeToString(h.Sum(nil))) } resp, err := c.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("request error: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response error: %w", err) } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("JSON parse error: %w", err) } if resp.StatusCode >= 400 { msg, _ := result["message"].(string) return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, msg) } return result, nil } ``` --- ## Step 2: Create Transaction **Endpoint:** `POST /api/v1/transaction/create` ### Required Fields | Field | Type | Description | | ----------------------------- | ------ | --------------------------------------------------------------------------- | | `ref` | string | Unique order reference (max 250 chars). Must be unique per merchant. | | `amount` | number | Amount in dollars with up to 2 decimal places (e.g., 10.00, NOT 1000 cents) | | `currency` | string | ISO 4217 currency code (e.g., "USD") | | `websiteUrl` | string | Your website URL (must be publicly accessible, no localhost) | | `redirectUrl` | string | Success redirect URL (must be publicly accessible) | | `cancelUrl` | string | Cancel redirect URL (must be publicly accessible) | | `description` | string | Transaction description (max 3000 chars) | | `customerDetails.firstName` | string | Customer first name | | `customerDetails.lastName` | string | Customer last name | | `customerDetails.email` | string | Customer email | | `customerDetails.country` | string | ISO 3166-1 alpha-2 country code | | `customerDetails.ip` | string | Customer's real IP address (NOT your server IP) | | `customerDetails.phoneCode` | string | ISO alpha-2 country code for phone | | `customerDetails.phoneNumber` | string | Customer phone number | ### Optional Fields | Field | Type | Description | | ------------------------ | ------ | ----------------------------------------------------------- | | `metadata` | json | Custom key-value data, returned in webhooks | | `paymentMethods` | json | Whitelist of payment method keys | | `paymentMethodsFilter` | json | Blacklist of payment method keys | | `paymentMethodsSorter` | json | Custom display order | | `preferredPaymentMethod` | string | Pre-select a payment method on checkout | | `products` | array | Line items: name, quantity, image, amount, description, url | | `billingDetails` | object | firstName, lastName, street, city, state, zipCode, country | | `shippingDetails` | object | Same structure as billingDetails | | `brandName` | string | Custom brand name on checkout | | `colors` | object | Custom checkout colors | | `logoSource` | string | URL to custom logo | | `expiresAt` | string | Expiration datetime (format: Y-m-d H:i:s, default: 3 hours) | ### PHP ```php $client = new EPaySeClient( 'https://sandbox-gateway.epayse.com', 'your-client-id', 'your-secret-key' ); $response = $client->request('POST', 'api/v1/transaction/create', [ 'ref' => 'ORDER-' . time(), 'amount' => 49.99, 'currency' => 'USD', 'websiteUrl' => 'https://yourstore.com', 'redirectUrl' => 'https://yourstore.com/payment/success', 'cancelUrl' => 'https://yourstore.com/payment/cancel', 'description' => 'Order #12345', 'customerDetails' => [ 'firstName' => 'John', 'lastName' => 'Doe', 'email' => 'john@example.com', 'country' => 'US', 'ip' => $_SERVER['REMOTE_ADDR'], 'phoneCode' => 'US', 'phoneNumber' => '5551234567', ], 'metadata' => ['orderId' => '12345', 'source' => 'web'], ]); // Redirect customer to checkout $checkoutUrl = 'https://sandbox-gateway.epayse.com' . $response['data']['checkoutUrl']; header("Location: $checkoutUrl"); exit; ``` ### Node.js ```javascript const client = new EPaySeClient( 'https://sandbox-gateway.epayse.com', 'your-client-id', 'your-secret-key' ); const response = await client.request('POST', 'api/v1/transaction/create', { ref: `ORDER-${Date.now()}`, amount: 49.99, currency: 'USD', websiteUrl: 'https://yourstore.com', redirectUrl: 'https://yourstore.com/payment/success', cancelUrl: 'https://yourstore.com/payment/cancel', description: 'Order #12345', customerDetails: { firstName: 'John', lastName: 'Doe', email: 'john@example.com', country: 'US', ip: req.ip, // Express.js: customer's real IP phoneCode: 'US', phoneNumber: '5551234567', }, metadata: { orderId: '12345', source: 'web' }, }); // Redirect customer to checkout const checkoutUrl = `https://sandbox-gateway.epayse.com${response.data.checkoutUrl}`; res.redirect(checkoutUrl); ``` ### Python ```python client = EPaySeClient( base_url="https://sandbox-gateway.epayse.com", client_id="your-client-id", secret_key="your-secret-key", ) response = client.request("POST", "api/v1/transaction/create", { "ref": f"ORDER-{int(time.time())}", "amount": 49.99, "currency": "USD", "websiteUrl": "https://yourstore.com", "redirectUrl": "https://yourstore.com/payment/success", "cancelUrl": "https://yourstore.com/payment/cancel", "description": "Order #12345", "customerDetails": { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "country": "US", "ip": request.remote_addr, # Flask: customer's real IP "phoneCode": "US", "phoneNumber": "5551234567", }, "metadata": {"orderId": "12345", "source": "web"}, }) # Redirect customer to checkout checkout_url = f"https://sandbox-gateway.epayse.com{response['data']['checkoutUrl']}" return redirect(checkout_url) ``` ### Go ```go client := epayse.NewClient( "https://sandbox-gateway.epayse.com", "your-client-id", "your-secret-key", ) response, err := client.Request("POST", "api/v1/transaction/create", map[string]interface{}{ "ref": fmt.Sprintf("ORDER-%d", time.Now().Unix()), "amount": 49.99, "currency": "USD", "websiteUrl": "https://yourstore.com", "redirectUrl": "https://yourstore.com/payment/success", "cancelUrl": "https://yourstore.com/payment/cancel", "description": "Order #12345", "customerDetails": map[string]string{ "firstName": "John", "lastName": "Doe", "email": "john@example.com", "country": "US", "ip": r.RemoteAddr, // net/http: customer's real IP "phoneCode": "US", "phoneNumber": "5551234567", }, "metadata": map[string]string{"orderId": "12345", "source": "web"}, }) if err != nil { log.Fatal(err) } // Redirect customer to checkout data := response["data"].(map[string]interface{}) checkoutUrl := "https://sandbox-gateway.epayse.com" + data["checkoutUrl"].(string) http.Redirect(w, r, checkoutUrl, http.StatusFound) ``` ### Success Response ```json { "status": "SUCCESS", "message": "Transaction created successfully", "data": { "transactionId": "01kd096yg112e40pcyfwsahyev", "checkoutUrl": "/checkout/01kd096yg112e40pcyfwsahyev?expires=..." } } ``` --- ## Step 3: Handle Checkout ### Redirect Mode (recommended) After creating a transaction, redirect the customer to the returned `checkoutUrl`: ``` https://sandbox-gateway.epayse.com/checkout/{transactionId}?expires=... ``` The customer completes payment on the EPaySe hosted checkout page, then is redirected back to your `redirectUrl` (success) or `cancelUrl` (cancel). ### Iframe Mode Use `POST /api/v1/transaction/create-with-iframe` (same request body). The response includes iframe embed data: ```json { "status": "SUCCESS", "data": { "transactionId": "01kd096yg112e40pcyfwsahyev", "checkoutUrl": "/checkout/01kd096yg112e40pcyfwsahyev?expires=...", "iframe": { "fullCheckoutUrl": "https://sandbox-gateway.epayse.com/checkout/01kd096yg112e40pcyfwsahyev?signature=...", "paymentMethods": [ { "key": "card_visa", "name": "Visa", "url": "https://sandbox-gateway.epayse.com/checkout-field/01kd096yg112e40pcyfwsahyev/card_visa?signature=...", "embedCode": "