WooCommerce Xendit Payment Gateway

1. Overview

WooCommerce Xendit Payment Gateway by PatSaTECH connects your store to Xendit using the modern Payment Sessions API in PAYMENT_LINK mode. Customers leave your checkout briefly to pay on Xendit’s PCI-compliant hosted page, then return to your thank-you page.

This design keeps sensitive payment data off your server and lets one integration cover cards, e-wallets, virtual accounts, QR and retail outlets across Southeast Asia.

2. Features

  • Hosted checkout (all methods)
  • Optional Cards / E-Wallets / VA / QR gateways
  • Live & Test mode keys
  • Webhook token verification
  • Idempotent webhooks
  • Return-URL API verification
  • Full & partial refunds
  • HPOS compatible
  • WooCommerce Blocks support
  • Classic shortcode checkout
  • Currency & min-amount checks
  • WC_Logger debug toggle
  • Settings link on Plugins page
  • Translation-ready (.pot)
  • Safe uninstall (options only)

3. Requirements

ComponentMinimum
WordPress6.0
WooCommerce8.0 (tested up to 11.2.1)
PHP7.4
SSL / HTTPSRequired (Xendit return URLs must be HTTPS)
Xendit accountSecret key with Money-in Write permission

Tested on: WordPress 7.1.3, WooCommerce 11.2.1 (classic and Blocks checkout), Xendit Test mode. Requires PHP 7.4 or later; code checked on PHP 8.4.

4. Installation

  1. In WordPress admin go to Plugins → Add New → Upload Plugin.
  2. Choose wooxendit-1.0.0.zip and click Install Now.
  3. Activate WooCommerce Xendit Payment Gateway.

FTP / SFTP

  1. Unzip the package.
  2. Upload the wooxendit folder to /wp-content/plugins/.
  3. Activate from the Plugins screen.

Step-by-step setup with screenshots

Tested on WooCommerce classic and Blocks checkout with Xendit sandbox (3DS card). Verified live:
  • Classic checkout payment
  • Blocks checkout payment
  • 3-D Secure card authentication
  • Partial refund and full refund
  • Declined card (order note added, order stays Pending)
  • Xendit dashboard test pings (200, logged as Info)

This walkthrough shows a complete setup on a real test store, from creating Xendit test keys to a paid WooCommerce order. Click any screenshot to open it full size. Keys and tokens are blurred.

Before you start: Xendit requires Two-Factor Authentication (2FA) on your dashboard account before it lets you generate a secret API key. Have your authenticator app ready, because Xendit asks for a 6-digit code when you create the key.

Part A: Xendit Dashboard

  1. Open Settings → Developers → API Keys (Test mode)

    Make sure the yellow Test Mode banner is showing. The public key is not needed by this plugin. You only need a secret key.

    Xendit API Keys page in Test mode
    API Keys page in Test mode (public key blurred).
  2. Turn on 2FA if Xendit asks

    If you click Generate secret key without 2FA, Xendit shows this prompt. Click Enable 2FA, link your authenticator app, then come back.

    Xendit 2FA required prompt
    2FA is required before a secret key can be generated.
  3. Generate a secret key with Money-in Write

    Give the key a name (e.g. woo-yourstore-test), set Money-in products → Write, leave everything else on None, click Generate key and enter your 2FA code. Copy the key (starts with xnd_development_) straight away, because Xendit shows it only once.

    Generate API key permissions dialog
    Only Money-in Write is needed.
  4. Reveal the webhook verification token

    Scroll to Webhooks → Webhook verification token and click View Webhook Verification Token (Xendit asks for your password). Copy the token for the plugin settings.

    Webhook verification token section
    Webhook verification token section.
  5. Find the Payment Session webhook fields

    In the webhook URL list, find the Payment Session group with Payment Session Completed and Payment Session Expired.

    Payment Session webhook URL fields
    Payment Session Completed and Expired fields.
  6. Enter the webhook URL in both fields, then click Test and save

    Use exactly:

    https://<your-site>/wc-api/wc_gateway_xendit_webhook/

    Set it for Payment Session Completed and Payment Session Expired. Once the token is saved in the plugin (Part B), Test and save should return status 200 with {"received":true}.

    Webhook test returning 200
    Correct URL saved, test returns 200 (token and webhook id blurred).
Seeing 400 or 401 on Test and save? Check that the URL ends in /wc-api/wc_gateway_xendit_webhook/, that the plugin is active, and that the same verification token is saved in the plugin settings.

Part B: WordPress / WooCommerce

  1. Upload the plugin

    Plugins → Add New → Upload Plugin, choose wooxendit-1.0.0.zip, then click Install Now.

    Upload plugin screen
  2. Activate

    Click Activate Plugin. A Settings link appears in the plugin row.

    Plugin activated
    Xendit Payment Gateway for WooCommerce 1.0.0 activated (WordPress 7.1.3, WooCommerce 11.2.1).
  3. Open WooCommerce → Settings → Payments

    Find Xendit in the payment providers list and click Manage.

    WooCommerce payments list
  4. Configure Xendit

    Tick Enable, keep Test mode on, paste the Test Secret Key and the Webhook Verification Token, then click Save changes. The webhook URL to copy is shown at the bottom of this page.

    Xendit gateway settings
    Top of the Xendit settings. Secrets are masked in the fields.
    Xendit settings bottom
    Bottom of the settings: Webhook URL to copy into Xendit, order statuses and debug log.

Part C: Test order (classic checkout)

  1. Choose Xendit at checkout

    Add a product to the cart, fill in billing details, select Xendit and click Place order.

    Classic checkout with Xendit selected
  2. Pay on the Xendit hosted page

    The customer is redirected to Xendit’s secure page. In test mode use one of the test card numbers from Xendit’s docs (or the Simulate scenario link on the page), with any future expiry date and any CVV.

    Xendit hosted payment page
    Order breakdown on the Xendit page
    The Xendit page lists the order lines (products, shipping, fees and tax), rounded to the currency’s decimals, and they add up exactly to the order total: Test Product US$0.91 + Shipping US$1.00 + Tax US$0.19 = US$2.10 (order #6457).
  3. Complete 3-D Secure

    Some test cards show a simulated 3DS / OTP step. Follow the on-screen instructions to approve.

    3DS OTP step
  4. Thank-you page

    The customer returns to the WooCommerce order-received page.

    Order received page
  5. Order is marked paid

    The webhook moves the order to Processing (or Completed, depending on your setting) and adds order notes with the Xendit session and payment IDs. If the customer’s return arrives after the webhook, the plugin detects the order is already paid and does not process it twice.

    Admin order paid via Xendit
    Order notes and internal IDs blurred.
  6. Confirm in Xendit

    In Xendit → Transactions the payment shows as Successful, with a reference starting wc-<order id>-.

    Xendit transactions successful
    Xendit → Transactions showing the successful test payments and refunds (references blurred).

Part D: Block checkout

  1. Select Xendit in the Checkout block

    On a page that uses the WooCommerce Checkout block, Xendit appears under Payment options with its title, description and icon. Select it and click Place order.

    Block checkout with Xendit selected
    Block checkout with Xendit selected (email and phone blurred).
  2. Pay and authenticate on Xendit

    Exactly as with classic checkout, the customer pays on Xendit’s hosted page and completes any 3-D Secure step. In the sandbox the OTP is shown on screen.

    Xendit 3DS authentication
    Xendit hosted card form with the customer details pre-filled (email and phone blurred).
    Xendit 3DS OTP
    3D Secure step for the test card.
  3. Back on the thank-you page

    The customer returns to the order-received page with Xendit as the payment method.

    Order received after block checkout
    Order received page for test order #6452 (email blurred).
  4. Order paid and Processing

    The webhook marks the order paid and sets it to Processing, just like a classic checkout order.

    Admin order paid after block checkout
    Order #6452 in Processing after payment (payment ID, IP, email and phone blurred).

5. Getting Xendit API keys

  1. Sign in to the Xendit Dashboard.
  2. Open Settings → API Keys.
  3. Enable 2FA on your Xendit account first, because Xendit will not generate secret keys without it.
  4. Generate a Secret Key with Money-in Write permission (and Read if you also query balances).
  5. Create separate keys for Test and Live modes.
  6. Store keys only on the server (this plugin’s settings). Never put secret keys in theme JS or public repos.
Test keys typically start with xnd_development_. Live keys start with xnd_production_.

6. Configuration

Go to WooCommerce → Settings → Payments → Xendit.

SettingDescription
Enable/DisableShow the main “all methods” hosted checkout at checkout.
Title / DescriptionCustomer-facing labels.
Test modeWhen checked, the Test Secret Key is used.
Test / Live Secret KeyXendit secret API keys.
Webhook Verification TokenFrom Xendit Webhooks settings; compared to x-callback-token.
Webhook URLCopy-only URL: https://<site>/wc-api/wc_gateway_xendit_webhook/. Set it in Xendit for all of these: Payment Session Completed and Expired, Payment Status (payment.capture/payment.failure), Payment Request Status (payment_request.expiry), Refund Status, and on the legacy page Payment Succeeded/Failed and Captured Succeeded. Xendit only sends events that have a URL. See Webhook setup.
Default countryOverride / USD country (ID, PH, TH, MY, VN, SG).
Paid order statusProcessing or Completed after successful payment.
Debug logWrites to WooCommerce → Status → Logs (source wooxendit).

Optional gateways (each has its own enable toggle): Xendit Cards, Xendit E-Wallets, Xendit Bank Transfer / OTC, Xendit QR. They reuse the main gateway’s API keys and filter allowed_payment_channels on the hosted page.

7. Webhook setup (step by step)

  1. Copy the Webhook URL from the plugin settings: https://<site>/wc-api/wc_gateway_xendit_webhook/.
  2. In Xendit Dashboard open Settings → Webhooks.
  3. Add the URL for both Payment Session Completed and Payment Session Expired.
  4. Add the same URL for the other events in the table below (payment status, payment request, refunds and, if shown, the legacy events).
  5. Click Test & Save, then copy the callback verification token.
  6. Paste the token into the plugin’s Webhook Verification Token field and save.
Xendit Webhooks sectionEventWhy
Payment SessionPayment Session Completed (payment_session.completed)Required. Marks the order paid.
Payment SessionPayment Session Expired (payment_session.expired)Required. Cancels the order, or marks it Failed if there were failed attempts.
Payment Status (Payments API v3)payment.capture / payment.failureRecommended. Failure adds the “payment attempt failed” note.
Payment Request Statuspayment_request.expiryRecommended. Adds an order note.
Refund Status / Unified Refundsrefund.succeeded / refund.failedRecommended. Adds the refund result to the order notes.
Legacy page: Payment Requests V2Payment Succeeded, Payment Failed, Captured SucceededRecommended on accounts that still show the legacy webhook page (payment.failed is handled).
Xendit only delivers event types that have a URL. Any event without a URL appears as URL Not Set in Webhook Logs and never reaches your store. Use the same URL, https://<site>/wc-api/wc_gateway_xendit_webhook/, for every event above. Unknown events are acknowledged with 200.
Xendit webhooks all events
Webhook URL set for the legacy Payment Succeeded / Failed / Captured Succeeded and Unified Refunds events.
Without a valid webhook token, callbacks are rejected with HTTP 401. The customer return URL still verifies payment via GET /sessions/{id}, but webhooks are required for async methods (VA, OTC, some e-wallets).

8. Countries, methods & currencies

CountryCodeCurrencyExample methods
IndonesiaIDIDRCards, OVO, DANA, GoPay, QRIS, VA, Alfamart
PhilippinesPHPHPCards, GCash, Maya, QRPh, 7-Eleven, Billease
ThailandTHTHBCards, PromptPay, TrueMoney, LINE Pay, ShopeePay
MalaysiaMYMYRCards, GrabPay, Touch ’n Go, FPX, ShopeePay
VietnamVNVNDCards, MoMo, ZaloPay, ShopeePay, VA
SingaporeSGSGDCards, PayNow QR (account-dependent)

USD is accepted by the Payment Sessions API; set Default country appropriately. Only channels activated on your Xendit account appear on the hosted page.

Conservative minimum order amounts enforced by the plugin:

CurrencyMinimum
IDR1,000
VND1,000
PHP / THB / MYR1
SGD / USD0.50

9. Testing with Xendit test mode

  1. Enable Test mode and paste a test secret key.
  2. Configure the webhook URL on the test webhook settings in Xendit (test mode dashboard).
  3. Place a test order; you will be redirected to checkout-staging.xendit.co (or a xen.to short link).
  4. Complete payment using Xendit test simulators / test credentials for your channels.
  5. Confirm the order moves to Processing/Completed and that an order note lists the session ID.

10. Order status flow

Xendit session statusWebhook eventWooCommerce
ACTIVE—pending (awaiting payment)
COMPLETEDpayment_session.completedprocessing or completed
EXPIREDpayment_session.expiredcancelled
CANCELED—cancelled

Order meta stored: _xendit_session_id, _xendit_payment_request_id, _xendit_payment_id, _xendit_reference_id, _xendit_testmode.

11. Refunds

From the WooCommerce order screen, create a refund as usual. The plugin calls POST /refunds with the order’s payment_request_id, amount, currency and reason mapping (REQUESTED_BY_CUSTOMER, FRAUDULENT, DUPLICATE, CANCELLATION).

Not every channel supports refunds or partial refunds. If Xendit returns REFUND_NOT_SUPPORTED or PARTIAL_REFUND_NOT_SUPPORTED, refund via the Xendit Dashboard or another channel-specific process.

12. Blocks vs classic checkout

Both are supported. Classic checkout uses the standard WooCommerce payment method list. Blocks checkout loads assets/js/blocks.js (no Node build step) and registers each enabled Xendit gateway via wc.wcBlocksRegistry.

Transaction ID

When a payment completes, the plugin saves the Xendit payment ID (py-…) as the WooCommerce Transaction ID. The Payment Session ID (ps-…) and Payment Request ID (pr-…) are stored as order meta and listed in the order notes. Use the payment ID to search for the payment in Xendit → Transactions.

Order with Xendit transaction ID
Order notes on #6452: payment completed via the payment_session.completed webhook (session and payment request IDs blurred).
Xendit transaction detail
The same payment in Xendit → Transactions, showing the Payment Session ID (blurred).

Partial and full refunds

  1. Open the order and click Refund.
  2. Enter quantities or amounts in the line-item refund fields (product, shipping, tax). WooCommerce adds them up into Refund amount.
  3. Click Refund $X via Xendit. The plugin calls the Xendit Refunds API and adds a note with the Xendit refund ID and status.
Why is the “Refund amount” box read-only? This is standard WooCommerce behaviour, not something the plugin does. When taxes are enabled, WooCommerce locks the total box so tax is refunded correctly. Enter the amounts per line item instead. If taxes are disabled, you can type the amount straight into the box.
Partial refund in WooCommerce
Refunding $0.50 of order #6452 with the Refund $0.50 via Xendit button.
Partial refund notes
Refund #6453 recorded: the refund.succeeded webhook confirms the Xendit refund (refund ID blurred).
Full refund in WooCommerce
Full refund. The order moves to Refunded (refund IDs blurred).
Refund history in Xendit
Refund history on the Xendit transaction. In test mode, refunds can show as Pending for a while.
Full refund in Xendit
Full refund shown in the Xendit transaction’s Refund History (IDs blurred).

Pending/abandoned and declined payments

What happensOrder statusOrder note
Customer leaves the Xendit page without payingPending payment until the session expiresXendit Payment Session created
Card declined or payment attempt fails (payment.failure webhook)Stays Pending, so the customer can retry on the same Xendit pageXendit payment attempt failed: <failure_code>
Session expires with no failed attempts (payment_session.expired)CancelledXendit session EXPIRED
Session expires after one or more failed attemptsFailedXendit session EXPIRED

All webhook handling is idempotent: a repeated webhook never adds duplicate notes or changes the status twice. A paid order is never cancelled or failed by a later event.

Not every Xendit channel sends failure notifications. Some methods simply let the session expire.
Pending Xendit order
Abandoned checkout: the order waits in Pending payment (IP, phone and session ID blurred).
Declined payment order note
Declined card (order #6449): the note “Xendit payment attempt failed: AUTHENTICATION_FAILED (CARDS)” is added and the order stays Pending (IDs, IP and phone blurred).
payment.failure webhook 200
The payment.failure webhook in Xendit, answered with 200 (IDs blurred).

Additional payment methods (Cards / E-Wallets / Bank Transfer / QR)

Besides the main Xendit gateway (all channels on one hosted page), you can enable separate checkout options that each open Xendit with only one group of channels:

  • Xendit Cards: credit and debit cards
  • Xendit E-Wallets: OVO, DANA, GCash, ShopeePay, GrabPay and more
  • Xendit Bank Transfer / OTC: virtual accounts and retail outlets
  • Xendit QR: QRIS, QRPh, PromptPay

Each one has its own Enable, Title and Description settings and reuses the API keys, webhook token and country from the main Xendit gateway.

Activate the channels first. A method only works if the matching channels are activated for your business country and currency in Xendit Dashboard → Settings → Payment Channels. Otherwise Xendit rejects checkout with INVALID_PAYMENT_CHANNEL. The customer sees a friendly “payment method not available” message and you get an admin notice. Disable any method whose channels you can’t activate.
Xendit Cards settings
Xendit E-Wallets settings
Xendit Bank Transfer / OTC settings
Xendit QR settings
Checkout with all Xendit methods
All Xendit options at checkout (phone blurred).
E-wallet channels not available error
Checkout test with e-wallet channels not activated on the Xendit account. The exact channel list Xendit rejects is written to the debug log.
QR channels not available error
The same situation for QR channels (QRIS, QRPH, PROMPTPAY).

Debug logs

Turn on Debug log in the Xendit settings, then open WooCommerce → Status → Logs and choose the file that starts with wooxendit-. API errors, webhook events and refund calls are logged there. Secret keys are never written to the log.

wooxendit debug log
wooxendit log: API errors such as INVALID_PAYMENT_CHANNEL are logged as Error, while webhook events and unmatched failure webhooks are logged as Info (reference blurred).

On the Xendit side, Developers → Webhook Logs lists every event Xendit tried to send. URL Not Set means that event type had no webhook URL, so it was never delivered. Open an entry to see the payload and your store’s response.

Xendit webhook logs
Webhook Logs: “URL Not Set” rows were never delivered because no URL was configured for that event type (IDs blurred).
Xendit webhook detail with 200 response
Webhook detail: payment_session.completed delivered to /wc-api/wc_gateway_xendit_webhook/ and answered with 200 {“received”: true}.

13. Troubleshooting & FAQ

Gateway does not appear at checkout

  • Is the gateway enabled?
  • Is a secret key present for the current mode?
  • Is store currency one of IDR, PHP, VND, THB, SGD, MYR, USD?
  • Is the cart total above the minimum for that currency?

Payment completed in Xendit but order still pending

  • Verify webhook URL and token.
  • Check Logs → source wooxendit.
  • Open the customer return URL path; it re-fetches the session.

API error INVALID_URL

Success/cancel return URLs must be HTTPS. Enable SSL on your store.

API error INVALID_PAYMENT_CHANNEL / “payment method not available”

The channels for that method are not activated on your Xendit account for your country and currency. Activate them in Xendit Dashboard → Settings → Payment Channels, or disable that method (Cards, E-Wallets, Bank Transfer / OTC, QR) in WooCommerce. Customers see a friendly message, and an admin notice tells you which method failed. The notice can be dismissed.

The “Refund amount” box is read-only

That’s standard WooCommerce behaviour when taxes are enabled. Enter the amounts in the line-item refund fields instead. See Partial and full refunds.

“Webhook: order not found” for reference_id test_session

Xendit dashboard Test and save pings use sample data. The plugin detects them, answers with 200 and logs them at info level only.

Card declined but the order stays Pending

That’s expected: the customer can retry while the Xendit session is active. A “Xendit payment attempt failed” note is added, and the order becomes Failed or Cancelled when the session expires. See Pending/abandoned and declined payments.

14. API reference used

PurposeCallDocumentation
Create hosted sessionPOST https://api.xendit.co/sessionsCreate a session
Verify sessionGET https://api.xendit.co/sessions/{id}Get session
RefundPOST https://api.xendit.co/refundsRefund a payment request
AuthHTTP Basic (secret key : empty)API keys
Webhook authx-callback-token headerHandling webhooks
Session webhookspayment_session.completed / .expiredSession webhook

15. Changelog

1.0.0 — 2026-10-16

  • Initial release.
  • Xendit Payment Sessions hosted checkout, plus optional Cards, E-Wallets, Bank Transfer / OTC and QR gateways.
  • HPOS compatible; works with both WooCommerce Blocks checkout and classic checkout.
  • Verified, idempotent webhooks: payment_session.completed/.expired, payment.capture, payment.failure (and legacy payment.failed), payment_request.expiry, refund.succeeded/.failed.
  • Declined attempts add a “Xendit payment attempt failed” note and keep the order Pending; an expired session marks the order Failed (after failed attempts) or Cancelled.
  • Xendit dashboard test pings are answered with 200 and logged at info level.
  • INVALID_PAYMENT_CHANNEL: friendly customer message, an admin notice and a channel-activation hint in settings.
  • Full and partial refunds through the Xendit Refunds API.
  • Debug logging and a detailed illustrated guide.

16. Support

PatSaTECH
Email: [email protected]
Web: https://www.patsatech.com

Please include your WooCommerce version, PHP version, a screenshot of Xendit webhook settings, and the relevant wooxendit log lines (with secrets redacted).

Our Partners

fraudlabs
opayo
nochex
Razorpay
durango merchant services
2checkout is now verifone
authorizenet
gravity forms
whmcs