Base URL
Auth
Not authenticated
1 Create Setup Intent POST /api/payment-methods/setup-intent
Request
Response
2 Bind Card (3DS) POST /api/billing-organizations/{id}/payment-methods
Request
Response
3 Check Transaction Status GET /api/ecpay-admin/query-trade/{merchantTradeNo}
Request
Response
1 Charge Invoice POST /api/invoices/{invoiceId}/charge
Request
Response
2 Check Transaction Status GET /api/ecpay-admin/query-trade/{merchantTradeNo}
Request
Response
Q Query Trade GET /api/ecpay-admin/query-trade/{merchantTradeNo}
Request
Response
C Manual Capture POST /api/ecpay-admin/capture/{merchantTradeNo}
Request
Response
A Raw DoAction POST /api/ecpay-admin/do-action
Request
Response
API Contract Reference

Exact request/response shapes for every endpoint used in the ECPay integration. All authenticated endpoints require Authorization: Bearer <jwt>.

POST /api/auth/signin Public
Request Body
{ "usernameOrEmail": "admin@example.com", "password": "secret" }
Response
{ "accessToken": "eyJhbGci...", "tokenType": "Bearer" }
POST /api/payment-methods/setup-intent JWT
Request Body
{ "country": "TW", "billingOrganizationId": "org_123", "amount": 100, "email": "test@example.com", "customerName": "Test User", "itemName": "Oddle Subscription", "tradeDesc": "Payment binding", "tradeNo": null }

amount = integer TWD. tradeNo optional (auto-generated). countryCode also accepted as alias for country.

Response — ApiResponse<SetupIntentResponse>
{ "success": true, "message": "...", "object": { "clientSecret": "token_from_ecpay_html", "provider": "ECPAY" } }

For ECPay, clientSecret is the HTML form that the frontend renders in an iframe or redirects to. For Stripe, it's the Stripe SetupIntent client secret.

POST /api/billing-organizations/{id}/payment-methods JWT
Request Body — CreatePaymentMethodRequest
{ "type": "CREDIT_CARD", "metadata": { "bindCardPayToken": "token_from_step1" }, "returnUrl": "https://app.com/callback", "invoiceId": "inv_123" }

billingOrganizationId is in the URL path. metadata.bindCardPayToken = the clientSecret from setup-intent. metadata.id also accepted (preferred for Stripe). returnUrl = where ECPay redirects after 3DS. invoiceId optional.

Response — ApiResponse<Map>
// If 3DS required (ECPay): { "success": true, "object": { "status": "requireAction", "nextAction": { "type": "threeDSecureRedirect", "redirectUrl": "https://ecpay.com/3ds/..." } } } // If direct success (Stripe/no 3DS): { "success": true, "object": { "status": "succeeded", "paymentMethodId": "pm_abc", "provider": "STRIPE", "cardLast4": "4242", "cardBrand": "visa" } }
POST /api/invoices/{invoiceId}/charge JWT
Request

No request body. Invoice ID is in the URL path.

Response — ApiResponse<Map>
// Success (direct capture): { "success": true, "object": { "status": "succeeded", "transactionId": "OD20260224abc123" } } // Success (auth-only, capture pending): { "success": true, "object": { "status": "processing", "transactionId": "OD20260224abc123" } } // Failure: { "success": false, "object": { "status": "failed", "errorCode": "10100058", "errorMessage": "Insufficient funds" } }

processing = authorized but capture is async (ECPay pattern). succeeded = already captured. transactionId = MerchantTradeNo for ECPay, PaymentIntent ID for Stripe.

GET /api/ecpay-admin/query-trade/{merchantTradeNo} ADMIN
Response
{ "ecpayResponse": { "success": true, "message": "...", "rtnCode": "1", "ecpayTradeNo": "2402181234567", "data": { /* raw key-value from ECPay */ } }, "localRecord": { "id": 42, "provider": "ECPAY", "merchantTradeNo": "OD20260224abc", "invoiceId": "inv_123", "billingOrganizationId": "org_456", "amount": 100.00, "status": "AUTHORIZED", "nextAction": "CAPTURE", "providerReference": "2402181234567", "paymentDate": "2026/02/24 10:30:00", "chargeFee": 3, "cardInfo": "XXXX-XXXX-XXXX-1234", "errorMessage": null }, "actionLog": [ { "id": 1, "action": "AUTHORIZE", "statusBefore": null, "statusAfter": "AUTHORIZED", "success": true, "errorMessage": null, "createdAt": "2026-02-24T10:30:00" } ] }
POST /api/ecpay-admin/capture/{merchantTradeNo} ADMIN
Request

No request body. MerchantTradeNo in URL. Transaction must be in AUTHORIZED state.

Response
{ "merchantTradeNo": "OD20260224abc", "status": "CAPTURED", "nextAction": null }
POST /api/ecpay-admin/do-action ADMIN
Request Body
{ "merchantTradeNo": "OD20260224abc", "tradeNo": "2402181234567", "action": "C", "totalAmount": 100 }

action values: C = Capture/Close, R = Refund, E = Cancel, N = Abandon.
tradeNo = ECPay's trade number (from query-trade response).
Bypasses the state machine — use with care.

Response
{ "success": true, "message": "1|OK", "rtnCode": "1", "ecpayTradeNo": "2402181234567", "data": { /* raw ECPay response */ }, "requestedAction": "C" }