NexusPay· QR Code response

QR Code in the payment response

Every POST /v1/payments (and the Ganap-compat create-checkout) returns a QR payload directly in the response. You do not need to redirect to a hosted checkout — you can render the QR on your own page, in your own app, with your own branding.

The three QR fields

The response contains three related fields. Pick the one that fits your UI:

FieldShapeWhen to use it
qr_codeRaw EMVCo payload — a plain string starting with 000201, ~250 chars.You want full control. Feed into any QR library and render your own image at any size, color, with your logo in the middle, etc.
qr_code_urlHTTPS URL to a pre-rendered PNG hosted on the provider's CDN.Fastest path — just embed as <img src>. No dependency on a QR library.
app_linkWallet deep link (e.g. gcash://…). May be null.Mobile web / mobile app flows. Offer a one-tap button that opens the payer's GCash app directly.

All three are populated for gcash_qr payments in live mode. In test modeqr_code_url points at a placeholder — use Dev Controls on the hosted checkout to drive outcomes (see Testing & Sandbox).

Example response

POST /v1/payments — 201 Created
{
  "id": "pay_0e1f2a4c9b8d6a3f5c7e1d2b",
  "object": "payment",
  "status": "pending",
  "amount": 500.00,
  "currency": "PHP",
  "method": "gcash_qr",

  "checkout_url": "https://nexuspayph.com/checkout/pay_0e1f2a4c9b8d6a3f5c7e1d2b",
  "qr_code":      "00020101021128680014ph.ppmi.p2m0113...6304ABCD",
  "qr_code_url":  "https://c.tsdpay.tech/v2/qr/TlVRK0tu...TE9",
  "app_link":     "gcash://payment/scan?payload=...",

  "expires_at":   1776840000,
  "created":      1776839700
}

Rendering tips by stack

The qr_code string is the EMVCo QR Code Specification for Payment Systems payload — the same shape every GCash / PayMaya / Maya QR uses. Any QR library can render it.

// Using the 'qrcode.react' library — the most popular React QR renderer.
// npm i qrcode.react
import { QRCodeCanvas } from 'qrcode.react';

export function PayQR({ payment }) {
  return (
    <div className="text-center">
      <h3>Scan with your GCash app</h3>
      <QRCodeCanvas
        value={payment.qr_code}
        size={280}
        level="M"                         // error correction
        includeMargin
        imageSettings={{                  // optional centered logo
          src: '/your-logo.png',
          height: 56,
          width: 56,
          excavate: true,
        }}
      />
      <p>Amount: ₱{payment.amount.toFixed(2)}</p>
    </div>
  );
}

UX tips

1. Desktop: show the QR. Mobile: offer the deep link.

Scanning a QR that's on the same phone you need to pay with is awkward — the customer can't scan the screen that's showing the QR. On mobile, prefer app_link:

jsx
function PayAction({ payment }) {
  const isMobile = typeof navigator !== 'undefined' &&
    /iPhone|Android/i.test(navigator.userAgent);

  if (isMobile && payment.app_link) {
    return <a href={payment.app_link} className="btn">Open in GCash</a>;
  }
  return <QRCodeCanvas value={payment.qr_code} size={280} />;
}

2. Show the countdown to expiry

expires_at is a Unix timestamp (seconds). Convert and render a countdown so customers know when the QR goes stale. Typical checkout window is 10–15 minutes:

jsx
import { useEffect, useState } from 'react';

function ExpiresIn({ expiresAt }) {
  const [left, setLeft] = useState(() => expiresAt * 1000 - Date.now());
  useEffect(() => {
    const id = setInterval(() => setLeft(expiresAt * 1000 - Date.now()), 1000);
    return () => clearInterval(id);
  }, [expiresAt]);
  if (left <= 0) return <span>Expired — please create a new payment.</span>;
  const m = Math.floor(left / 60000), s = Math.floor((left % 60000) / 1000);
  return <span>Expires in {m}:{String(s).padStart(2, '0')}</span>;
}

3. Poll status, or just listen for the webhook

Once the QR is on screen, you need to know when the customer paid. Two options:

  • Webhook (recommended) — register an endpoint and NexusPay POSTs payment.succeeded / payment.failed / payment.expired. Update your UI via websocket or server-sent events from there. See Webhooks.
  • Polling — hit GET /v1/payments/{id} every 5 seconds while the checkout is on screen. Stop once status !== 'pending' orexpires_at passes.

4. Don't regenerate the QR unnecessarily

qr_code is stable for the lifetime of the payment — the same value is returned on every GET /v1/payments/{id}. Create one payment, render once, update UI on status change.

5. Idempotency: replays are free

If your merchant backend retries a POST /v1/payments with the same Idempotency-Key, NexusPay returns the existing payment — same qr_code, same qr_code_url. Your customer doesn't see a new QR. See Idempotency.

Troubleshooting

qr_code is null in my response

  • Test mode never returns a real QR — use the hosted checkout_url.
  • Non-QR methods (e.g. card_3ds in the future) won't populate QR fields. Check method is gcash_qr.
  • Upstream provider occasionally fails to return QR data. In that case checkout_url still works — NexusPay's own branded checkout fetches the QR itself.

Scan works in Camera but not in GCash

GCash's scanner only accepts EMVCo P2M payloads. Our qr_code is exactly that. If your generated image doesn't scan in GCash, double-check you rendered the raw qr_code string — not a shortened / URL-wrapped version.

My QR library complains about encoding

EMVCo payloads are ASCII-only. If you see encoding errors, make sure you're passing the raw string without any transformation. Most libraries default to error correction: M, which is what GCash expects.

Still prefer a hosted checkout?

Just redirect to checkout_url. You get NexusPay-branded QR, countdown, retry UX, mobile deep link button — all maintained by us. You can always switch to rendering your own later; the API is the same.