Accept your first payment in 10 minutes
This guide walks through the complete hosted-checkout payment flow from token request to webhook verification. You will need sandbox API credentials from the merchant portal.
https://api.payelecthq.com/pay/sandbox/api/v1. Replace with https://api.payelecthq.com/pay/api/v1 for production.
-
1
Get your sandbox API keys
Open the merchant portal, navigate to Developer → API Keys, and copy:
client_id— your public identifier (safe to log)client_secret— treat like a password; never expose to the browserwebhook_secret— used to verify incoming webhook signatures
Make sure the key mode is Sandbox. Sandbox keys only work against the sandbox URL.
-
2
Get an access token
Before creating a payment you must exchange your credentials for a short-lived access token (expires in 10 minutes).
POST /pay/sandbox/api/v1/authentication/tokencurl -X POST https://api.payelecthq.com/pay/sandbox/api/v1/authentication/token \ -H "Content-Type: application/json" \ -d '{ "client_id": "pe_sandbox_xxxxxxxxxxxxxxxx", "secret_id": "your_client_secret" }'<?php $response = (new \GuzzleHttp\Client())->post( 'https://api.payelecthq.com/pay/sandbox/api/v1/authentication/token', [ 'json' => [ 'client_id' => 'pe_sandbox_xxxxxxxxxxxxxxxx', 'secret_id' => 'your_client_secret', ], ] ); $body = json_decode($response->getBody(), true); $token = $body['data']['token']; // store for next stepconst res = await fetch( 'https://api.payelecthq.com/pay/sandbox/api/v1/authentication/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: 'pe_sandbox_xxxxxxxxxxxxxxxx', secret_id: 'your_client_secret', }), } ); const { data } = await res.json(); const accessToken = data.token; // expires in 10 minutesimport requests resp = requests.post( 'https://api.payelecthq.com/pay/sandbox/api/v1/authentication/token', json={ 'client_id': 'pe_sandbox_xxxxxxxxxxxxxxxx', 'secret_id': 'your_client_secret', }, ) resp.raise_for_status() access_token = resp.json()['data']['token']Successful response:
{ "status": "success", "data": { "token": "eyJhbGciOiJub25lIn0.eyJtZXJjaGFudF9pZCI6MTIzLCJtb2RlIjoic2FuZGJveCJ9", "expires_at": "2026-07-12T10:30:00Z", "mode": "sandbox" } } -
3
Create a payment
Send the access token as a Bearer header and provide the payment details. The response contains a
payment_url— redirect your customer there.POST /pay/sandbox/api/v1/payment/createcurl -X POST https://api.payelecthq.com/pay/sandbox/api/v1/payment/create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <access_token>" \ -d '{ "amount": "50000", "currency": "GNF", "country": "GN", "return_url": "https://your-store.com/payment/return", "cancel_url": "https://your-store.com/payment/cancel", "custom": "order_id:8821", "idempotency_key": "order-8821-attempt-1" }'<?php $response = (new \GuzzleHttp\Client())->post( 'https://api.payelecthq.com/pay/sandbox/api/v1/payment/create', [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'Content-Type' => 'application/json', ], 'json' => [ 'amount' => '50000', 'currency' => 'GNF', 'country' => 'GN', 'return_url' => 'https://your-store.com/payment/return', 'cancel_url' => 'https://your-store.com/payment/cancel', 'custom' => 'order_id:8821', 'idempotency_key' => 'order-8821-attempt-1', ], ] ); $body = json_decode($response->getBody(), true); $paymentUrl = $body['data']['payment_url']; // Redirect the user: header('Location: ' . $paymentUrl); exit;const res = await fetch( 'https://api.payelecthq.com/pay/sandbox/api/v1/payment/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ amount: '50000', currency: 'GNF', country: 'GN', return_url: 'https://your-store.com/payment/return', cancel_url: 'https://your-store.com/payment/cancel', custom: 'order_id:8821', idempotency_key: 'order-8821-attempt-1', }), } ); const { data } = await res.json(); window.location.href = data.payment_url; // redirect the customerimport requests resp = requests.post( 'https://api.payelecthq.com/pay/sandbox/api/v1/payment/create', headers={'Authorization': f'Bearer {access_token}'}, json={ 'amount': '50000', 'currency': 'GNF', 'country': 'GN', 'return_url': 'https://your-store.com/payment/return', 'cancel_url': 'https://your-store.com/payment/cancel', 'custom': 'order_id:8821', 'idempotency_key': 'order-8821-attempt-1', }, ) resp.raise_for_status() payment_url = resp.json()['data']['payment_url'] # return an HTTP 302 to payment_url in your web frameworkSuccessful response:
{ "status": "success", "data": { "token": "pymt_01J4ABCDEFGHIJKLMNOPQRSTU", "payment_url": "https://api.payelecthq.com/pay/sandbox/v1/user/authentication/form/pymt_01J4ABCDEFGHIJKLMNOPQRSTU", "sandbox_scenario": "success" } }Idempotency If you send the sameidempotency_keytwice (e.g. on a network retry), PayElect returns the original payment URL instead of creating a duplicate charge. Use a stable key like<order_id>-<attempt>. -
4
Redirect the customer to the payment_url
The
payment_urlis the PayElect-hosted checkout page. Your customer selects a payment provider (Orange Money, Wave, MTN MoMo, card, etc.), authenticates with their wallet, and completes the payment there.After the payment completes (success, failure, or cancellation), PayElect redirects the customer back to your
return_urlorcancel_urlwith result parameters appended as a query string:GET https://your-store.com/payment/return ?type=success &data[token]=pymt_01J4ABCDEFGHIJKLMNOPQRSTU &data[trx_id]=TRX0000012345678 &data[amount]=50000 &data[custom]=order_id%3A8821 &message[code]=200 &message[success][0]=SUCCESSNever trust the redirect params alone to confirm payment. A user could forge these. Always use the webhook (Step 5) or the status poll (Step 6) as the authoritative source. -
5
Handle the webhook
PayElect POSTs a signed JSON event to your webhook URL when the payment status changes. Register your webhook URL in the merchant portal under Developer → Webhooks.
The
payment.succeededevent looks like this:{ "eventId": "evt_01J4XYZABC123456789", "eventType": "payment.succeeded", "apiVersion": "2026-07-12", "createdAt": "2026-07-12T09:45:01Z", "environment": "sandbox", "data": { "trxId": "TRX0000012345678", "token": "pymt_01J4ABCDEFGHIJKLMNOPQRSTU", "amount": "50000.00000000", "currency": "GNF", "status": "SUCCESS", "custom": "order_id:8821", "merchantId": 42 } }Verify the HMAC-SHA256 signature before processing:
<?php $webhookSecret = 'your_webhook_secret'; $rawBody = file_get_contents('php://input'); $sigHeader = $_SERVER['HTTP_X_PAYELECTHQ_SIGNATURE'] ?? ''; // Header format: "t=1720773901,v1=abc123..." $parts = []; foreach (explode(',', $sigHeader) as $part) { [$k, $v] = explode('=', $part, 2); $parts[$k] = $v; } $timestamp = $parts['t'] ?? ''; $received = $parts['v1'] ?? ''; // Replay-attack guard: reject events older than 5 minutes if (abs(time() - (int) $timestamp) > 300) { http_response_code(400); exit('Timestamp too old'); } $signed = $timestamp . '.' . $rawBody; $expected = hash_hmac('sha256', $signed, $webhookSecret); if (!hash_equals($expected, $received)) { http_response_code(400); exit('Invalid signature'); } $event = json_decode($rawBody, true); // Deduplicate using eventId — store processed IDs in your DB if (isEventAlreadyProcessed($event['eventId'])) { http_response_code(200); exit('Already processed'); } // Always use data.status as source of truth if ($event['eventType'] === 'payment.succeeded' && $event['data']['status'] === 'SUCCESS') { fulfillOrder($event['data']['custom']); // e.g. parse "order_id:8821" markEventProcessed($event['eventId']); } http_response_code(200); echo 'OK';const crypto = require('crypto'); // Express middleware example app.post('/webhook/payelecthq', express.raw({ type: 'application/json' }), (req, res) => { const webhookSecret = process.env.PAYELECTHQ_WEBHOOK_SECRET; const sigHeader = req.headers['x-payelecthq-signature'] || ''; const rawBody = req.body; // Buffer const parts = Object.fromEntries( sigHeader.split(',').map(p => p.split('=')) ); const timestamp = parts['t'] || ''; const received = parts['v1'] || ''; // Replay-attack guard if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) { return res.status(400).send('Timestamp too old'); } const signed = `${timestamp}.${rawBody}`; const expected = crypto .createHmac('sha256', webhookSecret) .update(signed) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return res.status(400).send('Invalid signature'); } const event = JSON.parse(rawBody); if (event.eventType === 'payment.succeeded' && event.data.status === 'SUCCESS') { fulfillOrder(event.data.custom); } res.status(200).send('OK'); });import hmac import hashlib import time import json from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = 'your_webhook_secret' @app.route('/webhook/payelecthq', methods=['POST']) def webhook(): sig_header = request.headers.get('X-PayElectHQ-Signature', '') raw_body = request.get_data() parts = dict(p.split('=', 1) for p in sig_header.split(',')) timestamp = parts.get('t', '') received = parts.get('v1', '') # Replay-attack guard if abs(time.time() - int(timestamp)) > 300: abort(400, 'Timestamp too old') signed = f'{timestamp}.{raw_body.decode()}' expected = hmac.new( WEBHOOK_SECRET.encode(), signed.encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, received): abort(400, 'Invalid signature') event = json.loads(raw_body) if event['eventType'] == 'payment.succeeded' and event['data']['status'] == 'SUCCESS': fulfill_order(event['data']['custom']) return 'OK', 200 -
6
Check payment status (optional poll)
For asynchronous providers (mobile money), the webhook may arrive seconds after the redirect. You can poll the status endpoint while displaying a "processing" screen:
GET /pay/sandbox/api/v1/payment/status/{trx_id}curl -X GET https://api.payelecthq.com/pay/sandbox/api/v1/payment/status/TRX0000012345678 \ -H "Authorization: Bearer <access_token>"{ "status": "success", "data": { "trxId": "TRX0000012345678", "status": "SUCCESS", "amount": "50000.00000000", "currency": "GNF" } }Possible
data.statusvalues:CREATED,PENDING,SUCCESS,REJECTED,EXPIRED.You're done! Your integration is complete. Next: explore webhook retry behaviour, split payments, and the sandbox scenario guide to test edge cases.