Create your first customer

From key to customer + contact + onboarding in one script.

View as Markdown

This guide pushes a complete customer record through the API in a single Node script. You end with a customer, a primary contact, and an active onboarding — visible in https://my.pivotal.app/customers/<slug> when the script finishes.

Prerequisites

  • A live API key (pivotal_…) from Admin > API Keys.
  • Node 20+ or Bun. The script uses native fetch.
  • A scratch slug — pick something that won’t collide with a real customer in your workspace. We’ll use aurora-demo.

Export the key once:

export PIVOTAL_API_KEY=pivotal_…

The script

create-customer.ts
const API = "https://my.pivotal.app/api/v1";
const KEY = process.env.PIVOTAL_API_KEY!;
if (!KEY) throw new Error("PIVOTAL_API_KEY is required");
const h = {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
};
async function call<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API}${path}`, { ...init, headers: { ...h, ...(init?.headers ?? {}) } });
const body = await res.json();
if (!res.ok) {
console.error("API error:", body);
throw new Error(`${res.status} ${body?.error?.code ?? "unknown"}`);
}
return body as T;
}
// 1. Create the customer
const customer = await call<{ id: string; display_id: number; slug: string }>(
"/customers",
{
method: "POST",
headers: { "Idempotency-Key": "first-customer-aurora-demo" },
body: JSON.stringify({
name: "Aurora Outfitters",
slug: "aurora-demo",
domain: "aurora-demo.com",
status: "onboarding",
}),
},
);
console.log(`Customer: ${customer.display_id} (${customer.slug})`);
// 2. Attach a primary contact
const contact = await call<{ id: string; display_id: number; email: string }>(
`/customers/${customer.display_id}/contacts`,
{
method: "POST",
body: JSON.stringify({
name: "Mira Chen",
email: "mira@aurora-demo.com",
title: "Head of Ops",
is_primary: true,
labels: ["billing", "technical"],
}),
},
);
console.log(`Contact: ${contact.display_id} (${contact.email})`);
// 3. Open an onboarding
const onboarding = await call<{ id: string; display_id: number; phase: string; state: string }>(
"/onboardings",
{
method: "POST",
headers: { "Idempotency-Key": "first-customer-aurora-demo-onb" },
body: JSON.stringify({
customer_id: customer.display_id.toString(),
phase: "before_getting_started",
state: "active",
target_launch_date: "2026-07-15T00:00:00Z",
}),
},
);
console.log(`Onboarding: ${onboarding.display_id} (${onboarding.phase})`);
console.log(`\nOpen https://my.pivotal.app/customers/${customer.display_id}-${customer.slug}`);

Run it:

bun create-customer.ts
# or: ts-node create-customer.ts

What happened

Three rows landed:

  1. A customer in your workspace with display_id assigned by Pivotal — the small integer that will appear in the URL.
  2. A contact under that customer. Because is_primary: true, the customer detail page surfaces her at the top of the contacts card.
  3. An onboarding at the before_getting_started phase with a launch date 6+ weeks out.

The customer’s activity feed now has three rows, each labelled API: <key-name> as the actor. The same events flow into the per-workspace audit log at Admin > Logs.

Trying it again safely

Re-run the script. Because we sent Idempotency-Key on both creates, the second pass returns the cached responses instead of creating duplicates. Nice property to lean on inside webhook handlers and cron jobs.

Drop the Idempotency-Key headers and re-run — you’ll get 409 slug_taken on the customer create. That’s the expected behavior. Pick a fresh slug or send a PATCH instead.

Cleaning up

The aurora-demo customer is soft-deletable:

curl -X DELETE "https://my.pivotal.app/api/v1/customers/${DISPLAY_ID}" \
-H "Authorization: Bearer $PIVOTAL_API_KEY"

The associated contact and onboarding survive the delete but disappear from list views (the parent customer is gone from list views too). Reach out if you need a hard delete.

Next