Collect feedback in an Astro site
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.
PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_… # .env
NORDVA_SECRET_KEY=nv_live_… # .env, server only 01Browser: feedback form (publishable key)
src/components/FeedbackForm.astro
---
// Rendered on the server; the script below runs in the browser.
const KEY = import.meta.env.PUBLIC_NORDVA_PUBLISHABLE_KEY;
---
<form id="feedback">
<textarea name="text" required minlength="5" maxlength="2000"></textarea>
<button type="submit">Send feedback</button>
<p id="feedback-msg" role="status"></p>
</form>
<script define:vars={{ KEY }}>
const form = document.getElementById("feedback");
const msg = document.getElementById("feedback-msg");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = new FormData(form).get("text");
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, page_url: location.href }),
});
const { data, error } = await res.json();
msg.textContent = error ? error.message : "Thanks, received (" + data.category + ").";
});
</script> 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, API route
src/pages/api/feedback-list.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = async () => {
const res = await fetch("https://api.nordva.dev/v1/feedback?category=bug&status=open&limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${import.meta.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 new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } });
}; 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 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.