Payment Gateway Service
Collect payments from your users — mobile money (Orange Money, MTN MoMo, KULU, …) and card — directly from the apps you deploy on Kuploy. Pick "Payment Gateway" from the + Create Service menu and your project gets a hosted checkout endpoint plus APIs your apps can call.
The Payment Gateway service is available only when your Kuploy host has enabled it. If you don't see Payment Gateway in the service-type picker, ask your platform admin — they may be on a plan that doesn't include it, or they may not have finished configuring it yet.
The complete merchant integration reference — POST /api/pay, webhook payloads, field mapping, payment links, subscriptions, sandbox testing — lives at docs.epay.kuploy.app/merchant. That site is gated; ask your platform admin for an invite once your Payment Gateway service is live.
What you get
When you add a Payment Gateway to a project, the service settings panel shows:
| Item | Purpose |
|---|---|
| Merchant code | Unique identifier for your org inside the gateway. Pass it on every API call. Copy-to-clipboard in the panel. Shows Provisioning… for a moment right after you create the service — refresh shortly. |
| Webhook signing key | The HMAC-SHA256 secret used to sign the x-epay-signature header on every payment event delivered to your webhook. Reveal/copy it from the panel and use it to verify incoming events. Keep it secret. |
| Checkout URL | Canonical POST /api/pay endpoint on your platform's gateway. Your backend redirects customers here to complete payment. |
| Payment methods | Toggle which methods are enabled for your merchant (Orange Money, MoMo, Card, KULU, …). The list is driven by whatever your platform admin has configured on the gateway — so new methods appear automatically as your platform adds them. |
| Webhook URL | Where payment events for your app are delivered. Editable any time. |
You configure the details inside the service settings panel — no platform-admin help required once the service is created.
Your organization has a single merchant identity. If you add the Payment Gateway service to more than one project, they all share the same merchant code and signing key — payment events for the org are delivered to every webhook URL you've configured across those services.
Create a Payment Gateway service
- Open your project in the Kuploy dashboard.
- Click + Create Service and choose Payment Gateway.
- Give the service a descriptive name (shown in dashboards; usually the name of the app it will serve).
- Pick the payment methods you want to enable. You can change this later.
- Optionally set a webhook URL — an endpoint on one of your deployed apps that the gateway can POST to when something happens (payment succeeded, subscription renewed, etc.).
- Click Create. Your platform provisions a merchant identity for your org and wires everything up. The merchant code may show Provisioning… for a few moments on first creation — refresh the panel and it appears.
Accepting a one-off payment
From your app's backend, redirect the customer to the hosted checkout:
POST https://<gateway-host>/api/pay?merchantCode=<your-merchant-code>
amount=1000
currency=XOF
reference=order-12345
returnUrl=https://yourapp.example/thanks
The gateway shows a counter, the customer picks a method (Orange Money, MoMo, Card, …), and on success your app is notified through the webhook you configured above. Full field reference in the epay-gateway payment integration docs.
Always pass your own reference (an order ID, invoice ID, whatever anchors the payment in your own system). It comes back on every webhook and lets your app match payments to records without parsing anything brittle.
Recurring subscriptions
The service also supports recurring billing — useful if your app has paid tiers of its own. From your backend:
- Create a plan with a price and a billing interval via the service's API.
- On sign-up, call the subscription API with the customer's identifier and your plan ID.
- React to
customer.subscription.*andinvoice.*events at your webhook endpoint.
The management API shape — including the full subscription-lifecycle events — is documented at docs.epay.kuploy.app/merchant/subscriptions. Access is invite-only; ask your platform admin if you need credentials.
Reacting to webhook events
The gateway emits Stripe-shaped events so the same handling code works for both native card subscriptions and mobile-money subscriptions. The events you'll usually care about:
| Event | Meaning for your app |
|---|---|
customer.subscription.created | New subscription is live; unlock the paid features. |
customer.subscription.updated | Plan change, renewal, or status flip — re-check what the customer can access. |
customer.subscription.deleted | Subscription was canceled; revoke paid features at end of period. |
invoice.payment_succeeded | Money landed; clear any past-due state on your side. |
invoice.payment_failed | Payment failed; decide whether to dunning/email the customer. |
Every event carries metadata.externalReference — set that to your own customer or subscription ID when you create the subscription, then you can ignore the rest of the payload if you want.
Verifying event signatures
Every event delivered to your webhook carries an x-epay-signature header: a lowercase-hex HMAC-SHA256 of the exact raw request body, keyed with your Webhook signing key (the secret shown in the service panel). There is no timestamp envelope.
Verify it before trusting any payload — compute the HMAC over the raw body and compare in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signatureHeader, signingKey) {
const expected = createHmac("sha256", signingKey).update(rawBody).digest("hex");
if (!signatureHeader || expected.length !== signatureHeader.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Compute the HMAC over the unparsed request body — re-serializing parsed JSON can change bytes (key order, spacing) and break the check. Read the raw body first, verify, then parse. Reject any request whose signature doesn't match.
Kuploy receives each event from the gateway, verifies it, and relays it verbatim (same body, same x-epay-signature) to the webhook URL(s) you configured. Because the signature is keyed with your signing key, you verify it directly — exactly as if the gateway delivered to you.
The Webhook URL in your service settings is the delivery address you own, and the only one you need to set. You don't register an endpoint with the gateway yourself; that side is configured for you.
Your endpoint is called once per event, with a 10-second timeout, and a call that fails is not retried. A missed event is gone, not queued.
This is the single most important thing to build around — see Reconciling payments.
Reconciling payments
Treat the webhook as the fast path, never the only one.
If your app is redeploying, restarting, briefly unreachable, or just slow to answer, that event is lost. Nothing retries it. The payment itself still succeeded — the customer was charged and the gateway recorded it; only your notification went missing.
The consequence worth internalising: an order still marked unpaid in your database is not evidence that the customer didn't pay.
So build the slow path alongside the fast one:
- Record the order as pending when you start the payment, keyed by the
referenceyou passed to the checkout. - Settle it from the webhook when the event arrives. This is the common case and it is immediate.
- Poll for anything still pending — on a schedule, and again whenever a customer opens the order. Ask the gateway for that payment's current status and settle from the answer. The status call is in the merchant reference.
- Make both paths idempotent. They will race: an event can arrive while
your poll is in flight. Settling the same
referencetwice must be a no-op, not a second fulfilment or a second receipt.
Without step 3, every lost event is an order that stays pending until a human notices — usually the customer, who has been charged and has nothing to show for it.
A "payment failed, please retry" shown off the back of a missing webhook is a double charge waiting to happen. Check the payment's real status first.
Managing payment methods
In the service settings, the Payment Methods section lists every method your platform supports. Toggle individual methods on or off per merchant — useful when you want to, say, launch with Orange Money and MoMo first, then enable cards later.
Changes take effect on the next payment: already-initiated checkouts keep their original method list.
Tearing down
To remove the Payment Gateway service from a project, open its settings and click Delete Service. The local binding is cleared immediately. Because your org has a single shared merchant, it is only released once you remove your last Payment Gateway service — until then, your other services keep using the same merchant code and signing key. Any in-flight payments or active subscriptions tied to the merchant stop when it's released — migrate customers first if you have active recurring billing.
Known limitations
These pieces exist conceptually but aren't fully surfaced in the UI yet. The integration paths above all work around them today.
- Invoice PDFs — payment history renders as a transaction list. Downloadable PDF invoices (with line items, tax breakdown, hosted receipt URL) land when your payment gateway's invoice engine ships — at that point they appear automatically in the service settings without any code change on your side.
- Self-serve customer portal for your end-users — today, your app is responsible for its own "update card" / "cancel subscription" UI by calling the gateway's management API. A hosted portal page your customers can visit directly is being built on the payment gateway; when live, it'll be available per merchant at a URL the service panel surfaces.
- Refunds, disputes, tax — not yet exposed through the gateway's management API. If you need one of these before it ships, contact your platform admin — they can act on your behalf from the gateway admin console.
For the full, current state of what the payment gateway itself supports, see the canonical reference at docs.epay.kuploy.app/merchant. Access is invite-only; ask your platform admin.
Common pitfalls
- "Payment Gateway" doesn't appear in + Create Service — your host either doesn't offer the feature on their plan yet, or the platform admin hasn't finished configuring it. Ask them.
- Checkout URL returns an error — double-check the
merchantCodequery parameter matches exactly what the service settings panel shows; codes are case-sensitive. - No webhook hits — set a Webhook URL in the service settings (events are only relayed once it's configured), and make sure it's reachable from the public internet. Test it with
curlfrom outside your network. Internal, loopback and private-network addresses (localhost,10.x,192.168.x,172.16–31.x,169.254.x,*.internal,*.local, …) are refused for safety — and a refused target counts as a failed delivery, which per Reconciling payments is not retried. A cluster-internal service name will never work here, however reachable it looks from inside your app.https://is strongly preferred: the body describes a payment. - Signature check fails — verify against the raw request body using the Webhook signing key from the panel (not the merchant code), as shown in Verifying event signatures.