Handling Errors and Rate Limits
Every SGPayNowQR error code, recommended retry/backoff handling, and how to read the X-RateLimit-* response headers.
Every error response uses the same envelope, so you can handle them uniformly. Inspect error.code for a stable, machine-readable identifier and log meta.request_id for support.
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Quota used up: 50 of 50 requests on the "free" tier this billing period. Resets 2026-11-12T00:00:00+08:00. Upgrade to pay as you go at https://developers.sgpaynowqr.com/billing.",
"upgrade_url": "https://developers.sgpaynowqr.com/billing",
"reset_at": "2026-11-12T00:00:00+08:00"
},
"meta": {
"api_version": "v1",
"request_id": "req_abc123def456"
}
}Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid parameters or missing required fields |
| 400 | INVALID_JSON | Request body is not valid JSON |
| 400 | INVALID_CONTENT_TYPE | Content-Type must be application/json |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 429 | RATE_LIMIT_EXCEEDED | Quota for the billing period used up. error.reset_at and Retry-After say when it resets; error.upgrade_url links to billing |
| 500 | INTERNAL_ERROR | Server error during QR generation |
Retry guidance
4xx errors are caused by the request — fix the input rather than retrying. VALIDATION_ERROR, INVALID_JSON, and UNAUTHORIZED will keep failing identically. For 429 RATE_LIMIT_EXCEEDED, stop until the quota resets: the response carries a Retry-After header (seconds until the reset), error.reset_at (the reset time) and error.upgrade_url (where to raise the limit). For 500 INTERNAL_ERROR, retry with exponential backoff (e.g. 1s, 2s, 4s) and cap the attempts.
Rate limits
Limits are per account, shared across all your API keys, per monthly billing period. Pay-as-you-go accounts follow their Stripe billing date. Free accounts reset each month on the day they signed up, at 00:00:00+08:00. Only successful (2xx) responses count toward the quota. On pay as you go, the limit is the codes your maximum monthly bill covers: you set it on the billing page, and requests stop when the bill so far reaches it. The X-RateLimit-Resetheader and the usage endpoint’s reset field give your exact reset time.
| Plan | Limit per billing period |
|---|---|
| Free | 50 codes, then requests stop until the next period |
| Pay as you go | Up to your maximum monthly bill: S$50 by default (11,760 codes), settable from S$10 (914) to S$500 (191,760) |
Reading the rate-limit headers
Every response carries your current quota state. Track these to avoid hitting the limit:
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 2026-11-12T00:00:00+08:00
X-Request-Id: req_abc123def456X-RateLimit-Remaining— requests left this billing period; throttle as it approaches 0.X-RateLimit-Reset— when the quota refills (ISO 8601, SGT).Retry-After— on a quota 429 only: seconds until the quota refills.
Usage warnings
Once the account reaches 80% of its limit, successful responses from POST /generate and GET /usage include meta.usage.warning, a message you can log or show to whoever runs the integration. It is absent below 80%. The MCP generate_qr tool adds the same message as a short usage note in its result.
"meta": {
"api_version": "v1",
"request_id": "req_abc123def456",
"usage": {
"used": 40,
"limit": 50,
"period": "2026-10-12",
"warning": "You've used 40 of 50 requests (80%) this billing period. Quota resets 2026-11-12. Upgrade to pay as you go at https://developers.sgpaynowqr.com/billing."
}
}Next, see TypeScript SDK Quickstart or the full API reference.