Consenti

Admin API Routes

Base path: /consenti/admin/v1 (default — set via basePath in createConsenti()). All routes require a valid JWT passed as Authorization: Bearer <token>.

ℹ️Obtain a token via POST /consenti/admin/v1/auth/login. Include it in the Authorization header of every subsequent request.
💡Explore and test all routes interactively in the Swagger UI.
MethodPathDescription
POST/auth/loginAuthenticate (mode local only) — returns a JWT
GET/auth/meGet current authenticated user
POST/auth/logoutInvalidate session
POST/auth/refreshReissue a fresh token — extends the session
GET/auth/oidc/authorizeStart OIDC authorization (PKCE)
GET/auth/oidc/callbackOIDC redirect target — exchanges code for a JWT
GET/auth/saml/metadataSAML SP metadata XML
POST/auth/saml/acsSAML Assertion Consumer Service — returns a JWT
POST/auth/totp/setupGenerate a TOTP secret + QR code for the current user
POST/auth/totp/verifyVerify a TOTP code and enable it
POST/auth/totp/disableDisable TOTP for the current user
GET/profilesList all profiles
GET/profiles?summary=1List profiles as ProfileSummary[] (lightweight, with template names)
POST/profilesCreate a profile — 422 on compliance errors; conflict detection on active group
GET/profiles/:idGet a profile
PUT/profiles/:idUpdate a profile — stable id, increments version in place
DELETE/profiles/:idDelete a profile — removes the DB row only; on-disk version snapshots remain (see Archived Profiles)
POST/profiles/:id/activateActivate a profile — writes locale JSONs to compliance group directory
POST/profiles/:id/deactivateDeactivate a profile — removes compliance group locale files
GET/profiles/archivedList profile-id directories on disk with no matching DB row (deleted profiles) — id, version count, last-modified
GET/profiles/:id/versionsList every saved version of this profile (newest first) — works for archived ids too
GET/profiles/:id/versions/:entryIdRead a specific version's locale file — works for archived ids too
POST/profiles/validateValidate cookies + categories against a compliance group (no save)
GET/compliance-coverageActive profile per compliance group
GET/consent-templatesList consent templates
GET/consent-templates/:idGet a consent template
POST/consent-templatesCreate a consent template
PUT/consent-templates/:idUpdate a consent template
DELETE/consent-templates/:idDelete a consent template — 422 if active profiles use it
POST/consent-templates/:id/copyDuplicate a consent template
GET/consent-templates/:id/profile-usageList profiles using this template
GET/ui-templatesList UI templates
GET/ui-templates/:idGet a UI template
POST/ui-templatesCreate a UI template
PUT/ui-templates/:idUpdate a UI template
DELETE/ui-templates/:idDelete a UI template
POST/ui-templates/:id/copyDuplicate a UI template
GET/ui-templates/:id/profile-usageList profiles using this template
GET/analytics/opt-inOpt-in rate stats by locale and date
GET/consentsList consent records (paginated)
GET/consents/:visitorIdGet consent record for a visitor
GET/consents/:visitorId/historyGet consent change history for a visitor
GET/visitorsList visitor records (paginated)
GET/usersList admin users
GET/users/:idGet an admin user
POST/usersCreate an admin user
PUT/users/:idUpdate an admin user (including allowedTenants)
DELETE/users/:idDelete an admin user
POST/users/:id/rolesAssign a role to a user
DELETE/users/:id/roles/:roleIdRevoke a role from a user
GET/rolesList roles
POST/rolesCreate a role
PUT/roles/:idUpdate a role
DELETE/roles/:idDelete a role
GET/roles/:id/permissionsGet permissions assigned to a role
POST/roles/:id/permissionsAssign a permission to a role
DELETE/roles/:id/permissions/:permIdRevoke a permission from a role
GET/permissionsList all available permissions
GET/apikeysList API keys
POST/apikeysCreate an API key
DELETE/apikeys/:idRevoke an API key
POST/apikeys/:id/reactivateRe-enable a revoked API key
DELETE/apikeys/:id/permanentPermanently delete an API key
GET/settingsGet tenant-wide dashboard settings
PATCH/settingsUpdate tenant-wide dashboard settings
GET/setup/statusWhether the first-run setup wizard is complete
GET/setup/configResolved server config (secrets redacted) + readiness flags
GET/setup/compliance-groupsThe 8 built-in compliance groups with metadata
POST/setup/seed-profilesSeed default profiles for the given compliance groups
POST/setup/completeMark the first-run setup wizard complete
GET/auditGet audit log (paginated)
GET/stats/overviewConsent overview statistics
GET/stats/timelineDaily consent counts
GET/stats/categoriesPer-category acceptance rates
GET/stats/countriesConsents by country
GET/stats/gpcGPC detection statistics
GET/export/consentsExport consent records (CSV or JSON)
GET/export/consents/xlsxExport consent records as XLSX
GET/export/auditExport audit log (CSV or JSON)
GET/export/translations/:profileIdExport all translatable fields as CSV
GET/tenantsList tenants (multi-tenant mode)
POST/tenantsCreate a tenant
PUT/tenants/:idUpdate a tenant
DELETE/tenants/:idDelete a tenant
GET/tcf/vendorsList IAB TCF vendors
GET/tcf/purposesList IAB TCF purposes
GET/tcf/registration-statusTCF registration confirmation status + live IAB CMP-List lookup
POST/tcf/confirm-registrationConfirm TCF cmpId/cmpVersion registration
GET/gpp/registration-statusGPP registration confirmation status
POST/gpp/confirm-registrationConfirm GPP cmpId/cmpVersion registration

Auth

POST /auth/login

Authenticates with email and password. Returns a JWT for use in all subsequent admin requests.

// POST /consenti/admin/v1/auth/login HTTP/1.1
// Content-Type: application/json

{
  "email": "[email protected]",
  "password": "your-password"
}

GET /auth/me

// GET /consenti/admin/v1/auth/me HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /auth/logout

// POST /consenti/admin/v1/auth/logout HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /auth/refresh

Reissues a fresh token from the current (still-valid) one, extending the session another 30 minutes — the dashboard calls this on user activity to implement a sliding inactivity timeout rather than a flat expiry from login. 401s the same as any other authenticated route if the current token is already invalid/expired.

// POST /consenti/admin/v1/auth/refresh HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

SSO — OIDC / SAML

Active only when auth.mode is 'oidc' or 'saml' — see the Advanced Configurationguide's Auth section for the full setup. Both flows upsert (create-if-missing) an admin user by email on first successful login.

MethodPathDescription
GET/auth/oidc/authorizeStarts OIDC authorization (PKCE) — 302-redirects to the IdP
GET/auth/oidc/callbackOIDC redirect target — exchanges the code, verifies the ID token, returns a JWT
GET/auth/saml/metadataSAML SP metadata XML for your IdP configuration
POST/auth/saml/acsSAML Assertion Consumer Service — validates the assertion, returns a JWT

TOTP (per-user MFA)

Each admin user can independently enable TOTP on top of whichever auth.mode is active. Opt-in per user — not currently enforced as a required second factor on POST /auth/login itself.

MethodPathDescription
POST/auth/totp/setupGenerates a TOTP secret + QR-code URL for the current user
POST/auth/totp/verifyVerifies a submitted code and enables TOTP for the current user
POST/auth/totp/disableDisables TOTP for the current user

Profiles

GET /profiles

// GET /consenti/admin/v1/profiles HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /profiles

Creates a profile. When the profileJson.complianceGroup field is set, Consenti runs server-side compliance validation before saving. Returns 422 if there are blocking errors, or if any locale is missing mandatory content (body text, button labels, modal heading, category headings — see Mandatory content validation below); returns 201 with a warnings array for soft warnings.

profileJson.mainBanner/gpcBanner/preferenceModal hold the default locale'sresolved content only — this is what's stored in the DB row. Every other locale listed in profileJson.locales is submitted via a sibling localeContent field ({ [locale]: { mainBanner, gpcBanner?, preferenceModal } }) and is written directly to that locale's on-disk version file — never persisted in the DB row. This keeps the row small regardless of how many locales a profile has.

// POST /consenti/admin/v1/profiles HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "GDPR Profile",         // required
  "defaultLocale": "en",          // required
  "profileJson": {                // required
    "complianceGroup": "opt-in",  // optional — enables compliance validation + geo-routing
    "isActive": true,             // optional — marks this profile active for the complianceGroup
    "gpcMode": "ignore",          // optional — "ignore" | "honor" | "strict"
    "expiryDays": 365,            // optional — profile-wide consent expiry, default 365
    "enhanceAccessibility": true, // optional — 44px buttons, 3px focus ring, WCAG 2.1 AA
    "showFooterMetadata": true,   // optional — shows Consent ID, Date, Privacy Settings link in banner/modal footer
    "locales": ["en", "fr"],      // every locale this profile has content for
    "cookies": {
      "necessary": {},
      "analytics": { "listenGpc": true }
    },
    "mainBanner": {                // defaultLocale ("en") content only — stored on the row
      "position": "bottom",
      "heading": "We use cookies",
      "htmlText": "This site uses cookies to improve your experience.",
      "buttons": {
        "accept-all": { "text": "Accept All", "style": "primary", "action": "custom", "cookies": "*" }
      }
    },
    "preferenceModal": {
      "heading": "Cookie Preferences",
      "buttons": {
        "save-preferences": { "text": "Save Preferences", "style": "primary", "action": "submit" }
      },
      "categories": {
        "necessary": { "heading": "Necessary", "htmlText": "Required.", "legalBasis": "mandatory", "cookies": ["necessary"] },
        "analytics": { "heading": "Analytics", "htmlText": "Usage stats.", "legalBasis": "consent", "cookies": ["analytics"] }
      }
    }
  },
  "localeContent": {               // every OTHER locale in profileJson.locales — written to disk, never stored in the DB row
    "fr": {
      "mainBanner": {
        "position": "bottom",
        "heading": "Nous utilisons des cookies",
        "htmlText": "Ce site utilise des cookies pour améliorer votre expérience.",
        "buttons": {
          "accept-all": { "text": "Tout accepter", "style": "primary", "action": "custom", "cookies": "*" }
        }
      },
      "preferenceModal": {
        "heading": "Préférences de cookies",
        "buttons": {
          "save-preferences": { "text": "Enregistrer", "style": "primary", "action": "submit" }
        },
        "categories": {
          "necessary": { "heading": "Nécessaires", "htmlText": "Requis.", "legalBasis": "mandatory", "cookies": ["necessary"] },
          "analytics": { "heading": "Analytique", "htmlText": "Statistiques d'utilisation.", "legalBasis": "consent", "cookies": ["analytics"] }
        }
      }
    }
  }
}

Mandatory content validation

Every locale submitted (the default locale on profileJson, plus each entry in localeContent) must have non-blank body text on the main/GPC banner, a non-blank label on every button, a non-blank preference-modal heading, and a non-blank heading on every category. Banner/GPC heading and the modal's intro text are the only optional fields — the dashboard wizard nudges but never blocks on those. A failing save returns 422 with which locale/section/field is blank (see the response tab above) instead of silently accepting a blank banner.

GET /profiles/:id

// GET /consenti/admin/v1/profiles/my-profile HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
ℹ️profileJson only ever holds defaultLocale's (here, en's) content directly — fr's content isn't included in this response at all, even though it's listed in locales. It lives only in that locale's on-disk version file; fetch it with GET /profiles/:id/versions/:entryId?locale=fr (using the current version as :entryId).

PUT /profiles/:id

All fields are optional — only supplied fields are applied. id is stable — the row is mutated in place and version is incremented, so the response's id always matches the :id in the URL. A new resolved-JSON snapshot is written under the incremented version for audit/history purposes. Accepts the same sibling localeContent field as POST /profiles; any locale already on the profile but not included in this request's localeContent is carried forward unchanged from the previous version, not dropped.

// PUT /consenti/admin/v1/profiles/prof-a1b2c3 HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "Updated Profile Name",   // optional
  "defaultLocale": "fr",            // optional
  "profileJson": { ... }            // optional — replaces entire profileJson
}
ℹ️When profileJson.complianceGroup is set, the response is instead wrapped as { profile, warnings } — same shape as POST /profiles.

DELETE /profiles/:id

// DELETE /consenti/admin/v1/profiles/my-profile HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /profiles/:id/copy

Duplicates a profile as a brand new, always-inactive profile — own id, version resets to 1. Optional { name } body; defaults to Copy of {name}. Copying an active profile does not deactivate the original or touch its compliance group — only the new copy's isActive is forced false.

// POST /consenti/admin/v1/profiles/prof-a1b2c3/copy HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{ "name": "GDPR Profile (draft)" }   // optional

GET /profiles?summary=1

Returns a lightweight ProfileSummary[] — no blob, includes template names. Used by the dashboard profile list view. Without ?summary=1, the full profile including profileJson blob is returned.

// GET /consenti/admin/v1/profiles?summary=1 HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /profiles/:id/activate

Activates a profile for its complianceGroup. Consenti copies locale JSON files from ${profileId}/${version}/ to ${complianceGroup}/ — the static hot-serve path. Only one profile per compliance group can be active at a time; activating a new one deactivates the previous one automatically.

// POST /consenti/admin/v1/profiles/prof-a1b2c3/activate HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /profiles/:id/deactivate

Deactivates a profile. Removes ${complianceGroup}/ locale files from disk so the profile is no longer served on the hot path. The version snapshot under ${profileId}/${version}/ is preserved for history/audit.

// POST /consenti/admin/v1/profiles/prof-a1b2c3/deactivate HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /profiles/archived

Lists profile-id directories on disk with no matching DB row — profiles DELETE removed the row for, but whose version-snapshot tree it never touches. Directory-listing only: no file content is read building this list, so it's cheap even with a large history. lastModifiedis the newest version directory's mtime.

// GET /consenti/admin/v1/profiles/archived HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /profiles/:id/versions

Returns every version snapshot of this profile on disk, newest first — read straight from the version-directory tree, no DB query. version is a plain incrementing integer, matching Profile.version at the point that snapshot was written. Works for archived ids too — it never actually depended on the DB row existing.

// GET /consenti/admin/v1/profiles/prof-a1b2c3/versions HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /profiles/:id/versions/:entryId

Reads the resolved locale JSON for one entry from versions above (entryId is that entry's version number). Accepts an optional ?locale=en query param; falls back to default.json when omitted. Returns the raw JSON file content — not wrapped in an envelope.

// GET /consenti/admin/v1/profiles/prof-a1b2c3/versions/2?locale=fr-FR HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Profile save conflict detection

Only one profile per complianceGroup can be active at a time. When creating or updating a profile with isActive: true and another active profile already exists for the same compliance group, the backend returns 200 with:

{
  "conflict": { "id": "existing-uuid", "name": "GDPR Profile v1" },
  "requiresChoice": true
}

POST /profiles/validate

Validates a set of cookies against a compliance group without saving anything. Returns the same errors / warnings structure that POST /profiles uses server-side. The dashboard wizard calls this at step 2 for a server-round-trip confirmation before allowing the operator to proceed.

// POST /consenti/admin/v1/profiles/validate HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "complianceGroup": "opt-out-strict",   // required — ComplianceGroupId
  "cookies": {                            // required — CookieMap from the linked Consent Template
    "necessary": { "purpose": "necessary" },
    "analytics": { "purpose": "analytics", "listenGpc": false }
  },
  "categories": {                         // required — CategoryMap from the linked Consent Template
    "necessary": { "heading": "Necessary", "htmlText": "Required.", "legalBasis": "mandatory", "cookies": ["necessary"] },
    "analytics": { "heading": "Analytics", "htmlText": "Usage stats.", "legalBasis": "consent", "cookies": ["analytics"] }
  }
}

GET /compliance-coverage

Returns one entry per compliance group showing which profile is currently active for that group. Used by the dashboard's compliance coverage panel to surface groups that have no active profile.

// GET /consenti/admin/v1/compliance-coverage HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Consent Templates

Reusable parameter definitions plus the categories that own their legal basis, edited together. A consent template is attached to a profile in the wizard; the profile inherits its cookies/categories for compliance validation and consent storage.

GET /consent-templates & /consent-templates/:id

[
  {
    "id": "template-uuid",
    "name": "Standard Consent Template",
    "tenantId": "default",
    "cookies": {
      "necessary": { "purpose": "necessary", "listenGpc": false },
      "analytics": { "purpose": "analytics", "listenGpc": true }
    },
    "categories": {
      "necessary": { "heading": "Necessary", "htmlText": "Required.", "legalBasis": "mandatory", "cookies": ["necessary"] },
      "analytics": { "heading": "Analytics", "htmlText": "Usage stats.", "legalBasis": "consent", "cookies": ["analytics"] }
    },
    "createdAt": "2026-06-01T00:00:00.000Z",
    "updatedAt": "2026-07-01T10:00:00.000Z"
  }
]

POST /consent-templates

New templates start blank — no prefilled cookies or categories. Use the dashboard Load Defaults button to populate a starter set, then customise.

Every parameter requires a purpose necessary | functional | preferences | analytics | marketing — and a boolean listenGpc. Legal basis is notset per-parameter — every parameter must be listed in exactly one category's cookiesarray, and that category's legalBasis applies. Requests violating any of this, or leaving a parameter in zero or multiple categories, are rejected with 400.

// POST /consenti/admin/v1/consent-templates HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "E-commerce Consent Template",
  "cookies": {
    "necessary": { "purpose": "necessary", "listenGpc": false },
    "analytics": { "purpose": "analytics", "listenGpc": true },
    "marketing": { "purpose": "marketing", "listenGpc": true, "cpraCategory": "sharing" }
  },
  "categories": {
    "necessary": { "heading": "Necessary", "htmlText": "Required for the site to function.", "legalBasis": "mandatory", "cookies": ["necessary"] },
    "analytics": { "heading": "Analytics", "htmlText": "Usage stats.", "legalBasis": "consent", "cookies": ["analytics"] },
    "marketing": { "heading": "Marketing", "htmlText": "Personalised ads.", "legalBasis": "consent", "cookies": ["marketing"] }
  }
}

Consent Template safety guards

Deletion guard: DELETE /consent-templates/:id returns 422 when active profiles reference the template. Deactivate those profiles first.

{
  "error": "Template is in use by active profiles",
  "activeProfiles": [
    { "id": "profile-uuid", "name": "GDPR Profile", "complianceGroup": "opt-in" }
  ]
}

Parameter removal guard:When updating a template and parameters are removed, the backend runs compliance validation across all profiles using the template. If any profile's compliance group requires a removed parameter, the save is blocked with 422:

{
  "error": "Removing these parameters breaks compliance for affected profiles",
  "blockingProfiles": [
    { "id": "profile-uuid", "name": "GDPR Profile", "complianceGroup": "opt-in" }
  ],
  "removedCookieIds": ["analytics"]
}

GET /consent-templates/:id/profile-usage

Returns all profiles (ProfileSummary[]) that use this template.

[
  {
    "id": "profile-uuid",
    "name": "GDPR Profile",
    "complianceGroup": "opt-in",
    "isActive": true
  }
]

UI Templates

Reusable banner and modal layout settings. A UI template is attached to a profile in the wizard; the profile inherits button arrays, position, and overlay settings.

New templates start blank. Use the dashboard Load Defaults amber callout to populate a sensible starter structure, then customise.

The same safety guards as consent templates apply: PUT and DELETE check profile usage and block destructive operations when active profiles would be affected. Use GET /ui-templates/:id/profile-usage to preview impact before saving. UI templates are visuals only — categories are owned by the Consent Template, not here.

Button id is a machine identifier (e.g. "accept-all"), not display text — UI templates never carry text. The visitor-facing label for each button is authored per-locale on the profile (matched to the template's buttons array by position) and shown as id (action)in the profile editor so authors know which button they're labeling.

{
  "id": "template-uuid",
  "name": "Default Banner",
  "mainBanner": {
    "position": "bottom",
    "overlayOpacity": 0.4,
    "showClose": false,
    "headingTag": "h2",
    "stackButtonsOnBreakpoint": 576,
    "buttons": {
      "accept-all": { "type": "primary", "action": "custom", "cookies": "*" },
      "reject-optional": { "type": "primary", "action": "custom", "cookies": "!" },
      "customize": { "type": "secondary", "action": "manage" }
    }
  },
  "preferenceModal": {
    "position": "center",
    "showClose": true,
    "persistent": false,
    "trapFocus": true,
    "buttons": {
      "accept-all": { "type": "primary", "action": "custom", "cookies": "*" },
      "save-preferences": { "type": "primary", "action": "submit" },
      "reject-optional": { "type": "text", "action": "custom", "cookies": "!" }
    }
  }
}

Analytics

GET /analytics/opt-in

Opt-in rate statistics aggregated by locale and date. All query params are optional.

Query paramDescription
tenantIdFilter by tenant (default: all).
profileIdFilter by profile ID.
complianceGroupFilter by compliance group.
fromStart date (ISO 8601, e.g. 2026-01-01).
toEnd date (ISO 8601).
localeFilter by BCP 47 locale.
// GET /consenti/admin/v1/analytics/opt-in?from=2026-01-01&to=2026-07-01 HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Consents

GET /consents

// GET /consenti/admin/v1/consents HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// Query params (all optional):
// ?page=1&limit=50&profileId=my-profile&from=2026-01-01&to=2026-12-31&q=search-term
// q searches across visitorId, profileId, locale, source

GET /consents/:visitorId

// GET /consenti/admin/v1/consents/visitor-uuid HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /consents/:visitorId/history

// GET /consenti/admin/v1/consents/visitor-uuid/history HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Visitors

GET /visitors

IPs are never stored raw — only a SHA-256 hash. The ipHash field is included for audit purposes only.

// GET /consenti/admin/v1/visitors HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// Query params (all optional):
// ?page=1&limit=50&from=2026-01-01&to=2026-12-31&q=search-term
// q searches across visitorId, country
ℹ️A proof-of-notice endpoint (GET /visitors/:visitorId/notice-shown) exists in the codebase but is disabled and not used by the widget — see Upcoming Features for why.

Users

GET /users

// GET /consenti/admin/v1/users HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /users

The optional allowedTenants array scopes the user to specific tenants — they can only view and manage data for those tenants. An empty array (or omitting the field) grants access to all tenants. Users with role superadmin always have access to all tenants regardless of this field.

// POST /consenti/admin/v1/users HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "Jane Smith",                           // required
  "email": "[email protected]",                    // required
  "password": "secure-password",                  // required
  "roleId": "role-uuid",                          // optional
  "allowedTenants": ["tenant-uuid-1", "tenant-uuid-2"]  // optional — empty = all tenants
}
ℹ️Tenant scoping enforcement: list routes (profiles, consents, visitors, audit) silently return empty results for out-of-scope tenants. Individual resource routes (GET /consents/:visitorId, etc.) return 403 Forbidden instead.

Roles

GET /roles

// GET /consenti/admin/v1/roles HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /roles

// POST /consenti/admin/v1/roles HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "editor",               // required
  "description": "Can edit profiles but not manage users"  // optional
}

GET /roles/:id/permissions

// GET /consenti/admin/v1/roles/role-uuid/permissions HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

API Keys

API keys allow server-to-server access to the public API without user credentials. The raw key is only returned on creation — store it immediately.

GET /apikeys

// GET /consenti/admin/v1/apikeys HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /apikeys

// POST /consenti/admin/v1/apikeys HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "Production Widget",       // required
  "expireBy": "2027-06-01T00:00:00.000Z"  // optional — omit for a key that never expires
}

DELETE /apikeys/:id

Revokes the key (soft — sets it inactive). Reversible with reactivate below.

// DELETE /consenti/admin/v1/apikeys/key-uuid HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /apikeys/:id/reactivate

Re-enables a previously revoked key — same hash, no new secret to distribute. Whatever system already has the old raw secret saved starts working again immediately. If the key had an expireBydate that has already passed, it's cleared so the key doesn't immediately re-expire.

// POST /consenti/admin/v1/apikeys/key-uuid/reactivate HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

DELETE /apikeys/:id/permanent

Permanently removes the key row. Unlike DELETE /apikeys/:id, this cannot be undone.

// DELETE /consenti/admin/v1/apikeys/key-uuid/permanent HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Settings

Tenant-wide dashboard settings — the Public and Admin API origin allowlists shown on the API Config page (split into two panels, one per API).

  • allowedOrigins is the fallback POST /consenti/api/v1/consent's origin check uses when the specific profile being submitted to doesn't set its own profileJson.allowedOrigins (profile-level always takes precedence when present). The public API has no auth token, so this is its only access gate.
  • adminAllowedOrigins is an additional CORS-layer check on top of Bearer-token auth for browser-originated /consenti/admin/v1/* requests (server-to-server callers without an Origin header are unaffected). Unauthenticated static assets (widget.js/widget.css) are exempt. Be careful: include the dashboard's own origin, or you'll lock yourself out of the dashboard along with everyone else.

Both lists accept full origins (https://example.com) or a wildcard subdomain pattern (*.example.com). Empty/unset on either means no restriction.

GET /settings

// GET /consenti/admin/v1/settings HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

PATCH /settings

Partial update — only send fields being changed.

// PATCH /consenti/admin/v1/settings HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{ "allowedOrigins": ["https://example.com", "https://foo.example.com"] }

Setup Wizard

Backs the dashboard's one-time first-run wizard (#/setup) — a 4-step welcome / resolved-config / default-profiles / confirmation flow shown once, the first time any admin logs in, then gated shut by tenant_settings.setup_completed(per tenant in multi-tenant mode). Never reset from the dashboard once complete — there is no "run it again" entry point. All routes require settings:update, same as Settings above.

GET /setup/status

// GET /consenti/admin/v1/setup/status HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /setup/config

The same merged DEFAULT_CONFIG + user config createConsenti computes at boot, with auth.adminPassword, auth.masterSecret, compliance.dataSigningHash, storage credentials, and OIDC/SAML secrets replaced with a redaction marker.

// GET /consenti/admin/v1/setup/config HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /setup/compliance-groups

// GET /consenti/admin/v1/setup/compliance-groups HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /setup/seed-profiles

Idempotent — skips any group that already has an active profile. groups must be a subset of the 8 built-in compliance group ids; a 400 is returned otherwise.

// POST /consenti/admin/v1/setup/seed-profiles HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{ "groups": ["opt-in", "opt-out", "notice-only"] }

POST /setup/complete

Called on both wizard finish and skip.

// POST /consenti/admin/v1/setup/complete HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Audit Log

GET /audit

// GET /consenti/admin/v1/audit HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// Query params (all optional):
// ?page=1&limit=50
// &action=profile:created
// &resourceType=profile
// &from=2026-01-01&to=2026-12-31
// &q=search-term (searches action, resourceType, resourceId, userId)

Stats

GET /stats/overview

// GET /consenti/admin/v1/stats/overview HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /stats/timeline

// GET /consenti/admin/v1/stats/timeline HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// ?days=30   (default: 30)

GET /stats/categories

// GET /consenti/admin/v1/stats/categories HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /stats/countries & /stats/gpc

// GET /consenti/admin/v1/stats/countries HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// GET /consenti/admin/v1/stats/gpc HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Export

GET /export/consents

Streams all consent records. Use format=json for JSON or omit for CSV (default). The XLSX endpoint (/export/consents/xlsx) requires the optional xlsx peer dependency.

// GET /consenti/admin/v1/export/consents HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// Query params (all optional):
// ?format=csv           — "csv" (default) | "json"
// &profileId=my-profile
// &from=2026-01-01
// &to=2026-12-31

GET /export/audit

// GET /consenti/admin/v1/export/audit HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

// ?format=csv&from=2026-01-01&to=2026-12-31

GET /export/translations/:profileId

Exports all translatable string fields for every locale defined on the profile as a CSV file — one row per locale. Useful for bulk-editing translations in a spreadsheet and re-importing them.

// GET /consenti/admin/v1/export/translations/my-profile HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Tenants (Multi-tenant)

ℹ️Tenant management is only active when multiTenant.enabled: true is set in createConsenti(). In single-tenant mode these routes still exist but operate on the implicit "default" tenant.

GET /tenants

// GET /consenti/admin/v1/tenants HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /tenants

// POST /consenti/admin/v1/tenants HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{
  "name": "Partner Site",  // required
  "slug": "partner-site"   // required — used in host-based tenant resolution
}

IAB TCF

ℹ️TCF routes are only active when tcf.enabled: true is set in createConsenti().

GET /tcf/vendors

// GET /consenti/admin/v1/tcf/vendors HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /tcf/purposes

// GET /consenti/admin/v1/tcf/purposes HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

GET /tcf/registration-status

Draft/confirmed diff plus a live IAB CMP-List lookup for cmpId (?refresh=truebypasses the 7-day cache) — powers the dashboard's TCF Registration panel.

// GET /consenti/admin/v1/tcf/registration-status HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /tcf/confirm-registration

Records confirmation as a hash of cmpId/cmpVersion/publisherCC (never the raw values). 409if IAB's CMP List shows cmpId deregistered, 404 if not found yet.

// POST /consenti/admin/v1/tcf/confirm-registration HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{ "acknowledge": true }

IAB GPP (US National)

ℹ️GPP routes are only active when gpp.enabled: true is set in createConsenti(). Unlike TCF, IAB publishes no CMP-List equivalent for GPP, so confirmation here is self-attestation only.

GET /gpp/registration-status

// GET /consenti/admin/v1/gpp/registration-status HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

POST /gpp/confirm-registration

// POST /consenti/admin/v1/gpp/confirm-registration HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// Content-Type: application/json

{ "acknowledge": true }