WeChat Pay Service Provider: Entity-First Payment Routing for Multi-Store Systems
This article details a production-grade WeChat Pay service provider integration for multi-store, multi-entity scenarios, covering domain modeling, versioned payment routing, sub-merchant onboarding state machines, cashier configuration layers, callback verification, refund snapshot isolation, three-level reconciliation, and a 10-item launch checklist.
The article presents a comprehensive architecture for collecting payments via WeChat Pay's service provider model in complex multi-store, multi-entity environments such as chain brands, franchise systems, and mall counters. It argues that payment integration is not merely about calling APIs but about building an operating infrastructure that correctly handles fund ownership, routing stability, and auditability.
Core Principle: Entity First, Routing Frozen, Server-Side Verification
The author establishes three immutable rules:
Determine fund ownership before creating the business order.
Freeze the payment routing before calling the channel API.
Verify server-side payment facts before driving fulfillment.
These rules prevent configuration drift, ensure historical payments remain tied to their original entity, and avoid treating frontend success as financial truth.
Part 1: Domain Modeling — Entity, Store, Cashier Are Distinct
The article distinguishes three concepts often conflated:
Operating Entity (经营主体) : legal entity responsible for transaction, invoicing, after-sales, and settlement.
Store (门店) : physical or logical fulfillment unit.
Cashier (收银台) : user-facing payment entry point (mini-program, POS, QR code).
Relationships can be one-to-one, one-to-many, or many-to-many. Example: a direct-operated chain with one entity and 50 stores shares a single sub_mchid but uses store_id, scene_info.store_info.id, and device_id to differentiate locations. A franchise brand with 20 franchisee companies maps stores nearly one-to-one with sub_mchid.
WeChat's service provider model dictates that the sub-merchant is the actual fund recipient; the service provider's sp_mchid must not become a fund pool with internal ledger redistribution.
Part 2: Why Service Provider Model — Three Isolated Lines
The model resolves the tension between many small entities lacking payment build capability and a platform wanting unified checkout. It separates:
Business line : unified product, membership, marketing, order, fulfillment.
Technical line : service provider maintains certificates, signatures, callbacks, query, refund, bill download.
Fund line : consumer payment flows via sub_mchid into the corresponding sub-merchant account.
Platform unifies experience and tech but must not blur actual operator and fund ownership. Value: unified cashier, unified store ops data, unified risk control, unified financial reconciliation, while each entity retains independent sign-up, settlement, and liability.
Pitfall: a cart containing goods from multiple independent entities cannot be paid via a single sub_mchid. Must split orders or evaluate WeChat's combined payment or platform collection products based on business license and fund model, not API convenience.
Part 3: Payment Domain — Six Core Objects & Transactional Routing Freeze
The author recommends a dedicated Payment Domain shielding upstream (order, membership, marketing, after-sales) from downstream (WeChat Pay, other channels, product shapes). Six domain objects:
Operating Entity — Defines transaction & settlement responsibility. Key fields: subject_id, licenses, status.
Store — Expresses fulfillment location. Key fields: store_id, subject_id, region, format.
WeChat Sub-Merchant — Stores WeChat-side collection identity. Key fields: sub_mchid, onboarding status, auth status.
Cashier — Defines user payment entry. Key fields: cashier_id, app, scene, device.
Payment Route — Decides which config an order uses. Key fields: rules, priority, version, effective time.
Payment Order — Freezes one payment fact. Key fields: entity snapshot, store snapshot, amount, status.
Integration flow: business order carries tenant_id, subject_id, store_id, cashier_id, scene → Payment Domain validates ownership relations → resolves current effective WeChat config → generates global unique out_trade_no → saves route snapshot → calls WeChat Pay.
Four validations must all pass before local order creation: store belongs to tenant, store's current entity is valid, entity has usable sub_mchid, cashier scene has product authorization. Fail locally rather than sending bad request to WeChat.
Code snippet (Java) shows @Transactional method that loads store & cashier, checks cashier-store match, resolves route, asserts WeChat readiness, inserts PayOrder with snapped spMchid, subMchid, appid, routeVersion.
@Transactional
public PayOrder createPayOrder(CreatePayCommand cmd) {
Store store = storeRepo.requireEnabled(cmd.tenantId(), cmd.storeId());
Cashier cashier = cashierRepo.requireEnabled(cmd.cashierId());
if (!cashier.storeId().equals(store.id())) {
throw new BizException("收银台不属于当前门店");
}
PaymentRoute route = routeEngine.resolve(
cmd.tenantId(), store.subjectId(), store.id(),
cashier.scene(), cmd.payChannel(), cmd.orderTime());
route.assertWechatReady();
return payOrderRepo.insert(PayOrder.builder()
.outTradeNo(idGenerator.nextPayNo())
.bizOrderNo(cmd.bizOrderNo())
.subjectId(store.subjectId())
.storeId(store.id())
.cashierId(cashier.id())
.amount(cmd.amountFen())
.spMchid(route.spMchid())
.subMchid(route.subMchid())
.appid(route.appid())
.routeVersion(route.version())
.status(PayStatus.CREATED)
.build());
}Routing config must be versioned: new config effective at specified time, old config retained for query, refund, reconciliation. Payment order snapshots at least sp_mchid, sub_mchid, app ID, scene, route version, callback version.
Part 4: Sub-Merchant Onboarding as Operable State Machine
Daily work shifts from payment to merchant onboarding. WeChat supports service-provider-assisted onboarding via API: data collection, sensitive info encryption, image upload, submit, review, sign, capability auth.
Product should not use a simple on/off switch but a state machine: Draft, PendingSubmit, UnderReview, PendingSupplement, PendingSign, Signed, CapabilityConfiguring, ReadyToCollect, Frozen, Cancelled. Each transition records original application ID, WeChat application ID, operator, failure reason, next action.
For group clients, distinguish entity-level documents (business license, legal rep, settlement account) from store-level documents (store photos, address, store name, special category proofs). Reuse entity docs across stores but never mix different entities' documents.
Reliable activation checklist:
Service provider sp_mchid, sp_appid, API private key, cert serial, APIv3 key or WeChat public key valid.
Sub-merchant onboarding & sign complete, sub_mchid obtained. sp_appid bound to sp_mchid; sub-merchant app uses sub_appid bound to sub_mchid.
Required product permissions (JSAPI, Native, refund) opened; sub-merchant authorized capabilities completed.
Payment authorization directory, callback URLs, app domains configured per environment.
End-to-end verification: small real payment, query, refund, bill download all pass.
Only after all checks pass should platform flip config from CONFIGURING to READY.
Part 5: Cashier Configuration — Four Layers, Not a Parameter Mirror
Cashier config describes "which user, at which entry, pays which entity, via which method." Four layers:
Entity-level : sub_mchid, settlement & refund auth status, available products, e-invoice, profit-sharing.
App-level : service provider or sub-merchant Official Account, Mini Program, AppID & OpenID system.
Store-level : store_id, name, address, available payment scenes.
Cashier instance : device ID, terminal type, payment page, default channel, timeout, canary strategy.
Scene-to-product mapping:
WeChat web page → JSAPI → Payment authorization directory, OpenID's AppID
Brand Mini Program → Mini Program Pay → sp_appid, sp_openid, mini program version
Merchant-owned Mini Program → Mini Program Pay → sub_appid bound to sub_mchid, sub_openid Desktop POS showing QR → Native Pay → Product auth, QR expiry, query polling
Clerk scans user payment code → Payment Code Pay → Terminal device, store network, revoke & query strategy
JSAPI/Native partner endpoint: POST /v3/pay/partner/transactions/jsapi. Request uses either sp_openid (user pays in service provider app) or sub_appid + sub_openid (user pays in sub-merchant app). Never mix OpenIDs across AppIDs.
Full JSON example for multi-store scenario:
{
"sp_appid": "wx_service_appid",
"sp_mchid": "1900000001",
"sub_mchid": "1900000099",
"description": "华东一店-到店服务订单",
"out_trade_no": "P202608120001",
"time_expire": "2026-08-12T21:30:00+08:00",
"notify_url": "https://pay.example.com/callback/wechat/pay",
"attach": "routeVersion=17",
"amount": {
"total": 9900,
"currency": "CNY"
},
"payer": {
"sp_openid": "oExampleOpenId"
},
"scene_info": {
"payer_client_ip": "203.0.113.10",
"device_id": "POS-SH-001-03",
"store_info": {
"id": "STORE-SH-001",
"name": "华东一店",
"area_code": "310101",
"address": "上海市黄浦区示例路一号"
}
}
}Amount must be integer cents. out_trade_no unique per sp_mchid, retry params identical. attach only for short non-sensitive routing hint; full route snapshot stays in own DB. JSAPI authorization directory validates protocol, domain, path, case-sensitive, must end with slash. Multi-tenant SaaS sharing a payment domain usually config at domain level; strict isolation uses deepest path with automated pre-flight probe.
Part 6: Payment Result — Callback Primary, Query Fallback, Frontend Not Fact
Dangerous: marking order paid on frontend success. Frontend only for interaction. Correct: WeChat callback primary; active query fallback for network loss/callback loss; T+1 bill final audit. All three share same idempotent state machine.
APIv3 callback handling order: verify signature using timestamp, nonce, cert/public key ID + raw request body; then decrypt resource with APIv3 key via AES-256-GCM. Return HTTP 200/204 quickly, process heavy business async. Never re-serialize parsed body for verification.
Java callback code: lock by out_trade_no, return if already paid, verify sub_mchid matches snapshot, verify amount matches, mark paid with transaction_id and success time, save, append outbox event PAYMENT_PAID.
public void onPaid(Transaction tx) {
PayOrder order = payOrderRepo.lockByOutTradeNo(tx.getOutTradeNo());
if (order.isPaid()) {
return;
}
if (!order.subMchid().equals(tx.getSubMchid())) {
throw new SecurityException("支付主体不一致");
}
if (order.amount() != tx.getAmount().getTotal()) {
throw new SecurityException("支付金额不一致");
}
order.markPaid(tx.getTransactionId(), tx.getSuccessTime());
payOrderRepo.save(order);
outboxRepo.append("PAYMENT_PAID", order.toEvent());
}Key is DB unique constraints & state transition constraints: unique indexes on transaction_id, out_trade_no, callback notification ID; PAID only from allowed prior states; downstream actions (ship, issue card, send coupon) driven via outbox events to avoid long transactions in callback thread.
If callback missing, background polls at 5s, 30s, 1m, 3m backoff; Native cashier frontend may poll own query API. Final truth always server-side query & callback.
Part 7: Refund & Reconciliation — True Scalability Test
Refund must use original payment order snapshot's sub_mchid, not current routing. Store may have transferred, entity deactivated, config switched — historical refund liability unchanged.
Service provider does not automatically have refund permission for all sub-merchants; requires sub-merchant authorization per official flow. Before refund: query original order confirm paid. Refund API returns "accepted" not final success; final state via refund callback or query. Partial refunds must ensure cumulative ≤ original amount, each refund uses stable unique refund ID.
Reconciliation as three-level structure:
Payment-order level : internal payment order vs WeChat transaction line-by-line — amount, status, sub_mchid, transaction time.
Store level : aggregate by store_id — payment, refund, discount, fee — for daily store closing.
Entity level : aggregate by subject_id, sub_mchid — settlement caliber for finance confirmation & exception handling.
WeChat service provider can request trade/fund bills after 10 AM T+1. Download link short-lived, request signed per APIv3, verify checksum after download. Bill's sub-merchant ID is key dimension for multi-entity reconciliation.
Reconciliation is not generating an "amounts match" report but continuously handling four discrepancy types: WeChat success / business not; business success / WeChat not; amount or entity mismatch; system cannot find matching order. Each type needs auto-fix strategy, manual ticket, audit trail.
Part 8: Three Business Models — Routing Differs Completely
Case 1: One Entity, Many Direct Stores — single sub_mchid. Routing selects app/device/product per store/cashier. Focus: operational data isolation — every order must stably record store_id, device_id for shift handover, cashier permissions, daily close, performance stats. Don't apply for many merchant IDs just for "finance per store"; internal profit-sharing via internal ledger first.
Case 2: One Brand, Many Franchise Entities — unified mini-program/SaaS, each franchisee a sub-merchant. Order routes to sub_mchid per fulfillment store. Critical: prevent config drift. On franchisee exit/transfer/change, create new entity + route version with explicit cutover time. Pre-cutover orders stay with old entity; post-cutover new orders go to new entity; historical refunds follow original payment snapshot. Store-entity relation never overwritten.
Case 3: Mall Counter & Mixed Lease — same physical store has self-operated, joint-operated, independent tenants. Cashier location cannot dictate payee; must identify transaction relationship at product/business-order level. If single receipt belongs to one entity, verify product entity consistency before settlement, generate single payment order. If cart spans multiple independent entities, split payments (or evaluate combined payment) — never collect whole into one mall sub-merchant then offline "clear", breaking fund/order/invoice consistency.
Part 9: Pre-Launch Checklist — 10 Items
Can every order uniquely answer: who sells, which store fulfills, which cashier initiates, which account receives?
Is payment routing versioned? Do historical payments, refunds, queries forever use snapshot?
Are sub_mchid, AppID, OpenID, payment scene locally consistency-checked?
Do payment auth directory, callback domain, certs, public key, APIv3 key have expiry alerts?
Do order, callback, query, refund all have idempotency keys and state machine protection?
Does frontend success page only show query result, not directly modify business order?
Has service provider obtained required payment & refund authorizations?
Does store entity switch support dual-version, canary validation, quick rollback?
Can T+1 reconciliation locate entity, store, order, discrepancy reason?
Do logs retain WeChat Pay Request-ID for troubleshooting while avoiding private keys, APIv3 keys, full OpenIDs, sensitive onboarding data?
Payment system maturity shows not in happy path but in entity switch, callback loss, duplicate notification, refund timeout, bill discrepancy — whether system still gives unique, auditable answer.
Closing: Three Bottom Lines
Multi-store, multi-entity collection is not a channel integration project but an operating architecture engineering. WeChat service provider model gives unified tech access and independent sub-merchant collection, but platform must still solve entity modeling, store affiliation, cashier config, route versioning, payment state machine, refund auth, financial reconciliation. APIs are the last mile; real moat is productizing, configurating, auditing these capabilities.
If only one lesson: Determine fund ownership first, then create business order; freeze payment routing first, then call channel API; verify server-side facts first, then drive fulfillment. Hold these three, multi-entity system earns right to scale.
References
WeChat Pay Service Provider Access Guide
Service Provider Development Required Parameters
JSAPI/Mini Program Order API
Configure JSAPI Payment Authorization Directory
Payment Callback & Query Implementation Guide
Order Refund Development Guide
Service Provider Bill Download Development Guide
WeChat Pay APIv3 Java Official SDK
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Niu Liu
A slightly rustic name 🤠 A tech veteran navigating the internet wave Hardcore tech: fixing all bugs and tough challenges
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
