Taiwan Payments for Indie Devs: ECPay Integration Guide
2026-02-26 · Jazz Lien
Why ECPay Over Stripe?
Stripe works in Taiwan, but consumers don't use it. ECPay (綠界) is what Taiwanese expect:
- Credit card (Visa, Mastercard, JCB)
- ATM transfer (most common for larger amounts)
- 超商代碼 (convenience store payment codes)
- LINE Pay
- Apple Pay / Google Pay
If you're selling to Taiwanese customers, ECPay removes friction.
Registration
As Individual (個人)
Start here. Requirements:
- Taiwan ID or ARC
- Taiwan bank account
- Phone number
Limitations:
- Lower transaction limits
- Some payment methods restricted
As Business (行號/公司)
Upgrade when revenue hits NT$240K/year (tax threshold). Requirements:
- Business registration
- 統一編號 (tax ID)
- Business bank account
Benefits:
- Higher limits
- All payment methods
- Invoice integration (電子發票)
API Integration
ECPay uses form-encoded POST requests. No SDK required — good news for Edge Runtime.
Creating a Payment
const params = {
MerchantID: process.env.ECPAY_MERCHANT_ID,
MerchantTradeNo: `ORDER${Date.now()}`, // Unique per transaction
MerchantTradeDate: formatDate(new Date()), // yyyy/MM/dd HH:mm:ss
PaymentType: 'aio',
TotalAmount: 299,
TradeDesc: 'Glimmer Credits',
ItemName: '1 Credit Pack',
ReturnURL: 'https://yoursite.com/api/ecpay/webhook',
ClientBackURL: 'https://yoursite.com/payment/complete',
ChoosePayment: 'ALL', // Let user choose
EncryptType: 1,
};
// Add CheckMacValue
params.CheckMacValue = generateCheckMac(params);
// Redirect user to ECPay
const form = new URLSearchParams(params);
// Return HTML form that auto-submits to ECPay
CheckMacValue Generation
ECPay verifies requests with SHA256 hash:
function generateCheckMac(params: Record<string, string>): string {
// 1. Sort keys alphabetically
const sorted = Object.keys(params)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
// 2. Wrap with HashKey and HashIV
const raw = `HashKey=${HASH_KEY}&${sorted}&HashIV=${HASH_IV}`;
// 3. URL encode
const encoded = encodeURIComponent(raw).toLowerCase();
// 4. SHA256 hash, uppercase
return sha256(encoded).toUpperCase();
}
Webhook Verification (Edge-Compatible)
ECPay POSTs payment results to your webhook. Verify before trusting:
export async function POST(request: Request) {
const formData = await request.formData();
const params = Object.fromEntries(formData);
// Extract and remove CheckMacValue
const receivedMac = params.CheckMacValue;
delete params.CheckMacValue;
// Regenerate and compare
const expectedMac = generateCheckMac(params);
if (receivedMac !== expectedMac) {
return new Response('0|InvalidCheckMac', { status: 400 });
}
// Process payment
if (params.RtnCode === '1') {
// Payment successful
await addCredits(params.CustomField1, parseInt(params.CustomField2));
}
// ECPay expects "1|OK" response
return new Response('1|OK');
}
Edge Runtime SHA256
No crypto.createHash on Edge. Use Web Crypto:
async function sha256(message: string): Promise<string> {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
Testing
ECPay provides a staging environment:
- Staging Merchant ID:
3002607
- Staging HashKey/HashIV: provided in docs
Use staging for development. Switch to production credentials only after testing.
Test credit card:
- Number:
4311-9522-2222-2222
- Expiry: Any future date
- CVV:
222
Common Gotchas
1. MerchantTradeNo Must Be Unique
Each transaction needs a unique ID. Use timestamp + random:
const tradeNo = `${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
2. Date Format Is Specific
ECPay wants yyyy/MM/dd HH:mm:ss in Taiwan timezone:
function formatECPayDate(date: Date): string {
return date.toLocaleString('sv-SE', { timeZone: 'Asia/Taipei' })
.replace(/-/g, '/')
.replace('T', ' ');
}
3. ItemName Character Limit
Max 400 characters. Truncate if needed.
4. Webhook Retries
ECPay retries webhooks if you don't respond 1|OK. Make webhook handlers idempotent — check if payment already processed before adding credits.
Production Checklist
ECPay documentation (Chinese): https://www.ecpay.com.tw/Service/API_Dwnld