Skip to main content

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:

CredentialFormatPurpose
mchidNumeric stringMerchant ID issued by WeChat Pay
appidwx + hex stringWeChat App ID (service account / mini-program)
API certificateRSA 2048-bit (apiclient_key.pem)Signs outgoing requests
Certificate serialHex stringIdentifies which cert signed the request
APIv3 Key32-byte stringDecrypts AES-256-GCM callback payloads
WeChat Pay public keyRSA 2048-bitVerifies 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:

  1. Verify signature — Check HTTP headers:

    • Wechatpay-Serial — identifies which platform cert/public key to use
    • Wechatpay-Signature — Base64 RSA-SHA256 of timestamp\nnonce\nbody\n
    • Must verify against WeChat Pay's public key (not merchant's)
  2. 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:

CredentialFormatPurpose
app_idNumeric stringApp ID from Alipay Open Platform
Merchant private keyRSA2 2048-bit (PKCS8)Signs all outgoing requests
Alipay public keyRSA2 2048-bitVerifies all responses and callbacks
app_auth_token (ISV)StringDelegated 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):

  1. Build request params: app_id, method, format, charset, sign_type, timestamp, version, biz_content (JSON)
  2. Remove sign, sign_type from the param set
  3. Sort remaining params alphabetically by key
  4. Concatenate as key=value&key2=value2 (URL-encode values)
  5. Sign with RSA2 private key
  6. Append sign and sign_type=RSA2 to 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:

  1. Remove sign, sign_type from the received params
  2. Sort remaining params alphabetically by key
  3. Concatenate as key=value&key2=value2
  4. Verify the sign value using Alipay's public key (RSA2)
  5. Must respond with success (plain text) to acknowledge; otherwise Alipay retries up to 7 times

ISV Model (服务商/ISV):

  1. Merchant authorizes the ISV's app on Alipay Open Platform
  2. ISV receives an app_auth_token via OAuth-style authorization callback
  3. All API calls include app_auth_token in the request params
  4. 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

AspectWeChat Pay APIv3Alipay OpenAPIItPay Protocol
Signing algorithmRSA-SHA256RSA2 (SHA256withRSA)HMAC-SHA256
Signing payloadmethod\nurl\nts\nnonce\nbody\nSorted key=value pairsts + method + path + body
Auth headerAuthorization: WECHATPAY2-SHA256-...sign param in request bodyAuthorization: ItPay {id}:{sig}
Key typeAsymmetric (certificate)Asymmetric (RSA2)Symmetric (HMAC secret)
Callback formatAES-256-GCM encrypted JSONForm-encoded signed paramsHMAC-signed JSON (optional E2EE)
Callback verificationVerify + decrypt (2 steps)RSA2 verify of sorted paramsHMSA verify (1 step)
IdempotencyVia id fieldVia out_trade_noVia 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:

ResponsibilityDescription
Credential managementStore and rotate channel-specific keys/certs
Request signingTransform ItPay's HMAC-signed request into the channel's signing format
QR generationCall the channel's QR creation API, return the raw QR data or URL
Callback translationReceive channel callbacks, verify, decrypt, translate to ItPay's webhook format
Error mappingMap channel error codes to ItPay's unified error model
Retry logicChannel-specific retry schedules and idempotency handling
Refund routingCall channel refund API, map status back to ItPay's refund state machine

Key Implementation Details

WeChat Pay → ItPay Mapping

ItPay ConceptWeChat Pay APIv3 Equivalent
POST /v1/payment-intentsPOST /v3/pay/transactions/native
QR codecode_url (render as QR image)
Webhook payment.succeededDecrypted TRANSACTION.SUCCESS callback
RefundPOST /v3/refund/domestic/refunds
Service providerPOST /v3/pay/partner/transactions/native with sub_mchid
Install (auto-pay)JSAPI + contract (签约) API

Alipay → ItPay Mapping

ItPay ConceptAlipay Equivalent
POST /v1/payment-intentsalipay.trade.precreate
QR codeqr_code (EMVCo TLV — render directly)
Webhook payment.succeededVerified async notification (form-encoded)
Refundalipay.trade.refund
Service providerapp_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 │
│ │ &timestamp=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