Nastardocs v1 · API v1

Last updated 2026-09-23 · API v1 · OpenAPI 3.0.0

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.

Interactive reference (https://api.nastar.dev/v1/docs) · raw OpenAPI JSON — both served directly by the backend, generated on boot from @nestjs/swagger reading the actual route decorators.
The generated document is OpenAPI 3.0.0, not 3.1 — a current tooling limitation on our side, not a claim of 3.1 compliance. Most 3.0-compatible tooling reads it fine.

Two auth models

Nastar deliberately keeps dashboard auth and automation auth separate — automation should never touch a human's session.

Session cookieSetup token
Used byThe dashboard (browser)IDEs, CLIs, CI — see AI IDE integration
Obtained viaPOST /v1/auth/otp/verifyPOST /v1/organizations/:orgId/tokens (session-authenticated; the token is shown once)
Sent asnastar_session cookie (Secure, HttpOnly, SameSite=Lax)Authorization: Bearer nst_...
ScopeEvery org/site the signed-in user belongs toOne 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)

RoutePurpose
POST /v1/auth/otp/requestRequest a one-time code by email. Rate-limited per email and per IP.
POST /v1/auth/otp/verifyVerify the code, issue a session cookie. Single-use, atomically consumed.
POST /v1/auth/logoutClear the session cookie.
GET /v1/auth/meThe signed-in principal's { userId, email }.
GET /v1/auth/sessionsEvery active (unrevoked, unexpired) session on the account, newest first, each marked isCurrent.
DELETE /v1/auth/sessions/:sessionIdRevoke 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)

RoutePurpose
POST /v1/organizationsCreate an organization; the creator becomes its first admin.
GET /v1/organizationsList organizations the caller is a member of.
GET /v1/organizations/:orgId/membersList an organization's members and roles.
POST /v1/organizations/:orgId/sitesCreate a site inside an organization the caller belongs to.
GET /v1/organizations/:orgId/sitesList an organization's sites.

Invites — /v1/organizations/:orgId/invites (session)

RoutePurpose
POST .../invitesInvite 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 .../invitesList pending (unaccepted, unrevoked, unexpired) invites — any member can view.
DELETE .../invites/:inviteIdRevoke 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:

RoutePurpose
GET /v1/invites/:tokenPreview — 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/acceptAccept (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)

RoutePurpose
GET /v1/sites/:siteIdCurrent draft config, publish status, and public site key.
GET /v1/sites/:siteId/consent-recordsNumbered 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-copyUpdate the draft banner copy.
POST /v1/sites/:siteId/categories/:categoryIdUpdate a consent category in the draft.
POST /v1/sites/:siteId/servicesAdd a service (third-party integration) mapped to a category.
DELETE /v1/sites/:siteId/services/:serviceIdRemove a service.
POST /v1/sites/:siteId/google-consent-modeUpdate 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-analyticsUpdate 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/appearanceUpdate 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/languagesReplace 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/:localeUpdate 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/publishPublish the current draft as a new immutable, versioned config.

Numbered pages, not a cursor — page replaces the cursor this route used before. All parameters are optional.

ParameterMeaning
page1-based page number. Default 1.
limitPage size. Default 50, max 200.
fromInclusive YYYY-MM-DD — the first calendar day to include, read in timeZone.
toInclusive YYYY-MM-DD — the last calendar day to include, read in timeZone.
timeZoneAn 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.
eventTypeOne of given, updated, withdrawn.
outcomeOne 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:

FieldMeaning
recordsEach event: { id, anonymousId, decisions, eventType, outcome, configVersion, occurredAt } — never any account identity.
pageThe 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.
pageSizeThe limit actually applied.
totalMatching record count, capped at 10,000 — see totalIsCapped.
totalIsCappedtrue 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.

CodeMeaning
invalid_colorA colour field is not a #rgb/#rrggbb hex value.
contrast_text_on_surfaceBody text is under 4.5:1 against the panel background (WCAG 2.2 AA, 1.4.3).
contrast_muted_on_surfaceMuted text — category descriptions — is under 4.5:1 against the panel.
contrast_border_on_surfaceThe border is under 3:1 against the panel (1.4.11). Borders are what distinguish the secondary buttons.
contrast_accent_on_surfaceThe accent is under 3:1 against the panel, so the focus ring and primary fill would not be distinguishable.
contrast_label_on_accentNeither black nor white reaches 4.5:1 on the accent, so no readable label colour exists for it.
invalid_logo_urlThe logo URL is malformed or not HTTPS.
logo_not_an_imageThe 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_urlA 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_localeA language tag does not match /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.
default_language_not_publisheddefaultLanguage is not in publishedLanguages.
cannot_remove_default_languageThe default language was dropped from publishedLanguages. Change the default first.
locale_incompleteReturned 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)

RoutePurpose
POST .../tokensCreate a scoped token (org admin only). The raw token is returned once in this response and never retrievable again.
GET .../tokensList tokens (hashed — never the raw value) with name, scopes, expiry, revocation status.
DELETE .../tokens/:tokenIdRevoke a token immediately.

Scopes: sites:read, sites:write, sites:publish.

Automation — /v1/agent (setup token)

RouteRequired scopePurpose
GET /v1/agent/meany valid tokenThe token's organization, site binding (if any), and scopes.
GET /v1/agent/sites/:siteIdsites:readSame site payload as the session route, scoped to this token's organization/site.
POST /v1/agent/sites/:siteId/banner-copysites:writeUpdate draft banner copy.
POST /v1/agent/sites/:siteId/google-consent-modesites:writeUpdate the draft's Google Consent Mode v2 mapping.
POST /v1/agent/sites/:siteId/google-analyticssites:writeUpdate the draft's Google Analytics settings: { enabled, measurementId, consentMode, categoryId }.
POST /v1/agent/sites/:siteId/publishsites:publishPublish — 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.

RoutePurpose
GET /v1/public/config/:publicSiteKeyThe 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/:publicSiteKeyRecord 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.