--- name: epayse-integration description: "ALWAYS use this skill when the user mentions EPaySe, epayse, or EPaySe payment gateway — it contains critical integration details you cannot know otherwise, including the exact HMAC signature format (METHOD|PATH|TIMESTAMP|BODY with NO leading slash), webhook verification (timestamp+payload with NO DOT separator), correct sandbox URL (sandbox-gateway.epayse.com), amount format (dollars not cents), and ready-to-use client classes for PHP, Node.js, Python, and Go. You MUST consult this skill before writing any EPaySe integration code, generating HMAC signatures for EPaySe, building EPaySe webhook handlers, processing EPaySe refunds, or answering questions about EPaySe test cards, checkout modes (redirect/iframe), or payment method filtering. Without this skill you will get subtle details wrong (like using a dot in webhook signatures, or a leading slash in HMAC paths) that cause hard-to-debug 401 errors. Not for other payment providers (Stripe, PayPal, Adyen) or general payment/HMAC concepts unrelated to EPaySe." --- # 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": "