Channel Adapter Architecture
How ItPay integrates with real-world payment channels — WeChat Pay, Alipay, PromptPay, and beyond.
ItPay's core innovation is presenting a unified API over fundamentally different channel protocols. WeChat Pay uses RSA-SHA256 certificates and AES-256-GCM encrypted callbacks; Alipay uses RSA2 asymmetric signing with form-encoded notifications; SEA channels use EMVCo QR with their own TLV tag layouts. The Channel Adapter pattern abstracts each into a common interface.
The Adapter Interface
Every channel adapter implements a common Go interface:
type ChannelAdapter interface {
// Create a QR code charge for the given payment request
CreateQRCharge(ctx context.Context, req *QRChargeRequest) (*QRChargeResponse, error)
// Query the status of an in-flight payment
QueryPayment(ctx context.Context, paymentID string) (*PaymentStatus, error)
// Process a refund against a completed payment
Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error)
// Verify a callback/webhook notification from the channel
VerifyCallback(ctx context.Context, headers map[string]string, body []byte) (*CallbackResult, error)
// Return channel metadata
ChannelInfo() *ChannelInfo
}
Channel Profiles
WeChat Pay (微信支付)
API Version: APIv3 (current/recommended). APIv2 is legacy (XML, HMAC-SHA256, being phased out globally).
QR Standard: Proprietary. Not EMVCo-compliant.
The POST /v3/pay/transactions/native endpoint returns a code_url in the format:
weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00
This is a WeChat-internal URL scheme — it is not an EMVCo QR payload. The merchant must convert this URL into a standard QR Code (ISO/IEC 18004) image. When the user scans it with WeChat, the app recognizes the weixin:// scheme and opens the payment confirmation screen.
QR Validity: 2 hours (fixed). After expiry, a new code_url must be requested.
Credentials:
| Credential | Format | Purpose |
|---|---|---|
mchid | Numeric string | Merchant ID issued by WeChat Pay |
appid | wx + hex string | WeChat App ID (service account / mini-program) |
| API certificate | RSA 2048-bit (apiclient_key.pem) | Signs outgoing requests |
| Certificate serial | Hex string | Identifies which cert signed the request |
| APIv3 Key | 32-byte string | Decrypts AES-256-GCM callback payloads |
| WeChat Pay public key | RSA 2048-bit | Verifies WeChat Pay's response signatures |
Request Signing:
Authorization: WECHATPAY2-SHA256-RSA2048
mchid="1230000109",
nonce_str="7K8oQ9iJ1s",
timestamp="1715712345",
serial_no="3775B6A45ACD48A6B",
signature="<RSA-SHA256 of method\\nurl\\ntimestamp\\nnonce\\nbody\\n>"
The signing payload is: HTTP_METHOD\nURL_PATH\nTIMESTAMP\nNONCE\nBODY\n (with newlines). Signed with the merchant's API certificate private key.
Callback (支付通知):
WeChat Pay sends POST to notify_url with an AES-256-GCM encrypted payload:
{
"id": "EV-2018022511223320873",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "<base64>",
"nonce": "<16 chars>",
"associated_data": ""
}
}
Verification requires two steps:
-
Verify signature — Check HTTP headers:
Wechatpay-Serial— identifies which platform cert/public key to useWechatpay-Signature— Base64 RSA-SHA256 oftimestamp\nnonce\nbody\n- Must verify against WeChat Pay's public key (not merchant's)
-
Decrypt payload — Use APIv3 key + nonce + associated_data to AES-256-GCM decrypt
ciphertext
Service Provider Mode (服务商):
POST /v3/pay/partner/transactions/native
Uses sp_appid + sp_mchid (service provider's own credentials) + sub_mchid (sub-merchant's ID). Payment flow is identical from the user's perspective, but settlement goes to the sub-merchant's account.
Refunds:
POST /v3/refund/domestic/refunds
- Max 50 partial refunds per order
- 365-day refund window from payment date
- 150 QPS success rate limit
- Fee refunded proportionally
Alipay (支付宝)
API Version: OpenAPI (Gateway: openapi.alipay.com)
QR Standard: EMVCo-compliant (Merchant Presented QR).
The alipay.trade.precreate API returns a qr_code string that is a fully-formed EMVCo Merchant Presented QR payload (TLV-encoded). The merchant can render this directly as a QR Code image — any EMVCo-compliant scanner can read it. This is different from WeChat Pay's proprietary URL scheme.
qr_code = "00020101021229370016A000000677010112..."
QR Validity: Default varies, typically configurable. Recommended: 15 minutes for dynamic QR.
Credentials:
| Credential | Format | Purpose |
|---|---|---|
app_id | Numeric string | App ID from Alipay Open Platform |
| Merchant private key | RSA2 2048-bit (PKCS8) | Signs all outgoing requests |
| Alipay public key | RSA2 2048-bit | Verifies all responses and callbacks |
app_auth_token (ISV) | String | Delegated authorization token |
Asymmetric signing model — Alipay uses RSA2 (SHA256withRSA) exclusively. Both sides sign with their own private key; both sides verify with the other's public key:
// Merchant signs request with its RSA2 private key
// Alipay verifies with merchant's public key (uploaded to Alipay dashboard)
// Alipay signs response with its RSA2 private key
// Merchant verifies with Alipay public key (downloaded from dashboard)
Request Signing:
Alipay uses a key-value concatenation method (not structured JSON signing like Stripe/WeChat):
- Build request params:
app_id,method,format,charset,sign_type,timestamp,version,biz_content(JSON) - Remove
sign,sign_typefrom the param set - Sort remaining params alphabetically by key
- Concatenate as
key=value&key2=value2(URL-encode values) - Sign with RSA2 private key
- Append
signandsign_type=RSA2to the request
Callback (异步通知):
Alipay sends a POST to the merchant's notify_url with form-encoded parameters (not JSON). The sign field is the RSA2 signature of the sorted param string.
Verification:
- Remove
sign,sign_typefrom the received params - Sort remaining params alphabetically by key
- Concatenate as
key=value&key2=value2 - Verify the
signvalue using Alipay's public key (RSA2) - Must respond with
success(plain text) to acknowledge; otherwise Alipay retries up to 7 times
ISV Model (服务商/ISV):
- Merchant authorizes the ISV's app on Alipay Open Platform
- ISV receives an
app_auth_tokenvia OAuth-style authorization callback - All API calls include
app_auth_tokenin the request params - ISV signs with its own private key, but the token identifies which merchant the action is for
Refunds:
alipay.trade.refund
- Full or partial refunds supported
- Fee refunded proportionally (same as WeChat)
- Refund window: 90-365 days depending on industry
- Async: refund result delivered via callback
Alipay A2A (Agent-to-Agent):
Alipay's A2A protocol is a specialized flow where two Alipay users (applications) can initiate payments between each other. It does not replace the standard QR flow — it's an additional integration mode for app-to-app payments. The QR format is still EMVCo-based with different TLV tags.
PromptPay (Thailand)
QR Standard: EMVCo-compliant (Thai QR Payment Standard — Tag 26-51 country-specific TLVs)
PromptPay is a national QR standard administered by the Bank of Thailand. All Thai banks support it. The QR payload follows EMVCo with Thai-specific TLVs in the country-specific region (Tag 26-51).
Integration:
Merchants generate QR through their bank's API or through a PromptPay-registered service provider. The QR payload contains the merchant's PromptPay ID (tax ID or phone number + national ID) and the static/dynamic amount.
Callback: Bank-specific webhook patterns. No unified callback protocol like WeChat/Alipay — callbacks vary by acquiring bank.
PayNow (Singapore)
QR Standard: SGQR-combined (EMVCo with Singapore-specific TLVs)
PayNow is Singapore's national fast payment system, managed by the Association of Banks in Singapore (ABS). SGQR is the unified QR standard that can encode multiple payment schemes in a single QR.
DuitNow QR (Malaysia)
QR Standard: EMVCo-compliant (DuitNow-specific TLVs in country-specific region)
DuitNow is Malaysia's national QR standard, managed by Payments Network Malaysia (PayNet). The QR payload follows EMVCo with Malaysian-specific TLVs.
Security Model Comparison
| Aspect | WeChat Pay APIv3 | Alipay OpenAPI | ItPay Protocol |
|---|---|---|---|
| Signing algorithm | RSA-SHA256 | RSA2 (SHA256withRSA) | HMAC-SHA256 |
| Signing payload | method\nurl\nts\nnonce\nbody\n | Sorted key=value pairs | ts + method + path + body |
| Auth header | Authorization: WECHATPAY2-SHA256-... | sign param in request body | Authorization: ItPay {id}:{sig} |
| Key type | Asymmetric (certificate) | Asymmetric (RSA2) | Symmetric (HMAC secret) |
| Callback format | AES-256-GCM encrypted JSON | Form-encoded signed params | HMAC-signed JSON (optional E2EE) |
| Callback verification | Verify + decrypt (2 steps) | RSA2 verify of sorted params | HMSA verify (1 step) |
| Idempotency | Via id field | Via out_trade_no | Via Idempotency-Key header |
Channel Adapter Implementation Pattern
Each channel adapter in ItPay follows the same skeleton:
┌──────────────────────────────────────────────┐
│ ItPay API Gateway │
│ (Unified API for all merchants) │
├──────────┬──────────┬──────────┬──────────────┤
│ WeChat │ Alipay │PromptPay │ PayNow / │
│ Adapter │ Adapter │ Adapter │ DuitNow / .. │
├──────────┴──────────┴──────────┴──────────────┤
│ Channel-Specific API Calls │
│ 🔴 WeChat: RSA-SHA256 + AES-GCM callbacks │
│ 🔵 Alipay: RSA2 + form-encoded callbacks │
│ 🟢 SEA Banks: EMVCo QR + bank webhooks │
└──────────────────────────────────────────────┘
Each adapter handles:
| Responsibility | Description |
|---|---|
| Credential management | Store and rotate channel-specific keys/certs |
| Request signing | Transform ItPay's HMAC-signed request into the channel's signing format |
| QR generation | Call the channel's QR creation API, return the raw QR data or URL |
| Callback translation | Receive channel callbacks, verify, decrypt, translate to ItPay's webhook format |
| Error mapping | Map channel error codes to ItPay's unified error model |
| Retry logic | Channel-specific retry schedules and idempotency handling |
| Refund routing | Call channel refund API, map status back to ItPay's refund state machine |
Key Implementation Details
WeChat Pay → ItPay Mapping
| ItPay Concept | WeChat Pay APIv3 Equivalent |
|---|---|
POST /v1/payment-intents | POST /v3/pay/transactions/native |
| QR code | code_url (render as QR image) |
Webhook payment.succeeded | Decrypted TRANSACTION.SUCCESS callback |
| Refund | POST /v3/refund/domestic/refunds |
| Service provider | POST /v3/pay/partner/transactions/native with sub_mchid |
| Install (auto-pay) | JSAPI + contract (签约) API |
Alipay → ItPay Mapping
| ItPay Concept | Alipay Equivalent |
|---|---|
POST /v1/payment-intents | alipay.trade.precreate |
| QR code | qr_code (EMVCo TLV — render directly) |
Webhook payment.succeeded | Verified async notification (form-encoded) |
| Refund | alipay.trade.refund |
| Service provider | app_auth_token in every request |
| Install (auto-pay) | Alipay agreement (免密支付) API |
Service Provider (服务商) Mode
For both WeChat Pay and Alipay, ItPay operates as a service provider/platform — merchants are sub-merchants who authorize ItPay to process payments on their behalf.
WeChat Pay 服务商 Flow:
1. ItPay (服务商) applies for service provider status with WeChat Pay
2. Sub-merchants onboard via WeChat's sub-merchant creation API
3. ItPay calls partner endpoints with sp_mchid + sub_mchid
4. Each sub-merchant configures their settlement account
5. ItPay can optionally enable profit sharing (分账)
Alipay ISV Flow:
1. ItPay registers as an ISV on Alipay Open Platform
2. Merchant authorizes ItPay's app (receives app_auth_token)
3. ItPay includes app_auth_token in every API call for that merchant
4. Settlement goes directly to merchant's Alipay account
5. ItPay collects platform fees via separate invoicing or channel split
Adapter Flow Diagrams
Generic Adapter Pattern Swimlane
ItPay Core Channel Adapter External Channel Customer
│ │ │ │
│ 1. PaymentIntent │ │
│ ───────────────────►│ │ │
│ │ │ │
│ │ 2. Translate: │ │
│ │ ItPay → Channel │ │
│ │ • Map fields │ │
│ │ • Sign request │ │
│ │ • Build headers │ │
│ │ ──┐ │ │
│ │ │ │ │
│ │ 3. Channel API Call │ │
│ │ ───────────────────►│ │
│ │ │ │
│ │ 4. Raw Response │ │
│ │ ◄───────────────────│ │
│ │ │ │
│ │ 5. Translate: │ │
│ │ Channel → ItPay │ │
│ │ • Parse response │ │
│ │ • Map error codes │ │
│ │ • Normalize QR │ │
│ │ ──┐ │ │
│ │ │ │ │
│ 6. Normalized │ │ │
│ Response │ │ │
│ ◄───────────────────│ │ │
│ │ │ │
│ │ │ 7. Customer │
│ │ │ scans QR │
│ │ │ ◄────────────────│
│ │ │ │
│ │ │ 8. Payer │
│ │ │ authorizes │
│ │ │ ◄────────────────│
│ │ │ │
│ │ 9. Callback/Webhook │ │
│ │ ◄───────────────────│ │
│ │ │ │
│ │10. Verify + Decrypt │ │
│ │ • Validate sig │ │
│ │ • Decrypt payload │ │
│ │ • Translate event │ │
│ │ ──┐ │ │
│ │ │ │ │
│11. Normalized │ │ │
│ Webhook Event │ │ │
│ ◄───────────────────│ │ │
Translation Logic (Internal)
// ItPay internal representation
type QRChargeRequest struct {
Amount Money
Description string
Channel string
Expiry time.Duration
Metadata map[string]string
Idempotency string
}
// Translated to channel-specific call
type ChannelAPIRequest struct {
URL string
Method string
Headers map[string]string
Body []byte
SigningKey interface{}
}
WeChat Pay Adapter: Request Flow
ItPay Core WeChat Adapter WeChat Pay API
│ │ │
│ POST PaymentIntent │
│ { │ │
│ amount: 699 CNY, │ │
│ description: ... │ │
│ metadata: {...} │ │
│ } │ │
│ ───────────────────►│ │
│ │ │
│ │ Map fields: │
│ │ amount → total_fee: 699 │
│ │ id → out_trade_no: pi_... │
│ │ description → description │
│ │ notify_url → (adapter's │
│ │ internal callback URL) │
│ │ ──┐ │
│ │ │ │
│ │ Build Authorization: │
│ │ method = "POST" │
│ │ url = "/v3/pay/transactions/native" │
│ │ timestamp = now() │
│ │ nonce = random_str() │
│ │ body = json_body │
│ │ sign_str = "POST\n/v3/pay/transactions/native\n{ts}\n{nonce}\n{body}\n" │
│ │ signature = RSA_SHA256(sign_str, private_key) │
│ │ ──┐ │
│ │ │ │
│ │ HTTP request: │
│ │ POST /v3/pay/transactions/native │
│ │ ───────────────────► │
│ │ Authorization: WECHATPAY2-SHA256-RSA2048 │
│ │ mchid="1230000109", │
│ │ nonce_str="7K8oQ9iJ1s", │
│ │ timestamp="1715712345", │
│ │ serial_no="3775B6A45ACD48A6B", │
│ │ signature="<base64>" │
│ │ Content-Type: application/json │
│ │ Accept: application/json │
│ │ User-Agent: WeChatPay-Ipay-Adapter/1.0 │
│ │ │
│ │ Response (201 Created): │
│ │ ◄───────────────────│
│ │ { │
│ │ "code_url": "weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00", │
│ │ "prepay_id": "wx201410272009395544657a690389285100" │
│ │ } │
│ │ │
│ │ Normalize response: │
│ │ qr_code: "weixin://wxpay/bizpayurl/up?..." │
│ │ status: "pending" │
│ │ channel_id: wx_pid │
│ │ type: "wechat_url" │
│ │ ──┐ │
│ │ │ │
│ Normalized Response │ │
│ ◄───────────────────│ │
│ { │ │
│ qr_code: "weixin:...", │
│ status: "pending", │ │
│ expires_at: ... │ │
│ } │ │
Alipay Adapter: Request Flow
ItPay Core Alipay Adapter Alipay OpenAPI
│ │ │
│ POST PaymentIntent │
│ { │ │
│ amount: 699 CNY, │ │
│ description: ... │ │
│ metadata: {...} │ │
│ } │ │
│ ───────────────────►│ │
│ │ │
│ │ Map fields: │
│ │ amount → total_amount: 699.00 │
│ │ id → out_trade_no: pi_... │
│ │ description → subject │
│ │ notify_url → (adapter's │
│ │ internal callback URL) │
│ │ ──┐ │
│ │ │ │
│ │ Build signed params: │
│ │ app_id: "2021000123456789" │
│ │ method: "alipay.trade.precreate"│
│ │ format: "JSON" │
│ │ charset: "utf-8" │
│ │ sign_type: "RSA2" │
│ │ timestamp: "2026-05-27 09:30:00"│
│ │ version: "1.0" │
│ │ biz_content: { │
│ │ out_trade_no: "pi_01J7XZ...", │
│ │ total_amount: "699.00", │
│ │ subject: "AI Summary", │
│ │ store_id: "itpay_merchant_1", │
│ │ qr_code_timeout_express: "15m"│
│ │ } │
│ │ ── sort keys alphabetically │
│ │ ── concat as key=value&... │
│ │ ── sign with RSA2 private key │
│ │ ── append sign param │
│ │ ──┐ │
│ │ │ │
│ │ HTTP request: │
│ │ POST https://openapi.alipay.com/gateway.do │
│ │ Content-Type: application/x-www-form-urlencoded │
│ │ ───────────────────► │
│ │ app_id=2021000123456789 │
│ │ &method=alipay.trade.precreate │
│ │ &format=JSON │
│ │ &charset=utf-8 │
│ │ &sign_type=RSA2 │
│ │ ×tamp=2026-05-27+09%3A30%3A00 │
│ │ &version=1.0 │
│ │ &biz_content=%7B%22out_trade_no%22%3A... │
│ │ &sign=<base64 RSA2 signature> │
│ │ │
│ │ Response: │
│ │ ◄───────────────────│
│ │ { │
│ │ "alipay_trade_precreate_response": { │
│ │ "code": "10000", │
│ │ "msg": "Success", │
│ │ "out_trade_no": "pi_01J7XZ...", │
│ │ "qr_code": "00020101021229370016A000000677010112..." │
│ │ }, │
│ │ "sign": "<alipay RSA2 signature>" │
│ │ } │
│ │ │
│ │ Verify Alipay response signature: │
│ │ 1. Remove "sign" from root │
│ │ 2. Sort remaining keys │
│ │ 3. Concatenate key=value&... │
│ │ 4. Verify with Alipay public key │
│ │ ──┐ │
│ │ │ │
│ │ Normalize response: │
│ │ qr_code: "000201010212..." │
│ │ status: "pending" │
│ │ channel_id: alipay_trade_no │
│ │ type: "emvco_tlv" │
│ │ ──┐ │
│ │ │ │
│ Normalized Response │ │
│ ◄───────────────────│ │
│ { │ │
│ qr_code: "000201...", │
│ status: "pending", │ │
│ expires_at: ... │ │
│ } │ │
Callback/Webhook Translation Flow
When the payment channel notifies ItPay, the adapter reverses the translation:
Channel Adapter ItPay Core Merchant
│ │ │ │
│ (WeChat) POST callback │ │ │
│ ───────────────────► │ │ │
│ Headers: │ │ │
│ Wechatpay-Serial: 3775B6A45ACD48A6B │ │
│ Wechatpay-Signature: <base64> │ │
│ Wechatpay-Timestamp: 1715712345 │ │
│ Wechatpay-Nonce: 8K7oQ9iJ1s │ │
│ Body: (AES-256-GCM encrypted JSON) │ │
│ │ │ │
│ │ Step 1: Verify signature │
│ │ sign_str = "1715712345\n8K7oQ9iJ1s\n<body>\n" │
│ │ pub_key = lookup cert by serial 3775B6A... │
│ │ ok = RSA_verify(sign_str, signature, pub_key) │
│ │ ──┐ │ │
│ │ │ │ │
│ │ Step 2: Decrypt payload │
│ │ resource.ciphertext → AES-256-GCM │
│ │ decrypt(apiv3_key, nonce, aad) │
│ │ → { │
│ │ "out_trade_no": "pi_01J7XZ...", │
│ │ "transaction_id": "4200001234", │
│ │ "trade_state": "SUCCESS", │
│ │ "amount": { total: 699, ... } │
│ │ } │
│ │ ──┐ │ │
│ │ │ │ │
│ │ Step 3: Translate to ItPay format │
│ │ event_type: "payment_intent.succeeded" │
│ │ data.id: "pi_01J7XZ..." │
│ │ data.amount.value: 699 │
│ │ data.amount.currency: "CNY" │
│ │ data.channel: "wechat" │
│ │ ──┐ │ │
│ │ │ │ │
│ (Alipay) POST callback │ │ │
│ ───────────────────► │ │ │
│ Content-Type: application/x-www-form-urlencoded │
│ Body: (form-encoded params with `sign`) │
│ │ │ │
│ │ Step 1: Verify signature │
│ │ Remove sign, sign_type │
│ │ Sort remaining params │
│ │ Verify RSA2 with Alipay public key │
│ │ ──┐ │ │
│ │ │ │ │
│ │ Step 2: Translate to ItPay format │
│ │ trade_status: "TRADE_SUCCESS" │
│ │ → event_type: "payment_intent.succeeded" │
│ │ data.id: out_trade_no (pi_01J7XZ...) │
│ │ data.amount.value: buyer_pay_amount │
│ │ ──┐ │ │
│ │ │ │ │
│ │ Acknowledge to channel: │
│ │ WeChat: HTTP 200 "{\"code\":\"SUCCESS\"}" │
│ │ Alipay: HTTP 200 "success" (plain text) │
│ │ ───────────────────►│(channel) │
│ │ │ │
│ Normalized Webhook │ │ │
│ ◄───────────────────│ │ │
│ │ │ │
│ │ Webhook to Merchant │
│ │ ────────────────────────────────────►│
│ │ X-ItPay-Signature: t=...,v1=... │
│ │ Body: { type: \"payment_intent.succeeded\", data: {...} } │
Normalized Output Format (Adapter → ItPay Core)
// Every adapter returns this normalized structure
type QRChargeResponse struct {
QRCode string // WeChat: "weixin://..." URL
// Alipay: "000201010212..." EMVCo TLV
// PromptPay: EMVCo TLV with Thai TLVs
ChannelID string // WeChat: prepay_id or transaction_id
// Alipay: trade_no
Status string // "pending" | "succeeded" | "failed"
ExpiresAt time.Time
Raw json.RawMessage // Original channel response (for debugging)
}
// Every adapter returns this normalized callback structure
type CallbackResult struct {
EventType string // Mapped to ItPay event type
PaymentID string // ItPay's internal payment intent ID
Channel string // Channel name
Amount Money // Payer amount in local currency
ChannelTx string // Channel's transaction ID
PayerID string // Channel-specific payer identifier
Raw json.RawMessage // Original decrypted callback payload
}
Next Steps
- Security Model — Protocol-level security architecture
- API Key Management — Managing multi-channel credentials
- Deliveries (Digital Goods) — Post-payment fulfillment
- Channel Matrix — Full per-country integration requirements