> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OEM Partner API

> Provision, meter, and manage the sub-tenant orgs embedded in your product — authentication, endpoints, usage reconciliation, lifecycle, and webhooks.

The OEM Partner API lets an embedding partner provision and operate the VisIQ
sub-tenant organizations inside their own product, entirely programmatically.
It is the machine surface behind the OEM engagement model: create a governed
tenant per end customer, read reconciliation-grade usage, manage keys /
entitlement / branding, and offboard cleanly.

**Base URL** — `https://api.visiqlabs.com/v1/partners/oem`

<Note>
  Every response carries the header **`X-VisIQ-Partner-Api-Version: 2026-07-11`**.
  See [versioning & deprecation](/partners/oem-versioning) for the compatibility
  contract. To exercise the whole flow without touching billing, use a test-mode
  key — see the [sandbox guide](/partners/oem-sandbox).
</Note>

## Authentication

Authenticate every request with your partner **provisioning key** as a bearer
token:

```http theme={null}
Authorization: Bearer vqp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

A provisioning key (`vqp_…`) is **not** a vendor API key and never authenticates
the governance data plane. It is scoped to `oem:provision`, tied to your active
OEM partner account, and carries a **mode** (`live` or `test`). Keys are stored
hashed at rest and shown once at mint time.

Failures are fail-closed and give no oracle:

| Condition                                           | Response                                                                |
| --------------------------------------------------- | ----------------------------------------------------------------------- |
| Missing / malformed / revoked / wrong-scope key     | `401 { "error": "unauthorized" }`                                       |
| Rate limit exceeded                                 | `429 { "error": "rate_limited", "retryAfter": <s> }`                    |
| An id that is not one of your (mode-scoped) tenants | `404 { "error": "not_found" }`                                          |
| Invalid body / query                                | `400 { "error": "invalid_request" \| "invalid_period", "detail": "…" }` |

Rate-limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`)
accompany throttled responses.

## Provisioning

### Create a sub-tenant

```http theme={null}
POST /tenants
{ "companyName": "Acme Robotics", "externalRef": "crm-8842" }
```

Returns `201` with the new tenant id and the sub-tenant's operational harness
API key **once** (`apiKey`, never retrievable again). `externalRef` is echoed
back for your bookkeeping. A duplicate company name (per partner, per mode)
returns `409 tenant_exists`; the per-partner tenant cap returns
`409 tenant_cap_reached`.

```json theme={null}
{ "tenantId": "…", "apiKey": "vq_prod_…", "apiKeyId": "…", "isSandbox": false }
```

### List sub-tenants

```http theme={null}
GET /tenants
```

Returns your sub-tenants (mode-scoped) with lifecycle `status`
(`active` · `suspended` · `closed`).

## Usage & reconciliation

Usage is counted with the **same rules the royalty statement generator uses**
(governed action and retrieval audit events), valued at your current
contract-year list snapshot when present, else the platform list rate.

```http theme={null}
GET /usage?period=2026-07
GET /tenants/{id}/usage?period=2026-Q3
```

`period` accepts `YYYY-MM` or `YYYY-Qn`, or pass an explicit `start` & `end`
ISO pair (window bounded to 400 days). The aggregate response also reports your
commit-pool drawdown state:

```json theme={null}
{
  "period": "2026-07",
  "tenants": [
    { "tenantId": "…", "companyName": "Acme Robotics",
      "events": { "allow": 812345, "recall": 40122, "total": 852467 },
      "listValueCents": 10656 }
  ],
  "totals": { "events": { "allow": …, "recall": …, "total": … }, "listValueCents": … },
  "commit": { "commitTotalCents": 2500000, "drawnDownCents": 640000,
              "poolRemainingCents": 1860000, "royaltyRatePct": 12 }
}
```

## Statements

```http theme={null}
GET /statements                 # list (newest first, paginated: limit/offset)
GET /statements/{id}            # full JSON incl. per-tenant lines + drawdown columns
GET /statements/{id}/csv        # a per-tenant CSV (one row per sub-tenant + a TOTAL row)
```

The JSON is the machine-readable contract. The CSV is a flat per-tenant export
with the header row:

```
tenant,usage_events,usage_value_cents,royalty_cents
```

one row per sub-tenant plus a final `TOTAL` row (whose `royalty_cents` is the
statement's metered royalty). All statement money fields — in both the JSON and
the CSV — are integer cents.

## Lifecycle

```http theme={null}
PATCH /tenants/{id}   { "action": "suspend" | "resume" | "close" }
```

`suspend` reversibly blocks the sub-tenant's traffic; `resume` clears it.
`close` is **terminal** (Twilio suspend → close → purge pattern): it stamps
`closedAt`, sets a 30-day `purgeAfter` grace window, and suspends the tenant if
it was not already. Any action on a closed tenant — and any *create* against it
(keys, simulated usage) — returns `409 tenant_closed`. After the grace window a
nightly reaper hard-deletes the sub-tenant and its audit rows.

### Offboarding export

Extract the governance evidence before purge:

```http theme={null}
GET /tenants/{id}/export?limit=10000
GET /tenants/{id}/export?cursor=a:<lastId>
```

Streams newline-delimited JSON (`application/x-ndjson`): a `tenant` metadata
line (first page only), then `allow_audit` and `recall_audit` rows in stable id
order. Follow the **`X-VisIQ-Export-Cursor`** response header until it is empty.
Page cap is 10,000 rows.

## Sub-tenant keys

```http theme={null}
POST   /tenants/{id}/keys           # mint an additional harness key (returned once)
GET    /tenants/{id}/keys           # list (id, prefix, created, last used, revoked)
DELETE /tenants/{id}/keys/{keyId}   # revoke
```

## Entitlement & branding

```http theme={null}
PATCH /tenants/{id}/entitlement
{ "plan": "team", "monthlyEventCap": 5000000, "features": { "advancedRedaction": true } }

PATCH /tenants/{id}/branding
{ "displayName": "Acme Guard", "logoUrl": "https://cdn.acme.com/logo.png",
  "palette": { "primary": "#7c3aed" }, "fromName": "Acme Security" }
```

`plan` is applied to the sub-tenant's subscription (`is_unlimited` only for
`enterprise`). **Caps are v1-informational** — surfaced and enforced via the
partner usage-threshold engine, not hard-blocked at ingest. Branding
`displayName` is rendered on end-user-visible HITL approval prompts (Slack
today; email/Teams white-labeling is on the roadmap). `GET` variants return the
current object.

## Webhooks

Subscribe an HTTPS endpoint to receive signed events. The subscribable event
types are:

`sub_tenant.created` · `sub_tenant.suspended` · `sub_tenant.resumed` ·
`sub_tenant.closed` · `entitlement.changed` · `usage.threshold_crossed` ·
`royalty_statement.ready` · `wholesale_invoice.finalized` · `claim.accepted`.

### Configuring webhooks

```http theme={null}
POST   /webhooks               # register an endpoint (returns the signing secret ONCE)
GET    /webhooks               # list endpoints (never returns a secret)
PATCH  /webhooks/{id}          # enable/disable or change the events filter
DELETE /webhooks/{id}          # remove an endpoint
POST   /webhooks/{id}/ping     # enqueue a connectivity ping to one endpoint
```

Register an endpoint:

```http theme={null}
POST /webhooks
{
  "url": "https://hooks.acme.com/visiq",   // required; must be public HTTPS
  "events": ["sub_tenant.created", "royalty_statement.ready"],  // optional; [] = all events
  "description": "prod receiver"            // optional, ≤ 500 chars
}
```

Returns `201` with the endpoint and its **signing secret exactly once** (`secret`,
never retrievable again — store it now):

```json theme={null}
{ "id": "…", "url": "https://hooks.acme.com/visiq",
  "events": ["sub_tenant.created", "royalty_statement.ready"],
  "status": "active", "secret": "…64 hex…" }
```

The `url` must be a **public HTTPS** endpoint. It is validated both structurally
and by DNS resolution at registration and again at every delivery: a non-HTTPS
URL, a private/loopback/link-local/metadata host, or a name that resolves to such
an address is rejected (`400`) — an SSRF safeguard, and deliveries pin the
connection to the vetted address. `PATCH` accepts `status` (`active` | `disabled`)
and/or `events`; re-enabling clears the failure streak. Up to **5 endpoints** per
partner (`409 endpoint_cap_reached` beyond that). A `ping` is a system event
(type `ping`, not subscribable) delivered on demand to one endpoint.

### Verifying deliveries

Each delivery carries:

| Header                        | Meaning                             |
| ----------------------------- | ----------------------------------- |
| `X-VisIQ-Signature`           | `t=<unix_ts>,v1=<hex hmac-sha256>`  |
| `X-VisIQ-Event-Id`            | idempotency key — dedupe on this    |
| `X-VisIQ-Event-Type`          | e.g. `sub_tenant.created`           |
| `X-VisIQ-Partner-Api-Version` | the envelope version (`2026-07-11`) |

The JSON body is the envelope `{ id, type, created, data }`. Verify by recomputing
the HMAC over the signed payload `"<t>.<raw_body>"` with your endpoint's signing
secret, in constant time, and rejecting timestamps outside a small tolerance:

```
signed_payload = t + "." + raw_request_body
expected       = hex( HMAC_SHA256(secret, signed_payload) )
# constant-time compare expected against the v1 value
```

Deliveries retry with exponential backoff (`1m, 5m, 30m, 2h, 12h`) and
dead-letter after the schedule is exhausted. An endpoint that accumulates 20
consecutive delivery failures is auto-disabled (re-enable it with a `PATCH`).

## Usage thresholds

Register usage triggers that fire a `usage.threshold_crossed` webhook the first
time a metric crosses the threshold within a period (a fire-once-per-period latch —
each period, e.g. `2026-07` or `2026-Q3`, fires at most once and then re-arms for
the next period).

```http theme={null}
POST   /usage-thresholds       # register a trigger
GET    /usage-thresholds       # list triggers
DELETE /usage-thresholds/{id}  # remove a trigger
```

```http theme={null}
POST /usage-thresholds
{
  "vendorId": "…",              // optional; omit = aggregate across your sub-tenants
  "metric": "events",           // "events" | "list_value_cents"
  "period": "month",            // "month" | "quarter" (default "month")
  "threshold": 5000000          // positive integer (events, or list value in cents)
}
```

A pinned `vendorId` must be one of your own (mode-scoped) sub-tenants, else `404`.
Up to **50 thresholds** per partner (`409 threshold_cap_reached` beyond that).
`GET` echoes each trigger's `lastFiredPeriod` so you can see when it last latched.
