Skip to main content

Collect feedback in a Vue or Nuxt 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.

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

01Browser: feedback form (publishable key)

components/FeedbackForm.vue

<script setup lang="ts">
import { ref } from "vue";
const KEY = useRuntimeConfig().public.nordvaPublishableKey;
const text = ref("");
const state = ref<"idle" | "sent" | "error">("idle");

async function submit() {
  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.value, user_id: "user_123", page_url: location.href }),
  });
  const { data, error } = await res.json();
  state.value = error ? "error" : "sent";
}
</script>

<template>
  <p v-if="state === 'sent'">Thanks, received.</p>
  <form v-else @submit.prevent="submit">
    <textarea v-model="text" required minlength="5" maxlength="2000" />
    <button type="submit">Send feedback</button>
    <p v-if="state === 'error'" role="alert">Could not send. Try again.</p>
  </form>
</template>

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, Nuxt server route

server/api/feedback-list.get.ts

export default defineEventHandler(async () => {
  const res = await fetch("https://api.nordva.dev/v1/feedback?category=bug&status=open&limit=20", {
    method: "GET",
    headers: {
      Authorization: `Bearer ${useRuntimeConfig().nordvaSecretKey}`,
    },
  });
  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;
});
// nuxt.config.ts: runtimeConfig: { nordvaSecretKey: "", public: { nordvaPublishableKey: "" } }

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.