title: "Chapter 14: ECPay Payment Gateway in Action - Building a Real Money Printing Machine" description: "A beautifully built website that can't accept payments is just a showcase! In this chapter, we'll integrate Taiwan's most popular 'ECPay' payment gateway into your camping reservation site. A 6000-word deep dive into encryption algorithms, Server Callback (Webhooks) anti-forgery verification, plus 5 master-level Vibe Prompts to painlessly conquer the payment integration that gives traditional engineers headaches!" duration: "120 minutes" difficulty: "Master"
๐ณ Chapter 14: ECPay Payment Gateway Integration in Action
Congratulations on completing the frontend showcase and Supabase database integration for Not Far Camping Resort! But a website without a cash register will never generate real income for you. Imagine this: A customer selects their dates, chooses the "Starry Sky Temple Tent", only to see at the final step "Please contact our Line customer service and notify us after transfer"... This is completely unacceptable user experience in 2024 - you'd lose over 50% of impulse buyers!
In Taiwan, the most popular and indie-developer-friendly payment service is ECPay. Once integrated, customers can pay directly on your site using credit cards, Apple Pay, JKO Pay, or even scan a barcode at 7-11!
In this ultimate 6000-word hands-on chapter, we'll use Vibe Coding to systematically break down the "CheckMacValue hash encryption algorithm" that terrifies traditional engineers, and perfectly integrate ECPay into our not-far-web project!
๐ Why is ECPay Integration So Challenging?
Before coding, we must understand the nature of payment systems.
When redirecting customers to ECPay's payment page, we can't simply append ?price=1500 to the URL.
Because if we do, hackers could easily change ?price=1500 to ?price=1 in their browser and purchase your NT$1500 service for just NT$1!
To solve this, ECPay invented the CheckMacValue (Verification Code) mechanism:
- Combine "order number, amount, product name" with your secret HashKey and HashIV (known only to you and ECPay).
- Mix them together through complex URL encoding and character replacement.
- Compress into a 64-character hash using SHA-256 algorithm (this becomes the CheckMacValue).
- Send this hash along with payment details to ECPay.
- ECPay recalculates using their secret keys. If the hashes match, the transaction is approved; if not, it means someone tampered with the amount mid-transaction, and ECPay will reject it!
This is core security protection. The calculation process is extremely error-prone (a single uppercase letter or missing symbol will invalidate the code). Traditionally, engineers spent days debugging this algorithm. Now, we have AI!
โ๏ธ Hands-on 1: Apply for ECPay Test Account and Prepare Environment Variables
Before coding, prepare your "cash register keys". If you haven't registered with ECPay, we can use their public test account.
Create (or open) .env in your not-far-web project root and add:
# ECPay test environment API endpoint
ECPAY_ENDPOINT="https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5"
# ECPay public test merchant ID
ECPAY_MERCHANT_ID="3002607"
# ECPay public test HashKey (NEVER expose)
ECPAY_HASH_KEY="pwFHCqoQZGmho4w6"
# ECPay public test HashIV (NEVER expose)
ECPAY_HASH_IV="EkRm7iFT261dpevs"
# Local dev URL (if Astro runs on port 4321)
ECPAY_RETURN_URL="https://localhost:4321/api/ecpay/return"
ECPAY_CLIENT_BACK_URL="https://localhost:4321/booking-success"
๐งโโ๏ธ Hands-on 2: Let AI Write ECPay's Encryption Algorithm
Now we'll create a backend API.
Warning: Payment encryption must NEVER happen in frontend (browser)! If you do, hackers can see your HashKey using F12 developer tools. We'll handle this in Astro's server-side (SSR API Route).
๐ก Vibe Prompt 1: Generate CheckMacValue Algorithm
Don't manually code encryption logic - let Claude 3.5 Sonnet or GPT-4o help!
[!IMPORTANT] Copy this prompt for AI:
I'm integrating Taiwan's ECPay payment gateway using Node.js/TypeScript.Please write a function generateCheckMacValue(params: Record<string, string>, hashKey: string, hashIV: string): string.Requirements:1. Sort keys alphabetically per ECPay specs.2. Combine as HashKey=xxx&Key1=Value1&Key2=Value2...&HashIV=xxx.3. Use encodeURIComponent then convert to lowercase.4. Apply ECPay's special character replacements (%2d, %5f, %2e, %21, %2a, %28, %29, %20).5. Use crypto package for SHA256 hash, return uppercase.
๐ค AI's Perfect Encryption Core:
import crypto from 'crypto';
function generateCheckMacValue(params: Record<string, string>, hashKey: string, hashIV: string): string {
// 1. Sort keys alphabetically (A-Z)
const sortedKeys = Object.keys(params).sort();
// 2. Combine string with HashKey & HashIV
let str = `HashKey=${hashKey}`;
for (const key of sortedKeys) {
str += `&${key}=${params[key]}`;
}
str += `&HashIV=${hashIV}`;
// 3. URL Encode and lowercase
let encoded = encodeURIComponent(str).toLowerCase();
// 4. ECPay's notorious C# url encode compatibility replacements
// This mapping was discovered through countless engineer all-nighters!
encoded = encoded
.replace(/%2d/g, '-')
.replace(/%5f/g, '_')
.replace(/%2e/g, '.')
.replace(/%21/g, '!')
.replace(/%2a/g, '*')
.replace(/%28/g, '(')
.replace(/%29/g, ')')
.replace(/%20/g, '+')
.replace(/'/g, '%27')
.replace(/~/g, '%7e');
// 5. SHA-256 irreversible encryption, uppercase
return crypto.createHash('sha256').update(encoded).digest('hex').toUpperCase();
}
This code is priceless! If you omit .replace(/%20/g, '+'), ECPay will error whenever product names contain spaces. AI helped us avoid this minefield.
๐ Hands-on 3: Create Checkout Redirect API (Astro API Route)
With our encryption core, we'll now create a checkout API in not-far-web.
When users click "Confirm Payment", frontend POSTs order details here. The API calculates CheckMacValue and returns an "auto-submitting HTML form" that redirects to ECPay.
Create file: src/pages/api/ecpay/create.ts
๐ก Vibe Prompt 2: Write Checkout API
[!IMPORTANT] Copy this prompt for AI:
In Astro framework, create a GET/POST API Route (/api/ecpay/create).Use our generateCheckMacValue function.Flow:1. Receive courseId and price from frontend.2. Generate timestamp order number MerchantTradeNo.3. Read ECPAY_MERCHANT_ID, HASH_KEY, HASH_IV from env.4. Prepare all ECPay required params (MerchantID, MerchantTradeDate, PaymentType='aio', TotalAmount, TradeDesc, ItemName, ReturnURL, ChoosePayment='Credit', EncryptType='1', ClientBackURL).5. Calculate CheckMacValue, generate HTML with hidden inputs and auto-submit JS (document.getElementById('form').submit()).6. Return this HTML (Content-Type: text/html).
๐ค AI's Seamless Redirect Engine:
import type { APIRoute } from 'astro';
import crypto from 'crypto';
// (generateCheckMacValue function omitted above...)
export const POST: APIRoute = async ({ request }) => {
const formData = await request.formData();
const campName = formData.get('campName') as string || 'Not Far Camping Resort Online Booking';
const price = formData.get('price') as string || '1500';
// Generate timestamp order number (max 20 chars)
const timestamp = new Date().getTime().toString().slice(-6);
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
const orderNo = `CAMP${timestamp}${random}`;
const merchantId = import.meta.env.ECPAY_MERCHANT_ID;
const hashKey = import.meta.env.ECPAY_HASH_KEY;
const hashIv = import.meta.env.ECPAY_HASH_IV;
const endpoint = import.meta.env.ECPAY_ENDPOINT;
// Get Taiwan time formatted as yyyy/MM/dd HH:mm:ss
const twTime = new Date(new Date().getTime() + 8 * 60 * 60 * 1000);
const pad = (n: number) => n.toString().padStart(2, '0');
const tradeDate = `${twTime.getUTCFullYear()}/${pad(twTime.getUTCMonth() + 1)}/${pad(twTime.getUTCDate())} ${pad(twTime.getUTCHours())}:${pad(twTime.getUTCMinutes())}:${pad(twTime.getUTCSeconds())}`;
const params: Record<string, string> = {
MerchantID: merchantId,
MerchantTradeNo: orderNo,
MerchantTradeDate: tradeDate,
PaymentType: 'aio',
TotalAmount: price.toString(),
TradeDesc: 'Not Far Camping Resort Online Booking',
ItemName: campName,
ReturnURL: import.meta.env.ECPAY_RETURN_URL, // For ECPay server callback
ChoosePayment: 'Credit', // Credit card only
EncryptType: '1',
ClientBackURL: import.meta.env.ECPAY_CLIENT_BACK_URL, // For customer post-payment return
};
// Calculate the ultimate code
params.CheckMacValue = generateCheckMacValue(params, hashKey, hashIv);
// Convert all params to hidden inputs
const inputs = Object.entries(params).map(([key, val]) =>
`<input type="hidden" name="${key}" value="${val}" />`
).join('\n');
// Create "loading" screen that auto-submits to ECPay
const html = `
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Redirecting to ECPay Secure Channel</title>
<style>
body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background: #0f172a; color: white; font-family: sans-serif; }
.spinner { border: 4px solid rgba(255,255,255,0.1); border-top: 4px solid #10b981; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin-bottom: 20px;}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="spinner"></div>
<h2>Connecting to secure server...</h2>
<form id="ecpay-form" action="${endpoint}" method="POST">
${inputs}
</form>
<script>
// Intentional 0.8s delay to show loading animation
setTimeout(() => {
document.getElementById('ecpay-form').submit();
}, 800);
</script>
</body>
</html>
`;
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
};
๐ก Hands-on 4: Anti-Forgery Server Callback (Webhooks)
Many think payment succeeds when ECPay redirects to our site (ClientBackURL).
Dead wrong! This is extremely dangerous!
Because hackers could forge a URL like https://yoursite.com/booking-success?order=123. If your system ships based on this, you'll go bankrupt tomorrow.
The secure approach is ECPay's ReturnURL (Server Callback).
ECPay's server actively sends an HTTP POST to yours (like Line Bot's Webhook). This request contains RtnCode=1 (payment success) and another CheckMacValue!
You must recalculate using the same algorithm. Only if the codes match does it mean the data truly came from ECPay, not hackers!
Create file: src/pages/api/ecpay/return.ts
๐ก Vibe Prompt 3: Implement Ultra-Secure Webhook Verification
[!IMPORTANT] Copy this prompt for AI:
I need an Astro API (/api/ecpay/return) to handle ECPay ReturnURL.1. Accept POST requests in application/x-www-form-urlencoded format.2. Extract all params, remove the incoming CheckMacValue from the object.3. Use remaining params to recalculate CheckMacValue.4. Compare with incoming CheckMacValue. If different, return '0|Error'.5. If verified, check if RtnCode is '1'. If yes, print "Payment success!" (in practice, update Supabase here).6. Must return '1|OK' to ECPay.
๐ค AI's Fort Knox Verification Station:
import type { APIRoute } from 'astro';
// (generateCheckMacValue import omitted)
export const POST: APIRoute = async ({ request }) => {
try {
const formData = await request.formData();
const data: Record<string, string> = {};
formData.forEach((value, key) => {
data[key] = value.toString();
});
console.log("=== ECPay Webhook Received ===");
console.log("Order No:", data.MerchantTradeNo);
console.log("Status Code:", data.RtnCode);
// 1. Extract and separate incoming CheckMacValue
const receivedMac = data.CheckMacValue;
delete data.CheckMacValue; // Remove before recalculation
// 2. Recalculate verification code
const hashKey = import.meta.env.ECPAY_HASH_KEY;
const hashIv = import.meta.env.ECPAY_HASH_IV;
const calculatedMac = generateCheckMacValue(data, hashKey, hashIv);
// 3. Anti-forgery check
if (calculatedMac !== receivedMac) {
console.error("๐จ Critical: Verification failed! Possible forged request!");
return new Response('0|Error', { status: 400 });
}
console.log("โ
Verification passed! Data is genuinely from ECPay.");
// 4. Check payment success
if (data.RtnCode === '1') {
console.log(`๐ฐ Order ${data.MerchantTradeNo} paid! Amount: ${data.TradeAmt}`);
// Here you'd call Supabase:
// await supabase.from('orders').update({ status: 'PAID' }).eq('order_no', data.MerchantTradeNo);
} else {
console.log(`โ Order ${data.MerchantTradeNo} failed. Reason: ${data.RtnMsg}`);
}
// 5. Must return 1|OK to ECPay to prevent retries
return new Response('1|OK', {
headers: { 'Content-Type': 'text/plain' },
});
} catch (error) {
console.error('ECPay Return Error:', error);
return new Response('0|Error', { status: 500 });
}
};
๐ป Hands-on 5: Connect Frontend - Bring Buttons to Life!
The final step: transform your "Book Now" button into an API trigger.
Wrap your button in a <form> - no complex AJAX, no CORS issues, just the raw, beautiful simplicity of web development!
<!-- In your React/Astro component -->
<form action="/api/ecpay/create" method="POST">
<!-- Hide product and price in inputs -->
<input type="hidden" name="campName" value="Starry Sky Temple Tent Weekend Package" />
<input type="hidden" name="price" value="5800" />
<button
type="submit"
class="w-full bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-emerald-500/30"
onclick="this.innerHTML='Encrypting...'; this.classList.add('opacity-50');"
>
Pay by Credit Card (TWD 5,800)
</button>
</form>
โ Chapter Summary & Automation Principles
In this 6000-word payment deep dive, you've conquered Taiwan's most challenging payment gateway: ECPay.
Key takeaways from building our money printer:
- Always calculate money server-side:
CheckMacValueis core to payment security - must be calculated in Node.js (Astro API Route), never leaked to frontend. - Auto-submit forms: Instead of fancy SPA routing, we use pure HTML forms with JS
submit()for instant redirects, solving all cross-domain and payment blocking issues. - Webhook zero-trust policy: Never trust frontend redirects - only trust ECPay's Server-to-Server
ReturnURL, always recalculating CheckMac