# Endpoints

Every route in this catalog. Operator routes are omitted.

# Account

## GET /healthz

Database is answering.

Auth: none.

Success: 200 `{"ok": true}`

Errors:
- 503 `database unavailable`


## GET /v1/plans

Prices a public signup can choose.

Auth: none.

Success: 200 `{"plans":[{"id":"free","selectable":true,"trial_days":30,"setup_fee_cents":0,"resolved_contact_cents":0},{"id":"unlimited","selectable":false}]}`

Notes:
- Public signup starts a 30-day no-card trial, then $1 per account/month. Includes 3 pixels and 500 new contacts per UTC calendar month. No extra charges. Resolution pauses at the limit or after trial until subscribed.
- `unlimited` is not selectable on public signup.


## POST /v1/accounts

Start a 30-day trial without a card. The signup token lasts one hour.

Auth: none.

Body:
- `email` (string, required). Stored lowercased.
- `display_name` (string, required).
- `external_user_id` (string, optional). Unique when you send it.

Success: 201 `account_id`, `status` `pending_email`, `plan` `free`, `signup_token`, `external_user_id`, `email_verified` false. `verification_url` is present only when the confirmation email was not sent.

Errors:
- 400 `invalid email`
- 400 `display name is required`
- 400 `invalid request`
- 409 `email already registered`
- 409 `external user already registered`
- 502 `could not send the confirmation email`

Notes:
- Send the signup token as `x-signup-token`. It expires one hour after this response. There is no refresh call.
- An unlimited key on this same call skips the email and also returns `api_key` and `prefix`. A paid signup does not.


## GET /v1/accounts/{account_id}

Status, plan, email, and whether the email is confirmed.

Auth: signup token.

Success: 200 `account_id`, `status`, `plan`, `email`, `email_verified`

Errors:
- 401 `signup token required`
- 401 `signup token expired`

Notes:
- Poll this until `email_verified` is true and, after checkout, until `status` is `active`.


## GET /v1/email/verify

Confirm the email. The confirmation link lands here.

Auth: none.

Query:
- `token` (string, required).

Success: 200 HTML. The page says "Email confirmed".

Errors:
- 400 `verification token required`
- 404 `verification link not found`
- 400 `verification link expired` (24 hours after the send)

Notes:
- Use `verification_url` from signup when it is present. Otherwise a person opens the email.


## POST /v1/accounts/{account_id}/verification-email

Send the confirmation email again.

Auth: signup token.

Success: 200 `{"sent": true, "status": "sent"}`, plus `verification_url` only when the mailer skipped the send. Already confirmed: `{"sent": false, "status": "already_verified"}`.

Errors:
- 404 `account not found`
- 401 `signup token required`
- 401 `signup token expired`
- 429 `wait before sending the confirmation email again` (one minute)
- 502 `could not send the confirmation email`


## POST /v1/accounts/{account_id}/checkout

Open Stripe Checkout for the $1 per account/month subscription. No usage charges.

Auth: signup token or account Bearer API key.

Success: 200 `url`, `monthly_cents` 100, `setup_fee_cents` 0, `resolved_contact_cents` 0

Errors:
- 404 `account not found`
- 401 `signup token required`
- 401 `signup token expired`
- 502 when Checkout cannot be opened. `detail` says why.

Notes:
- Give `url` to a person. This call does not charge.
- After payment the browser lands on `/v1/checkout/success`. Check `plan` `paid` and a future `paid_until` using `/v1/whoami`. An active trial is not payment confirmation. Do not call the success URL yourself.
- Cancel leaves the subscription unpaid. Open checkout again when ready.


## GET /v1/checkout/success

Browser return after Checkout. Marks the account active when the session is paid.

Auth: none.

Query:
- `session_id` (string, required). Stripe adds this.

Success: 200 HTML. The page says "Setup payment received".

Errors:
- 400 `session_id is required`
- 400 `stripe checkout is not configured`
- 402 HTML when the session is not paid
- 502 HTML when the session cannot be finished

Notes:
- Integrators poll `GET /v1/accounts/{account_id}` instead of calling this.


## GET /v1/checkout/cancelled

Browser return when Checkout is cancelled. No charge.

Auth: none.

Success: 200 HTML. No subscription is activated.


## POST /v1/stripe/webhook

Stripe notifies this process. An integrator does not call it.

Auth: Stripe signature.

Success: 200 `{"received": true}` for a completed checkout and for ignored event types.

Errors:
- 400 `invalid signature`
- 400 `event missing account`
- 502 when Stripe cannot be read

Notes:
- Header `stripe-signature`. The success page can finish the account without this call.


## POST /v1/api-keys

Issue an API key. The secret is in this response only.

Auth: signup token, or an existing API key.

Query:
- `account_id` (required with `x-signup-token`). Omit it when you send a Bearer key.

Success: 201 `{"api_key": "px_...", "prefix": "px_........"}`. `prefix` is the first 12 characters.

Errors:
- 401 `api key or signup token required`
- 401 `signup token required` or `signup token expired`
- 403 `account is not active`
- 403 `confirm your email before creating an API key` (signup token only)
- 401 `api key required`, `invalid api key`, or `api key revoked` on the Bearer path

Notes:
- The signup-token path needs `status` `active` and a confirmed email.
- A Bearer key on an active account can issue another key. This catalog has no customer revoke route.


## GET /v1/whoami

Whose key this is. The secret is not returned.

Auth: API key.

Success: 200 `account_id`, `plan`, `status`, `key_prefix`

Errors:
- 401 `api key required`
- 401 `invalid api key`
- 401 `api key revoked`

Notes:
- A bad key on this route is 401. On setup routes a bad key is 403.


# Pixel setup

## GET /v1/setup

Whether this account has a pixel installed.

Auth: API key, account in good standing.

Success: 200 `good_standing` true, `pixel_count`, `installed_count`, `setup_complete`

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `that install method is not available`

Notes:
- `setup_complete` is true when at least one pixel has `install_status` `installed`.


## POST /v1/pixels

Mint a pixel for a site and return the install snippet.

Auth: API key, account in good standing.

Body:
- `domain` (string, required). A host, such as `example.com`. `https://` and a leading `www.` are removed. A path, query, port, space, or `@` is refused.
- `method` (string, optional). `cms`, `gtm`, and `utm` are refused.

Success: 201 `pixel_id`, `domain`, `install_status` (`not_checked` on a new pixel), `checked_at`, `snippet`, `consent_enabled`, `check_error`.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `domain is required`
- 400 `domain must be a host, not a page`
- 400 `domain must be a host`
- 400 `that install method is not available` when `method` is `cms`, `gtm`, or `utm` (body or query)
- 409 `domain already registered` when another account owns that host
- 502 `pixel could not be created`

Notes:
- The same account posting the same domain again gets the existing pixel, still as 201.
- When `consent_enabled` is true the snippet is the consent tag (`/px/{pixel_id}.js`). Otherwise it is the immediate pixel snippet.
- Paste the consent tag if you are about to turn consent on. The immediate snippet starts identification without a choice.


## GET /v1/pixels

Pixels on this key's account, oldest first. Snippets are not included.

Auth: API key, account in good standing.

Success: 200 `{"pixels": [{"pixel_id", "domain", "install_status", "checked_at"}]}`

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `that install method is not available`


## GET /v1/pixels/{pixel_id}

One pixel, including the current snippet.

Auth: API key, account in good standing.

Success: 200 same shape as create.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 404 `pixel not found`
- 400 `that install method is not available`

Notes:
- Another account's pixel is 404. A script alias is not accepted here. Use the canonical `pixel_id`.


## POST /v1/pixels/{pixel_id}/check

Fetch `https://{domain}/` and record whether the HTML contains this pixel id.

Auth: API key, account in good standing.

Success: 200 `pixel_id`, `domain`, `install_status`, `checked_at`, `check_error`

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 404 `pixel not found`
- 400 `that install method is not available`
- 500 `install checker is not configured`

Notes:
- `installed` means the pixel id is in a 200 page. `not_installed` means a 200 page without the id. `unreachable` means the fetch failed.
- A failed fetch leaves a previous `installed` status in place and still sets `check_error`.
- One same-host redirect is followed. The fetch times out in a few seconds.


## PATCH /v1/pixels/{pixel_id}

Change the domain. Refused after the pixel is installed.

Auth: API key, account in good standing.

Body:
- `domain` (string, required). Same host rules as create.
- `method` optional, and `cms` / `gtm` / `utm` are refused.

Success: 200 the pixel, with snippet.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `domain is required`
- 400 `domain must be a host, not a page`
- 400 `domain must be a host`
- 400 `that install method is not available` when `method` is `cms`, `gtm`, or `utm` (body or query)
- 404 `pixel not found`
- 409 `installed pixel cannot be renamed`
- 409 `domain already registered`


## DELETE /v1/pixels/{pixel_id}

Delete a pixel that is not installed.

Auth: API key, account in good standing.

Success: 204 empty body.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 404 `pixel not found`
- 409 `installed pixel cannot be deleted`
- 400 `that install method is not available`


## GET /v1/pixels/{pixel_id}/snippets/{action}

Extra paste snippet for one commerce action.

Auth: API key, account in good standing.

Success: 200 `action`, `pixel_id`, `snippet`, `button_snippet` (null when that file is absent).

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 404 `pixel not found`
- 404 `snippet not found`

Notes:
- `action` is `viewed_product`, `add_to_cart`, or `checkout_completed`.


## POST /v1/pixels/{pixel_id}/email-snippet

Email the immediate install snippet.

Auth: API key, account in good standing.

Body:
- `email` (string, required).
- `method` optional. `cms`, `gtm`, and `utm` are refused.

Success: 200 `{"sent": true, "install_status": "..."}`

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `invalid email`
- 404 `pixel not found`
- 429 `wait before sending the snippet again` (10 minutes)

Notes:
- This emails the immediate snippet, including when consent is on. Paste the consent tag from the consent call when the popup should gate identification.


# Contacts and billing

## GET /v1/pixels/{pixel_id}/contacts

Read resolved people. Pass `on` or `after`, one of them.

Auth: API key, account in good standing.

Query:
- `on` (`YYYY-MM-DD`). People whose first resolved UTC day is that date, in id order.
- `after` (integer, zero or greater). People with a greater id. The same `after` returns the same rows.

Success: Day: `pixel_id`, `on`, `contacts`. Cursor: `pixel_id`, `after`, `high_water`, `contacts`. Each contact is `id`, `hem`, `resolved_at` (UTC ISO), `contact` (the stored object).

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `pass on or after`
- 400 `pass on or after, not both`
- 400 `on must be YYYY-MM-DD`
- 400 `after must be zero or greater`
- 404 `pixel not found`

Notes:
- `high_water` is the last id in this page, or the `after` you sent when there are no rows. Pass it as the next `after`.
- An update keeps the original id and the original UTC day. The day filter follows that first day.
- An unlimited key may read any pixel. A paid key may read its own. Another account's pixel is 404.


## POST /v1/pixels/{pixel_id}/contacts

Store people that are already resolved. A customer read uses GET.

Auth: API key, account in good standing.

Body:
- `contacts` (array). Each item is `hem`, `seen_at`, `contact`.
- `hem` is 64 hex characters when `contact` is present.
- `seen_at` is an ISO timestamp. A trailing `Z` is accepted.
- `contact` is an object. `null` or `{}` is a miss and is skipped.

Success: 200 `{"pixel_id", "stored"}`. `stored` counts people written, including updates. Misses are not counted.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`
- 400 `contact entry must be an object`
- 400 `contact must be an object`
- 400 `hem must be a 64-character hex string`
- 400 `seen_at must be an ISO timestamp`
- 400 `invalid request`
- 404 `pixel not found`

Notes:
- Updating a known `hem` replaces `contact` and keeps the id and the first `resolved_at`.


## GET /v1/billing/quote

What this account would owe. This call does not charge.

Auth: API key, account in good standing.

Success: 200 `plan`, `contact_count`, `setup_cents`, `contact_cents`, `usage_cents`, `total_cents`. Paid is $1 per account/month. Trial and unlimited are zero. No usage charges.

Errors:
- 401 `api key required` when the Authorization header is missing
- 403 `invalid api key`
- 403 `api key revoked`
- 403 `account is not in good standing` when the account is not `active`, or the plan is not `free`, `paid`, or `unlimited`


# Consent

## POST /v1/pixels/{pixel_id}/consent

Turn on the permission popup and publish the policy.

Auth: API key, account active.

Body:
- `controller_name` (string, required).
- `purposes` (array, at least one). Allowed: `analytics`, `resolution`, `newsletter`, `email_sharing`, `phone_sharing`, `partner_email`, `partner_calls`, `partner_sms`. Unknown names are dropped.
- `controller_type` optional. `company` (default), `person`, or `organization`.
- `controller_contact`, `controller_jurisdiction`, `rule_version` optional strings.
- `data_sharing` boolean. False when omitted.
- `contact_channels` optional. Allowed: `email`, `phone`, `sms`, `agentic`.
- `disclosed_parties` optional array of strings.
- `site_policy_url` optional. The site's own privacy policy, a full https:// link. The popup links to it, and the hosted page becomes a short identification notice that adds to it.

Success: 200 `pixel_id`, `consent_enabled` true, `policy_version`, `policy_hash`, `policy_url`, `snippet`, `purposes`. The snippet is `<script async src="{origin}/px/{pixel_id}.js"></script>`.

Errors:
- 400 `invalid request`
- 400 `controller name is required`
- 400 `choose at least one purpose`
- 400 `controller type must be company, person, or organization`
- 401 `invalid api key`
- 403 `account is not active`
- 404 `pixel not found`

Notes:
- Install `snippet`, not the immediate pixel snippet. Identification loads only after the visitor accepts `resolution` on the current policy.
- Saving the same policy text keeps `policy_version`. A text change increments it. Visitors must accept the new hash.
- An unlimited key may configure any pixel. The path id must be the canonical pixel id.


## GET /v1/pixels/{pixel_id}/consent

Consent log for one pixel, newest first, at most 200 rows.

Auth: API key, account active.

Query:
- `state` optional: `accepted`, `declined`, or `withdrawn`.

Success: 200 `pixel_id`, `events`. Each event includes the receipt fields plus `receipt`, `signature`, and `signature_alg` `hmac-sha256`.

Errors:
- 400 `invalid state`
- 401 `invalid api key`
- 403 `account is not active`
- 404 `pixel not found`


## GET /px/{pixel_id}.js

The permission popup. Browsers load this. It is not cached.

Auth: none.

Success: 200 `application/javascript`. `Cache-Control: no-store`.

Errors:
- 404 `consent tag not found` when the pixel is missing, consent is off, or there is no policy hash

Notes:
- The path accepts the canonical pixel id or a script alias. The banner records the canonical id.
- A visitor with Global Privacy Control is treated as a decline and nothing is stored.
- Accepting `resolution` on the current policy is what loads the identification tag.


## GET /v1/policy/{pixel_id}

The current privacy notice as HTML.

Auth: none.

Same handler: `GET /api/v1/policy/{pixel_id}`.

Success: 200 HTML.

Errors:
- 404 `policy not found`

Notes:
- A script alias in the path resolves to the canonical pixel.


## POST /v1/consent/record

The popup writes one choice. A saved choice is 202. An ignored choice is 204 with an empty body.

Auth: none.

Same handler: `POST /api/v1/pixel/fire`.

Body:
- `pixel_id` (string). Canonical id or script alias.
- `visitor_id` (string).
- `url` (string). Page URL. The host must be the pixel domain or a subdomain of it.
- `consent`: `accepted`, `declined`, or `withdrawn`.
- `affirmative` true is required for `accepted`.
- `policy_version_hash` must equal the current policy hash for `accepted`.
- `purposes` for `accepted` must be a non-empty subset of the configured purposes.
- `gpc` true is ignored and stored as nothing.

Success: 202 `recorded` true, `receipt_id`, `policy_version_hash`, `purposes`, `receipt`, `signature`, `signature_alg`. The receipt is `pixel_id`, `receipt_id`, `visitor_id`, `origin`, `consent_state`, `policy_hash`, `purposes`, `recorded_at`.

Errors:
- 403 `{"recorded": false, "reason": "site_mismatch", "domain": "..."}` when the page host is a different site

Notes:
- 204 (empty) covers bad JSON, an unknown shape, Global Privacy Control, a missing id, an unknown pixel, consent turned off, an accept that is not affirmative, a stale policy hash, or a purpose that was not configured.
- Decline and withdraw store an empty purpose list.
- Verify a receipt with `POST /v1/consent/verify`. The signing secret is not in this response.


## GET /v1/consent/check

Whether the latest receipt allows a purpose.

Auth: none.

Query:
- `pixel_id` (required). Canonical id or a script alias.
- `visitor_id` (required).
- `required_purpose` optional, one of the purpose names.

Success: 200 `permitted`, `reason`, and when a row exists: `receipt_id`, `policy_version_hash`, `current_policy_hash`, `purposes`, `recorded_at`, `receipt`, `signature`, `signature_alg`.

Errors:
- 400 `invalid required_purpose`

Notes:
- `reason` is `ok`, `no_consent_record`, `consent_off`, `declined_or_withdrawn`, `policy_version_stale`, or `purpose_not_granted`.
- `permitted` is true only when `reason` is `ok`. No row, and an unknown pixel id, both return `permitted` false and `reason` `no_consent_record` without a receipt.


## GET /v1/consent/receipt

The latest receipt for a visitor, with a signature.

Auth: none.

Same handler: `GET /api/v1/consent/receipt`.

Query:
- `pixel_id` (required).
- `visitor_id` (required).

Success: 200 the check object plus `valid` true, when a receipt exists.

Errors:
- 404 `no consent record`

Notes:
- Accepts a script alias. An unknown id is 404 `no consent record`.


## POST /v1/consent/verify

Check a receipt signature.

Auth: none.

Body:
- `receipt` (object, required). The receipt object from record, check, or the log.
- `signature` (string).

Success: 200 `{"valid": true}` or `{"valid": false}`.

Errors:
- 400 `invalid request`
- 400 `receipt is required`


# Scripts the tag loads

## GET /pixel.js

Identification script referenced by the immediate snippet.

Auth: none.

Success: 200 `application/javascript`.


## GET /pixel.dev.js

Dev beacon. Customer installs use `/pixel.js` or the consent tag.

Auth: none.

Success: 200 `application/javascript`.


## GET /pixel_popup.js

Popup script referenced by the immediate snippet.

Auth: none.

Success: 200 `application/javascript`.


## GET /pixels/{pixel_id}/p.js

Redirect to Delivr's tag for this pixel id.

Auth: none.

Success: 302 `Location` on the Delivr CDN.

Errors:
- 404 `pixel not found` when the id is not 1 to 80 letters, digits, `_`, or `-`

Notes:
- This process does not host that file. A stub pixel id has no tag on the CDN.


## GET /external_api/install-pixel/delivr-pixel

Whether this pixel id belongs on this host. Used by the tag, not by account setup.

Auth: none.

Query:
- `pid` or `dpid` (the pixel id).
- `host` (the page host).

Success: 200 `{"pixel_id": "<id>"}` when the host is that pixel's domain or a subdomain. Otherwise `{"pixel_id": null}`.

Notes:
- Response header `Access-Control-Allow-Origin: *`.


# Unlimited key

## POST /v1/accounts/{account_id}/activate

Mark an account active without changing its plan. A paid customer key receives 403.

Auth: unlimited API key.

Success: 200 `account_id`, `status` `active`, `plan` (unchanged).

Errors:
- 401 `unlimited key required`
- 401 `invalid api key`
- 403 `unlimited key required`
- 404 `account not found`

Notes:
- Public trial becomes active through email confirmation. This call is for a holder of the unlimited key.


## POST /v1/pixels/pipeline

Mint a `cms` or `utm` pixel on a customer's paid account. A paid key receives 403.

Auth: unlimited API key.

Body:
- `domain` (string, required). Same host rules as `POST /v1/pixels`.
- `pipeline` (string, required). `utm` or `cms`.
- `account_id` or `external_user_id`. One of them is required.

Success: 201 `pixel_id`, `account_id`, `domain`, `install_status`, `pipeline`, `snippet`, `pixel_js_url`, `delivr_script_url`.

Errors:
- 400 `pipeline must be utm or cms`
- 400 `account_id or external_user_id is required`
- 400 domain host errors
- 401 `api key required` or `invalid api key`
- 403 `unlimited key required`
- 403 `customer account is not in good standing` (the target must be `active` and `paid`)
- 404 `account not found`
- 409 `domain already registered`
- 502 `pixel could not be created`


# HTML pages

## GET /

HTML front. The Host header picks the pixel site, the permission site, or the API home.

Auth: none.

Success: 200 HTML.


## GET /site/pixel

Pixel marketing page.

Auth: none.

Success: 200 HTML.


## GET /site/permission

Permission marketing page.

Auth: none.

Success: 200 HTML.


## GET /demo

HTML form that provisions a consent tag on the shared demo account.

Auth: none.

Success: 200 HTML.

Notes:
- Customer pixels come from `POST /v1/pixels` after signup. `POST /demo/provision` writes the demo account.


## POST /demo/provision

Form post for the demo page. Creates or reuses a pixel on the demo account and turns consent on.

Auth: none.

Success: 200 HTML containing the tag.

Notes:
- Do not call this to open a customer account.


## GET /log

HTML page. A person pastes a pixel id and an API key to read that pixel's consent log.

Auth: none.

Success: 200 HTML. The page calls `GET /v1/pixels/{pixel_id}/consent`.
