Endpoints
Reference for every partner-* endpoint. All endpoints authenticate with your master partner key via the x-access-key header.
Required Headers
| Header | Description |
|---|---|
x-access-key | Your master partner api key (warm_…). Must belong to an account flagged is_partner=true. |
Content-Type | application/json (POST endpoints only) |
Base URL
https://api.warmai.uk/functions/v1Common error codes
| Code | Meaning |
|---|---|
400 | Validation error (missing required field, malformed value) |
401 | x-access-key missing, unknown, or revoked |
402 | Payment-required (insufficient balance) — currently only partner-transfer-topup returns this |
403 | Key is valid but the account is not a partner — or the resource doesn’t belong to the caller |
404 | Referenced child / api_key / website doesn’t exist |
405 | Wrong HTTP method (e.g. GET on a POST endpoint) |
409 | Conflict (domain already registered, key already adopted, etc.) |
429 | Rate limit (currently only partner-rotate-child-key returns this — max 10 rotations per child per 24h) |
500 | Internal — 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the customer (shown in partner-list-children results) |
domain | string | Yes | The customer’s tracker domain, e.g. acme.com. Bare domain — no scheme, no path. |
monthly_id_quota | int | No | IDs the child gets per period. Default 100. |
id_price_pence_override | number | No | Your per-ID rate for this child, in pence. Informational. |
webhook_url | string | No | HTTPS URL to receive visitor identification events. |
webhook_secret | string | No | If webhook_url is set: your own secret (min 8 chars). If omitted, we mint one and return it. |
brand_name | string | No | Whitelabel 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
| Code | Trigger |
|---|---|
400 | name or domain missing; domain failed validation (not a bare host.tld); webhook_url not a valid http(s) URL |
409 | domain 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
| Field | Type | Required | Description |
|---|---|---|---|
child_api_key_id | uuid | Yes | The child to update |
name | string | No | New display name |
monthly_id_quota | int | No | New per-period quota. Also resets ids_used_this_period to 0 (treat as a quota refresh, not just a cap raise). |
id_price_pence_override | number|null | No | New per-ID rate. Pass null to clear. |
status | string | No | One of "active", "suspended", "revoked". Suspended/revoked children stop accepting tracker traffic. |
extend_period_days | int | No | Bumps 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_url | string|null | No | The 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_secret | string|null | No | HMAC 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
| Code | Trigger |
|---|---|
400 | child_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 |
403 | The child belongs to a different partner |
404 | child_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
| Param | Type | Required | Description |
|---|---|---|---|
tracker_site | string | No | Filter to one registered domain (e.g. acme.com). Omit to include every tracker the calling key has access to. |
since | string (ISO 8601) | No | Earliest last_seen to include. Default: 90 days. Pass the last identified_at you’ve seen for incremental sync. |
source | string | No | all (default), tracker, or api. |
search | string | No | Free-text ILIKE over display name / domain / company / IP. |
limit | int | No | 1-500, default 100. |
offset | int | No | Default 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
| Code | Trigger |
|---|---|
401 | x-access-key missing, unknown, or inactive |
500 | Database 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
| Param | Type | Description |
|---|---|---|
child_api_key_id | uuid | Single-child lookup. Returns children: [] if no match. |
include_inactive | boolean | When 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
| Field | Type | Description |
|---|---|---|
child_api_key_id | uuid | Use this everywhere else as the child’s identifier |
child_user_id | uuid | The phantom user id (rarely needed externally) |
name | string | Display name, or "(unnamed)" if null |
domain | string|null | Tracker domain — null if no active website |
tracking_script_id | uuid|null | Used in the install snippet — null if no active website |
status | string | api_key.status — active / suspended / revoked |
created_at | timestamptz | When the child was provisioned |
monthly_id_quota | int | Current period quota |
ids_used_this_period | int | IDs consumed so far this period |
topup_balance | int | Non-expiring IDs available after quota is exhausted |
current_period_end | timestamptz | When the period rolls over |
sub_status | string | subscription.status — independent of api_key.status |
id_price_pence_override | numeric|null | Your per-ID unit cost for this child |
has_webhook | boolean | true 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
| Field | Type | Required | Description |
|---|---|---|---|
api_key_id | uuid | Yes | The tracker to adopt. Must belong directly to you (no parent_api_key_id), be status: "active", and have at least one active website. |
name | string | Yes | Display name for the new child |
monthly_id_quota | int | No | Default 0 (PAYG — set later via partner-update-child) |
id_price_pence_override | number | No | Per-ID rate |
webhook_url | string | No | Overwrites any existing webhook on the api_key |
webhook_secret | string | No | If webhook_url is set: your own secret (min 8 chars), or we mint one |
brand_name | string | No | Defaults 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
| Code | Trigger |
|---|---|
403 | api_key_id belongs to a different user |
404 | api_key_id doesn’t exist |
409 | The 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:
- Deploy the rotation handler to your integration code
- Have it ready to accept either the old OR the new key
- Call this endpoint
- Cut traffic over to the new key
- 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
| Field | Type | Required | Description |
|---|---|---|---|
child_api_key_id | uuid | Yes | The 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
| Code | Trigger |
|---|---|
400 | child_api_key_id missing or not a UUID |
403 | The child_api_key_id belongs to a different partner |
404 | child_api_key_id doesn’t exist |
429 | Rate 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
| Field | Type | Required | Description |
|---|---|---|---|
child_api_key_id | uuid | Yes | The child to credit. Must be one of your managed children. |
amount | int | Yes | Positive 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
}| Field | Description |
|---|---|
partner_topup_balance | Your topup_balance after the debit |
child_topup_balance | The child’s topup_balance after the credit |
transferred | Echo of amount for confirmation |
Side effects
- Fires a
child.topup_changedwebhook event on your partner webhook URL (if set), withreason: "gift". - If the recipient was suspended at transfer time AND the transfer lifts them back into burnable territory, fires a paired
child.reactivatedwithreason: "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_balancefield in this response.
Error-specific notes
| Code | Trigger |
|---|---|
400 | child_api_key_id missing or not a UUID; amount missing, not a positive integer |
402 | Your topup_balance is less than amount. Response body includes partner_topup_balance (your current balance) and requested_amount for diagnostics. |
403 | The child_api_key_id belongs to a different partner |
404 | child_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
| Field | Type | Required | Description |
|---|---|---|---|
child_api_key_id | uuid | Yes | The 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
}| Field | Description |
|---|---|
released_api_key_id | Echoed for confirmation |
topup_returned | IDs added back to your partner subscription’s topup_balance |
Error-specific notes
| Code | Trigger |
|---|---|
400 | child_api_key_id missing |
403 | The child belongs to a different partner |
404 | child_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.