Skip to main content

Collect feedback in a React app

Accept feedback from inside your product, have each message classified, and forward it to wherever your team already works. No inbox to babysit.

Keys

Browser calls use a publishable key (nv_pub_live_…), which is safe to ship. Reading and admin calls use a secret key (nv_live_…) on your server only. Create both under Dashboard → API keys.

VITE_NORDVA_PUBLISHABLE_KEY=nv_pub_live_…  # .env (Vite)
NORDVA_SECRET_KEY=nv_live_…  # on your server only

01Browser: feedback form (publishable key)

src/FeedbackForm.tsx

import { useState } from "react";

const KEY = import.meta.env.VITE_NORDVA_PUBLISHABLE_KEY;

export function FeedbackForm() {
  const [text, setText] = useState("");
  const [state, setState] = useState<"idle" | "sent" | "error">("idle");

  async function submit(e: React.FormEvent) {
    e.preventDefault();
    const res = await fetch("https://api.nordva.dev/v1/feedback", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ feedback_text: text, user_id: "user_123", page_url: location.href }),
    });
    const { data, error } = await res.json();
    setState(error ? "error" : "sent"); // data.category and data.sentiment come back classified
  }

  return (
    state === "sent" ? <p>Thanks, received.</p> : (
      <form onSubmit={submit}>
        <textarea required minLength={5} maxLength={2000} value={text} onChange={(e) => setText(e.target.value)} />
        <button type="submit">Send feedback</button>
        {state === "error" && <p role="alert">Could not send. Try again.</p>}
      </form>
    )
  );
}

Feedback is an Indie and Builder feature; on Free the API answers PLAN_UPGRADE_REQUIRED. Register the page's origin under Project → Allowed origins.

02Server: list open bugs, Node.js

feedback-list.ts

export async function listOpenBugs() {
  const res = await fetch("https://api.nordva.dev/v1/feedback?category=bug&status=open&limit=20", {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.NORDVA_SECRET_KEY!}`,
    },
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const { data } = await res.json();
  // data is an array; page with the cursor in meta
  return data;
}

03Zero-code alternative: hosted widget

any HTML page

<script src="https://cdn.nordva.dev/v1/feedback.js"
  data-key="nv_pub_live_…"
  data-position="bottom-right"
  data-accent="#A8552E"
  data-user-id="user_123"></script>

04Verify from a terminal

curl -s "https://api.nordva.dev/v1/feedback?limit=5" \
  -H "Authorization: Bearer nv_live_…" | jq '.data[] | {category, sentiment, feedback_text}'

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.