Partner API
Provision and manage OnyxMetric organizations on behalf of your own customers. Create an organization, add a site, set a plan, mint a sign-in link, read a summary, and delete everything again.
The Partner API is for platforms that embed OnyxMetric for their own customers and bill those customers themselves. A partner creates one organization per customer, attaches sites, chooses a plan, and can hand the customer a one-time sign-in link to the dashboard. No end user ever sees an OnyxMetric checkout.
Base path: /api/partner
Authentication
Every request carries a partner key:
Authorization: Bearer <partner key>Partner keys are issued by OnyxMetric. The server stores only their SHA-256 hashes in the PARTNER_API_KEY_HASHES environment variable as comma-separated id:hash entries, for example acme:9f2c...,acme:41b7.... The id (letters, digits, - or _, up to 64 characters) is the stable identity of the partner: organizations are owned by the id, not by a key, so keys can change without losing access to them. An id may have several hashes at once. To add a key, generate a long random string, hash it, add id:hash to the variable, and give the partner the original string:
KEY=$(openssl rand -hex 32)
printf '%s' "$KEY" | shasum -a 256 | cut -d' ' -f1 # add this to PARTNER_API_KEY_HASHESThe variable is read on every request, so rotation needs no restart. A missing or unknown key returns 401.
To rotate without downtime: add a second id:hash entry for the same id, let the partner switch to the new key, then remove the old entry. Entries without an id: prefix are ignored.
Rate limits
Limits are per partner id (not per key) and per route, over a one-minute window. Over the limit returns 429.
| Routes | Limit |
|---|---|
| Create organization, add site, set plan, summary | 60 / minute |
| Sign-in link, delete organization | 10 / minute |
The counters live in Redis. If Redis is unavailable the limiter lets requests through, so these limits fail open until it recovers.
A partner can only see and change organizations it created. Any other organization id, including one that exists but belongs to someone else, returns 404.
Create an organization
POST /api/partner/organizations
{ "externalId": "your-customer-id", "name": "Acme Store", "ownerEmail": "owner@acme.com" }Response:
{ "organizationId": "9f2c...", "created": true, "ownerStatus": "member" }Idempotent per (partner, externalId): repeating the call returns the same organization with created: false. Organizations start on the free tier.
ownerStatus says how the owner is attached:
"member": the email was new, so an owner account was created for it and is already the organization's owner. Use the sign-in link to log them in."invited": the email already belongs to an existing account. That account is never attached to your organization automatically. The organization is created without an owner and a normal invitation email is sent to the address; the account holder accepts it while signed in. Until then no sign-in link can be minted for that address.
Add a site
POST /api/partner/organizations/:id/sites
{ "domain": "acme.com", "name": "Acme Store" }Response: { "siteId": 123 }. Idempotent on domain. A domain that cannot be used returns a generic 409 {"error":"Conflict"}. A new site starts with the feature flags of the organization's current tier.
Set the plan
PUT /api/partner/organizations/:id/tier
{ "tier": "growth", "overage": false }tier accepts any key in the table below. overage is optional; when omitted the current setting is kept. Response:
{
"tier": "growth",
"limits": { "sessions": 25000, "recordings": 1500, "retentionDays": 395, "replayRetentionDays": 30 },
"overage": false
}Tiers
| Tier | Sessions / month | Recordings / month | Replay kept | Data kept |
|---|---|---|---|---|
free | 2,500 | 0 (replay off) | - | 30 days |
starter | 10,000 | 300 | 14 days | 90 days |
growth | 25,000 | 1,500 | 30 days | 395 days |
pro | 100,000 | 5,000 | 90 days | 395 days |
scale | 250,000 | 20,000 | 365 days | 730 days |
At the cap
- Session replay stops recording once the recordings cap is reached, unless
overageistrue. The free tier can never record, with or withoutoverage: the server refuses replay data for it. - Analytics keep counting up to 120% of the sessions cap. Past 120% only 1 visitor in 10 is collected (the same visitors for the rest of the month, so their sessions stay whole), custom events and errors are still collected, and replay recording stops, unless
overageistrue. - Retention is enforced daily: data older than the tier's
retentionDays(and recordings older thanreplayRetentionDays) is deleted. Moving to a tier with a longer window applies at once; moving to a shorter one takes effect 30 days later, so the data stays available (and exportable) until then. Organizations outside the partner program keep today's behavior: recordings are deleted after 30 days, and analytics data is not touched. - The cap is enforced from an events-per-session conversion (10 events per session) refreshed about every 30 minutes, so the cut-off is approximate. The
usageblock in the summary is the exact count.
Sign-in link
POST /api/partner/organizations/:id/login-link
{ "email": "owner@acme.com" }Response: { "url": "https://..." }. The link signs the member in once and expires after 10 minutes. It is returned to you, never emailed. Links are only minted for accounts your key created through this API (ownerStatus: "member") that belong to the organization. For any other existing account, and for platform administrators, the call returns 409 {"error":"existing_account"}: those people sign in themselves. An address with no account returns 404.
Summary
GET /api/partner/organizations/:id/summary?siteId=123&days=30
siteId defaults to the organization's first site, days to 30 (1 to 365).
{
"visitors": 4210,
"pageviews": 12877,
"revenue": 8123.5,
"currency": "USD",
"topSources": [{ "source": "google", "visitors": 1800, "revenue": 3100 }],
"usage": { "sessions": 9120, "sessionCap": 25000, "recordings": 410, "recordingCap": 1500 }
}revenue sums the revenue property of purchase events. usage covers the current calendar month across all of the organization's sites, so you can show meters and bill overage.
Delete an organization
DELETE /api/partner/organizations/:id returns 204.
Removes the organization's sites, events and replays, then the organization itself. Accounts your key created that belong to no other organization are deleted with it; any other account is left untouched. Replay recordings (including stored payloads), v1 and v2 replay metadata and all analytics tables are removed table by table before the organization rows, and the call can safely be repeated if it fails part-way. Deletion in the analytics store is asynchronous and completes shortly after the call returns.