Accept an Online Payment
Securely accept payments online.
Build a payment form or use a prebuilt checkout page to start accepting online payments.
Qashier-hosted page
This is a Qashier-hosted page for when platform is web. It looks like this image below.

The payment methods in this image are illustrative. What your customer sees is whatever the merchant has enabled, which varies by account and country — see Payment methods.
Base URL
https://api.qashier.com/v3/payment/qpay
Authentication
API requests are authenticated using HTTP Basic Auth. Provide your API key as the username and leave the password empty:
curl https://api.qashier.com/v3/payment/qpay/checkout/sessions \
-u YOUR_API_KEY:
Alternatively, construct the header manually:
Authorization: Basic <base64(YOUR_API_KEY:)>
Rate Limits
- 100 requests per second per API key
If you exceed the rate limit, the API returns a 429 Too Many Requests response with a rate_limit_error.
Error Handling
Error responses follow this structure:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "Missing required parameter: return_url",
"param": "return_url"
}
}
| Type | Description |
|---|---|
api_error | A temporary problem with Qashier's servers |
authentication_error | Failure to properly authenticate the request |
idempotency_error | An Idempotency-Key was re-used with different parameters |
invalid_request_error | The request has invalid or missing parameters |
rate_limit_error | Too many requests sent too quickly |
Redirect your customer to Qashier Checkout [Client-side] [Server-side]
Add a checkout button to your website that calls a server-side endpoint to create a Checkout Session.
<html>
<head>
<title>Buy cool new product</title>
</head>
<body>
<form action="/create-checkout-session" method="POST">
<button type="submit">Checkout</button>
</form>
</body>
</html>
A Checkout Session is the programmatic representation of what your customer sees when they're redirected to the payment form. You can configure it with options such as:
- Line items to charge
- Currency to use (ISO 4217 currency code format, e.g., "SGD" for Singapore Dollar)
You must populate success_url with the URL value of a page on your website that Checkout returns your customer to after they complete the payment.
Checkout Sessions expire 24 hours after creation by default.
After creating a Checkout Session, redirect your customer to the url of CheckoutSession returned in the response.
// This example sets up an endpoint using the Express framework.
const express = require('express');
const fetch = require('node-fetch');
const app = express();
const url = "https://api.qashier.com/v3/payment/qpay/checkout/sessions";
app.post('/create-checkout-session', async (req, res) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + Buffer.from('YOUR_API_KEY:').toString('base64')
},
body: JSON.stringify({
currency: 'SGD', // ISO 4217 currency code (e.g., 'SGD', 'MYR', 'INR')
line_items: [
{
unit_amount: 2000,
quantity: 1,
},
],
expires_at: Math.floor(Date.now() / 1000) + 86400, // 24 hours from now (max allowed)
return_url: 'http://localhost:4242/return',
success_url: 'http://localhost:4242/success',
})
});
const data = await response.json();
res.redirect(303, data.url);
});
app.listen(4242, () => console.log(`Listening on port ${4242}!`));
Payment methods
You do not choose the payment methods. Qashier Checkout renders whatever the merchant has enabled on their account, and the available set differs by country — a Singapore merchant and a Thai one are offered different methods, and both change over time as we add support for new ones.
This means your integration does not need to change when a method is added or
removed: you create the session, we render what applies, and the same
checkout.session.completed tells you when the attempt has finished.
Completed does not mean paid. A declined payment also completes the session and
emits checkout.session.completed — the two outcomes differ only in
data.object.payment_status, which is paid on success and unpaid on a
decline. Fulfil on payment_status, never on the event name alone.
Abandonment produces no event at all. A session that reaches expires_at
without a payment expires silently — checkout.session.expired is only sent
when you expire a session yourself through the API. To react to abandoned
checkouts, retrieve the session after expires_at and confirm
status: expired before treating it as abandoned. Do not infer abandonment
from webhook silence alone: a delayed checkout.session.completed could
otherwise misclassify a paid order.
To see what a specific account will offer, create a test Checkout Session against it and open the returned URL — the checkout renders exactly what that account has enabled, which no list here could stay true to. If a method you expect is missing, it is an account setting rather than an integration problem; contact your Qashier representative to have it enabled.
Confirm your endpoint
Confirm your endpoint is accessible by starting your web server (for example, localhost:4242) and running the following command:
curl -X POST -is "http://localhost:4242/create-checkout-session" -d ""
You should see a response in your terminal that looks like this:
HTTP/1.1 303 See Other
Location: https://qpay.qashier.com/c/pay/cs_test_...
...
Testing
You should now have a working checkout button that redirects your customer to Qashier Checkout.
- Click the checkout button.
- You're redirected to the Qashier Checkout payment form.
If your integration isn't working:
- Open the Network tab in your browser's developer tools.
- Click the checkout button and confirm it sent an XHR request to your server-side endpoint (
POST /create-checkout-session). - Verify the request is returning a 200 status.
- Use
console.log(data)inside your button click listener to confirm the correct data returned.
Show a success page [Client-side] [Server-side]
It's important for your customer to see a success page after they successfully submit the payment form. Host this success page on your site.
Create a minimal success page:
<html>
<head><title>Thanks for your order!</title></head>
<body>
<h1>Thanks for your order!</h1>
<p>
We appreciate your business!
If you have any questions, please email
<a href="mailto:orders@example.com">orders@example.com</a>.
</p>
</body>
</html>
Next, find the new transaction in the Qashier HQ Dashboard. Successful payments appear in the Dashboard's Transaction. When you click a payment, it takes you to the payment details page.
Handle post-payment events
Qashier sends a checkout.session.completed event when a payment attempt on a Checkout Session finishes — on success and on decline alike. Check data.object.payment_status before acting on it: paid means money moved, unpaid means the attempt failed. On a paid event it might trigger you to:
- Send an order confirmation email to your customer.
- Log the sale in a database.
- Start a shipping workflow.
Listen for these events rather than waiting for your customer to be redirected back to your website. Triggering fulfilment only from your Checkout landing page is unreliable. Setting up your integration to listen for asynchronous events.
Handle the following events when collecting payments with the Checkout:
| Event | Description | Action |
|---|---|---|
| checkout.session.completed | Sent when a payment attempt finishes, whether it succeeded or was declined. data.object.payment_status is paid on success and unpaid on a decline. | Only when payment_status is paid: send the customer an order confirmation and fulfill (Fulfillment is the process of providing the goods or services purchased by a customer, typically after payment is collected) their order. |
| checkout.session.expired | Sent when you expire a session through the expire API. A session that reaches expires_at on its own expires without emitting this event. |
Test your integration
To test your Qashier-hosted payment form integration:
- Create a Checkout Session.
- Fill out the payment details with a method from the following table, using
the CVV and Expiry that row specifies.
- Enter any billing postal code.
- Click Pay. You're redirected to your
success_url. - Go to Qashier HQ and look for the payment on the Transactions page. If your payment succeeded, you'll see it in that list.
- Click your payment to see more details, like a Checkout summary with billing information and the list of purchased items. You can use this information to fulfil the order.
Cards
Test card numbers are specific to the account you are testing against, because the card processor behind an account differs by country. Using a number from the wrong row fails at the processor, not in your integration — so check the country column before you conclude something is broken.
| Country | Card Number | CVV | Expiry | Brand | Description |
|---|---|---|---|---|---|
| SG, MY | 4005520000000129 | 002 | 10/30 | Visa | Successful transaction |
| SG, MY | 5413330089010640 | 002 | 10/30 | Mastercard | Successful transaction |
| SG, MY | 4147463011110083 | Any | Any | Visa | 3DS frictionless — authenticated |
| SG, MY | 5239290700000028 | Any | Any | Mastercard | 3DS frictionless — authenticated |
| SG, MY | 4147463011110091 | Any | Any | Visa | 3DS frictionless — not authenticated |
| SG, MY | 5239290700000036 | Any | Any | Mastercard | 3DS frictionless — not authenticated |
| SG, MY | 4147463011110117 | Any | Any | Visa | 3DS frictionless — attempted |
| SG, MY | 5239290700000044 | Any | Any | Mastercard | 3DS frictionless — attempted |
| SG, MY | 4147463011110042 | Any | Any | Visa | 3DS frictionless — rejected |
| SG, MY | 5239290700000051 | Any | Any | Mastercard | 3DS frictionless — rejected |
| SG, MY | 4147463011110109 | Any | Any | Visa | 3DS challenge — authenticated |
| SG, MY | 4147463011110034 | Any | Any | Visa | 3DS challenge — rejected |
| SG, MY | 5239290700000010 | Any | Any | Mastercard | 3DS challenge — rejected |
| TH | 4111111111111111 | 123 | Any | Visa | Successful transaction |
| TH | 5372074248113841 | 123 | Any | Mastercard | Successful transaction |
| TH | 4953261730509988 | 123 | Any | Visa | 3DS challenge before it completes |
| TH | 4943129900084541 | 123 | Any | Visa | Declined — insufficient funds |
| TH | 4111111111000025 | 123 | Any | Visa | Declined by the issuer |
Any means any value works — any future expiry, any CVV of the right length.
The 3DS rows exercise authentication only and are not built to carry a payment
through to settlement. Frictionless means the issuer decides without asking the
customer anything; challenge puts a step in front of them, and is the path worth
running because it is the only one where your success_url is reached later than
you might assume. The Thailand 3DS challenge row is the equivalent.
The two Declined rows still emit checkout.session.completed — with
payment_status: unpaid, as described in Handle post-payment events. They are the
rows to use when testing that your fulfilment logic reads payment_status
rather than the event name.
Cards are only one method. Testing a QR or wallet method does not involve a card
number at all: complete the payment in the app the checkout hands you to. The
webhook behaves the same for every method — checkout.session.completed fires
when the attempt finishes, and payment_status says whether it was paid.
Disclose Qashier to your customers
Qashier collects information on customer interactions with Elements to provide services to you, prevent fraud, and improve its services. This includes using cookies and IP addresses to identify which Elements a customer saw during a single checkout session. You're responsible for disclosing and obtaining all rights and consents necessary for Qashier to use data in these ways. For more information, visit our TBD privacy center.
API Reference
For full endpoint details, request/response schemas, and webhook specifications, see the Online Payments API Reference.