KemoPay
Developer Docs

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. 1

    Create Payment

    Your backend calls POST /api/payments with the amount, currency, gateway, and your internal reference. Never call this from the browser.

  2. 2

    Display QR or Redirect

    Render qr_data as a QR image, or send the customer to redirect_url if the gateway returned one. Show a countdown using expires_at.

  3. 3

    Wait for Webhook

    After you configure a webhook URL, KemoPay notifies your server asynchronously whenever the payment status changes (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. 4

    Verify via API

    Before fulfilling the order, confirm the payment with GET /api/payments/{payment_id}/status from 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
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 own backend.

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_id for 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_url when 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.

Rule of thumb: Always trust the webhook over any frontend polling. The browser can lie; the webhook cannot.

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 status changes (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_id and 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 timestamp be the value of X-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-Signature with a constant-time comparison (e.g. hash_equals in PHP). Do not prefix with sha256=.
  • 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.

Tip: After verifying the signature, re-check the payment with 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 + "." + body does 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.