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

HTTPCodeMeaning
400VALIDATION_ERRORInvalid parameters or missing required fields
400INVALID_JSONRequest body is not valid JSON
400INVALID_CONTENT_TYPEContent-Type must be application/json
401UNAUTHORIZEDMissing or invalid API key
429RATE_LIMIT_EXCEEDEDQuota for the billing period used up. error.reset_at and Retry-After say when it resets; error.upgrade_url links to billing
500INTERNAL_ERRORServer 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.

PlanLimit per billing period
Free50 codes, then requests stop until the next period
Pay as you goUp 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_abc123def456
  • X-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.

← All guides