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

List your contacts in an Astro site

The same person joins your waitlist, subscribes to your changelog, sends feedback and later pays you. Launch already stores each of those; Contacts shows them as one record, so you can ask which signups became customers and what your customers have asked for.

Keys

Secret key (nv_live_…) on your server only. There is no browser part: revenue and contact data must not reach a page a visitor can read. Create both under Dashboard → API keys.

PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_…  # .env
NORDVA_SECRET_KEY=nv_live_…  # .env, server only

01Server: API route

src/pages/api/contacts.ts

import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  const res = await fetch("https://api.nordva.dev/v1/contacts?paying=true&limit=25", {
    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: [{ id, email, source, stage, waitlist_status, stripe_status, plan, mrr_minor, currency, feedback_count, first_seen_at }]
  // filters: paying, has_feedback, status, source, stage, q; page with meta.next_cursor
  return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } });
};

One record per person across the waitlist, subscribers, feedback and Stripe, matched on email. Stage and note are the only fields Launch stores on a contact; everything else is read from its source. Indie and up for the full view.

02Verify from a terminal

curl -s "https://api.nordva.dev/v1/contacts?paying=true&limit=5" \
  -H "Authorization: Bearer nv_live_…" | jq '.data[] | {email, source, stage}'

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.