Send in-app notifications in a Next.js app
Three endpoints and no queue to run. Send a notification to a user id from your server or an agent, read unread ones from the browser, mark them read.
Keys
Sending uses a secret key on your server. Polling from the browser uses a publishable key. Create both under Dashboard → API keys.
NEXT_PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_… # .env.local
NORDVA_SECRET_KEY=nv_live_… # .env.local, never NEXT_PUBLIC_ 01Server: route handler
app/api/notify/route.ts
export async function POST(req: Request) {
const input = await req.json();
const res = await fetch("https://api.nordva.dev/v1/notifications", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORDVA_SECRET_KEY!}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: input.user_id,
title: "Export ready", // ≤ 120 chars
body: "Your CSV is ready to download.", // ≤ 500 chars
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 Response.json(data);
} The notifications API is available on the Builder plan; other plans receive PLAN_UPGRADE_REQUIRED.
02Browser: poll unread and mark read (publishable key)
const KEY = process.env.NEXT_PUBLIC_NORDVA_PUBLISHABLE_KEY!;
const USER_ID = "user_123"; // your own user id, any string
async function poll() {
const res = await fetch(`https://api.nordva.dev/v1/notifications/unread?user_id=${encodeURIComponent(USER_ID)}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
const { data } = await res.json();
// data.notifications: [{ id, title, body, action_url, icon, created_at, read_at }]
// data.poll_interval_seconds: 30 — the API throttles to 120 polls per user per hour
return data.notifications;
}
async function markRead(id: string) {
await fetch(`https://api.nordva.dev/v1/notifications/${id}/read`, { method: "PATCH", headers: { Authorization: `Bearer ${KEY}` } });
}
poll();
setInterval(poll, 30_000); Poll every 30 seconds, as the response's poll_interval_seconds suggests. Faster polling hits the per-user throttle.
03Verify 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 failureerror.codeis a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message. - POST and PATCH requests accept an
Idempotency-Keyheader; 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.