Skip to Content
Partner APIQuick Start

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 verification

key 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.

If you set webhook_url in step 1, every identified visitor POSTs to that URL automatically with the headers:

HeaderDescription
X-Warm-Eventvisitor_identified or visitor_not_identified
X-Warm-TimestampUnix timestamp of the event
X-Warm-SignatureHMAC-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_balance

After 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.


What’s next

  • Endpoints — full reference for every field, error code, and edge case.
  • Webhooks — partner-level event types (child.created, child.suspended, etc.) and signature verification.
Last updated on