Skip to content

Contacts

The same person joins your waitlist, subscribes to your changelog, sends feedback and later pays you. Contacts shows them as one record.

It is not a CRM. There are no deals, pipelines, tasks or email campaigns. It is a view over data Launch already holds, plus what you record on a person: an optional stage and note, and details and consents.

By email, after normalisation: lowercase, +alias removed, and dots ignored on Gmail. So [email protected] on the waitlist and [email protected] in Stripe are one contact. Two genuinely different addresses are two contacts; Launch does not guess.

SourceMatched by
Waitlist signupsEmail
Changelog subscribersEmail
Stripe customers (connect Stripe)Email
Lemon Squeezy customers (connect Lemon Squeezy)Email
FeedbackThe optional email on the submission, or your user_id (see below)
NotificationsYour user_id

Feedback and notifications usually carry only your own user_id. If your Stripe customers have that id in metadata, name the metadata key when connecting Stripe and Launch links user id → customer → email.

Contacts works without Stripe; you just see no payment data. Stripe customers who never used a Launch form appear with source: "stripe", Lemon Squeezy customers with source: "lemonsqueezy". stripe_status is the payment status from whichever provider it is; the name is older than Lemon Squeezy support. A Stripe customer without an email cannot be matched and is not a contact, though they still count in revenue.

Terminal window
curl "https://api.nordva.dev/v1/contacts?paying=true&has_feedback=true" \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"
FilterValues
payingtrue / false: has an active or past-due subscription
has_feedbacktrue / false
statusWaitlist status: waiting, invited, unsubscribed
sourceA utm_source or referrer hostname, or direct, referral, stripe
stagepotential, in_talks, customer, recurring, not_a_fit, inactive
qPart of an email address
consentPeople who have granted this consent now, such as email_marketing; waitlist and changelog are built in
detailname for people who have that detail, or name=value for that value (any case), such as stage=building
limit, cursorCursor pagination; meta.next_cursor and meta.has_more

Each contact has id, email, first_seen_at, source, waitlist_status, referral_count, subscribed, feedback_count, stripe_status (none, customer, trialing, active, past_due, canceled), plan, mrr_minor, currency, stage, details (name to value), consents (the purposes granted now) and user_ids (your own user ids known for the person, from identify(), feedback or a paying customer’s metadata: what a notification to them reaches).

GET /v1/contacts/{id} adds the waitlist signup with its first-touch source, every subscription, lifetime paid per currency, the latest five feedback submissions with summaries, notifications sent to that user id and read (broadcasts to everyone are not counted per person), the note, and detail_records and consent_records: each detail and consent with its source and when it was set.

Terminal window
curl -X PATCH https://api.nordva.dev/v1/contacts/ct_9f2c... \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "stage": "in_talks", "note": "Wants SSO before rolling out to the team." }'

Send null to clear either. A note replaces the previous one. The same call also takes details and consents, below.

A detail is something your site learned about a person, as a name and a value: building = saas, company_size = 2-10. A consent is a permission the person gave, by purpose: email_marketing granted, or withdrawn later. Both live on the contact, so you can list, filter and export by them. Goals and events are for what visitors do in general; a detail is about one person.

On a form that already has data-nordva-identify (see web analytics), mark the fields you want kept:

<form data-nordva-identify action="/signup" method="post">
<input type="email" name="email" required>
<select name="building" data-nordva-detail="building">
<option value="saas">A SaaS product</option>
<option value="mobile_app">A mobile app</option>
<option value="agency">Client work</option>
</select>
<label>
<input type="checkbox" name="news" data-nordva-consent="email_marketing">
Email me product news and offers
</label>
<button>Sign up</button>
</form>

When the form is submitted:

  • A field with data-nordva-detail="name" is sent as the detail name with the field’s value. A radio button or checkbox counts only when it is checked; an empty field is skipped.
  • A ticked checkbox with data-nordva-consent="purpose" records that consent as granted. An unticked box records nothing: it does not withdraw a consent the person gave before.
  • Nothing else in the form is read.

A submitted form is sent whether or not the visit may carry ids: the person gave you the address by submitting it. Without ids (in the “Ask first” modes before anyone accepts, or with Global Privacy Control) it goes without a visitor id, so the person, their details and consents are kept but their visits are not linked to them. A nordva('identify', …) call from your own code still waits for ids.

In the browser:

nordva('identify', {
details: { building: 'saas', company_size: '2-10' },
consents: { email_marketing: true },
});

From your server, the same fields go in the body of POST /v1/analytics/identify, or on an existing contact with PATCH /v1/contacts/{id}:

Terminal window
curl -X PATCH https://api.nordva.dev/v1/contacts/ct_9f2c... \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "details": { "company_size": "11-50", "building": null }, "consents": { "email_marketing": false } }'

To add someone your server knows but who has no visit (an account made in an app, an import), leave out visitor_id:

Terminal window
curl -X POST https://api.nordva.dev/v1/analytics/identify \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "user_id": "user_42", "email": "[email protected]", "details": { "world": "world3" } }'

A later identify that carries only a user_id reaches the person you identified earlier with that user id and an email. Details and consents need a person with an email; without one they are dropped.

NamesLowercase letters, digits and _, starting with a letter, up to 40 characters. Used for both detail names and consent purposes
Detail valuesText, a number or true/false, stored as text, up to 200 characters. null removes the detail
Consentstrue grants, false withdraws (from a browser only true is recorded, since anyone can type any address there). Each keeps granted_at, withdrawn_at and its source: browser, api or dashboard
Per callAt most 20 details and consents together
Per personAt most 50 details. A new one beyond that is skipped; an existing one is always updated
Built inwaitlist (on the waitlist and not unsubscribed) and changelog (subscribed to the changelog). You can filter by them but never set them

In the API, a call that breaks a rule is refused with the reason. From the browser, a name that breaks a rule is dropped, a value longer than 200 characters is cut short, and the rest of the identify still goes through, so one bad field cannot lose the person.

GET /v1/contacts/fields lists the detail names and consent purposes in use, with how many people have each and the most common values. Check it before inventing a name, so the same thing is not recorded under two.

Launch stores what your site sends; it cannot see what your form said or what the person agreed to. Record a consent only for what the person actually agreed to, with a box they ticked themselves (never ticked in advance), and word the box for one purpose. Record a withdrawal when they withdraw, for example from your own unsubscribe link. Whether a consent is valid for your emails under the law that applies to you is your responsibility, not Launch’s.

GET /v1/contacts/export takes the same filters as the list and returns a CSV link for your email tool, valid for 15 minutes:

Terminal window
curl "https://api.nordva.dev/v1/contacts/export?consent=email_marketing&detail=building=saas" \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"

The answer has download_url, expires_at, row_count and truncated (true past 10,000 rows). The link needs no key, so treat it as a secret. The CSV has one row per person: email, first seen, source, stage, the granted consents, waitlist status, changelog subscription, feedback count, Stripe status, plan, MRR and currency, then one column per detail, named detail_<name>. Cells that start like a spreadsheet formula are escaped. Exporting needs Indie or higher.

FreeIndieBuilder
Who is listedWaitlist people, first 100EveryoneEveryone
Stripe status, plan, MRRFor the people listedYesYes
Stage and noteYesYesYes
Details, consents and their filtersFor the people listedYesYes
CSV exportNoYesYes

A contact never outlives the person’s data. When you delete a signup in the dashboard, disconnect Stripe or Lemon Squeezy, or a customer is deleted in Stripe, a contact that no longer exists in any source is removed together with its note, details and consents.

“Which of my waitlist signups are paying now?”, “What has [email protected] asked for?”, “Mark [email protected] as in talks.”, “Export everyone who agreed to marketing email.” The assistant uses list_contacts, get_contact, update_contact, list_contact_fields and export_contacts.