Using Checky Widget on Headless Storefront
This guide shows how to add a "Check Gift Card Balance" widget to a headless Shopify storefront (Hydrogen, Next.js, Nuxt, plain JS, etc.).
You only need to call a single HTTPS endpoint from your frontend — no SDK, no auth headers, no server-side proxy required.
The Endpoint
POST https://checky-api.punchy.dev/balance
Content-Type: application/json
{
"shop": "your-store.myshopify.com",
"gc_code": "1234",
"customer": "John Doe"
}
| Field | Required | Description |
| shop | yes | Your permanent `*.myshopify.com` domain. Use the canonical Shopify domain, not your custom storefront domain. |
| gc_code | yes | The gift card code or its last 4 digits, depending on how the merchant configured the app. Minimum 4 characters. |
| customer | no | Customer's full name or email. When provided, it's used as an extra match to disambiguate cards. |
CORS is open, so you can call the endpoint directly from the browser.
Possible responses
All responses are JSON.
Found (`200`)
{
"giftCard": {
"maskedCode": "•••• •••• •••• 1234",
"createdAt": "2025-09-12T14:08:21Z",
"initialValue": "$50.00",
"balance": "$32.50",
"expiresOn": "2026-09-12",
"enabled": true,
"deactivatedAt": null
}
}
- `balance` and `initialValue` are preformatted strings (currency symbol + 2 decimals) — display them as‑is, don't reparse them.
- `expiresOn` is `null` when the card never expires.
- `deactivatedAt` is `null` for active cards. If set, render a "deactivated on …" notice.
- `maskedCode` is the only code Shopify exposes — the raw code is never returned.
Not found (`200`)
{ "giftCard": {} }
Returned when no card matches, or when the search is ambiguous. Detect this by checking that `giftCard.maskedCode` is empty.
Bad request (`404`)
{ "message": "Missing params" }
`shop` or `gc_code` is missing.
Unknown shop (`404`)
{ "message": "Shop not found" }
The provided `shop` domain doesn't have the app installed.
Server error (`500`)
{ "message": "Oops! Something went wrong" }
Treat as a transient error and offer the user a retry.
Tip: branch on the response **body shape** (`giftCard.maskedCode` vs. `message`) rather than the HTTP status, since "not found" and "found" both return `200`.
Example: plain HTML + JS widget
<div class="gc-checker">
<label>
Gift card last 4 digits
<input id="gc-code" type="text" maxlength="4" placeholder="XXXX" />
</label>
<!-- Optional, only if you want stricter matching -->
<label>
Your name or email
<input id="gc-customer" type="text" placeholder="John Doe" />
</label>
<button id="gc-check" type="button">Check</button>
<p id="gc-error" class="error" hidden></p>
<div id="gc-result" hidden>
<p><strong id="gc-masked"></strong> — <span id="gc-status"></span></p>
<p>Balance: <strong id="gc-balance"></strong></p>
<p>Initial amount: <span id="gc-initial"></span></p>
<p>Created: <span id="gc-created"></span></p>
<p>Expires: <span id="gc-expires"></span></p>
<p id="gc-deactivated" hidden>Deactivated on <span id="gc-deactivated-on"></span></p>
</div>
<p id="gc-empty" hidden>No Gift Card found</p>
</div>
<script>
const SHOP = "your-store.myshopify.com";
const API = "https://checky-api.punchy.dev/balance";
async function checkBalance() {
const code = document.getElementById("gc-code").value.trim();
const customer = document.getElementById("gc-customer")?.value.trim() || "";
document.getElementById("gc-error").hidden = true;
document.getElementById("gc-result").hidden = true;
document.getElementById("gc-empty").hidden = true;
if (code.length < 4) {
document.getElementById("gc-error").textContent = "Please enter at least 4 digits";
document.getElementById("gc-error").hidden = false;
return;
}
const btn = document.getElementById("gc-check");
btn.disabled = true;
btn.textContent = "Checking...";
try {
const res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ shop: SHOP, gc_code: code, customer }),
});
if (!res.ok) {
const { message } = await res.json().catch(() => ({}));
throw new Error(message || `Request failed (${res.status})`);
}
const { giftCard } = await res.json();
if (!giftCard?.maskedCode) {
document.getElementById("gc-empty").hidden = false;
return;
}
document.getElementById("gc-masked").textContent = giftCard.maskedCode;
document.getElementById("gc-status").textContent = giftCard.enabled ? "Enabled" : "Disabled";
document.getElementById("gc-balance").textContent = giftCard.balance;
document.getElementById("gc-initial").textContent = giftCard.initialValue;
document.getElementById("gc-created").textContent = new Date(giftCard.createdAt).toLocaleDateString();
document.getElementById("gc-expires").textContent = giftCard.expiresOn
? new Date(giftCard.expiresOn).toLocaleDateString()
: "Never";
if (giftCard.deactivatedAt) {
document.getElementById("gc-deactivated").hidden = false;
document.getElementById("gc-deactivated-on").textContent = new Date(giftCard.deactivatedAt).toLocaleDateString();
}
document.getElementById("gc-result").hidden = false;
} catch (err) {
document.getElementById("gc-error").textContent = err.message || "Something went wrong";
document.getElementById("gc-error").hidden = false;
} finally {
btn.disabled = false;
btn.textContent = "Check";
}
}
document.getElementById("gc-check").addEventListener("click", checkBalance);
</script>
Quick checklist
- Send the canonical `*.myshopify.com` domain as `shop`.
- Require at least 4 characters in `gc_code` before calling the API.
- Always check `giftCard.maskedCode` to distinguish "found" from "not found".
- Render `balance` and `initialValue` exactly as returned — they're already formatted with the correct currency.
- Show a friendly retry option on `500` responses.