Skip to content

Part 3 · Operate

Customers API

Send your users and what they do from your app or your website, so Customers fills itself. Copy-paste snippets for Next.js, plain JavaScript and curl.

01What it is

Two endpoints and two browser calls put your own users into Customers. POST /api/v1/customers creates or updates people by your own user id or their email. POST /api/v1/events records what they did, one event at a time or up to a batch. On your website, instinct.identify and instinct.track do the same through the website script.

Every call needs a key made for one venture in Customers, Connect, sent as a bearer token. The key can write your users and their events and read nothing.

02Why it exists

Stripe knows who pays. Only your app knows who signed up, how far they got in setup and when they last did something. With those, Customers can tell you who is stuck and who went quiet, and draft the message.

Sending the same customer or event twice is safe: customers are matched by your id, then email, and an event is stored once per its id, so a retried call never doubles anything.

03How to use it

  1. 01In Customers, Connect, create a key under Your app and copy it. It is shown once. Keep it on your server, never in the browser.
  2. 02After each signup, send POST /api/v1/customers with { "id": your user id, "email", "name", "plan" }. Add "status" ("signed_up", "trial", "active", "past_due" or "canceled"), "trial_ends_at", "mrr_cents" with "currency", or "setup" ({ "done", "total", "step" }) when you know them. Up to the batch limit at once as { "customers": [ … ] }.
  3. 03For what users do, send POST /api/v1/events with { "events": [ { "id", "name", "at", "userId", "props" } ] }. "id" is yours and makes the event idempotent; "name" is lower snake case, such as setup_step.
  4. 04A setup_step event with "done" and "total" in props moves the customer’s setup progress, which the stuck-in-setup rule reads.
  5. 05In the browser, after sign-in: instinct.identify({ id, email, name, plan }) and instinct.track(name, props). These need the website script on the page and a visitor’s analytics consent.
  6. 06Each answer carries counts and your own ids only. A 429 carries RateLimit headers saying when to retry.

04What good looks like

You give it
POST /api/v1/customers with { "id": "u_42", "email": "sam@acme.test", "plan": "Team", "status": "trial" } sent twice.
You get
The first answer says created 1; the second says unchanged 1. One customer, On trial, plan Team.
Why
The second call finds the same customer by its id and nothing in it differs, so nothing is written.

Derived from the rules in the code

05What weak looks like, and what it costs

You give it
The same call with a key made in Settings for website events only.
You get
403 forbidden_scope, naming customers:write.
Why
Writing customers needs a key holding customers:write, which only a Customers key has.

Derived from the rules in the code

06Read next

Copy and paste

Next.js, in a route handler or server action after sign-up

// .env: CUSTOMERS_KEY=igtm_...  (from Customers, Connect)
export async function reportSignup(user: { id: string; email: string; name?: string; plan?: string }) {
  await fetch('https://instinctgtm.com/api/v1/customers', {
    method: 'POST',
    headers: { authorization: `Bearer ${process.env.CUSTOMERS_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ id: user.id, email: user.email, name: user.name, plan: user.plan, status: 'signed_up' }),
  }).catch(() => {}); // never hold up your own sign-up for it
}

export async function reportEvent(userId: string, name: string, props: Record<string, string | number | boolean> = {}) {
  await fetch('https://instinctgtm.com/api/v1/events', {
    method: 'POST',
    headers: { authorization: `Bearer ${process.env.CUSTOMERS_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ events: [{ id: crypto.randomUUID(), name, at: new Date().toISOString(), userId, props }] }),
  }).catch(() => {});
}

Plain JavaScript, on a page that runs the website script

// After someone signs in:
instinct.identify({ id: user.id, email: user.email, name: user.name, plan: user.plan });

// When they do something that matters:
instinct.track('setup_step', { step: 'connected_calendar', done: 2, total: 5 });

curl

curl -X POST https://instinctgtm.com/api/v1/customers \
  -H "Authorization: Bearer $CUSTOMERS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id":"u_42","email":"sam@acme.test","name":"Sam","plan":"Team","status":"trial","trial_ends_at":"2026-11-20T00:00:00Z"}'

curl -X POST https://instinctgtm.com/api/v1/events \
  -H "Authorization: Bearer $CUSTOMERS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"id":"evt_0001_u42","name":"setup_step","at":"2026-11-05T10:00:00Z","userId":"u_42","props":{"step":"invite_team","done":3,"total":5}}]}'