Overview
Already on EPay? Swap the gateway and key and you are live.
PoPoPay is fully compatible with the EPay V1 protocol. If your system (card shop, AI site, store plugin) already supports EPay, no code changes are needed: replace the gateway URL, merchant ID and merchant key with the ones PoPoPay issues.
Endpoints
| Purpose | URL | Method |
|---|---|---|
| API order (server side, returns a payment link) | {gateway}/mapi.php | POST |
| Redirect order (browser goes straight to checkout) | {gateway}/submit.php | GET / POST |
| Query & refund API (orders, refunds, settlements) | {gateway}/api.php | GET / POST |
Some systems only ask for one “gateway URL”. Enter the domain and the system appends /mapi.php and the rest itself.
Merchant ID and key
Contact official support to open an integration app. The gateway domain, pid and key are issued to you directly:
| EPay field | PoPoPay value | Notes |
|---|---|---|
pid | Merchant ID of your integration app | Numeric, 6+ digits |
key | Merchant key of your integration app | 32 characters, used only for signing. Keep it secret |
If you suspect the key has leaked, ask support to reset it. Update your system right after the reset, or every signature will fail.
IP allowlist
/mapi.php and /api.php are server-to-server calls and are protected by an IP allowlist. Send your server's public egress IP to support, otherwise requests are rejected with “illegal request, please add the API IP to the allowlist”.
/submit.php is opened by the shopper's browser, so the allowlist does not apply. The signature alone proves the request is genuine.
Signing
Almost every integration issue is a signature mismatch. This section gives a complete worked example: if the same inputs don't produce the same sign, walk through the four steps below to find the difference.
Four rules
- Filter: drop
sign,sign_typeand every parameter with an empty value - Sort: order parameters by name, ASCII ascending (a→z)
- Join: build
k1=v1&k2=v2, without URL-encoding the values - Sign: append the merchant key directly (no
&key=separator), then take the lowercase 32-char MD5
Worked example
Step through the tabs below. param is empty so it is not signed; in step ③ the key is appended with no separator at all.
pid = 100001
type = alipay
out_trade_no = MCH20260728001
notify_url = https://shop.com/notify
return_url = https://shop.com/return
name = VIP
money = 1.00
clientip = 1.2.3.4
param = ← empty, dropped
key (secret) = a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Finally send sign=f9da6305f0307dc4af967215c2ff1aac and sign_type=MD5 with the request.
Three common pitfalls
| Symptom | Cause |
|---|---|
| Your sign differs from ours | Most often the values were URL-encoded before signing. Non-ASCII text, :// and & must be signed as-is; encoding only happens when sending the HTTP request |
| Adding an empty parameter breaks the sign | Empty values must be dropped. Sending param= and including it in the string causes a mismatch |
| Occasional failures, most orders fine | Check product names for angle brackets or leading/trailing spaces. The value you send must equal the value you sign |
API order
Called from your server. Returns a payment link or QR content, and you decide how to show it. Body is application/x-www-form-urlencoded, POST only, IP allowlist required.
Request parameters
| Name | Required | Description |
|---|---|---|
pid | Yes | Merchant ID |
type | Yes | Payment method: alipay / wxpay / qqpay |
out_trade_no | Yes | Your order number, unique within the app (max 64 chars) |
notify_url | Yes | Async notification URL. Deliver goods based on this |
name | Yes | Product name |
money | Yes | Amount in CNY yuan, up to 2 decimals, minimum 0.01 |
clientip | Yes | Shopper IP |
return_url | No | Browser redirect after payment |
device | No | Device type, detected automatically if omitted |
param | No | Custom data, echoed back in the notification |
sign | Yes | Signature |
sign_type | Yes | Always MD5 |
Response fields
| Name | Description |
|---|---|
code | 1 success, -1 failure |
msg | Failure reason, empty on success |
trade_no | Platform order number |
out_trade_no | Your order number |
money | Order amount |
payurl | Payment link (http/https) |
qrcode | QR content (weixin://, alipays://, …) |
urlscheme | Mini-program link |
Only one of payurl, qrcode and urlscheme is returned, depending on the channel. Your code must handle all three.
{
"code": 1,
"msg": "",
"trade_no": "PN20260728120000123",
"out_trade_no": "MCH20260728001",
"money": "1.00",
"payurl": "https://pay.example.com/xxxx"
}Redirect order
Send the shopper's browser to this URL (form POST or GET query string). The order is created and the browser is redirected to checkout, so you don't need to build a payment page. No IP allowlist (the caller is the shopper); the signature alone proves authenticity.
Request parameters
| Name | Required | Description |
|---|---|---|
pid | Yes | Merchant ID |
out_trade_no | Yes | Your order number |
notify_url | Yes | Async notification URL |
return_url | Yes | Redirect after payment |
name | Yes | Product name |
money | Yes | Amount (CNY yuan) |
type | No | Payment method. If omitted, the shopper picks one on our checkout page |
param | No | Custom data |
sign | Yes | Signature |
sign_type | Yes | Always MD5 |
The response is an auto-redirecting HTML page. If a parameter or the signature is wrong, a readable error page explains why.
Async notification
After a successful payment we call your notify_url with GET, parameters in the query string. Always deliver based on this notification, never on the browser redirect.
Notification parameters
| Name | Description |
|---|---|
pid | Merchant ID (same as in the order) |
trade_no | Platform order number |
out_trade_no | Your order number |
type | Payment method, as you sent it |
name | Product name |
money | Order amount |
trade_status | Only TRADE_SUCCESS means paid |
param | Your custom data (absent if not sent) |
sign | Signature. Always verify it |
sign_type | MD5 |
What to return
After processing, output exactly success (ok is also accepted), with no HTML or anything else.
Without success, we retry with increasing intervals. Your handler must be idempotent: the same out_trade_no may arrive several times but must be delivered only once.
GET https://shop.com/notify?money=1.00&name=VIP&out_trade_no=MCH20260728001
&pid=100001&sign=xxxxxxxx&sign_type=MD5&trade_no=PN20260728120000123&trade_status=TRADE_SUCCESSVerify the signature with URL-decoded values. Most frameworks (PHP $_GET, Java getParameter) already decode them for you.
Resend a notification
If your service was down and missed a notification, call /api.php?act=notify to have it resent. No need to contact support.
Return redirect
After paying, the shopper is redirected to your return_url with the same parameters as the notification, so you can show a “payment successful” page. Required for submit.php, optional for mapi.php.
The redirect is not guaranteed and must never trigger delivery. Whether it happens, and with which parameters, depends on the upstream channel; some leave the shopper on their own page, just like the original EPay. Your success page should query the order again (act=order) instead of trusting the redirect.
Query & refund API
Order queries, refunds and settlements all use this URL, selected by act. These calls are not signed; instead you pass key (your merchant key) directly, as defined by the EPay protocol.
Because key appears in the URL, call it from your server only, never from front-end code. IP allowlist required.
Common parameters
| Name | Required | Description |
|---|---|---|
act | Yes | Action, see below |
pid | Yes | Merchant ID |
key | Yes | Merchant key (plain text) |
act=order — query an order
Extra parameter: trade_no (platform number) or out_trade_no (your number). If both are sent, trade_no wins.
| Field | Description |
|---|---|
status | 1 = paid, 0 = anything else (unpaid, failed, closed) |
trade_no / out_trade_no | Platform number / your number |
api_trade_no | Upstream channel order number |
type | Payment method (as you sent it) |
money | Order amount |
name | Product name |
addtime / endtime | Created / paid time |
param | Custom data |
GET /api.php?act=order&pid=100001&key=YOUR_KEY&out_trade_no=MCH20260728001act=orders — list orders
Extra parameters: limit (page size, max 50, default 20) and page (starting at 1). Returns a data array with the same fields as act=order, newest first.
act=query — account info
| Field | Description |
|---|---|
money | Account balance |
active | 1 = active, 0 = disabled |
order_today / order_lastday | Orders today / yesterday |
key | Echoes the key you sent (as in V1), so “test connection” buttons can compare it |
type / account / username | Settlement method and account in the protocol. PoPoPay handles settlement centrally and does not expose it, so these are empty strings |
orders | Lifetime order count in the protocol. Not tracked, always 0; use act=orders for statistics |
act=settle — settlement records
Returns a data array (latest 50) with id, money (amount), addtime and remark. Settlement is per account, so all apps under one account share these records.
act=refund — request a refund (POST)
| Name | Required | Description |
|---|---|---|
trade_no | One of | Platform order number |
out_trade_no | One of | Your order number |
money | Yes | Refund amount. Some channels require the full order amount |
trade_no and out_trade_no cannot both be empty; if both are sent, trade_no wins.
Here code=0 means success, the opposite of every other act. That is how EPay V1 defines it, and PoPoPay keeps it for compatibility, so don't check for code=1.
act=notify — resend notification (extension)
Extra parameter: trade_no or out_trade_no. Only paid orders can be resent. code=1 means the resend has been queued; delivery is asynchronous and does not confirm you received it. This act is a PoPoPay extension, not part of V1, for merchants to recover missed notifications themselves.
Popular systems
Every system names these fields differently. The tables below map common admin settings to PoPoPay values, so you can fill them in without reading the protocol. First ask support to open an integration app and get your pid and key.
General mapping
| Field name in your system | What to enter |
|---|---|
| Payment API URL / Gateway URL / API URL | The gateway domain from support, e.g. https://gateway-domain |
| Merchant ID / Merchant No. / PID / Partner ID | Your app's merchant ID (pid) |
| Merchant key / Secret / KEY / Token | Your app's merchant key (key) |
| Sign type / Encryption | MD5 |
| Payment method code / Channel code | alipay (Alipay), wxpay (WeChat Pay) |
| API version / Protocol | Choose “EPay” or “Rainbow EPay”; pick V1 if versions are listed |
Card shop systems
| System | Where | Notes |
|---|---|---|
| ACG-Faka | Admin → Payment API → Add, type “Rainbow EPay / EPay” | Enter just the domain as the API URL; /submit.php is appended automatically. Merchant ID = pid, key = key |
| Dujiaoka | Admin → Payment settings → Add payment method, choose Epay | Add one entry for Alipay and one for WeChat Pay with the same pid / key; only the payment type differs (alipay / wxpay) |
| Rainbow card shop and forks | Admin → Payment settings → EPay | Same native protocol; usually just replace gateway, PID and KEY |
AI sites / API relays
| System | Where | Notes |
|---|---|---|
| One-API / New-API / forks | System settings → Payment (or the Alipay / EPay options) | EPay API URL = gateway domain, EPay merchant ID = pid, EPay key = key. Some versions require a trailing /; follow the placeholder |
| Custom sites / other plugins | Any plugin that supports EPay | Fill in each field using the general mapping above |
Don't forget the IP allowlist: send your server's public egress IP to support, otherwise /mapi.php and /api.php will be rejected. Systems that only use /submit.php redirects are not affected.
Code samples
The three core pieces: signing, creating an order and verifying notifications. Replace GATEWAY, PID and KEY with your own values and run.
<?php
const GATEWAY = 'https://pay.example.com'; // Gateway URL, issued by official support after onboarding
const PID = '100001';
const KEY = 'YOUR_MERCHANT_KEY';
/** Sign: drop empty values and sign/sign_type, sort by key, join, append the key, then MD5 */
function epaySign(array $params, string $key): string {
unset($params['sign'], $params['sign_type']);
$params = array_filter($params, fn($v) => $v !== '' && $v !== null);
ksort($params);
$parts = [];
foreach ($params as $k => $v) {
$parts[] = "$k=$v"; // note: values are NOT urlencoded
}
return md5(implode('&', $parts) . $key);
}
/** Create an order and return the payment target */
function epayCreate(array $biz): array {
$params = array_merge($biz, ['pid' => PID]);
$params['sign'] = epaySign($params, KEY);
$params['sign_type'] = 'MD5';
$ch = curl_init(GATEWAY . '/mapi.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
curl_close($ch);
$data = json_decode($resp, true);
if (!$data || ($data['code'] ?? -1) != 1) {
throw new Exception('Order failed: ' . ($data['msg'] ?? $resp));
}
// only one of payurl / qrcode / urlscheme is returned; handle all three
$data['pay_target'] = $data['payurl'] ?? $data['qrcode'] ?? $data['urlscheme'] ?? '';
return $data;
}
// ---------- Create order ----------
$order = epayCreate([
'type' => 'alipay',
'out_trade_no' => 'MCH' . date('YmdHis'),
'notify_url' => 'https://shop.com/notify.php',
'return_url' => 'https://shop.com/return.php',
'name' => 'VIP',
'money' => '1.00',
'clientip' => $_SERVER['REMOTE_ADDR'],
]);
header('Location: ' . $order['pay_target']);
// ---------- Receive the async notification (notify.php) ----------
$params = $_GET; // already urldecoded by PHP
if (epaySign($params, KEY) !== ($params['sign'] ?? '')) {
exit('sign error'); // bad signature: do not deliver
}
if (($params['trade_status'] ?? '') !== 'TRADE_SUCCESS') {
exit('success'); // not paid: acknowledge to stop retries
}
// idempotent: the same order may be notified more than once, deliver only once
if (!alreadyShipped($params['out_trade_no'])) {
ship($params['out_trade_no'], $params['money']);
}
exit('success'); // output only success, no HTMLTroubleshooting
Error messages are returned in Chinese; the first column shows the meaning.
| Message | Cause and fix |
|---|---|
| Signature verification failed | Usually the values were URL-encoded before signing; non-ASCII text and :// must be signed as-is. Also check that no empty parameter was included. Compare step by step with the worked example in “Signing” |
| Illegal request, add API IP to allowlist | Send your server's public egress IP to support. Not an internal IP, and not the shopper's IP |
| Merchant does not exist | Wrong pid, or someone else's. Check the pid support gave you |
| Integration app disabled | Ask support to re-enable the app |
| No product configured for payment method xxx | The app has no product for this payment method; ask support to configure one |
| Duplicate merchant order number | out_trade_no must be unique within the app. If this came from a retry, query the original with act=order instead of creating a new number |
| money invalid / below 0.01 / more than 2 decimals | Fix the amount. The unit is yuan, not cents |
| No payment link available | No channel is available for this amount. Try another amount, or ask support about channel status and amount ranges |
| Order does not exist | Wrong number, or the order belongs to another app under the same account. Query with the pid / key used to create it |
| No async notification received | Make sure notify_url is a public http/https URL with no login, returning only success. Then resend with act=notify |
| No redirect back after payment | Depends on the upstream channel; some never redirect. This is normal — deliver based on the async notification |
Go-live checklist
- Support has configured a product for every payment method you use
- Your server's public egress IP is on the allowlist
- The sign string has no URL encoding, no empty values, and the key appended directly
- Order responses handle
payurl/qrcode/urlscheme - Async notifications are verified and reply only
success - Delivery is idempotent: repeated notifications for one order deliver once
- Delivery is triggered by the async notification, not the redirect
Get your merchant ID and key
Contact official support to open an integration app. Your gateway URL, pid and key are issued directly, with hands-on help during testing.
Contact support