Quick Start
A working end-to-end flow: create a child customer, install the tracker on their site, fetch their identified visitors, and release them when they churn.
All code is TypeScript / fetch. Translate as needed for your stack.
0. Set up
You need your partner master api key and the base URL.
const PARTNER_KEY = process.env.WARMAI_PARTNER_KEY! // warm_…
const BASE = "https://api.warmai.uk/functions/v1"The PARTNER_KEY has full provisioning authority over every customer you’ve created. Store it in your secret manager, never in source control or client bundles.
1. Create a managed customer
When a new customer signs up in your product, mint them a child account:
const res = await fetch(`${BASE}/partner-create-child-key`, {
method: "POST",
headers: {
"x-access-key": PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Acme Corp",
domain: "acme.com",
monthly_id_quota: 1000,
id_price_pence_override: 6.0, // your unit cost for this customer, in pence
webhook_url: "https://api.your-product.com/warmai-webhook", // optional
brand_name: "Acme Corp", // optional, defaults to `name`
}),
})
const data = await res.json()
if (!data.success) throw new Error(data.error)
// Persist these in your database — keyed by your own customer id.
const {
api_key_id, // uuid — the child's identifier in our system
child_user_id, // uuid — the phantom user id (you almost never need this)
key, // warm_… — the child's plaintext api key, SHOWN ONCE
key_prefix, // warm_xxxx — safe to display, for confirming "yes this is the key"
} = data.child
const {
id: website_id,
tracking_script_id, // uuid — used in the install snippet
domain,
} = data.website
const {
monthly_id_quota,
current_period_end,
} = data.subscription
const webhook_secret = data.webhook?.secret // whsec_… — SHOWN ONCE, store for signature verificationkey and webhook_secret are returned once. If you don’t persist them at this step you’ll lose them — there’s no read endpoint for either, and recovery means destroying the customer and re-creating (loses tracker history). KAN-77 tracks adding a rotate endpoint.
2. Hand the customer their install snippet
Show this in your onboarding UI. tracking_script_id is the only value that changes per customer; the script URL is the same for everyone.
<script src="https://cdn.menoinfra.com/p/{tracking_script_id}.js" async></script>For example, if tracking_script_id is aeec6fcf-7138-4742-ac4e-0adc0221c737:
<script src="https://cdn.menoinfra.com/p/aeec6fcf-7138-4742-ac4e-0adc0221c737.js" async></script>The customer (or their developer) pastes this into the <head> of their site. From that point on, sessions stream to our pipeline, identifications run on session-end, and matches show up via list-visitors and (if you configured one in step 1) your webhook.
The script is the same one direct customers install, so the same install gotchas apply: smart quotes (WordPress), WP Rocket / Cloudflare Rocket Loader rewriting the URL, CSP connect-src blocking t.menoinfra.com, consent managers gating the script until consent is granted. Email support@getwarmai.com if a customer hits an install issue and we’ll help diagnose.
3. Fetch identified visitors
Two patterns: server-to-server polling (for backfilling into your database) or webhook delivery (for real-time push).
Polling — list-visitors
Use the child’s api key (not yours) and scope to their tracker site:
const visitors = await fetch(
`${BASE}/list-visitors?tracker_site=acme.com&since=2026-05-30T00:00:00Z&limit=100`,
{
headers: { "x-access-key": CHILD_KEY }, // the warm_… from step 1
},
).then(r => r.json())since is an ISO 8601 timestamp; pass the last identified_at you’ve seen for that tracker and list-visitors returns everything newer. The endpoint accepts tracker_site, since, source (e.g. pulse for tracker, api for ip-to-company), search, limit, offset. Email support@getwarmai.com for the full param matrix while we rebuild the reference docs.
Webhook — push (recommended)
If you set webhook_url in step 1, every identified visitor POSTs to that URL automatically with the headers:
| Header | Description |
|---|---|
X-Warm-Event | visitor_identified or visitor_not_identified |
X-Warm-Timestamp | Unix timestamp of the event |
X-Warm-Signature | HMAC-SHA256 of ${timestamp}.${rawBody} using your child’s webhook_secret |
Verify the signature in your handler:
import crypto from "node:crypto"
function verifyWarmWebhook(rawBody: string, headers: Headers, secret: string): boolean {
const sig = headers.get("X-Warm-Signature")
const timestamp = headers.get("X-Warm-Timestamp")
if (!sig || !timestamp) return false
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}Payload top-level fields: event ("visitor_identified"), timestamp (ISO 8601), data (the visitor object — same shape as a list-visitors row). See Webhooks for the partner-level events (child.*) that use the same envelope.
4. Mirror state into your dashboard
To build a partner-side customer dashboard (usage, quota, billing), pull the full list periodically:
const { children } = await fetch(`${BASE}/partner-list-children`, {
headers: { "x-access-key": PARTNER_KEY },
}).then(r => r.json())
for (const child of children) {
// upsert into your DB keyed by child.child_api_key_id
console.log(`${child.name}: ${child.ids_used_this_period}/${child.monthly_id_quota}`)
}For a single-customer refresh (e.g. after they top up in your billing UI):
const { children } = await fetch(
`${BASE}/partner-list-children?child_api_key_id=${api_key_id}`,
{ headers: { "x-access-key": PARTNER_KEY } },
).then(r => r.json())
const child = children[0]For immediate-action events (child.created / updated / released / adopted), you don’t need to poll — see Webhooks to set up real-time push. The remaining events (period roll-over, quota exhaustion, topup balance changes) still need polling on a slow cadence — see KAN-79 / 80 / 81 on the roadmap.
5. Update a customer mid-lifecycle
Bump their quota, change their per-ID rate, rename them, or suspend them — all via one endpoint:
await fetch(`${BASE}/partner-update-child`, {
method: "POST",
headers: {
"x-access-key": PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
child_api_key_id: api_key_id,
monthly_id_quota: 5000, // bumps the cap, resets ids_used_this_period to 0
id_price_pence_override: 4.5, // new agreed rate
extend_period_days: 30, // pushes current_period_end out by 30 days from MAX(current, now)
status: "active", // or "suspended" (pauses billing + tracking) / "revoked"
}),
})See the endpoint reference for every field and behavioural note.
6. Release when they churn
When the customer leaves your platform, release them. The child’s api_key returns to your direct account (so historical webhooks keep validating), the phantom user is cleaned up, and any unspent topup_balance is folded back into your partner account:
const { topup_returned } = await fetch(`${BASE}/partner-release-child`, {
method: "POST",
headers: {
"x-access-key": PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ child_api_key_id: api_key_id }),
}).then(r => r.json())
// topup_returned is the int number of IDs added back to YOUR subscription.topup_balanceAfter release the tracker keeps working — it’s just now attached to your account directly, no longer a managed customer. Use partner-adopt-existing-key (covered in Endpoints) to convert it back into a managed child later if they return.