AppGild

Builder docs

License-Verify integration

If you're hosting your app yourself, you need a small piece of code to check that the person using your app actually paid AppGild for access. This page tells you exactly what to add, what each piece does, and how to fix common problems, written for builders who can copy and paste JavaScript but don't enjoy reading API reference docs.

The 30-second version

  1. Buyer signs up + subscribes on AppGild. We give them a license key like AG-VCHX-FCBE-LRGE-JP8P.
  2. Buyer opens your app. Your app shows a sign-in box that asks for the key.
  3. Your app calls AppGild (POST /api/license/verify) to ask "is this key active for my app?"
  4. If yes, your app unlocks. If no, your app shows an error and keeps the gate up.
  5. Every 15 minutes, your app silently re-checks so cancellations / refunds take effect without the buyer reloading.

Paste-ready code

Drop this into your app's main HTML file. The only thing you need to customize is the APP_SLUG line: replace YOUR-APP-SLUG with the slug from your AppGild listing URL. (If your listing is appgild.ai/app/cool-tool, your slug is cool-tool.)

<!-- Paste this near the top of your app's main HTML file -->

<div id="appgild-gate" style="max-width:420px;margin:80px auto;padding:24px;font-family:system-ui,sans-serif;border:1px solid #e5e7eb;border-radius:8px">
  <h2 style="margin:0 0 8px;font-size:18px">Enter your license key</h2>
  <p style="margin:0 0 16px;color:#6b7280;font-size:14px">
    Get your key from your <a href="https://appgild.ai/purchases" target="_blank">AppGild purchases page</a>.
  </p>
  <input id="appgild-key" placeholder="AG-XXXX-XXXX-XXXX-XXXX"
         style="width:100%;padding:8px 12px;font-size:14px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px">
  <button id="appgild-verify"
          style="width:100%;padding:10px;background:#4f46e5;color:white;border:0;border-radius:6px;font-size:14px;cursor:pointer">
    Verify access
  </button>
  <p id="appgild-error" style="margin:12px 0 0;color:#dc2626;font-size:13px;display:none"></p>
</div>

<script>
(function () {
  const APP_SLUG = 'YOUR-APP-SLUG';   // <-- CHANGE THIS to your app's slug (in your AppGild listing URL: appgild.ai/app/<slug>)
  const VERIFY_URL = 'https://appgild.ai/api/license/verify';
  const GRACE_MS = 15 * 60 * 1000;

  const gate = document.getElementById('appgild-gate');
  const errorEl = document.getElementById('appgild-error');
  function showError(msg) { errorEl.textContent = msg; errorEl.style.display = 'block'; }
  function clearError() { errorEl.textContent = ''; errorEl.style.display = 'none'; }

  function unlock() {
    gate.style.display = 'none';
    document.body.classList.add('appgild-unlocked');
  }

  async function verify(key) {
    try {
      const r = await fetch(VERIFY_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ license_key: key, app_slug: APP_SLUG }),
      });
      if (!r.ok) return { network_ok: false };
      const data = await r.json();
      return { network_ok: true, active: !!data.active };
    } catch (e) {
      return { network_ok: false };
    }
  }

  async function attempt(key, fromStorage) {
    clearError();
    const result = await verify(key);
    if (result.network_ok && result.active) {
      localStorage.setItem('appgild_key', key);
      localStorage.setItem('appgild_last_ok', String(Date.now()));
      unlock();
      return;
    }
    if (result.network_ok && !result.active) {
      localStorage.removeItem('appgild_key');
      localStorage.removeItem('appgild_last_ok');
      if (!fromStorage) showError('That key is not active. Check your AppGild Purchases page.');
      return;
    }
    const lastOk = parseInt(localStorage.getItem('appgild_last_ok') || '0', 10);
    if (lastOk && Date.now() - lastOk < GRACE_MS && fromStorage) { unlock(); return; }
    showError("Couldn't reach AppGild. Check your connection and try again.");
  }

  // Dormant-until-listed: while your app isn't a live AppGild listing yet,
  // the gate stays out of your way so you're never locked out while building.
  // Paste your builder key (AGB-...) from your AppGild dashboard to open your
  // own LIVE app anytime.
  fetch(VERIFY_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ app_slug: APP_SLUG }),
  }).then(r => r.ok ? r.json() : { enforced: true })
    .then(d => {
      if (d && d.enforced === false) { unlock(); return; }
      const cached = localStorage.getItem('appgild_key');
      if (cached) attempt(cached, true);
    })
    .catch(() => {
      const cached = localStorage.getItem('appgild_key');
      if (cached) attempt(cached, true);
    });

  document.getElementById('appgild-verify').onclick = () => {
    const key = (document.getElementById('appgild-key').value || '').trim().toUpperCase();
    if (!/^AGB?-[A-Z0-9-]{8,}$/.test(key)) { showError("That doesn't look like a valid license key."); return; }
    attempt(key, false);
  };

  setInterval(() => {
    const k = localStorage.getItem('appgild_key');
    if (k) attempt(k, true);
  }, GRACE_MS);
})();
</script>

If you see this error, do this

no_active_purchase

What it means: AppGild looked up the key + slug pair and didn't find an active purchase. This collapses three possible underlying causes into the same response (we do this on purpose so a stranger guessing keys can't tell which one is wrong):

  • The license key is wrong (typo, copy/paste error).
  • The license key is real but for a different app (wrong APP_SLUG in your code).
  • The subscription was canceled or refunded.

What to check: (1) The buyer's key matches what they see on their /purchases page. (2) Your APP_SLUG matches your listing's URL slug exactly. (3) The subscription is active.

invalid_request (400)

What it means: The request body was malformed. Either the key isn't in the AG-XXXX-XXXX-XXXX-XXXX format, or the slug is missing/contains weird characters.

What to check: Make sure you're sending JSON with license_key and app_slug string fields, and that the slug only contains lowercase letters, numbers, and hyphens.

rate_limited (429)

What it means: One IP address sent more than 30 verify calls in a minute. Real builders almost never hit this; it's usually a sign that something's calling the endpoint in a loop (a misconfigured useEffect, etc.).

What to check: Make sure your re-verify is on a 15-minute interval, not every render or every second. The reference snippet does this correctly.

CORS error in browser console

What it means: Browser blocked the call because of cross-origin policy. This usually only happens if you accidentally added credentials: 'include' to the fetch call. Don't do that. The reference snippet doesn't.

What to check: Remove any credentials option from the fetch() call. Our endpoint is CORS-open from any origin specifically so creator-hosted apps work.

Network error / "Couldn't reach AppGild"

What it means: The fetch call failed entirely (DNS, network down, firewall, AppGild outage, etc.). The reference snippet handles this with a 15-minute grace window: if the buyer was verified successfully within the last 15 minutes, they stay in. After that, the gate comes back.

What to check: Try opening appgild.ai/api/license/verify in a new tab. You should see "Method Not Allowed" (which means our servers are up and responding). If you see a different error, AppGild may be having an outage; check our status.

FAQ

Will this lock me out of my own app while I'm still building it?

No. The gate is dormant until your app is a live AppGild listing. While you're building (for example in Lovable's preview), the snippet asks AppGild whether your app is listed yet; until it is, it does nothing and your app stays fully open. The moment your listing goes live, the gate starts enforcing automatically, with no code change on your end.

To open your own live app anytime (to keep working on it after launch), paste your builder access key. Find it on your app's dashboard under "Your builder access key." It unlocks your app the way a paying customer would, but never appears in your sales data. If it ever leaks, regenerate it from the same place and the old key stops working instantly.

What's a license key and where does the buyer get it?

A license key is a unique random code (like AG-VCHX-FCBE-LRGE-JP8P) that AppGild generates the moment a buyer completes checkout. It's tied to exactly one purchase: that buyer, that app.

Buyers find it on their AppGild /purchases page, with a one-click copy button next to it. They paste it into your app's sign-in box.

Do I need to write my own user accounts / login?

No, the license key IS the login. The buyer doesn't create a separate account on your app. They paste their AppGild license key, your app caches it in localStorage, and they're in.

You only need your own user accounts if your app does things that require user identity beyond "is this person a paying customer" (e.g. saving per-user files, multi-user collaboration). For most simple apps, the license key is enough.

What happens if a buyer shares their license key with a friend?

Both will be able to use your app, until the original buyer cancels their subscription. At that point both lose access on the next 15-minute re-check (or instantly on page reload).

This is the same trade-off Netflix made with password sharing. Stopping it entirely requires heavier infrastructure (per-device tokens, server-side session enforcement, etc.) and gives most users a worse experience. For niche tools, "honor system + cancellation propagates" is the right balance.

What happens if the buyer cancels or gets refunded?

AppGild marks the purchase as canceled or refunded the moment Stripe tells us. The next time your app calls our verify endpoint, it returns { "active": false, "reason": "no_active_purchase" }. Your app then clears the cached key and shows the sign-in gate again.

Worst case (buyer cancels mid-session and never reloads): they lose access within 15 minutes (the next re-verify interval).

What about Netflix-style 'cancel but keep access until end of billing period'?

Handled automatically. When a buyer cancels mid-month, Stripe keeps their subscription marked active through the end of the period they already paid for. Our verify endpoint returns { "active": true } until that date, then flips to false. Your app doesn't need to do anything special.

Can I customize the look of the sign-in box?

Absolutely. The reference snippet uses inline styles so it works out-of-the-box, but the HTML is plain: you can swap classes, use Tailwind, drop in your own design system, etc. The only piece that has to stay functionally identical is the fetch() call and the unlock/lock logic.

I'm using React / Next.js / Vue / Svelte. Does this work?

Yes. The fetch() call is the same in every framework. Move the verify logic into your component's mount lifecycle (useEffect in React, onMount in Svelte, etc.), and use component state instead of localStorage for the unlocked flag if you prefer. The reference snippet is vanilla so it works literally anywhere; framework versions are a 5-minute port.

Is my app's source code protected? Can people just steal it?

Honest answer: no, no client-side license check on a JavaScript app is "stealing-proof." A determined attacker can dump your minified JS, comment out the verify call, and run their own copy. This is true of Adobe CS3, Sketch, JetBrains, and every other client-side license model.

What our gate DOES protect: casual URL sharing (you need a key, not just the URL), subscription expiration (re-checks catch it), and bulk distribution (anyone who tries to share a single key to many users has all of them lose access when the original buyer cancels).

For niche tools at $5–$50/month, this is the right trade-off. The audience isn't motivated to crack and redistribute; they just want the convenience of the legit version with updates and support.

What if my app needs a real backend?

You can still use the license_key gate; just verify on the server instead of (or in addition to) the browser. Have your backend call AppGild's verify endpoint on every privileged request, passing the key the client sent. This is more secure than the browser-only version (you control the verify call) at the cost of more roundtrips.

The trade-off is fine for most apps. Most builders use the browser version.

How fast does AppGild's verify endpoint respond?

Typical p50 is <200ms, p95 <500ms. The endpoint does a single DB lookup keyed by the unique license_key column, so it stays fast regardless of how many purchases AppGild has.

What does my app do offline?

The reference snippet has a 15-minute grace window: if your last successful verify was within the last 15 minutes, the app stays unlocked even on a failed call (Wi-Fi blip, AppGild outage, etc.). After that window, the gate comes back. This balances "don't lock paying users out over a network hiccup" with "don't enable indefinite offline bypass."

If your app is intentionally offline-first, you can extend the grace window, but past ~24 hours you're effectively making the license check optional, which removes the gate's value.

How to test before submitting

The cheapest way to verify your integration is correct: visit our Integration Verifier tool. Paste your deployed app's URL and a license key, and it'll tell you:

  • Whether AppGild thinks the key is active (proves the endpoint sees your request)
  • Whether the key + slug pair matches an active purchase (proves your APP_SLUG is right)
  • An iframe preview of your app so you can paste the key and confirm it unlocks

Other ways to test: open your app in two browsers, paste your key in one, watch what happens. Or buy your own app with a test account.

Still stuck?

If something isn't working and the troubleshooting above didn't fix it, message us with: (1) your app's slug, (2) the exact error or behavior you're seeing, (3) a link to your deployed app if it's public.