Chapter 8: The Money Printer Starts! ECPay Payment Integration and Webhook Order Callback Processing
In the business world, there's a harsh reality: If a website can't accept payments, no matter how beautiful its animations are or how perfect its architecture is, it's merely a "portfolio piece." Once it successfully integrates payment processing and can collect money 24/7, it officially upgrades into a golden-egg-laying "Digital Asset."
In Taiwan, the most stable and market-dominant system for establishing online credit card payments is ECPay (Green World FinTech).
However, ECPay's official documentation often reads like an obscure ancient text for novice engineers. Particularly because it requires using an extremely cumbersome SHA256 hash and sorting encryption mechanism.
Many engineers get stuck at this stage for weeks before giving up. But in the Vibe Tutor enterprise-grade source code, we've already navigated all the pitfalls for you and encapsulated the complex encryption logic into clean Next.js API Routes!
🎯 Chapter Objectives
- Understand ECPay payment flow principles and security mechanisms (why HashKey is needed).
- Decrypt the frontend checkout API (
/api/ecpay/checkout) encryption and auto-redirect flow. - Master asynchronous Webhook callback mechanisms (
/api/ecpay/return) to ensure no orders are missed.
🛒 Step 1: Frontend Form Encryption and Submission (Checkout Route)
When a user excitedly clicks the "💳 Buy VIP Pass Now" button on your pricing page, what happens?
The frontend form data (including courseId, price, tier) is sent via HTTP POST to our backend API Route: src/app/api/ecpay/checkout/route.ts.
In this critical API, we must accomplish three major tasks server-side:
- Verify user login status: If not logged in, block the payment. Because after payment, the system wouldn't know which account to assign this premium VIP course to.
- Generate unique order ID (MerchantTradeNo): Create an absolutely non-duplicative string (e.g.,
VIBE+ timestamp1710234567), which serves as the sole proof for future reconciliation. - Package ECPay parameters and encrypt: This includes product name, amount, ReturnURL (callback URL), and most importantly, the anti-tampering check code.
🛡️ Core Challenge: Calculating CheckMacValue (Anti-Tampering Check Code)
Why can't we just send the amount directly to ECPay? Because without encryption during transmission, tech-savvy hackers could intercept packets and change "NT$9,999" to "NT$1", causing ECPay to foolishly charge NT$1 while reporting payment success.
To solve this, ECPay requires us to take all parameters plus the exclusive secret HashKey and HashIV (known only to you and ECPay), sort them according to extremely strict rules, and perform SHA256 encryption.
In our source code, this headache-inducing encryption/decryption has been perfectly encapsulated:
import crypto from 'crypto';
// 1. Sort all parameters to be sent to ECPay alphabetically A-Z
const sortedKeys = Object.keys(params).sort();
// 2. Place password (HashKey) at the beginning, then concatenate parameters
let checkValue = `HashKey=${process.env.ECPAY_HASH_KEY}`;
for (const key of sortedKeys) {
checkValue += `&${key}=${params[key]}`;
}
// Place password (HashIV) at the end
checkValue += `&HashIV=${process.env.ECPAY_HASH_IV}`;
// 3. URLEncode and convert case (ECPay's most common pitfall)
checkValue = encodeURIComponent(checkValue).toLowerCase();
checkValue = checkValue.replace(/%2d/g, '-').replace(/%5f/g, '_').replace(/%2e/g, '.').replace(/%21/g, '!');
// 4. Perform powerful SHA256 hash encryption, convert to uppercase
const CheckMacValue = crypto.createHash('sha256').update(checkValue).digest('hex').toUpperCase();
// Finally, insert the calculated CheckMacValue into parameters
params.CheckMacValue = CheckMacValue;
This code runs on the server-side (Node.js), so your HashKey will never leak to frontend users.
After calculating the check code, our API returns a hidden HTML <form> containing all fields, with a small script to make the browser "auto-submit" and redirect to ECPay's payment page.
📡 Step 2: ECPay Background Callback Mechanism (Webhook / Return URL)
This is where novice engineers most commonly get confused and where customer complaints frequently originate. When users enter their credit card details on ECPay, confirm payment, and successfully get charged, ECPay performs "two independent actions":
- ClientRedirectURL (Frontend Redirect): The user's browser redirects to your "Payment Success Thank You Page". 【Critical Warning】: Never write to the database on this page's logic! Because users might lose internet connection or accidentally close their browser right after successful payment, causing their money to be charged but their access never granted—leading to serious complaints.
- ReturnURL (Server Background Webhook): This is the truly stable mechanism! ECPay's server silently sends an HTTP POST request to your server in the background, notifying you that "this order has been paid".
This API that receives the decree is located at src/app/api/ecpay/return/route.ts:
export async function POST(request: Request) {
// Parse form data from ECPay
const text = await request.text();
const params = new URLSearchParams(text);
// 1. Check ECPay status code, '1' means credit card payment succeeded
const RtnCode = params.get('RtnCode');
if (RtnCode === '1') {
// 2. During checkout, we used ECPay's CustomField to secretly store user UID and course ID
const userId = params.get('CustomField1');
const itemId = params.get('CustomField2');
// 3. ⚠️ Security: Must verify ECPay's CheckMacValue using same logic to ensure request isn't forged!
// (Verification logic omitted here, refer to actual source code)
// 4. Write to Supabase database with highest privileges (Service Role)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Super key that bypasses RLS protection from last chapter
);
// Record purchase, unlock access!
await supabase.from('vt_purchases').insert({
user_id: userId,
item_id: itemId,
item_type: 'course',
price: parseInt(params.get('TradeAmt') || '0') // Record actual received amount
});
}
// 5. 【Extremely Important】 Regardless of success, must return plain text "1|OK" to ECPay
// If you don't, ECPay will think your server is down and keep retrying your API!
return new NextResponse('1|OK', {
headers: { 'Content-Type': 'text/plain' },
});
}
🧪 Step 3: Using ECPay Test Credit Cards and Environment Variable Pitfalls
During development and testing, ECPay provides special "test credit cards" allowing you to complete the full payment and Webhook flow without actual charges.
💳 Official Test Credit Card Info
When checking out, select credit card payment and enter:
- Card Number:
4311-9522-2222-2222 - Expiry: Any future date (e.g.,
12/30) - CVV:
222 - OTP: If SMS verification appears, simply enter
123456to proceed.
🚨 Novice Trap: ReturnURL and Localhost
When setting up Webhooks, the system automatically sends your URL as ReturnURL to ECPay.
Here's a deadly pitfall: When deploying to Vercel (production), never set the environment variable URL to http://localhost:3000!
ECPay's servers are on the public internet and absolutely cannot reach your "localhost". This causes:
- ECPay checkout page shows "Payment Successful".
- But ECPay fails to send Webhook to localhost.
- Your database never receives payment notification, users' courses remain locked, causing serious complaints!
Solution:
In Vercel's Environment Variables, ensure your code captures the production Vercel URL (e.g., https://vibe-tutor-web.vercel.app), so ECPay's Webhook signals can accurately reach your database!
✅ Chapter Summary
This is the most classic, stable, and bulletproof asynchronous order processing flow. This powerful Webhook mechanism ensures: even if users' phones die or internet drops, as long as the bank successfully charges their card, ECPay's servers will persistently and securely deliver the order success notification to our database, instantly unlocking the user's dream course!
Congratulations! Your money printer is now fully assembled and powered up. In the next chapter, we'll explore how "Gamification" design psychology in UI/UX can make users want to buy another course right after their first purchase!