API reference
Every route below is generated from the real NestJS controllers and DTOs — not hand-written. The canonical, always-current source is the generated OpenAPI document, not this page.
@nestjs/swagger reading the actual route decorators. Two auth models
Nastar deliberately keeps dashboard auth and automation auth separate — automation should never touch a human's session.
| Session cookie | Setup token | |
|---|---|---|
| Used by | The dashboard (browser) | IDEs, CLIs, CI — see AI IDE integration |
| Obtained via | POST /v1/auth/otp/verify | POST /v1/organizations/:orgId/tokens (session-authenticated; the token is shown once) |
| Sent as | nastar_session cookie (Secure, HttpOnly, SameSite=Lax) | Authorization: Bearer nst_... |
| Scope | Every org/site the signed-in user belongs to | One organization, optionally one site, and an explicit set of scopes |
| Routes | /v1/auth, /v1/organizations, /v1/sites, /v1/invites | /v1/agent |
Auth — /v1/auth (session)
| Route | Purpose |
|---|---|
POST /v1/auth/otp/request | Request a one-time code by email. Rate-limited per email and per IP. |
POST /v1/auth/otp/verify | Verify the code, issue a session cookie. Single-use, atomically consumed. |
POST /v1/auth/logout | Clear the session cookie. |
GET /v1/auth/me | The signed-in principal's { userId, email }. |
GET /v1/auth/sessions | Every active (unrevoked, unexpired) session on the account, newest first, each marked isCurrent. |
DELETE /v1/auth/sessions/:sessionId | Revoke a session. 404 if it doesn't belong to you. Revoking your own current session also clears your cookie — equivalent to logout. |
Organizations & sites — /v1/organizations (session)
| Route | Purpose |
|---|---|
POST /v1/organizations | Create an organization; the creator becomes its first admin. |
GET /v1/organizations | List organizations the caller is a member of. |
GET /v1/organizations/:orgId/members | List an organization's members and roles. |
POST /v1/organizations/:orgId/sites | Create a site inside an organization the caller belongs to. |
GET /v1/organizations/:orgId/sites | List an organization's sites. |
Invites — /v1/organizations/:orgId/invites (session)
| Route | Purpose |
|---|---|
POST .../invites | Invite an email to join as admin, editor, or viewer (org admin only). Re-inviting the same address replaces its earlier pending invite. 400 if that address is already an active member. |
GET .../invites | List pending (unaccepted, unrevoked, unexpired) invites — any member can view. |
DELETE .../invites/:inviteId | Revoke a pending invite (org admin only). |
Accepting is a separate, unauthenticated-until-the-last-step flow under /v1/invites/:token, not under the organization path above — the token itself, not org membership, is what proves the caller is allowed to see and accept it:
| Route | Purpose |
|---|---|
GET /v1/invites/:token | Preview — organization name, role, invited email. No session required; whoever holds the link can see what it's for. 404 if invalid, revoked, expired, or already accepted. |
POST /v1/invites/:token/accept | Accept (session required). 403 if the signed-in account's email doesn't match the invited address — accepting never grants access to an email you don't control. |
Site configuration — /v1/sites/:siteId (session)
| Route | Purpose |
|---|---|
GET /v1/sites/:siteId | Current draft config, publish status, and public site key. |
GET /v1/sites/:siteId/consent-records | Numbered pages of the site's anonymous consent events, newest first, with date-range, event-type and outcome filters. See below. |
POST /v1/sites/:siteId/banner-copy | Update the draft banner copy. |
POST /v1/sites/:siteId/categories/:categoryId | Update a consent category in the draft. |
POST /v1/sites/:siteId/services | Add a service (third-party integration) mapped to a category. |
DELETE /v1/sites/:siteId/services/:serviceId | Remove a service. |
POST /v1/sites/:siteId/google-consent-mode | Update the draft's Google Consent Mode v2 mapping — which category (if any) maps to each of Google's four consent signals. |
POST /v1/sites/:siteId/google-analytics | Update the draft's Google Analytics settings: { enabled, measurementId, consentMode, categoryId }. Enabling it also force-syncs the Google Consent Mode analytics_storage mapping to the same category. |
POST /v1/sites/:siteId/appearance | Update the draft's banner appearance. Body is a partial AppearanceSettings, merged onto what is stored; the merged result is validated as a whole. See the error codes below. |
POST /v1/sites/:siteId/languages | Replace both fields at once: { publishedLanguages: string[], defaultLanguage: string }. The default must be one of the published languages, and cannot be removed while it is the default. |
POST /v1/sites/:siteId/translations/:locale | Update one locale's visitor-facing content. Body is a partial LocaleContent (bannerCopy, categories keyed by category key, services keyed by service id), deep-merged into that locale only. Other locales are untouched. |
POST /v1/sites/:siteId/publish | Publish the current draft as a new immutable, versioned config. |
Consent records — query parameters and response
Numbered pages, not a cursor — page replaces the cursor this route used before. All parameters are optional.
| Parameter | Meaning |
|---|---|
page | 1-based page number. Default 1. |
limit | Page size. Default 50, max 200. |
from | Inclusive YYYY-MM-DD — the first calendar day to include, read in timeZone. |
to | Inclusive YYYY-MM-DD — the last calendar day to include, read in timeZone. |
timeZone | An IANA zone name (e.g. Europe/Amsterdam) that from/to are read in. Default UTC; an unrecognised zone falls back to UTC rather than 400ing. |
eventType | One of given, updated, withdrawn. |
outcome | One of accepted_all, partial, rejected_all, or unclassified — the last selects records whose outcome couldn't be classified at write time (a published config for that configVersion no longer resolvable). |
The response:
| Field | Meaning |
|---|---|
records | Each event: { id, anonymousId, decisions, eventType, outcome, configVersion, occurredAt } — never any account identity. |
page | The page number served. A requested page past the last real page (given total and limit) is clamped down to that last page rather than executed, and this is the clamped value. |
pageSize | The limit actually applied. |
total | Matching record count, capped at 10,000 — see totalIsCapped. |
totalIsCapped | true once total has been clamped, i.e. the real count is larger than 10,000. A narrower from/to range is the way to see an exact count and page through all of it. |
Appearance, language and publish error codes
These routes return 400 with a machine-readable code. The contrast codes also carry details: { pair, ratio, required, suggestion }, so a caller can show the measured ratio and a colour that would pass rather than a bare refusal.
| Code | Meaning |
|---|---|
invalid_color | A colour field is not a #rgb/#rrggbb hex value. |
contrast_text_on_surface | Body text is under 4.5:1 against the panel background (WCAG 2.2 AA, 1.4.3). |
contrast_muted_on_surface | Muted text — category descriptions — is under 4.5:1 against the panel. |
contrast_border_on_surface | The border is under 3:1 against the panel (1.4.11). Borders are what distinguish the secondary buttons. |
contrast_accent_on_surface | The accent is under 3:1 against the panel, so the focus ring and primary fill would not be distinguishable. |
contrast_label_on_accent | Neither black nor white reaches 4.5:1 on the accent, so no readable label colour exists for it. |
invalid_logo_url | The logo URL is malformed or not HTTPS. |
logo_not_an_image | The logo URL resolves but does not serve an image. A network failure at save time warns instead of blocking — a transient outage must not stop a save. |
invalid_policy_url | A privacy or cookie policy URL is malformed or not HTTPS. An unreachable one only warns: a policy page may legitimately not be published yet. |
invalid_locale | A language tag does not match /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/. |
default_language_not_published | defaultLanguage is not in publishedLanguages. |
cannot_remove_default_language | The default language was dropped from publishedLanguages. Change the default first. |
locale_incomplete | Returned by publish, not by the translation route — a published locale is missing strings. details: { locale, missing } lists every gap at once (e.g. bannerCopy.title, categories.analytics.description) so they can be fixed in one pass. |
Translations may be saved for a locale that is not published — draft work in progress is allowed. Publish simply ignores them, and ignores translation entries for categories or services that no longer exist: an orphaned string should not be able to block a publish.
Setup tokens — /v1/organizations/:orgId/tokens (session)
| Route | Purpose |
|---|---|
POST .../tokens | Create a scoped token (org admin only). The raw token is returned once in this response and never retrievable again. |
GET .../tokens | List tokens (hashed — never the raw value) with name, scopes, expiry, revocation status. |
DELETE .../tokens/:tokenId | Revoke a token immediately. |
Scopes: sites:read, sites:write, sites:publish.
Automation — /v1/agent (setup token)
| Route | Required scope | Purpose |
|---|---|---|
GET /v1/agent/me | any valid token | The token's organization, site binding (if any), and scopes. |
GET /v1/agent/sites/:siteId | sites:read | Same site payload as the session route, scoped to this token's organization/site. |
POST /v1/agent/sites/:siteId/banner-copy | sites:write | Update draft banner copy. |
POST /v1/agent/sites/:siteId/google-consent-mode | sites:write | Update the draft's Google Consent Mode v2 mapping. |
POST /v1/agent/sites/:siteId/google-analytics | sites:write | Update the draft's Google Analytics settings: { enabled, measurementId, consentMode, categoryId }. |
POST /v1/agent/sites/:siteId/publish | sites:publish | Publish — kept as a distinct scope so a general write-capable token can't publish without explicit permission. |
A token bound to a specific site is rejected on any other site's routes with 403. A revoked or expired token is rejected with 401 on every route.
Public — /v1/public (unauthenticated)
What the browser SDK calls directly — no credentials, wildcard CORS.
| Route | Purpose |
|---|---|
GET /v1/public/config/:publicSiteKey | The published SiteConfig — banner copy, categories, services, and (when configured) appearance, translations, publishedLanguages, defaultLanguage and consentRelevantVersion. 404 if the site has never published. |
POST /v1/public/consent/:publicSiteKey | Record a consent decision: { anonymousId, configVersion, decisions, eventType } where eventType is given, updated, or withdrawn. |
This endpoint never returns organization internals, member data, or other visitors' consent records — only what's already part of the published config.
Errors
Validation failures return 400 with a structured body from class-validator (field-level messages). Missing/invalid auth returns 401. A resource that exists but the caller can't act on returns 403; a resource the caller isn't a member of at all returns 404 rather than leaking its existence — see Security.
Not yet built
Idempotency keys and per-token rate-limit headers are not implemented yet. Some list endpoints do paginate — the audit trail (GET /v1/organizations/:orgId/audit-events) and origin observations (GET /v1/sites/:siteId/observed-origins, behind hostname enforcement) are both cursor-paginated, and GET /v1/sites/:siteId/consent-records is the one with numbered pages (page/limit, not a cursor) — but organizations, sites, invites, tokens and sessions return every row unpaginated, which is low-volume enough in Phase 1 that this is a known, documented gap rather than a silent one.