KemoPay API Documentation
Integrate payments using KemoPay APIs. This is the public reference for endpoints, payment flow, webhooks, and security best practices.
Overview
- KemoPay is a payment orchestration platform that lets you accept payments through multiple rails with a single integration.
- Dynamic QR is the primary processing rail today. Additional rails may be enabled per merchant.
- Authenticate merchant API calls with a Bearer token issued from your merchant dashboard. Keep this token server-side only.
- After you configure a callback URL, final payment status updates are delivered by webhook. Treat that webhook as the source of truth.
Base URL
All production API calls are made against the KemoPay production host. Prepend this base URL to every endpoint shown below.
https://pay.kemofi.com
Marketing site: KemoPay
Payment Flow
Every payment moves through these four steps. Implement them in this exact order.
-
1
Create Payment
Your backend calls
POST /api/paymentswith the amount, currency, gateway, and your internal reference. Never call this from the browser. -
2
Display QR or Redirect
Render
qr_dataas a QR image, or send the customer toredirect_urlif the gateway returned one. Show a countdown usingexpires_at. -
3
Wait for Webhook
After you configure a webhook URL, KemoPay notifies your server asynchronously whenever the payment
statuschanges (for example pending → paid). This callback is the source of truth — not the browser.Do NOT trust any status observed in the browser. The tab can be closed, reloaded, or tampered with. Only the webhook (or a backend status check) is authoritative. -
4
Verify via API
Before fulfilling the order, confirm the payment with
GET /api/payments/{payment_id}/statusfrom your backend. Use this as a fallback when webhooks are delayed and as a final guard at checkout.
Authentication
Every merchant API call must include your API key as a Bearer token. Your API key is issued privately in your merchant dashboard — never hard-code or share it.
Authorization: Bearer YOUR_API_KEY
Get Enabled Gateways
Returns the payment options currently enabled for your merchant account. Use it to render only the methods the customer can actually pay with.
Sample Request
GET /api/gateways/enabled HTTP/1.1 Authorization: Bearer YOUR_API_KEY Accept: application/json
Sample Response
{
"success": true,
"data": {
"gateways": [
{ "code": "gcash", "name": "GCash" },
{ "code": "maya", "name": "Maya" },
{ "code": "grab_pay", "name": "GrabPay" },
{ "code": "shopeepay", "name": "ShopeePay" },
{ "code": "qrph", "name": "QR Ph" }
],
"count": 5
},
"error": null
}
Create Payment
Creates a new payment and returns the data you need to collect funds (QR or redirect URL).
Sample Request
POST /api/payments HTTP/1.1 Authorization: Bearer YOUR_API_KEY Accept: application/json Content-Type: application/json
JSON Payload
{
"amount": 500.00,
"currency": "PHP",
"gateway": "gcash",
"reference": "ORDER-20260228-0001"
}
cURL Example
curl -X POST https://pay.kemofi.com/api/payments \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"amount":500.00,"currency":"PHP","gateway":"gcash","reference":"ORDER-20260228-0001"}'
Sample Response
{
"success": true,
"data": {
"payment_id": "uuid-value",
"transaction_id": "uuid-value",
"gateway": "gcash",
"amount": 500,
"currency": "PHP",
"gross_amount": 500,
"gatewayhub_platform_fee_percent": 1.5,
"gatewayhub_platform_fee": null,
"gatewayhub_net_amount": null,
"status": "pending",
"qr_data": "000201...",
"expires_at": "2026-02-28T12:00:00+08:00",
"redirect_url": null,
"checkout_url": null,
"merchant": {
"name": "Your business name",
"logo": "https://...",
"theme_color": "#1D4ED8"
}
},
"error": null
}
Response Field Reference
- payment_id
- Unique identifier for this payment. Store it against your order and use it for every status check or webhook match.
- transaction_id
- Same value as
payment_idfor compatibility with clients that expect a transaction identifier. - qr_data
- QR value returned by the gateway. It can be an EMVCo payload string that your app must encode, or a data URI image that can be used directly as an image source. Do not modify the value.
- expires_at
- ISO-8601 timestamp of when the QR or redirect session expires. After this, the payment cannot be completed.
- redirect_url
- If the gateway returns a hosted checkout page or wallet deep link, send the customer here. When null, use qr_data.
- checkout_url
- Same as
redirect_urlwhen present; otherwise null. - merchant
- Branding for hosted checkout: display name, logo URL, and theme color.
- status
- Current payment state. On creation this is always
pending. Final states arrive via webhook. - gross_amount
- The original amount before the KemoPay platform fee.
- gatewayhub_platform_fee_percent
- The KemoPay platform fee rate: 1.5% of gross_amount.
- gatewayhub_platform_fee
- The KemoPay deduction, rounded to two decimals. It is null until the payment is paid and fee processing is complete. Provider fees are not included.
- gatewayhub_net_amount
- gross_amount minus gatewayhub_platform_fee. It is null until the payment is paid and fee processing is complete.
The fee fields retain their existing JSON names for API compatibility. They represent KemoPay platform fees.
Get Payment Status
Fetch the current status of a payment from your backend. Call this before fulfilling an order, or as a fallback when a webhook is delayed.
Sample Request
GET /api/payments/{payment_id}/status HTTP/1.1 Authorization: Bearer YOUR_API_KEY Accept: application/json
cURL Example
curl -X GET https://pay.kemofi.com/api/payments/{payment_id}/status \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json"
Sample Response
{
"success": true,
"data": {
"status": "success",
"gross_amount": 500,
"gatewayhub_platform_fee_percent": 1.5,
"gatewayhub_platform_fee": 7.5,
"gatewayhub_net_amount": 492.5
},
"error": null
}
Merchant Wallet
Read your KemoPay wallet balance, net payment receipts, and send available funds to a bank or e-wallet. Each paid payment is credited after the KemoPay platform fee is deducted.
Get Wallet Balance
GET /api/wallets/balances?currency=PHP HTTP/1.1 Authorization: Bearer YOUR_API_KEY
Sample Balance Response
{
"success": true,
"data": {
"currency": "PHP",
"wallet_balance": 492.50,
"available_balance": 492.50,
"settled_balance": 0,
"pending_net_settlement": 492.50,
"today_gross": 500,
"today_platform_fees": 7.50
},
"error": null
}
List Wallet Receipts
Returns the latest net payment credits. Use currency to filter by currency and per_page to set a page size from 1 to 100 (default 20).
GET /api/wallets/transactions?currency=PHP&per_page=20 HTTP/1.1 Authorization: Bearer YOUR_API_KEY
Sample Receipt
{
"reference": "ORDER-20260228-0001",
"status": "paid",
"currency": "PHP",
"gross_amount": 500,
"platform_fee": 7.50,
"net_amount": 492.50,
"wallet_status": "available"
}
Get Supported Banks and E-Wallets
Use PayMongo's receiving institutions list to find the destination BIC for InstaPay or PESONet.
GET /api/wallets/payout-institutions?provider=instapay HTTP/1.1 Authorization: Bearer YOUR_API_KEY
Create an External Payout
The payout amount is deducted from your available net wallet balance. Each request needs a unique Idempotency-Key. InstaPay supports up to PHP 50,000 per transfer; use PESONet for larger amounts, up to PHP 10,000,000.
POST /api/wallets/payouts HTTP/1.1 Authorization: Bearer YOUR_API_KEY Idempotency-Key: order-payout-1001 Content-Type: application/json { "provider": "instapay", "amount": 492.50, "currency": "PHP", "destination_account": { "number": "1234567890", "name": "Juan Dela Cruz", "bic": "BNORPHMM", "bank_name": "Test Bank" } }
Payouts return submitting, pending, succeeded, or failed. A pending payout is still being processed; do not submit it again with a different key.
List Payouts and Check Status
GET /api/wallets/payouts?currency=PHP&per_page=20 HTTP/1.1 Authorization: Bearer YOUR_API_KEY GET /api/wallets/payouts/{payout_id} HTTP/1.1 Authorization: Bearer YOUR_API_KEY
Payouts use KemoPay's activated PayMongo wallet. Keep the PayMongo secret key and merchant API key on your server. Configure PayMongo to send transfer.outward.successful and transfer.outward.failed events to KemoPay so final statuses are recorded.
Keep the merchant API key on your server. Do not call wallet endpoints directly from browser or mobile client code.
Webhook Handling
A webhook is a server-to-server notification. When a payment status changes, KemoPay sends an HTTP POST to your endpoint so you do not need to poll.
Delivery
Register your HTTPS endpoint and signing secret on the API Credentials page. KemoPay delivers webhooks with these headers:
POST https://your-server.example.com/your-webhook-endpoint HTTP/1.1 Content-Type: application/json User-Agent: KemoPay-Webhooks/1.0 X-Merchant-Timestamp: 1700000000 X-Merchant-Signature: <hex-hmac-sha256>
How It Works
- KemoPay sends a POST whenever a payment’s
statuschanges (not only when it becomes paid). - Deliveries are asynchronous — usually within seconds, but network delays can happen.
- Your endpoint must respond with HTTP 200 quickly (under ~5 seconds). Do heavy work in a queue.
- If your endpoint errors or times out, KemoPay retries with backoff.
- Duplicate deliveries are possible. Make your handler idempotent by keying on
payment_idand ignoring already-applied updates.
Example Payload
{
"event": "payment.updated",
"timestamp": "2026-02-28T12:00:00+08:00",
"data": {
"payment_id": "uuid-value",
"status": "paid",
"amount": 500,
"currency": "PHP",
"gateway": "gcash",
"reference": "ORDER-20260228-0001",
"gross_amount": 500,
"gatewayhub_platform_fee_percent": 1.5,
"gatewayhub_platform_fee": 7.5,
"gatewayhub_net_amount": 492.5,
"provider_reference": "provider-ref-optional",
"paid_at": "2026-02-28T12:01:42+08:00",
"created_at": "2026-02-28T12:00:10+08:00",
"updated_at": "2026-02-28T12:01:42+08:00"
}
}
The fee fields retain their existing JSON names for API compatibility. They represent the KemoPay 1.5% platform fee and exclude provider processing or conversion fees.
Signature Verification
Each delivery is signed with the same secret you configure for webhooks so you can verify it came from KemoPay.
- Use the raw request body bytes exactly as received (no re-encoding or whitespace changes).
- Let
timestampbe the value ofX-Merchant-Timestamp(Unix time as a decimal string). - Compute the hex digest
hash_hmac('sha256', timestamp + '.' + body, your_secret)— the signing input is the timestamp, a single dot, then the raw JSON body. - Compare that digest to
X-Merchant-Signaturewith a constant-time comparison (e.g.hash_equalsin PHP). Do not prefix withsha256=. - If they do not match, return 401 and do not act on the payload.
- Optionally reject requests whose timestamp is too far from your server clock to limit replay windows.
PHP Receiver Example
Save this as your HTTPS endpoint, set KEMOPAY_WEBHOOK_SECRET in your server environment, and add your own idempotent order update where indicated.
<?php
$secret = getenv('KEMOPAY_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_MERCHANT_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_MERCHANT_SIGNATURE'] ?? '';
if (! is_string($secret) || $secret === '' || ! is_string($rawBody)
|| ! is_string($timestamp) || ! ctype_digit($timestamp)
|| ! is_string($signature) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
try {
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
http_response_code(400);
exit;
}
$data = is_array($event) ? ($event['data'] ?? null) : null;
if (! is_array($event)
|| ($event['event'] ?? null) !== 'payment.updated'
|| ! is_array($data)
|| ! is_string($data['payment_id'] ?? null)
|| ! is_string($data['status'] ?? null)) {
http_response_code(400);
exit;
}
// Implement this in your application: persist idempotently by payment_id and status,
// then queue fulfillment only for "paid" using payment_id as the idempotency key.
processPaymentUpdateIdempotently($data);
http_response_code(200);
echo 'ok';
Define processPaymentUpdateIdempotently() in your application before enabling this endpoint. Return a 2xx response only after recording the update; webhooks may be delivered more than once.
GET /api/payments/{payment_id}/status before releasing goods — this guards against replayed or spoofed payloads.
Security Notes
- Never expose your API key in the frontend. Do not embed it in JavaScript, mobile apps, or public repositories. All merchant API calls must originate from your backend.
-
Always verify payments from your backend.
Before shipping, granting credits, or unlocking a service, confirm the status with
GET /api/payments/{payment_id}/status. - Do not trust client-side status. A success screen in the browser is not proof of payment. The customer may have closed the tab, faked the redirect, or manipulated local state.
-
Verify webhook signatures on every request.
Reject any payload whose HMAC of
timestamp + "." + bodydoes not match. Generate a new signing secret on the API Credentials page if you suspect the old one leaked. - Use HTTPS everywhere. Your webhook endpoint and your checkout pages must be served over TLS.
- Keep handlers idempotent. The same webhook may be delivered more than once. Apply the update only if the payment is not already in that state.
Errors
All API errors use a consistent response envelope.
Error Response Structure
{
"success": false,
"data": [],
"error": "Human-readable error message"
}
Common Status Codes
- 401 — Unauthenticated or invalid API key.
- 403 — Merchant account is inactive or gateway is not allowed.
- 404 — Resource not found.
- 422 — Validation error in request payload.
- 429 — Rate limit exceeded.