Skip to Content
Partner APIEndpoints

Endpoints

Reference for every partner-* endpoint. All endpoints authenticate with your master partner key via the x-access-key header.

Required Headers

HeaderDescription
x-access-keyYour master partner api key (warm_…). Must belong to an account flagged is_partner=true.
Content-Typeapplication/json (POST endpoints only)

Base URL

https://api.warmai.uk/functions/v1

Common error codes

CodeMeaning
400Validation error (missing required field, malformed value)
401x-access-key missing, unknown, or revoked
402Payment-required (insufficient balance) — currently only partner-transfer-topup returns this
403Key is valid but the account is not a partner — or the resource doesn’t belong to the caller
404Referenced child / api_key / website doesn’t exist
405Wrong HTTP method (e.g. GET on a POST endpoint)
409Conflict (domain already registered, key already adopted, etc.)
429Rate limit (currently only partner-rotate-child-key returns this — max 10 rotations per child per 24h)
500Internal — retry; if it persists, contact support

Every error response is { "success": false, "error": "<message>" }.


Create Child Key

POST /partner-create-child-key

Mint a brand-new managed customer. Creates a phantom user, an api_key under your account, a website with a fresh tracking_script_id, and a 30-day subscription period. Returns the plaintext api key and (if requested) webhook secret exactly once.

Request Body

FieldTypeRequiredDescription
namestringYesDisplay name for the customer (shown in partner-list-children results)
domainstringYesThe customer’s tracker domain, e.g. acme.com. Bare domain — no scheme, no path.
monthly_id_quotaintNoIDs the child gets per period. Default 100.
id_price_pence_overridenumberNoYour per-ID rate for this child, in pence. Informational.
webhook_urlstringNoHTTPS URL to receive visitor identification events.
webhook_secretstringNoIf webhook_url is set: your own secret (min 8 chars). If omitted, we mint one and return it.
brand_namestringNoWhitelabel display name. Defaults to name.

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-create-child-key \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "domain": "acme.com", "monthly_id_quota": 1000, "id_price_pence_override": 6.0, "webhook_url": "https://api.your-product.com/warmai-webhook" }'

Response (201 Created)

{ "success": true, "child": { "api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "child_user_id": "8a4c1d3b-9e7f-4a2b-b0c1-2d3e4f5a6b7c", "key": "warm_aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV", "key_prefix": "warm_aB3c", "name": "Acme Corp", "status": "active" }, "website": { "id": "f1e2d3c4-b5a6-9788-7766-554433221100", "tracking_script_id": "12345678-90ab-cdef-1234-567890abcdef", "domain": "acme.com" }, "subscription": { "id": "98765432-10fe-dcba-9876-543210fedcba", "monthly_id_quota": 1000, "current_period_end": "2026-06-30T12:00:00.000Z", "status": "active" }, "webhook": { "url": "https://api.your-product.com/warmai-webhook", "secret": "whsec_a1b2c3d4e5f607182930405160708091a2b3c4d5" } }

child.key and webhook.secret are returned once and are not stored in plaintext. Persist them immediately or you’ll lose them.

Error-specific notes

CodeTrigger
400name or domain missing; domain failed validation (not a bare host.tld); webhook_url not a valid http(s) URL
409domain is already registered on an active website (each tracker domain can only be live on one account)

Update Child

POST /partner-update-child

Mutate any subset of a child’s configuration. Any field you omit is left untouched.

Request Body

FieldTypeRequiredDescription
child_api_key_iduuidYesThe child to update
namestringNoNew display name
monthly_id_quotaintNoNew per-period quota. Also resets ids_used_this_period to 0 (treat as a quota refresh, not just a cap raise).
id_price_pence_overridenumber|nullNoNew per-ID rate. Pass null to clear.
statusstringNoOne of "active", "suspended", "revoked". Suspended/revoked children stop accepting tracker traffic.
extend_period_daysintNoBumps current_period_end forward by this many days from MAX(current_period_end, now) — so adding 30 days to a customer who still has 45 days left correctly pushes them to 75 days, not back to 30.
webhook_urlstring|nullNoThe child’s visitor-identification webhook destination. Pass an https:// URL to set or update; pass null to clear it entirely (also clears the secret). Independent from the <script> tag — adding/removing the webhook never affects tracking.
webhook_secretstring|nullNoHMAC secret for signing visitor identification webhooks. Pass a string (min 8 chars) to use a partner-supplied secret; pass null to mint a fresh one server-side. If omitted while webhook_url is being set, the existing secret is preserved or a new one is minted if none existed. Returned in the response once when newly minted; never re-exposed if preserved.

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-update-child \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "monthly_id_quota": 5000, "id_price_pence_override": 4.5, "extend_period_days": 30 }'

Setting / clearing the child’s webhook

# Set or update — server mints a secret if none supplied curl -X POST https://api.warmai.uk/functions/v1/partner-update-child \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "webhook_url": "https://api.acme.com/warm-webhook" }' # Rotate just the secret (URL stays the same) curl -X POST https://api.warmai.uk/functions/v1/partner-update-child \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "webhook_url": "https://api.acme.com/warm-webhook", "webhook_secret": null }' # Clear entirely (both URL and secret become null) curl -X POST https://api.warmai.uk/functions/v1/partner-update-child \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "webhook_url": null }'

Response (200 OK)

{ "success": true, "child": { "id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "user_id": "8a4c1d3b-9e7f-4a2b-b0c1-2d3e4f5a6b7c", "name": "Acme Corp", "status": "active", "created_at": "2026-05-30T14:22:00.000Z", "webhook_url": "https://api.your-product.com/warmai-webhook", "parent_api_key_id": "<your-master-api-key-id>" }, "subscription": { "id": "98765432-10fe-dcba-9876-543210fedcba", "monthly_id_quota": 5000, "ids_used_this_period": 0, "topup_balance": 0, "current_period_end": "2026-07-30T12:00:00.000Z", "status": "active", "id_price_pence_override": 4.5 }, "webhook": { "url": "https://api.your-product.com/warmai-webhook", "secret": "whsec_a1b2c3d4..." } }

The webhook field is only present when a fresh secret was minted in this call (shown once). When the URL was updated but the existing secret was preserved, webhook is omitted from the response.

Updating the webhook does not affect tracking. The <script> tag is tied to the website’s tracking_script_id, not to the webhook. You can ship the tracking script tag before the customer has provided a webhook destination, then add the webhook later when you have it — visitor data continues to collect either way (queryable via list-visitors until the webhook is configured).

Error-specific notes

CodeTrigger
400child_api_key_id missing; status not in allowed set; webhook_url not a valid URL or not a string/null; webhook_secret shorter than 8 characters
403The child belongs to a different partner
404child_api_key_id doesn’t match any api_key

List Visitors

GET /list-visitors

Pull identified visitors for a tracker. Returns the same identity shape as the visitor_identified webhook event — useful for backfilling missed events, paging history, or polling instead of (or alongside) webhook delivery.

Authenticates with the api_key that owns the tracker — the child’s key for managed customers, your master key for trackers on your own account.

Query Parameters

ParamTypeRequiredDescription
tracker_sitestringNoFilter to one registered domain (e.g. acme.com). Omit to include every tracker the calling key has access to.
sincestring (ISO 8601)NoEarliest last_seen to include. Default: 90 days. Pass the last identified_at you’ve seen for incremental sync.
sourcestringNoall (default), tracker, or api.
searchstringNoFree-text ILIKE over display name / domain / company / IP.
limitintNo1-500, default 100.
offsetintNoDefault 0.

Example Request

curl 'https://api.warmai.uk/functions/v1/list-visitors?tracker_site=acme.com&since=2026-06-01T00:00:00Z&limit=100' \ -H "x-access-key: warm_YOUR_API_KEY"

Response (200)

{ "success": true, "visitors": [ { "identification_type": "company", "company": "S&P Global", "domain": "spglobal.com", "company_data": { "name": "S&P Global", "domain": "spglobal.com", "industry": "Financial Services", "location": "Chicago, US", "linkedin_url": "https://linkedin.com/company/spglobal", "employee_count": 10001 }, "individual": null, "ip": "203.0.113.42", "last_seen": "2026-06-04T16:25:36Z", "first_seen": "2026-06-02T09:14:12Z", "visit_count": 3, "tracker_site": "acme.com", "confidence": 0.97 } ], "total_count": 1248, "limit": 100, "offset": 0 }

total_count is the full deduped set matching your filters; use it to drive pagination via offset.

Polling Pattern

Persist the most recent last_seen you’ve processed and pass it as since on the next call:

const since = lastSeenFromYourDb; // ISO 8601 const url = `${BASE}/list-visitors?tracker_site=${domain}&since=${encodeURIComponent(since)}&limit=500`; const { visitors } = await fetch(url, { headers: { "x-access-key": KEY } }).then(r => r.json()); // process, advance lastSeenFromYourDb to max(visitors.last_seen)

Error-specific notes

CodeTrigger
401x-access-key missing, unknown, or inactive
500Database error (rare; retry with backoff)

List Children

GET /partner-list-children

Enumerate the customers you’ve provisioned, with current quota / usage / billing status. Use the optional child_api_key_id query param to fetch one child instead of all of them.

Query Parameters

ParamTypeDescription
child_api_key_iduuidSingle-child lookup. Returns children: [] if no match.
include_inactivebooleanWhen true, also returns children with status IN ('suspended', 'revoked'). Default false.

Example Requests

# All active children curl https://api.warmai.uk/functions/v1/partner-list-children \ -H "x-access-key: warm_YOUR_PARTNER_KEY" # Single child curl "https://api.warmai.uk/functions/v1/partner-list-children?child_api_key_id=aeec6fcf-7138-4742-ac4e-0adc0221c737" \ -H "x-access-key: warm_YOUR_PARTNER_KEY" # Including suspended / revoked curl "https://api.warmai.uk/functions/v1/partner-list-children?include_inactive=true" \ -H "x-access-key: warm_YOUR_PARTNER_KEY"

Response (200 OK)

{ "success": true, "children": [ { "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "child_user_id": "8a4c1d3b-9e7f-4a2b-b0c1-2d3e4f5a6b7c", "name": "Acme Corp", "domain": "acme.com", "tracking_script_id": "12345678-90ab-cdef-1234-567890abcdef", "status": "active", "created_at": "2026-05-30T14:22:00.000Z", "monthly_id_quota": 5000, "ids_used_this_period": 1247, "topup_balance": 0, "current_period_end": "2026-07-30T12:00:00.000Z", "sub_status": "active", "id_price_pence_override": 4.5, "has_webhook": true } ] }

Response Field Reference

FieldTypeDescription
child_api_key_iduuidUse this everywhere else as the child’s identifier
child_user_iduuidThe phantom user id (rarely needed externally)
namestringDisplay name, or "(unnamed)" if null
domainstring|nullTracker domain — null if no active website
tracking_script_iduuid|nullUsed in the install snippet — null if no active website
statusstringapi_key.statusactive / suspended / revoked
created_attimestamptzWhen the child was provisioned
monthly_id_quotaintCurrent period quota
ids_used_this_periodintIDs consumed so far this period
topup_balanceintNon-expiring IDs available after quota is exhausted
current_period_endtimestamptzWhen the period rolls over
sub_statusstringsubscription.status — independent of api_key.status
id_price_pence_overridenumeric|nullYour per-ID unit cost for this child
has_webhookbooleantrue if this child has its own visitor-identification webhook configured. Distinct from your partner-level state-change webhook (see Webhooks) — the two channels are independent.

Note the two status fields: status (the api_key) and sub_status (the subscription). A child can be status: "active" but sub_status: "canceled" if the partner explicitly cancelled their subscription, or status: "suspended" with sub_status: "active" if you’ve paused the tracker but kept the billing live.


Adopt Existing Key

POST /partner-adopt-existing-key

Convert one of your existing direct trackers into a managed child customer. The api_key’s user_id moves to a new phantom user, parent_api_key_id is set to your master, and the child gets its own subscription row.

Use this when a tracker started life on your direct partner account (maybe a test / staging tracker, or one you provisioned manually before the API existed) and you now want to bill + manage it like any other customer.

Request Body

FieldTypeRequiredDescription
api_key_iduuidYesThe tracker to adopt. Must belong directly to you (no parent_api_key_id), be status: "active", and have at least one active website.
namestringYesDisplay name for the new child
monthly_id_quotaintNoDefault 0 (PAYG — set later via partner-update-child)
id_price_pence_overridenumberNoPer-ID rate
webhook_urlstringNoOverwrites any existing webhook on the api_key
webhook_secretstringNoIf webhook_url is set: your own secret (min 8 chars), or we mint one
brand_namestringNoDefaults to name

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-adopt-existing-key \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "api_key_id": "c1d2e3f4-5678-90ab-cdef-1234567890ab", "name": "Acme Corp", "monthly_id_quota": 1000, "id_price_pence_override": 6.0 }'

Response (200 OK)

{ "success": true, "child": { "api_key_id": "c1d2e3f4-5678-90ab-cdef-1234567890ab", "child_user_id": "8a4c1d3b-9e7f-4a2b-b0c1-2d3e4f5a6b7c", "name": "Acme Corp", "status": "active" }, "website": { "id": "f1e2d3c4-b5a6-9788-7766-554433221100", "tracking_script_id": "12345678-90ab-cdef-1234-567890abcdef", "domain": "acme.com" }, "subscription": { "id": "98765432-10fe-dcba-9876-543210fedcba", "monthly_id_quota": 1000, "current_period_end": "2026-06-30T12:00:00.000Z", "status": "active" }, "webhook": null }

The api_key’s existing plaintext is not re-shown by this endpoint — adoption doesn’t rotate the key. If you’ve lost the plaintext, you’ll need to create a fresh child via partner-create-child-key instead.

Error-specific notes

CodeTrigger
403api_key_id belongs to a different user
404api_key_id doesn’t exist
409The api_key already has a parent_api_key_id (already adopted); is not active; has no active website; or is the same key you’re authenticating with (you can’t adopt your own master)

Rotate Child Key

POST /partner-rotate-child-key

Mint a fresh plaintext api key for one of your managed children and replace the stored hash. The api_key_id stays the same so all foreign references (websites, subscriptions, webhook history, audit logs) remain intact — only the secret rotates.

Use this when you’ve lost the plaintext returned at child creation (partner-create-child-key returns it exactly once and never again). Without this endpoint, the only recovery path is partner-release-child + partner-create-child-key, which loses tracker history.

In-flight requests using the OLD key will start failing with 401 the moment this endpoint commits. Plan a graceful cutover:

  1. Deploy the rotation handler to your integration code
  2. Have it ready to accept either the old OR the new key
  3. Call this endpoint
  4. Cut traffic over to the new key
  5. Stop accepting the old key

Skipping the dual-accept window means anything still holding the old key will be 401’d until it picks up the new value.

Request Body

FieldTypeRequiredDescription
child_api_key_iduuidYesThe child to rotate

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-rotate-child-key \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737" }'

Response (200 OK)

{ "success": true, "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "key": "warm_qQX6HDqVDoSzbAHKP3e37WhhQMFg1qHS", "key_prefix": "warm_qQX6", "old_key_prefix": "warm_aB3c", "rotated_at": "2026-05-31T18:21:08.953Z" }

key is returned once. If you lose it again you’ll need to rotate again (rate-limited — see below).

Rate limit

Max 10 rotations per child per 24 hours. Past that, the endpoint returns 429 with:

{ "success": false, "error": "Rate limit: max 10 rotations per child per 24 hours", "recent_rotations_24h": 10 }

Tracked via the api_key_rotations audit table (one row per rotation, retained indefinitely for forensic tracing).

Error-specific notes

CodeTrigger
400child_api_key_id missing or not a UUID
403The child_api_key_id belongs to a different partner
404child_api_key_id doesn’t exist
429Rate limit (see above)

Transfer Topup

POST /partner-transfer-topup

Gift topup IDs from your own balance to one of your managed children’s balance. Inverse of the topup fold-back that partner-release-child does on churn. Typical use case: you sell a customer “extra 5,000 IDs” as an upsell in your own product, take payment in your own billing, then push the credit onto their topup_balance here — without giving them portal access.

Atomic on the DB side (both subscription rows locked FOR UPDATE inside one Postgres transaction). Either both balances change or neither does.

Request Body

FieldTypeRequiredDescription
child_api_key_iduuidYesThe child to credit. Must be one of your managed children.
amountintYesPositive integer — number of topup IDs to transfer from your balance to theirs.

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-transfer-topup \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "amount": 5000 }'

Response (200 OK)

{ "success": true, "partner_topup_balance": 12000, "child_topup_balance": 5000, "transferred": 5000 }
FieldDescription
partner_topup_balanceYour topup_balance after the debit
child_topup_balanceThe child’s topup_balance after the credit
transferredEcho of amount for confirmation

Side effects

  • Fires a child.topup_changed webhook event on your partner webhook URL (if set), with reason: "gift".
  • If the recipient was suspended at transfer time AND the transfer lifts them back into burnable territory, fires a paired child.reactivated with reason: "topup_purchased" in the same SQL transaction.
  • Does not currently emit an event for the debit side (your own balance going down). Until that ships, reconcile your own balance from the partner_topup_balance field in this response.

Error-specific notes

CodeTrigger
400child_api_key_id missing or not a UUID; amount missing, not a positive integer
402Your topup_balance is less than amount. Response body includes partner_topup_balance (your current balance) and requested_amount for diagnostics.
403The child_api_key_id belongs to a different partner
404child_api_key_id doesn’t exist, OR the partner / child has no subscriptions row (misconfigured state — contact support if you see this)

Release Child

POST /partner-release-child

Reverse an adoption. The child’s api_key returns to your direct account (cleared parent_api_key_id), the phantom user + its subscription are deleted, and any remaining topup_balance is folded back into your partner subscription so you don’t lose paid-for IDs.

Use this when a customer churns out of your platform. After release the tracker keeps working — it’s just no longer billed / scoped as a managed customer; it’s a direct tracker on your account.

Request Body

FieldTypeRequiredDescription
child_api_key_iduuidYesThe child to release

Example Request

curl -X POST https://api.warmai.uk/functions/v1/partner-release-child \ -H "x-access-key: warm_YOUR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "child_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737" }'

Response (200 OK)

{ "success": true, "released_api_key_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737", "topup_returned": 0 }
FieldDescription
released_api_key_idEchoed for confirmation
topup_returnedIDs added back to your partner subscription’s topup_balance

Error-specific notes

CodeTrigger
400child_api_key_id missing
403The child belongs to a different partner
404child_api_key_id doesn’t match any api_key

If your partner account has no subscription row when you release a child (a misconfigured state — contact support if you see this), the topup_returned value is reported but the credit is lost. This is a defensive log-and-continue: we never want a release to fail because of a missing partner subscription, but the IDs go nowhere.

Last updated on