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.
Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us