Trang này chưa có bản Tiếng Việt; bên dưới là bản English.
SDK downloads
The payout API std1 has SDKs in six languages: Java, PHP, Python, Go, Node.js and .NET. All six behave identically: the same parameters produce the same request bytes, and the same response is classified the same way with the same hint. None has an address built in; you configure the API address, merchant ID and key. The collection API compat1 (legacy protocol) has no SDK.
Downloads
| Language | Minimum version | File | Size | SHA-256 |
|---|---|---|---|---|
| Java | Java 8 | std1-sdk-java-1.0.0.zip | 89 KB | 6e5c3980a03f04cd48968b6425159f5110b1a0171fd89769fef7b342d91e8c4c |
| PHP | PHP 7.4 | std1-sdk-php-1.0.0.zip | 93 KB | 340dfd49cc9c89e90aea527fbe91eaa5de39b5c694ee20bc6fce0902a589ee12 |
| Python | Python 3.7 | std1-sdk-python-1.0.0.zip | 63 KB | dec28311467ae757b22299e6f9d4e40db9cf0134f41c0b95f3ad84d2ad836d47 |
| Go | Go 1.18 | std1-sdk-go-1.0.0.zip | 82 KB | d3e680e4fe1f85d511319d90016e7c233caab934f72377753b15b2eadff628fc |
| Node.js(TypeScript) | Node.js 16 | std1-sdk-node-1.0.0.zip | 120 KB | 9dafafaafaf666cc404149596e56d3ca6bb561e9344f2f696472fbf5ac86d327 |
| .NET | .NET Standard 2.0 | std1-sdk-dotnet-1.0.0.zip | 99 KB | 3e322b359bcb2800303166022d21c924cc72f7690e3a0078b626ac001ca99fe8 |
| all six | — | std1-sdk-all-1.0.0.zip | 398 KB | 79097c0dd230e8638c732f309049bc3612ba3ab5ef4868795d2ee896a29f2fc1 |
- Each package contains the sources, a Chinese and English README (installation, configuration, the three calls, callbacks, retry rules, common mistakes), a test script
run-tests.shand the golden vectorsvectors/std1.json. - Use the SHA-256 values above to check that a download is complete.
- If you write your own client instead, check every entry of the golden vectors (signatures, request bodies, result handling) before going live.
Configuration
- Gateway address:
https://api.xx2system.xyz - Merchant ID: yours (new ones look like
MCH58001) - Key: the API key under “Security” in the merchant portal (the one key shared by collection and payout; a 64-character hex string for new merchants), copied exactly, with no leading or trailing spaces or line breaks
Only five outcomes
| Outcome | Meaning | What to do |
|---|---|---|
SUCCESS | apply: accepted and funds frozen, not paid yet; query: found, see status; balance: returned | wait for the callback or query |
NOT_PROCESSED | nothing was done (bad parameter, bad signature, IP not allowed, rate limited, unknown merchant ID) | fix it and resend with the same order number |
REJECTED | the order is rejected (final) and this merchant order number is used up (except MERCHANT_DISABLED "商户未开通代付接口": not used up, resubmit with the same one once enabled) | to pay out, submit with a new merchant order number |
UNKNOWN | result unknown (timeout, broken connection, HTTP not 200, signature does not verify, system busy, order number conflict) | retry with the same order number and identical parameters, or query |
NOT_FOUND | the query found nothing | this does not prove the order was never created |
Examples
The examples below come from the README in each package.
Java
Apply for a payout
ApplyRequest request = ApplyRequest.builder()
.mchOrderNo("ORDER-20260917-0001")
.amountCents(Amounts.yuanToCents("19.99"))
.accountName("张三")
.accountNo("6222021234567890123")
.bankName("中国工商银行")
.notifyUrl("https://your-callback-host/std1/notify")
.extra("productId", "2") // optional payout product code, sent as an extra field; omit it to use your default payout product
.build();
Std1Result result = client.applyPayout(request);
// SUCCESS = accepted and funds frozen, NOT paid yet; see section 8 for the rest.
Receive callbacks
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
byte[] body = readAll(req.getInputStream()); // 只读正文字节,别碰 getParameter*
try {
PayoutNotification n = client.parseNotification(body);
saveIfNewer(n.getPayoutNo(), n.getStatus(), n); // 你的落库逻辑(按 payoutNo+status 去重)
resp.setStatus(200);
resp.getWriter().write(Std1Client.SUCCESS_REPLY);
} catch (InvalidNotificationException e) {
resp.setStatus(400); // e.getReason(): malformed / signature / merchant / fields
resp.getWriter().write("fail");
}
}
PHP
Apply for a payout
$request = new \Std1\Payout\ApplyRequest([
'mchOrderNo' => 'A20260917000001', // unique per merchant: the idempotency key
'amount' => \Std1\Payout\Amounts::yuanToCents('100.00'), // '10000' minor units; never 100.00 * 100
'accountName' => 'Nguyen Van A',
'accountNo' => '6222021234567890123',
'bankName' => '中国工商银行',
'notifyUrl' => 'https://<your-callback-host>/std1/notify', // optional
'remark' => 'salary', // optional, echoed back
], ['myBatch' => '0917', // optional merchant-defined fields
'productId' => '2']); // optional payout product code; omit it to use your default payout product
// Store these parameters with the order BEFORE sending: an UNKNOWN result must be retried with exactly the same parameters.
$result = $client->applyPayout($request);
// SUCCESS = accepted and funds frozen, NOT paid; REJECTED = rejected (final), this merchant order number is used up, use a NEW one to pay out;
// NOT_PROCESSED = fix and resend with the SAME order number; UNKNOWN = retry the same order number with identical parameters, or query.
Receive callbacks
$raw = file_get_contents('php://input'); // never $_POST or $_REQUEST
try {
$notification = $client->parseNotification($raw); // or Notification::parse($raw, $mchId, $key) without a client
} catch (\Std1\Payout\InvalidNotificationException $e) {
http_response_code(400);
echo 'fail';
exit;
}
// Persist first (de-duplicate by payoutNo + status; accept only forward moves via Statuses::isNewer), then:
echo \Std1\Payout\Client::SUCCESS_REPLY;
Python
Apply for a payout
request = std1.ApplyRequest(mch_order_no="ORDER-20260917-0001", amount=std1.yuan_to_cents("19.99"), account_name="张三",
account_no="6222021234567890123", bank_name="中国工商银行", notify_url="https://your-host/std1/notify",
extra={"productId": "2"}) # optional payout product code; omit it to use your default payout product
result = client.apply_payout(request)
# SUCCESS: accepted and funds frozen (NOT paid yet) · REJECTED: the order is rejected (final) and this merchant order number is used up; use a NEW merchant order number to pay out
# NOT_PROCESSED: nothing happened; fix it and resend with the SAME number · UNKNOWN: retry with the same number and identical parameters, or query
Receive callbacks
@app.post("/std1/notify")
def std1_notify():
try:
notification = client.parse_notification(request.get_data()) # raw body bytes
except std1.InvalidNotification:
return "fail", 400 # never reply success to an invalid callback
save_once(notification.payout_no, notification.status, notification) # persist first (dedupe on payoutNo + status)
return std1.SUCCESS_REPLY, 200
Go
Apply for a payout
cents, _ := std1payout.YuanToCents("19.99")
result, err := client.ApplyPayout(ctx, std1payout.ApplyRequest{
MchOrderNo: "ORDER-20260917-0001", Amount: cents,
AccountName: "张三", AccountNo: "6222021234567890123", BankName: "中国工商银行",
NotifyURL: "https://your-callback/std1/notify",
Extra: []std1payout.Param{{Name: "productId", Value: "2"}}, // optional payout product code; omit it to use your default payout product
})
// err != nil only for invalid input (nothing was sent).
// result.Outcome: SUCCESS (accepted, funds frozen, NOT paid yet), REJECTED (use a NEW order number to pay),
// NOT_PROCESSED (fix and resend with the SAME order number), UNKNOWN (retry with the same order number and
// identical parameters, or query; never switch order numbers).
Receive callbacks
http.Handle("/std1/notify", client.NotificationHandler(func(n *std1payout.Notification) error {
// de-duplicate by n.PayoutNo + n.Status, accept only forward moves (IsNewerStatus), store first
return saveResult(n) // nil → HTTP 200 "success"; error → 500 and the gateway retries later
}))
Node.js(TypeScript)
Apply for a payout
const result = await client.applyPayout({
mchOrderNo: 'ORDER-20260917-0001', // retries MUST reuse the same order number and identical parameters
amount: yuanToCents('100.00'), // integer minor units only: '10000', 10000 or 10000n
accountName: 'Nguyen Van A',
accountNo: '6222021234567890123',
bankName: 'BANK NAME',
notifyUrl: 'https://YOUR_CALLBACK_HOST/std1/notify',
remark: 'salary',
extra: [
['myTraceId', 'T-1'], // optional merchant-defined fields (signed, ignored by the gateway)
['productId', '2'], // optional payout product code; omit it to use your default payout product
],
});
if (result.outcome === Outcome.SUCCESS) {
// accepted and funds frozen, NOT paid yet — wait for the callback or query
} else if (result.outcome === Outcome.REJECTED) {
// the order is rejected (final) and this merchant order number is used up; use a NEW merchant order number to pay out
} else if (result.outcome === Outcome.NOT_PROCESSED) {
// nothing was done; fix the cause and resend with the SAME order number
} else {
// UNKNOWN: retry with the same order number and identical parameters, or query; never switch order numbers
}
Receive callbacks
app.post('/std1/notify', express.raw({ type: '*/*', limit: '64kb' }), async (req, res) => {
let notification;
try {
notification = client.parseNotification(req.body); // Buffer — never the parsed object, never merged with req.query
} catch (error) {
return res.status(400).type('text/plain').send('fail'); // do not reply success; the gateway will retry
}
await saveOnce(notification.payoutNo, notification.status, notification); // store first, then reply
res.status(200).type('text/plain').send(SUCCESS_REPLY);
});
.NET
Apply for a payout
var request = new ApplyRequest
{
MchOrderNo = "ORDER-20260917-0001", // your order number: unique per merchant, the idempotency key
Amount = Amounts.YuanToCents("100.00"), // "10000" — always minor units; or request.SetAmountCents(10000)
AccountName = "Nguyen Van A",
AccountNo = "6222021234567890123",
BankName = "中国工商银行",
NotifyUrl = "https://回调地址/std1/notify", // optional; without it there is no callback, only queries
Remark = "salary", // optional; echoed by queries and callbacks
};
request.AddExtra("myTag", "vip"); // optional merchant fields: signed, ignored by the gateway
request.AddExtra("productId", "2"); // optional payout product code; omit it to use your default payout product
// Store the order number and these exact parameters first: an UNKNOWN result must be resent unchanged
Std1Result result = await client.ApplyPayoutAsync(request);
switch (result.Outcome)
{
case Outcome.Success: // accepted and frozen, NOT paid yet: wait for the callback or query
SavePayoutNo(request.MchOrderNo, result.PayoutNo);
break;
case Outcome.Rejected: // rejected: the order is rejected (final) and this merchant order number is used up; use a NEW merchant order number to pay out
MarkRejected(request.MchOrderNo, result.Code, result.Message);
break;
case Outcome.NotProcessed: // not processed: fix the cause and resend with the SAME order number
Alert(result.HintEn);
break;
default: // Unknown: resend with the same number and identical parameters, or query
ScheduleRetryOrQuery(request.MchOrderNo);
break;
}
Receive callbacks
app.MapPost("/std1/notify", async (HttpRequest http) =>
{
byte[] body;
using (var buffer = new MemoryStream())
{
await http.Body.CopyToAsync(buffer);
body = buffer.ToArray();
}
PayoutNotification n;
try
{
n = client.ParseNotification(body);
}
catch (InvalidNotificationException e) // e.Reason: malformed / signature / merchant / fields
{
Console.Error.WriteLine("invalid std1 callback: " + e.Reason);
return Results.Text("fail", statusCode: 400);
}
await SaveIfNewerAsync(n); // dedupe by PayoutNo + Status; accept forward moves only
return Results.Text(Std1Client.SuccessReply);
});