The Partner Payment Gateway allows you to collect payments from your users directly, using your own payment methods. When enabled, a payment button with your system's name appears in myaccount on purchase and top-up pages. You handle all money collection; we handle product catalog, eSIM provisioning, and account management.
This guide describes everything you need to build for the integration to work.
What you need to implement
You must build three integration elements and provide one set of access keys:
| # | Element | What it does | Covered in |
|---|---|---|---|
| 1 | Order intake endpoint | Receives order details from us; returns a payment URL | Section 1 |
| 2 | Payment page | Where your user actually pays | Section 2 |
| 3 | Payment notification | Tells us the payment is done; triggers provisioning | Section 3 |
| 4 | Access keys | Authenticate requests on both sides | Section 4 |
Everything else — the purchase UI, product catalog, eSIM provisioning, transaction history, account balance — stays on our side. You do not need to build any of that.
1. Order intake endpoint
What it is
Build an HTTPS endpoint on your side that we can call when the user clicks the payment button. We will POST order data to it; you respond with a URL where we redirect the user to pay.
What we send you
| Field | Required | Purpose |
|---|---|---|
| Order ID | yes | The key linking the payment to the order. You must return this unchanged in the payment notification — it is the only way we can provision the service |
| Amount to charge | yes | Final amount to bill the user, already including any coupon discount |
| Currency | yes | Currency of the amount |
| Bundle (product) ID | yes | What the user is buying |
| Purchase type | yes | "Line creation" or "top-up" |
| User email | yes | For your receipt or invoice |
| User ID | yes | Links the order to the user on our side |
| Account ID | yes | Links the order to the account on our side |
| Line number (ICCID) | Top-ups only | The line being topped up. May be absent on new line purchases — see note below |
| Data volume and plan validity | yes | Unambiguous description of the purchased plan |
| Coupon code | If applied | Informational; the amount already reflects the discount |
| User return URL | yes | Where to redirect the user after payment |
| Order service reference | yes | You may use this in your own accounting |
Important: You must save all of these fields and return them unchanged in the payment notification (Section 3). We do not store a draft order on our side — your notification is the only source of truth for what to provision.
What you return
| Field | Required | Notes |
|---|---|---|
| Payment page URL | yes | Full URL, openable in the user's browser. The user arrives directly at the payment page — no extra authentication required |
| Your internal order ID | optional | We do not use it |
Requirements for your endpoint
- Respond within 10 seconds. The user is waiting with a loading indicator. No response = the user sees an error and does not receive the service.
- Do not change the amount. Charge exactly what we sent, in the currency we sent.
- Return errors explicitly. If you cannot accept the order, return a proper error response — not an empty or invalid URL.
- Communicate provider limits in advance. Minimum/maximum amounts, supported currencies, country restrictions — tell us before launch so we can filter orders on our side.
- Treat each call as a separate order. We may call your endpoint multiple times for the same user. Do not merge calls.
- Limit the validity of the payment link — recommended 30 minutes to a few hours.
- Do not report abandoned payments. If the user did not pay, send us nothing. We will close the order on our own.
Note on the line number (ICCID)
For top-ups, the line exists and we send its number. For new line purchases, the line may not exist yet at the moment of payment — it is created after we receive your notification. Therefore:
- Use the Order ID as the key linking payment to order, not the ICCID.
- Do not show the ICCID to your user on the payment page or receipt.
- Do not treat the ICCID as a required field in your database.
2. Payment page
Build a page where the user pays. The payment method is entirely your choice — cards, local payment systems, bank transfer, cash, wallet balance, manual staff confirmation. We have no requirements on how payment is collected.
The page must show:
- The amount and currency exactly as we sent
- A clear description of what the user is purchasing
- An option to cancel and go back
After the user pays, redirect them to the user return URL we sent in the order. That page shows a "payment successful" screen in myaccount. It does not confirm the payment or trigger provisioning — the notification (Section 3) does that.
Manual confirmation is allowed. A staff member can confirm payments manually. From our side, there is no difference — provisioning happens when we receive the notification, regardless of how it was generated. Account for the delay in user-facing messaging.
3. Payment notification
This is the most important part of the integration. Sending this notification is what triggers line creation or top-up and deducts the cost from your balance.
When to send it
Send the notification only after you have confirmed the payment is final and funds are settled — not at authorization, not at the start of payment processing.
What to include
| Field | Required | Notes |
|---|---|---|
| Payment success flag | yes | We only process notifications with an explicit success flag |
| Order ID | yes | Must match exactly what we sent you in Section 1 |
| Unique payment ID | yes | Becomes the transaction ID in myaccount. Must stay the same if you resend the notification for the same payment |
| Paid amount and currency | yes | Shown in the user's transaction |
| All order data from Section 1 | yes | Returned unchanged |
Order ID and payment ID are different fields. Order ID = what was paid for (we issued it). Payment ID = which payment was used (you issue it).
Requirements
Return all order data unchanged. We execute the purchase or top-up exactly according to what you send. A modified Order ID means we cannot provision the service. Modified data volume or plan validity means the user receives the wrong plan. Modified ICCID on a top-up means the service goes to the wrong line. The safest approach: save the full data set you received in Section 1 and return it as-is, without reassembling anything.
Use the same payment ID on resends. If you send a notification more than once for the same payment, the ID must be identical. A new ID = we treat it as a new payment = the user receives the service twice and you are charged twice.
Implement retry logic. If we do not confirm receipt, resend the notification with the same payment ID. Keep retrying until you receive a success response from us. Recommended schedule: after 1, 5, 15, 60 minutes — for at least 24 hours. We do not initiate retries from our side.
Parse the response body to determine success, not just the HTTP code. In the current implementation, some failures are returned with a 200 code. See Section 5 for the full response reference.
4. Access keys and security
The authorization method and key values are agreed individually. The general scheme:
| Direction | Who authenticates | How |
|---|---|---|
| We → you (order intake) | You verify the request is from us | An access key you issue to us; we attach it to every request |
| You → us (payment notification) | We verify the request is from you | A key we agree with you; you attach it to every notification |
Additional requirements:
- Both endpoints must use HTTPS with a valid certificate.
- If your infrastructure uses fixed IP addresses, we can restrict notification acceptance to those addresses. Tell us before launch if your addresses change dynamically.
- Keys must be shared over a secure channel — not in open messages or tickets.
- Plan for key rotation: both sides must be able to replace a key without interrupting payment processing.
5. How we respond to your notification
| Scenario | Code | Response body | Should you retry? |
|---|---|---|---|
| Processed, service provisioned | 200 | {"headers":{},"original":{"status":"success","message":"The webhook was processed"},"exception":null} |
No |
| Duplicate — already processed | 200 | "Webhook has been processed" |
No |
| Payment flag is not "success" | 200 | {"headers":{},"original":{"message":"The webhook was not processed. Reason: <status>"},...} |
No |
| Authorization failed | 200 | "Unauthorized IP address or private key incorrect" |
No — fix the access issue first |
| Processing error on our side | 400 | "The webhook was not processed." |
Yes |
| Timeout or 5xx | — | — | Yes |
Retry rule:
| What you received | Action |
|---|---|
| No response, or code is not 200 | Retry with the same payment ID |
Code 200, body contains "status":"success" |
Accepted — do not retry |
Code 200, body is "Webhook has been processed" |
Already accepted — do not retry |
Code 200, body contains "Reason:" or "Unauthorized" |
Do not retry — investigate the cause |
Parse by the substring "status":"success" rather than by strict JSON structure — the response format may be updated, and we will notify you in advance of any changes.
6. Testing checklist
Run these checks in the test environment before going live:
| # | Test | Expected result |
|---|---|---|
| 1 | Payment button on purchase and top-up pages | Button visible with your payment system name |
| 2 | Click button — new line purchase | User reaches your payment page; amount and currency match |
| 3 | Click button — top-up | Same; line number in the order matches the selected line |
| 4 | Purchase with coupon applied | Amount on your payment page reflects the discount |
| 5 | Successful payment — new line | Notification delivered, line created, your balance reduced |
| 6 | Successful payment — top-up | Notification delivered, plan added to the line, your balance reduced |
| 7 | Send duplicate notification | Service provisioned once, single deduction, success response |
| 8 | Notification with wrong authorization key | Rejected, service not provisioned |
| 9 | User abandons payment | Service not provisioned, reservation released |
| 10 | Our side unavailable during notification | You retry; after recovery, service provisioned once |
| 11 | User return after payment | User sees payment success screen in myaccount |
| 12 | Transaction reconciliation | Payment IDs in your system and in myaccount match |
7. Launch readiness — what you provide us
Before going live, send us:
- Order intake endpoint address (test and production)
- Access key for your order intake endpoint (test and production)
- Your payment system name (for the button label)
- List of supported currencies
- Minimum and maximum payment amount, if any limits apply
- List of IP addresses from which you will send notifications (if IP restriction applies)
- Technical support contact for disputed payment resolution
Three things that most often break the integration
- Return all order data unchanged; use Order ID as the key. We have no stored draft order — what you return is what we provision. The line number (ICCID) cannot be the key: for new line purchases, it may not exist at the time of payment.
- Use the same payment ID when resending. A new ID on a resend = a second charge and a second provisioned service.
- Retry the notification until you receive a success response. We never query you to check payment status — the notification is the only trigger for provisioning.
Appendix: Request examples (current implementation)
Order intake request (we send to you)
POST https://api.partner.example/payments/create
Authorization: Bearer <agreed token>
Content-Type: application/json
New line purchase:
{
"amount": 48500,
"currency": "NGN",
"marchant_reference": "a7Kx9",
"customer_email": "user@example.com",
"product_id": 1427,
"callback_url": "https://my.partner.example/payment/{gateway-id}/return/process",
"metadata": {
"iccid": "8944501234567890123",
"refill_mb": 5120,
"refill_days": 30,
"user_id": 883421,
"account_id": 40219,
"refill_type": "esimCreate",
"product_id": 1427,
"discount_coupon": null
}
}
Note: marchant_reference is spelled exactly as shown — this is part of the current contract, not a typo to correct. metadata.iccid on an esimCreate order is a pre-reserved inventory line; do not show it to the user and do not use it as your order key.
Your response:
{ "Checkout": "https://pay.partner.example/session/8f2a41c0-..." }
Checkout is case-sensitive. Return an error response (not an empty field) if you cannot process the order.
Top-up of an existing line:
{
"amount": 24.5,
"currency": "USD",
"marchant_reference": "Qm3fB",
"customer_email": "user@example.com",
"product_id": 1427,
"callback_url": "https://my.partner.example/payment/{gateway-id}/return/process",
"metadata": {
"iccid": "8944501234567890123",
"refill_mb": 10240,
"refill_days": 30,
"user_id": 883421,
"account_id": 40219,
"refill_type": "Refill",
"product_id": 1427,
"discount_coupon": "c9f21ab4e7"
}
}
Field reference:
| Field | Type | Notes |
|---|---|---|
amount |
number | Final amount to charge the user, including any coupon discount. NGN — integer; USD — with cents |
currency |
string | NGN if the user's currency is NGN; otherwise USD |
marchant_reference |
string, 5 chars | Spelled as shown (marchant, not merchant) — part of the current contract |
customer_email |
string | User's email |
product_id |
integer | Bundle ID on our side. Duplicated inside metadata |
callback_url |
string | User return URL after payment |
metadata |
object | Order state. Must be returned in the payment notification unchanged |
metadata.iccid |
string | Line number. For Refill — the user's existing line. For esimCreate — a pre-reserved inventory line: do not show to the user |
metadata.refill_mb |
integer | Plan data volume in MB |
metadata.refill_days |
integer | Plan validity in days |
metadata.user_id |
integer | User ID |
metadata.account_id |
integer | Account ID |
metadata.refill_type |
string | esimCreate — new line purchase; Refill — top-up. Case-sensitive |
metadata.product_id |
integer | Same as product_id at root level |
metadata.discount_coupon |
string or null | Coupon hash if applied; null if not. The amount already includes the discount |
Payment notification (you send to us)
POST https://my.partner.example/webhook/payment_gateway/{gateway-id}
key: <agreed key>
Content-Type: application/json
{
"status": "Paid",
"reference_number": "829174650183920475610394",
"amount": 24.5,
"currency": "USD",
"product_id": 1427,
"metadata": {
"iccid": "8944501234567890123",
"refill_mb": 10240,
"refill_days": 30,
"user_id": 883421,
"account_id": 40219,
"refill_type": "Refill",
"product_id": 1427,
"discount_coupon": null
}
}
Fields we actually read:
| Field | Required | How it is used |
|---|---|---|
status |
yes | We process only "Paid" (case-sensitive). Anything else — notification not processed |
reference_number |
yes | Unique payment ID. Becomes transaction ID as ptg_{reference_number}. Must stay the same on resends |
amount |
yes | Transaction amount shown in myaccount |
currency |
yes | Transaction currency |
product_id |
yes | Bundle ID. If absent at root level, metadata.product_id is used |
metadata.refill_type |
yes | Refill — top-up; esimCreate — line creation |
metadata.iccid |
yes | The line on which the operation is performed |
metadata.refill_mb |
yes | Used together with refill_days to identify the refill |
metadata.refill_days |
yes | See above |
metadata.user_id |
yes | The user receiving the service |
metadata.account_id |
yes | The user's account |
metadata.discount_coupon |
no | Coupon marked as used and linked to the transaction. Send null (not "") if no coupon was applied |
user_id (root level) |
— | Not read. Only metadata.user_id is used |
product_name, channel, paid_on, merchant_reference |
— | Not read; may be sent empty or omitted |