Documentation

NAYRUM Integration guide

In NAYRUM the buyer experience is always: pay for a product, Pro is granted automatically for that product, open the app and it is already Pro. There is no activation, redemption, code, or return trip to NAYRUM, and creators never activate purchases by hand.

The integration is not a second step for the buyer. It is only the technical bridge that lets an app running on your own server securely read the Pro state NAYRUM already granted for that specific product. Apps hosted by NAYRUM, free apps, and paid apps with nothing locked behind the purchase need none of this.

Using PWA Bridge? Stop here.

If your app goes through PWA Bridge, the whole flow below is already done for you: you paste the nayk_... key into the Bridge once, and the Bridge redeems the handoff ticket server-to-server, persists the buyer's subject, revalidates access on every app open, and exposes active | inactive | unknown to your app via the Access Layer. Your PWA never sees the key and never calls NAYRUM directly.

Keep reading only if you integrate your own server directly, without the Bridge.

1. Keep the key on your server

The key is shown only once, at the moment you connect the integration in Creator Studio. NAYRUM stores only a hash of it. Save it as an environment variable on your own backend and never ship it in browser code, a mobile bundle, or a public repository.

NAYRUM_API_KEY=nayk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx

2. Read the handoff ticket

When a buyer taps “Open PWA” in NAYRUM, we open your app URL with a single-use ticket that expires in two minutes:

https://your-app.example/?nayrum_ticket=<64 hex chars>

The ticket proves nothing by itself. Send it to your own backend and redeem it there.

3. Redeem it server-to-server

POST https://nayrum.app/api/public/entitlements/verify
Authorization: Bearer $NAYRUM_API_KEY
Content-Type: application/json

{ "token": "<nayrum_ticket>" }

A successful response returns an opaque subject scoped to your key, plus the buyer's access state. Store the subject in your own session.

4. Re-check access on every session

POST https://nayrum.app/api/public/entitlements/status
Authorization: Bearer $NAYRUM_API_KEY
Content-Type: application/json

{ "subject": "sub_..." }

This is how refunds, cancellations and expiries remove access inside your app too.

5. Confirm the wiring

POST https://nayrum.app/api/public/integrations/verify
Authorization: Bearer $NAYRUM_API_KEY
Content-Type: application/json

{ "product_id": "<your product id>" }

Then go back to Creator Studio, press “Test connection”, and tick “I’ve added this key to my server-side NAYRUM integration”. A paid app marked as needing the integration can only be submitted and published after that confirmation.

6. Access is per app, never global

Every purchase, entitlement, and Pro access flow in NAYRUM is scoped to a single product. Buying App A grants access only to App A; it never unlocks App B. There is no global, account-wide “Pro” flag in NAYRUM.

An integration key is bound to one creator and one product. It can verify entitlements for that product only — it never authorizes access to a different app, even one from the same creator. Keep each app’s key separate and never reuse a key across products.

When you call /entitlements/status, always do it with the key for the app the buyer is currently using, and gate only that app’s paid features on the result.

7. One reference implementation for every app

The same file works for any app you publish — Node, Express, Next.js route handlers, Cloudflare Workers, Deno. Nothing in it is app-specific: each app just runs it with its own NAYRUM_API_KEY.

// nayrum.js — server-side only, works for any NAYRUM product
const NAYRUM = "https://nayrum.app/api/public";

async function call(path, body) {
  const res = await fetch(NAYRUM + path, {
    method: "POST",
    headers: {
      authorization: "Bearer " + process.env.NAYRUM_API_KEY,
      "content-type": "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) return { active: false, error: res.status };
  return res.json(); // { active, subject, product_id, source, expires_at }
}

// Called once, when the buyer arrives with ?nayrum_ticket=...
export const redeemTicket = (token) => call("/entitlements/verify", { token });

// Called on every session / app load, using the stored subject
export const checkAccess = (subject) => call("/entitlements/status", { subject });

Wiring it up in your app is always the same three moves:

// 1. Entry point: if the URL has a ticket, redeem it and remember the subject.
const ticket = new URL(request.url).searchParams.get("nayrum_ticket");
if (ticket) {
  const r = await redeemTicket(ticket);
  if (r.active) session.nayrumSubject = r.subject; // persist in your own session
}

// 2. On every load: ask NAYRUM if that subject still has access.
const pro = session.nayrumSubject
  ? (await checkAccess(session.nayrumSubject)).active
  : false;

// 3. Gate only this app's paid features on `pro`.
//    Vignette: skip the 30-day trial limit.
//    Infinity Constellation: lift the star cap.
//    Living Doodle: lift the character cap and enable clear.

Never call these endpoints from browser code and never send the key to the client — the browser only ever receives the resulting pro boolean.

8. Standard setup form (drop-in, reusable)

Use the same form in every external app — Vignette, Living Doodle, Infinity Constellation and anything you publish later. It only pastes the key and verifies it; it does not activate anything. The purchase on NAYRUM already granted Pro for that product, and access stays scoped to that product.

Backend — the key is posted here, kept on your server, and verified against the product it belongs to. It is never returned to the browser.

// POST /admin/nayrum-integration  (your app's own backend, admin-only route)
const NAYRUM = "https://nayrum.app/api/public";

export async function saveNayrumKey({ key, productId }) {
  if (!/^nayk_[A-Za-z0-9_-]{8,}$/.test(key ?? "")) {
    return { status: "invalid_key" };
  }

  const res = await fetch(NAYRUM + "/integrations/verify", {
    method: "POST",
    headers: { authorization: "Bearer " + key, "content-type": "application/json" },
    body: JSON.stringify({ product_id: productId }),
  });

  if (!res.ok) return { status: "invalid_key" };
  const data = await res.json();
  if (data.error || data.product_id !== productId) return { status: "invalid_key" };

  await secrets.set("NAYRUM_API_KEY", key); // env var / secret store — never a DB column read by the client
  await secrets.set("NAYRUM_PRODUCT_ID", productId);
  return { status: "connected" }; // no key echoed back
}

Frontend — a plain form with the four states. It holds the key only long enough to POST it, then clears it.

function NayrumIntegrationForm({ productId }) {
  const [key, setKey] = useState("");
  const [status, setStatus] = useState("not_connected"); // not_connected | verifying | connected | invalid_key

  async function submit(e) {
    e.preventDefault();
    setStatus("verifying");
    const res = await fetch("/admin/nayrum-integration", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ key, productId }),
    });
    const data = await res.json().catch(() => ({}));
    setKey(""); // never keep the key in browser state
    setStatus(data.status === "connected" ? "connected" : "invalid_key");
  }

  return (
    <form onSubmit={submit}>
      <label>NAYRUM Integration Key</label>
      <input
        value={key}
        onChange={(e) => setKey(e.target.value)}
        placeholder="nayk_..."
        autoComplete="off"
        spellCheck={false}
      />
      <p>Add this key to your server-side NAYRUM integration.</p>
      <button disabled={status === "verifying" || key.length === 0}>
        {status === "connected" ? "Reconnect" : "Verify / Connect"}
      </button>
      <p>
        {status === "not_connected" && "Not connected"}
        {status === "verifying" && "Verifying…"}
        {status === "connected" && "Connected"}
        {status === "invalid_key" && "Invalid key"}
      </p>
    </form>
  );
}

Keep the admin route behind your own authentication, use one key per product, and after “Connected” go back to Creator Studio and tick “I’ve added this key to my server-side NAYRUM integration”.

Back to Creator Studio