Documentation
API Reference
Everything you need to integrate PayNow QR generation into your application.
Quickstart
sk_paynow_...) is shown once on the dashboard.
https://paynow.doubleam.com
curl -X POST https://paynow.doubleam.com/api/v1/generate \
-H "Content-Type: application/json" \
-H "x-api-key: sk_paynow_YOUR_KEY" \
-d '{
"payment_type": "uen",
"uen": "202521700C",
"amount": "10.50",
"merchant_name": "My Business"
}'
The response includes a qr_string (raw EMVCo payload), card_url (branded card image),
and image_url (plain QR). Use the card URL in WhatsApp templates, emails, or anywhere an image URL is needed.
Authentication
Pass your API key in the x-api-key header on every request to authenticated endpoints.
x-api-key: sk_paynow_YOUR_SECRET_KEY
Key Types
| Type | Prefix | Use | Domain Restriction |
|---|---|---|---|
| Secret | sk_paynow_ | Server-side calls. Full access. | No |
| Publishable | pk_paynow_ | Client-side / browser calls. | Yes — restricted to allowed domains (checked via Origin/Referer header) |
Secret keys must never be exposed in client-side code. Use publishable keys with domain restrictions for browser integrations.
POST /api/v1/generate
Generate a PayNow QR code. Returns the raw QR string, image URLs, and optionally a WhatsApp link. Requires API key.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_type | string | Yes | "uen" or "mobile" |
uen | string | If UEN | Business UEN number (8-12 alphanumeric chars) |
mobile | string | If mobile | SG mobile number. Accepts 91234567, 6591234567, +6591234567 |
amount | string|number | No | Amount in SGD. Tolerant parser: "200", "$350.50", "SGD 200.50" all work. Omit for any-amount QR. |
merchant_name | string | No | Merchant name on the card (max 25 chars, default "NA") |
reference | string | No | Your reference / invoice number (max 25 chars). This is the default way to attach a reference. |
generate_reference | boolean | No | Set true to auto-generate a reference (opt-in only) |
reference_prefix | string | No | Prefix for auto-generated reference (default "PN") |
whatsapp_to | string | No | WhatsApp number with country code (e.g. "6591234567"). When provided, the response includes a whatsapp_url. |
size | integer | No | QR image size in pixels (default 512) |
metadata | object | No | Arbitrary JSON stored with the payment request record |
Response
{
"qr_string": "00020101021226370009SG.PAYNOW...",
"image_url": "https://paynow.doubleam.com/api/v1/qr.png?...",
"card_url": "https://paynow.doubleam.com/api/v1/card.png?...",
"whatsapp_url": "https://wa.me/6591234567?text=...",
"payment": {
"paymentType": "uen",
"proxy": "202521700C",
"amount": "10.50",
"editable": false,
"merchantName": "My Business",
"reference": "INV-001",
"expiry": null,
"currency": "SGD",
"static": false
}
}
whatsapp_url is only present when whatsapp_to is provided. static is true when no amount is set (reusable QR).
GET /api/v1/card.png
Returns a branded PayNow card as a PNG image. Public endpoint — no API key required. This is what WhatsApp, email clients, and browsers fetch.
Query Parameters
Same as /generate: payment_type, uen/mobile, amount, merchant_name, reference, size.
Response
Content-Type: image/png — raw PNG bytes. Use the URL directly in <img> tags or WhatsApp template headers.
curl "https://paynow.doubleam.com/api/v1/card.png?payment_type=uen&uen=202521700C&amount=10.50&merchant_name=TEST" \
-o card.png
GET /api/v1/qr.png
Returns a plain QR code PNG (no branding). Public endpoint.
Same query parameters as /card.png. Use when you need a raw QR without the branded card layout.
GET /api/v1/records
List payment requests generated with your API key. Requires API key. Use this to pull records into your own system for reconciliation.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max records to return (1–100) |
offset | integer | 0 | Pagination offset |
Response
{
"records": [
{
"id": "uuid",
"payment_type": "uen",
"proxy": "202521700C",
"amount": "10.50",
"merchant_name": "My Business",
"reference": "INV-001",
"created_at": "2026-06-27T12:00:00.000Z"
}
],
"limit": 20,
"offset": 0
}
GET /health
Liveness check. Public, no auth.
{ "ok": true, "service": "doubleam-paynow-api", "version": "2.0.0" }
Reference Behaviour
The reference field in the QR encodes a bill/invoice number (max 25 chars) that the payer's bank app may display. Three modes:
| You Send | What Happens |
|---|---|
"reference": "INV-001" | Default. Your reference is used as-is in the QR. Ideal when you have your own invoice/order numbering. |
"generate_reference": true | Opt-in auto-gen. A unique reference is generated (e.g. PN-MQWI1Y4U9RQ). Use reference_prefix to set a custom prefix. |
| (neither) | No reference. The QR contains no bill number field. |
The card displays exactly the reference value that is encoded in the QR (up to 25 characters). There is no display/encode mismatch.
Errors
All errors return JSON with error and message fields.
| Status | Error Code | Meaning |
|---|---|---|
400 | bad_request | Request body is not valid JSON |
400 | invalid_input | Missing/invalid parameters (e.g. bad UEN, negative amount) |
401 | unauthorized | Missing or invalid API key |
403 | forbidden | Publishable key used from an unauthorized domain |
429 | rate_limited | Monthly quota or per-IP rate limit exceeded |
429 Rate Limit Response
{
"error": "rate_limited",
"message": "Monthly limit exceeded (50/50). Upgrade your plan for more requests.",
"usage": { "used": 50, "limit": 50 }
}
Rate Limits
| Limit | Scope | Value |
|---|---|---|
| Monthly quota | Per API key | 50 requests/month (Free plan). Only POST /generate counts. |
| Per-IP rate limit | Public image endpoints | 60 requests/minute for /card.png and /qr.png |
The GET /records endpoint does not count toward the monthly quota.
When the quota is exceeded, the API returns 429 with a JSON body showing current usage.
Code Examples
curl
curl -X POST https://paynow.doubleam.com/api/v1/generate \
-H "Content-Type: application/json" \
-H "x-api-key: sk_paynow_YOUR_KEY" \
-d '{
"payment_type": "uen",
"uen": "202521700C",
"amount": "150.00",
"merchant_name": "Acme Pte Ltd",
"reference": "INV-2026-042"
}'
Node.js (fetch)
const response = await fetch("https://paynow.doubleam.com/api/v1/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "sk_paynow_YOUR_KEY",
},
body: JSON.stringify({
payment_type: "uen",
uen: "202521700C",
amount: "150.00",
merchant_name: "Acme Pte Ltd",
reference: "INV-2026-042",
}),
});
const data = await response.json();
console.log(data.card_url); // branded card image URL
console.log(data.qr_string); // raw EMVCo payload
Python (requests)
import requests
resp = requests.post(
"https://paynow.doubleam.com/api/v1/generate",
headers={"x-api-key": "sk_paynow_YOUR_KEY"},
json={
"payment_type": "uen",
"uen": "202521700C",
"amount": "150.00",
"merchant_name": "Acme Pte Ltd",
"reference": "INV-2026-042",
},
)
data = resp.json()
print(data["card_url"]) # branded card image URL
print(data["qr_string"]) # raw EMVCo payload
PHP
$ch = curl_init("https://paynow.doubleam.com/api/v1/generate");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: sk_paynow_YOUR_KEY",
],
CURLOPT_POSTFIELDS => json_encode([
"payment_type" => "uen",
"uen" => "202521700C",
"amount" => "150.00",
"merchant_name" => "Acme Pte Ltd",
"reference" => "INV-2026-042",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data["card_url"]; // branded card image URL
echo $data["qr_string"]; // raw EMVCo payload
OpenAPI Spec
The full OpenAPI 3.1 specification is available for generating client SDKs and importing into API tools like Postman or Insomnia.
Download openapi.yaml