Skip to main content
20% off 3 months · code LAUNCH20 · until 31 October

Send in-app notifications in a Vue or Nuxt app

No queue to run. Send a notification to one user id or to everyone from your server or an agent, and show it in a bell that one script tag adds to your app, or read it through the API and draw your own.

Keys

Sending uses a secret key on your server, which also signs each user id with the notifications signing secret. The browser reads with a publishable key and that signature. Create both under Dashboard → API keys.

NUXT_PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_…  # .env
NUXT_NORDVA_SECRET_KEY=nv_live_…  # .env, server only

01Server: Nuxt server route

server/api/notify.post.ts

export default defineEventHandler(async (event) => {
  const input = await readBody(event);
  const res = await fetch("https://api.nordva.dev/v1/notifications", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${useRuntimeConfig().nordvaSecretKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      user_id: input.user_id,
      title: "Export ready",           // up to 200 characters
      body: "Your CSV is ready to download.", // up to 2,000 characters
      action_url: "https://app.example.com/exports",
      icon: "check",                   // check | warning | info | error
    }),
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const { data } = await res.json();
  // data.id: send an Idempotency-Key header to make retries safe
  return data;
});
// nuxt.config.ts: runtimeConfig: { nordvaSecretKey: "", public: { nordvaPublishableKey: "" } }

Every plan, per project per month: Free 1,000, Indie 20,000, Builder 200,000. Past that the API answers PLAN_LIMIT_REACHED. To post to everyone, send audience: "all" instead of user_id.

02Server: sign the user id (signing secret, never in the browser)

import { createHmac } from "node:crypto";

// Give the page the signed-in user's id and this hash, for example in the HTML you render.
export function userHash(userId: string): string {
  return createHmac("sha256", process.env.NORDVA_NOTIFICATIONS_SECRET!).update(userId).digest("hex");
}

Create the signing secret once, with POST /v1/notifications/signing-secret and a secret key or through the MCP server. It is shown once: store it as NORDVA_NOTIFICATIONS_SECRET on your server.

03Browser: the bell widget, one tag (publishable key)

your page's HTML

<div id="bell"></div>  <!-- where the bell goes, e.g. in your header -->
<script src="https://cdn.nordva.dev/v1/notifications.js"
  data-key="nv_pub_live_…"
  data-user-id="user_123"
  data-user-hash="…"
  data-target="#bell" async></script>

data-user-id is the signed-in user's id and data-user-hash is userHash(user id), both written into the page by your server. The widget polls, shows the unread count, marks read and works under a strict Content-Security-Policy. To draw your own bell instead, use the calls below.

04Browser: read the bell and mark read yourself (publishable key)

const KEY = useRuntimeConfig().public.nordvaPublishableKey;
const USER_ID = "user_123";   // the signed-in user's id, from your server
const USER_HASH = "…";         // userHash(USER_ID), from your server
const headers = { Authorization: `Bearer ${KEY}`, "X-Nordva-User-Hash": USER_HASH };

async function poll() {
  const res = await fetch(`https://api.nordva.dev/v1/notifications/inbox?user_id=${encodeURIComponent(USER_ID)}`, { headers });
  const { data } = await res.json();
  // data.notifications: [{ id, title, body, action_url, icon, created_at, read, broadcast }]
  // data.unread_count, data.poll_interval_seconds
  return data;
}

async function markRead(id: string) {
  await fetch(`https://api.nordva.dev/v1/notifications/${id}/read`, {
    method: "PATCH",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ user_id: USER_ID }),
  });
}

poll();
setInterval(poll, 60_000);

Poll no faster than the response's poll_interval_seconds; faster polling hits the per-user limit. Without a matching user hash the API answers USER_HASH_INVALID.

05Verify from a terminal

curl -s "https://api.nordva.dev/v1/notifications/unread?user_id=user_123" \
  -H "Authorization: Bearer nv_live_…" | jq '.data.notifications | length'

Behaviour worth knowing

  • Every response is { data, error, meta }. On failure error.code is a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message.
  • POST and PATCH requests accept an Idempotency-Key header; the same key with the same body returns the original response for 24 hours.
  • Rate limits per key: 30 requests a minute on Free, 120 on Indie, 500 on Builder. A 429 carries Retry-After.
  • Browser calls with a publishable key must come from an origin registered on the project, otherwise the API answers ORIGIN_NOT_ALLOWED.