ShopeePay QR
Topics covered on this page
ShopeePay
Accept online payments from ShopeePay users through your website using the ShopeePay payment method.
This guide walks you through the payment flow and details how to implement it.
How to enable
- Supported countries: Malaysia, Thailand, Singapore
- Minimum API version:
2017-11-02
To enable ShopeePay, send an email requesting this feature to support@omise.co. You will need to review and accept new terms and conditions.
Payment flow
Customers paying via ShopeePay go through a redirect payment flow. They are redirected from your website to ShopeePay's payment page, where they can scan the QR code and authorize and confirm the payment on their ShopeePay mobile app.
The following screenshots demonstrate this flow.
Using a mobile browser

❶ The customer chooses ShopeePay as their payment method. ❷ They are redirected to the ShopeePay app to confirm the payment. ❸ The payment details are shown. ❹ The customer can return to the merchant's confirmation page by pressing Back to merchant. ❺ The customer is redirected back to the merchant's page.
Using a desktop browser

❶ The customer chooses ShopeePay as their payment method. ❷ A QR code is displayed. ❸ The customer opens the ShopeePay app and uses the QR scanner. ❹ The customer scans the QR code. ❺ A summary page is shown before the customer confirms payment. ❻ The payment confirmation slip is displayed, and the customer can be redirected to the merchant's confirmation page. ❼ Once you receive a webhook completion event, confirm the payment with the customer.
ShopeePay supports the following payment methods by country:
| Payment method | Thailand | Singapore | Malaysia |
|---|---|---|---|
| Wallet Balance | ✅ | ✅ | ✅ |
| Credit Card | ✅ | ||
| Direct Debit / Bank Account | ✅ | ||
| SPayLater | ✅ | ✅ | ✅ |
Implementation
To create a charge using ShopeePay, make the following API requests:
- Create a payment source (
type:shopeepay) using Omise.js or one of the mobile SDKs (iOS or Android). - Create a charge using the source identifier from step 1.
- After receiving the charge completion webhook event, retrieve the charge to verify its status (optional, but recommended).
Use your public key to create the ShopeePay source on the client (a customer's browser or mobile phone). Use your secret key to create the ShopeePay charge on the server.
If both source creation and charge must happen server-side, you can combine them in a single API request using your secret key.
Creating a source
When the customer confirms they wish to pay with ShopeePay, create a source specifying amount, currency, and type.
| Parameter | Type | Description |
|---|---|---|
amount |
integer | (Required) See Limits |
currency |
string | (Required) THB, SGD, or MYR |
type |
string | (Required) shopeepay |
The following examples demonstrate creating a ShopeePay source for RM1,500. Replace omise_public_key and $OMISE_PUBLIC_KEY with the test public key from your dashboard.
In Omise.js, the
typeparameter is the first argument passed to thecreateSourcemethod.
Omise.setPublicKey(omise_public_key);
Omise.createSource('shopeepay', {
"amount": 150000,
"currency": "MYR"
}, function(statusCode, response) {
console.log(response);
});
For testing, you can make the same request using curl:
curl https://api.omise.co/sources \
-u $OMISE_PUBLIC_KEY: \
-d "amount=150000" \
-d "currency=MYR" \
-d "type=shopeepay"
Example response:
{
"object": "source",
"id": "src_test_5twksxr06g6ldfg3gaa",
"livemode": false,
"location": "/sources/src_test_5twksxr06g6ldfg3gaa",
"amount": 150000,
"barcode": null,
"bank": null,
"created_at": "2022-11-23T18:10:49Z",
"currency": "MYR",
"email": null,
"flow": "redirect",
"installment_term": null,
"absorption_type": null,
"name": null,
"mobile_number": null,
"phone_number": null,
"platform_type": null,
"scannable_code": null,
"references": null,
"store_id": null,
"store_name": null,
"terminal_id": null,
"type": "shopeepay",
"zero_interest_installments": null,
"charge_status": "unknown",
"receipt_amount": null,
"discounts": []
}
The id attribute is the source identifier (begins with src).
Creating a charge
Create a charge specifying return_uri, source, amount, and currency:
return_uri— the URL on your website to which the customer is redirected after completing payment authorization. Must use HTTPS.source— the source identifier returned in the previous step.amountandcurrency— must match the values used when creating the source.
Omise recommends not using the charge attributes
net,fee,fee_vat, andtransaction_feesuntil the chargestatusissuccessful.
Replace $OMISE_SECRET_KEY with the test secret key from your dashboard. Replace $SOURCE_ID with the id of the source.
curl https://api.omise.co/charges \
-u $OMISE_SECRET_KEY: \
-d "amount=150000" \
-d "currency=MYR" \
-d "return_uri=https://example.com/orders/345678/complete" \
-d "source=$SOURCE_ID"
Example response:
{
"object": "charge",
"id": "chrg_test_5twksxushmdv3if7ppe",
"location": "/charges/chrg_test_5twksxushmdv3if7ppe",
"amount": 150000,
"net": 0,
"fee": 0,
"fee_vat": 0,
"interest": 0,
"interest_vat": 0,
"funding_amount": 150000,
"refunded_amount": 0,
"transaction_fees": {
"fee_flat": null,
"fee_rate": null,
"vat_rate": "0.0"
},
"platform_fee": {
"fixed": null,
"amount": null,
"percentage": null
},
"currency": "MYR",
"funding_currency": "MYR",
"ip": null,
"refunds": {
"object": "list",
"data": [],
"limit": 20,
"offset": 0,
"total": 0,
"location": "/charges/chrg_test_5twksxushmdv3if7ppe/refunds",
"order": "chronological",
"from": "1970-01-01T00:00:00Z",
"to": "2022-11-23T18:10:49Z"
},
"link": null,
"description": null,
"metadata": {},
"card": null,
"source": {
"object": "source",
"id": "src_test_5twksxfiaojusv5zkyu",
"livemode": false,
"location": "/sources/src_test_5twksxfiaojusv5zkyu",
"amount": 150000,
"barcode": null,
"bank": null,
"created_at": "2022-11-23T18:10:47Z",
"currency": "MYR",
"email": null,
"flow": "redirect",
"installment_term": null,
"absorption_type": null,
"name": null,
"mobile_number": null,
"phone_number": null,
"platform_type": null,
"scannable_code": null,
"references": null,
"store_id": null,
"store_name": null,
"terminal_id": null,
"type": "shopeepay",
"zero_interest_installments": null,
"charge_status": "pending",
"receipt_amount": null,
"discounts": []
},
"schedule": null,
"customer": null,
"dispute": null,
"transaction": null,
"failure_code": null,
"failure_message": null,
"status": "pending",
"authorize_uri": "https://pay.omise.co/payments/pay2_test_5twksxuu4ehzo8d40mh/authorize",
"return_uri": "https://example.com/orders/345678/complete",
"created_at": "2022-11-23T18:10:49Z",
"paid_at": null,
"expires_at": "2022-11-30T18:10:49Z",
"expired_at": null,
"reversed_at": null,
"zero_interest_installments": true,
"branch": null,
"terminal": null,
"device": null,
"authorized": false,
"capturable": false,
"capture": true,
"disputable": false,
"livemode": false,
"refundable": false,
"reversed": false,
"reversible": false,
"voided": false,
"paid": false,
"expired": false
}
Creating a source and charge
Alternatively, you can create and charge a source in a single API request using your secret key:
curl https://api.omise.co/charges \
-u $OMISE_SECRET_KEY: \
-d "amount=150000" \
-d "currency=MYR" \
-d "return_uri=https://example.com/orders/345678/complete" \
-d "source[type]=shopeepay"
Setting the charge to expire
By default, a ShopeePay charge expires 60 minutes after creation. If you need to expire it earlier, use the following request:
curl https://api.omise.co/charges/$CHARGE_ID/expire \
-X POST \
-u $OMISE_SECRET_KEY:
Replace $CHARGE_ID with the id of the charge.
Completing the charge
A newly created charge has its status set to pending. Other possible values are successful, failed, and expired.
The following sequence diagram illustrates the full payment flow:
Authorizing the charge
Redirect the customer to the URL specified in authorize_uri so they can authorize the charge. After the customer completes authorization, they are redirected to the URL specified in return_uri.
You can simulate authorization in test mode by visiting the authorize_uri and manually marking the charge as Successful or Failed.
Receiving the charge completion event
Use webhook events to be notified when a charge is completed. Configure a webhook endpoint on your dashboard to receive the following events:
| Event | Description |
|---|---|
charge.create |
Fired when the charge is created with status: pending. |
charge.complete |
Fired when the charge is authorized (successfully or not). |
The key attribute of the event object contains charge.complete. The data attribute contains the full charge object. See Events API for the event object structure.
Checking the charge status
After receiving the charge.complete event, retrieve the charge using its id and verify that the status in the charge object matches the status in the webhook event.
| Status | Meaning |
|---|---|
successful |
Payment was received. Fulfill the order. |
failed |
Payment failed. Check failure_code and failure_message for details. |
expired |
The charge was not authorized within the expiry window and cannot be reused. |
Failure codes:
| Failure Code | Description | Recommended action |
|---|---|---|
payment_cancelled |
Payment cancelled by the customer. | Ask the customer to retry or select an alternative payment method. |
payment_expired |
Payment expired before authorization was completed. | Ask the customer to retry. A new charge must be created. |
payment_rejected |
Payment rejected by the issuer. | Ask the customer to check their account or use a different method. |
failed_processing |
General payment processing failure. | Ask the customer to retry or select an alternative payment method. |
invalid_account |
No valid account found for the selected payment method. | Ask the customer to verify their ShopeePay account and retry. |
insufficient_fund |
Insufficient funds, or the payment method has reached its limit. | Ask the customer to top up their account or use a different method. |
Testing
You can simulate the full payment flow in test mode without real funds.
- Create a source and charge using your test public and secret keys.
- Redirect to the
authorize_urireturned in the charge response. - On the test authorization page, select Successful or Failed to simulate the customer completing payment.
- Verify that your webhook endpoint receives the
charge.completeevent and that your application updates the order status accordingly. - Confirm that the customer is redirected to your
return_uri.
Test mode keys are prefixed with
pkey_test_(public) andskey_test_(secret). Live mode keys are prefixed withpkey_andskey_.
Voids and refunds
You can create a partial or full refund within 180 days of the transaction date via the Refunds API or from the charge page on your dashboard.
Important: Refunds and voids are not available for off-us transactions. When a customer pays using a mobile banking app instead of the ShopeePay wallet directly (an off-us transaction), the payment cannot be refunded or voided through Omise. In these cases, the merchant must handle the refund directly with the customer outside of the Omise system.
All ShopeePay payment methods support full refunds within 180 days. Partial refunds are also supported. Voids are not available for any ShopeePay payment method — use a refund instead.
Limits
All amounts are in the smallest currency unit. For example, MYR 1.00 = 100.
| Limit | Amount | Amount (MYR) |
|---|---|---|
| Minimum | 100 |
MYR 1.00 |
| Maximum | 499900 |
MYR 4,999.00 |
Note: Limits apply to MYR transactions. Limits for THB and SGD depend on each business. Contact support@omise.co for details.
FAQ
What is the difference between a redirect flow and an offline flow?
In a redirect flow (used by ShopeePay), the customer is redirected from your website to ShopeePay's payment page to authorize the payment, then redirected back to your return_uri when done. In an offline flow, the customer stays on your page and scans a QR code using their phone without any redirect.
What should my return_uri do when the customer lands on it?
Your return_uri page should not assume the payment was successful just because the customer arrived there. Always retrieve the charge using its id and check the status field. A customer can land on return_uri after a failed or cancelled payment as well.
Why must return_uri use HTTPS?
ShopeePay requires a secure redirect URL for all authorization flows. HTTP URLs will be rejected at charge creation.
What happens if the customer closes the ShopeePay app before confirming?
The charge remains pending until it expires. By default, the charge expires 60 minutes after creation. You will receive a charge.complete webhook with status: failed and failure_code: payment_expired when this occurs.
Can I set a custom expiry time for a charge?
You cannot set a custom expiry at charge creation, but you can expire a charge early at any time using the expire endpoint.
What is an off-us transaction and why can't it be refunded?
An off-us transaction occurs when a customer pays using a linked mobile banking app rather than their ShopeePay wallet balance directly. In this case, the payment is processed outside of Omise's settlement chain, so Omise cannot initiate a refund or void. The merchant must handle the refund directly with the customer.
Can I partially refund a ShopeePay charge?
Yes. Partial refunds are supported for ShopeePay charges, as long as the refund is initiated within 180 days of the transaction date and the charge was not an off-us transaction.
What happens if I receive a failed_processing failure code?
This indicates a general processing failure. Ask the customer to retry. If failures continue in live mode, contact support@omise.co.
Is ShopeePay available for all currencies?
ShopeePay supports THB (Thailand), SGD (Singapore), and MYR (Malaysia). You must use the correct currency for the customer's country — mismatched currency and country will result in a failed charge.
Is SPayLater available in all supported countries?
Yes. SPayLater is available in Thailand, Singapore, and Malaysia.
What are the payment amount limits for ShopeePay?
For MYR transactions, the minimum is 100 (MYR 1.00) and the maximum is 499900 (MYR 4,999.00). Limits for THB and SGD depend on each business. See Limits for details.
Where can I find my API keys?
See How to access Omise API keys.