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>.
POST /consenti/admin/v1/auth/login. Include it in the Authorization header of every subsequent request.| Method | Path | Description |
|---|---|---|
| POST | /auth/login | Authenticate (mode local only) — returns a JWT |
| GET | /auth/me | Get current authenticated user |
| POST | /auth/logout | Invalidate session |
| POST | /auth/refresh | Reissue a fresh token — extends the session |
| GET | /auth/oidc/authorize | Start OIDC authorization (PKCE) |
| GET | /auth/oidc/callback | OIDC redirect target — exchanges code for a JWT |
| GET | /auth/saml/metadata | SAML SP metadata XML |
| POST | /auth/saml/acs | SAML Assertion Consumer Service — returns a JWT |
| POST | /auth/totp/setup | Generate a TOTP secret + QR code for the current user |
| POST | /auth/totp/verify | Verify a TOTP code and enable it |
| POST | /auth/totp/disable | Disable TOTP for the current user |
| GET | /profiles | List all profiles |
| GET | /profiles?summary=1 | List profiles as ProfileSummary[] (lightweight, with template names) |
| POST | /profiles | Create a profile — 422 on compliance errors; conflict detection on active group |
| GET | /profiles/:id | Get a profile |
| PUT | /profiles/:id | Update a profile — stable id, increments version in place |
| DELETE | /profiles/:id | Delete a profile — removes the DB row only; on-disk version snapshots remain (see Archived Profiles) |
| POST | /profiles/:id/activate | Activate a profile — writes locale JSONs to compliance group directory |
| POST | /profiles/:id/deactivate | Deactivate a profile — removes compliance group locale files |
| GET | /profiles/archived | List profile-id directories on disk with no matching DB row (deleted profiles) — id, version count, last-modified |
| GET | /profiles/:id/versions | List every saved version of this profile (newest first) — works for archived ids too |
| GET | /profiles/:id/versions/:entryId | Read a specific version's locale file — works for archived ids too |
| POST | /profiles/validate | Validate cookies + categories against a compliance group (no save) |
| GET | /compliance-coverage | Active profile per compliance group |
| GET | /consent-templates | List consent templates |
| GET | /consent-templates/:id | Get a consent template |
| POST | /consent-templates | Create a consent template |
| PUT | /consent-templates/:id | Update a consent template |
| DELETE | /consent-templates/:id | Delete a consent template — 422 if active profiles use it |
| POST | /consent-templates/:id/copy | Duplicate a consent template |
| GET | /consent-templates/:id/profile-usage | List profiles using this template |
| GET | /ui-templates | List UI templates |
| GET | /ui-templates/:id | Get a UI template |
| POST | /ui-templates | Create a UI template |
| PUT | /ui-templates/:id | Update a UI template |
| DELETE | /ui-templates/:id | Delete a UI template |
| POST | /ui-templates/:id/copy | Duplicate a UI template |
| GET | /ui-templates/:id/profile-usage | List profiles using this template |
| GET | /analytics/opt-in | Opt-in rate stats by locale and date |
| GET | /consents | List consent records (paginated) |
| GET | /consents/:visitorId | Get consent record for a visitor |
| GET | /consents/:visitorId/history | Get consent change history for a visitor |
| GET | /visitors | List visitor records (paginated) |
| GET | /users | List admin users |
| GET | /users/:id | Get an admin user |
| POST | /users | Create an admin user |
| PUT | /users/:id | Update an admin user (including allowedTenants) |
| DELETE | /users/:id | Delete an admin user |
| POST | /users/:id/roles | Assign a role to a user |
| DELETE | /users/:id/roles/:roleId | Revoke a role from a user |
| GET | /roles | List roles |
| POST | /roles | Create a role |
| PUT | /roles/:id | Update a role |
| DELETE | /roles/:id | Delete a role |
| GET | /roles/:id/permissions | Get permissions assigned to a role |
| POST | /roles/:id/permissions | Assign a permission to a role |
| DELETE | /roles/:id/permissions/:permId | Revoke a permission from a role |
| GET | /permissions | List all available permissions |
| GET | /apikeys | List API keys |
| POST | /apikeys | Create an API key |
| DELETE | /apikeys/:id | Revoke an API key |
| POST | /apikeys/:id/reactivate | Re-enable a revoked API key |
| DELETE | /apikeys/:id/permanent | Permanently delete an API key |
| GET | /settings | Get tenant-wide dashboard settings |
| PATCH | /settings | Update tenant-wide dashboard settings |
| GET | /setup/status | Whether the first-run setup wizard is complete |
| GET | /setup/config | Resolved server config (secrets redacted) + readiness flags |
| GET | /setup/compliance-groups | The 8 built-in compliance groups with metadata |
| POST | /setup/seed-profiles | Seed default profiles for the given compliance groups |
| POST | /setup/complete | Mark the first-run setup wizard complete |
| GET | /audit | Get audit log (paginated) |
| GET | /stats/overview | Consent overview statistics |
| GET | /stats/timeline | Daily consent counts |
| GET | /stats/categories | Per-category acceptance rates |
| GET | /stats/countries | Consents by country |
| GET | /stats/gpc | GPC detection statistics |
| GET | /export/consents | Export consent records (CSV or JSON) |
| GET | /export/consents/xlsx | Export consent records as XLSX |
| GET | /export/audit | Export audit log (CSV or JSON) |
| GET | /export/translations/:profileId | Export all translatable fields as CSV |
| GET | /tenants | List tenants (multi-tenant mode) |
| POST | /tenants | Create a tenant |
| PUT | /tenants/:id | Update a tenant |
| DELETE | /tenants/:id | Delete a tenant |
| GET | /tcf/vendors | List IAB TCF vendors |
| GET | /tcf/purposes | List IAB TCF purposes |
| GET | /tcf/registration-status | TCF registration confirmation status + live IAB CMP-List lookup |
| POST | /tcf/confirm-registration | Confirm TCF cmpId/cmpVersion registration |
| GET | /gpp/registration-status | GPP registration confirmation status |
| POST | /gpp/confirm-registration | Confirm 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.
| Method | Path | Description |
|---|---|---|
| GET | /auth/oidc/authorize | Starts OIDC authorization (PKCE) — 302-redirects to the IdP |
| GET | /auth/oidc/callback | OIDC redirect target — exchanges the code, verifies the ID token, returns a JWT |
| GET | /auth/saml/metadata | SAML SP metadata XML for your IdP configuration |
| POST | /auth/saml/acs | SAML 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.
| Method | Path | Description |
|---|---|---|
| POST | /auth/totp/setup | Generates a TOTP secret + QR-code URL for the current user |
| POST | /auth/totp/verify | Verifies a submitted code and enables TOTP for the current user |
| POST | /auth/totp/disable | Disables 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
}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)" } // optionalGET /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 param | Description |
|---|---|
tenantId | Filter by tenant (default: all). |
profileId | Filter by profile ID. |
complianceGroup | Filter by compliance group. |
from | Start date (ISO 8601, e.g. 2026-01-01). |
to | End date (ISO 8601). |
locale | Filter 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, sourceGET /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, countryGET /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
}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).
allowedOriginsis the fallbackPOST /consenti/api/v1/consent's origin check uses when the specific profile being submitted to doesn't set its ownprofileJson.allowedOrigins(profile-level always takes precedence when present). The public API has no auth token, so this is its only access gate.adminAllowedOriginsis an additional CORS-layer check on top of Bearer-token auth for browser-originated/consenti/admin/v1/*requests (server-to-server callers without anOriginheader 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-31GET /export/audit
// GET /consenti/admin/v1/export/audit HTTP/1.1
// Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
// ?format=csv&from=2026-01-01&to=2026-12-31GET /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)
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.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.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 }