EPay API Integration Docs

PoPoPay is fully compatible with the EPay V1 protocol. If your system already supports EPay, just swap the gateway URL, merchant ID and key. Building from scratch? This page has everything you need.

  • EPay V1 protocol
  • MD5 signing
  • PHP / Python / Java samples
  • Updated

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

PurposeURLMethod
API order (server side, returns a payment link){gateway}/mapi.phpPOST
Redirect order (browser goes straight to checkout){gateway}/submit.phpGET / POST
Query & refund API (orders, refunds, settlements){gateway}/api.phpGET / 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 fieldPoPoPay valueNotes
pidMerchant ID of your integration appNumeric, 6+ digits
keyMerchant key of your integration app32 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

  1. Filter: drop sign, sign_type and every parameter with an empty value
  2. Sort: order parameters by name, ASCII ascending (a→z)
  3. Join: build k1=v1&k2=v2, without URL-encoding the values
  4. 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) = a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Finally send sign=f9da6305f0307dc4af967215c2ff1aac and sign_type=MD5 with the request.

Three common pitfalls

SymptomCause
Your sign differs from oursMost 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 signEmpty values must be dropped. Sending param= and including it in the string causes a mismatch
Occasional failures, most orders fineCheck product names for angle brackets or leading/trailing spaces. The value you send must equal the value you sign

API order

POST/mapi.php

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

NameRequiredDescription
pidYesMerchant ID
typeYesPayment method: alipay / wxpay / qqpay
out_trade_noYesYour order number, unique within the app (max 64 chars)
notify_urlYesAsync notification URL. Deliver goods based on this
nameYesProduct name
moneyYesAmount in CNY yuan, up to 2 decimals, minimum 0.01
clientipYesShopper IP
return_urlNoBrowser redirect after payment
deviceNoDevice type, detected automatically if omitted
paramNoCustom data, echoed back in the notification
signYesSignature
sign_typeYesAlways MD5

Response fields

NameDescription
code1 success, -1 failure
msgFailure reason, empty on success
trade_noPlatform order number
out_trade_noYour order number
moneyOrder amount
payurlPayment link (http/https)
qrcodeQR content (weixin://, alipays://, …)
urlschemeMini-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

GET/submit.php

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

NameRequiredDescription
pidYesMerchant ID
out_trade_noYesYour order number
notify_urlYesAsync notification URL
return_urlYesRedirect after payment
nameYesProduct name
moneyYesAmount (CNY yuan)
typeNoPayment method. If omitted, the shopper picks one on our checkout page
paramNoCustom data
signYesSignature
sign_typeYesAlways 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

GETnotify_url

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

NameDescription
pidMerchant ID (same as in the order)
trade_noPlatform order number
out_trade_noYour order number
typePayment method, as you sent it
nameProduct name
moneyOrder amount
trade_statusOnly TRADE_SUCCESS means paid
paramYour custom data (absent if not sent)
signSignature. Always verify it
sign_typeMD5

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_SUCCESS

Verify 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

GETreturn_url

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

GET/api.php?act={action}

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

NameRequiredDescription
actYesAction, see below
pidYesMerchant ID
keyYesMerchant 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.

FieldDescription
status1 = paid, 0 = anything else (unpaid, failed, closed)
trade_no / out_trade_noPlatform number / your number
api_trade_noUpstream channel order number
typePayment method (as you sent it)
moneyOrder amount
nameProduct name
addtime / endtimeCreated / paid time
paramCustom data
GET /api.php?act=order&pid=100001&key=YOUR_KEY&out_trade_no=MCH20260728001

act=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

FieldDescription
moneyAccount balance
active1 = active, 0 = disabled
order_today / order_lastdayOrders today / yesterday
keyEchoes the key you sent (as in V1), so “test connection” buttons can compare it
type / account / usernameSettlement method and account in the protocol. PoPoPay handles settlement centrally and does not expose it, so these are empty strings
ordersLifetime 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)

NameRequiredDescription
trade_noOne ofPlatform order number
out_trade_noOne ofYour order number
moneyYesRefund 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 systemWhat to enter
Payment API URL / Gateway URL / API URLThe gateway domain from support, e.g. https://gateway-domain
Merchant ID / Merchant No. / PID / Partner IDYour app's merchant ID (pid)
Merchant key / Secret / KEY / TokenYour app's merchant key (key)
Sign type / EncryptionMD5
Payment method code / Channel codealipay (Alipay), wxpay (WeChat Pay)
API version / ProtocolChoose “EPay” or “Rainbow EPay”; pick V1 if versions are listed

Card shop systems

SystemWhereNotes
ACG-FakaAdmin → 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
DujiaokaAdmin → Payment settings → Add payment method, choose EpayAdd 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 forksAdmin → Payment settings → EPaySame native protocol; usually just replace gateway, PID and KEY

AI sites / API relays

SystemWhereNotes
One-API / New-API / forksSystem 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 pluginsAny plugin that supports EPayFill 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.

epay.php
<?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 HTML

Troubleshooting

Error messages are returned in Chinese; the first column shows the meaning.

MessageCause and fix
Signature verification failedUsually 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 allowlistSend your server's public egress IP to support. Not an internal IP, and not the shopper's IP
Merchant does not existWrong pid, or someone else's. Check the pid support gave you
Integration app disabledAsk support to re-enable the app
No product configured for payment method xxxThe app has no product for this payment method; ask support to configure one
Duplicate merchant order numberout_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 decimalsFix the amount. The unit is yuan, not cents
No payment link availableNo channel is available for this amount. Try another amount, or ask support about channel status and amount ranges
Order does not existWrong number, or the order belongs to another app under the same account. Query with the pid / key used to create it
No async notification receivedMake sure notify_url is a public http/https URL with no login, returning only success. Then resend with act=notify
No redirect back after paymentDepends 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