Consenti

Public API Routes

Base path: /consenti/api/v1 (default — set via basePath in createConsenti()). No authentication required.

💡Explore and test all routes interactively in the Swagger UI.
MethodPathDescription
GET/profiles/:tenantId/:complianceGroup/:localeHot-serve — serve locale JSON directly from disk (zero DB)
GET/profiles/:tenantId/:profileId/:entryId/:localeServe a specific archived edit's locale JSON (dashboard preview)
GET/resolve-profileGeo-resolve compliance group, return file path + locale
GET/profiles/:idGet profile by ID (DB-backed, default locale)
GET/profiles/:id/:localeGet profile by ID and locale (DB-backed)
GET/profiles/auto/:localeLegacy: geo-resolve and return matching active profile
POST/consentSubmit a consent record
GET/consent/:visitorIdGet current consent for a visitor
PUT/consent/:visitorIdUpdate consent for a visitor
DELETE/consent/:visitorIdGDPR erasure — delete all data for visitor
GET/consent/:visitorId/verifyVerify consent is still valid

Static profile file serving (hot path)

The primary way to serve profiles at scale. Locale JSON files are written to disk when a profile is activated in the dashboard. These routes serve them directly from the filesystem — no DB query on the hot path.

GET /profiles/:tenantId/:complianceGroup/:locale

Serves the active locale JSON for a compliance group from ${storage.path}/profiles/${tenantId}/${complianceGroup}/${locale}.json. Returns Cache-Control: public, max-age=3600, stale-while-revalidate=60 — safe for CDN caching.

If the requested locale file does not exist, responds with 303 See Other to .../default, which serves default.json (the defaultLocale content).

// GET /consenti/api/v1/profiles/default/opt-in/en HTTP/1.1

GET /profiles/:tenantId/:profileId/:entryId/:locale

Serves the locale JSON for one version snapshot — used by the dashboard edit-history viewer. These files are written once and never modified. :profileIdis the profile's stable id; :entryId is the version number (from GET /admin/profiles/:id/versions).

// GET /consenti/api/v1/profiles/default/prof-a1b2c3/2/fr-FR HTTP/1.1

Resolve profile

GET /resolve-profile

Used by widgets configured with compliance.type: 'auto'. Call this once on page load to discover which compliance group and locale file the visitor should receive. The widget then fetches that file directly — zero subsequent DB queries.

Geo resolution uses the geoDataProvider configured in createConsenti(). The returned path is always a path on this server (/consenti/api/v1/profiles/...), even when S3 sync is enabled — see the s3Api config docs if you want an external CDN or edge function to serve straight from S3 instead.

Query paramRequiredDescription
dataPreferredBase64-encoded JSON with keys timezone, languages, language, locale. Sent automatically by the widget via encodeGeoHints().
tzLegacyIANA timezone string (e.g. Europe/Paris). Used when data is absent.
langLegacyAccept-Language value (e.g. fr-FR,fr;q=0.9).
localeLegacyPreferred BCP 47 locale (e.g. fr-FR).
// Widget encodes automatically — equivalent manual call:
const data = btoa(JSON.stringify({
  timezone: 'Europe/Paris',
  languages: ['fr-FR', 'fr'],
  language: 'fr-FR',
  locale: 'fr-FR',
}))
fetch(`/consenti/api/v1/resolve-profile?data=${encodeURIComponent(data)}`)
ℹ️When found is false, path is null — the tenant has no active profile for this compliance group yet. The widget automatically falls back to the embedded profile for the resolved complianceGroup, so no 404 is shown to the visitor.

DB-backed profile routes

These routes query the database on each request. Use them for server-side rendering or when you need a fully resolved profile object including the translations map. For high-traffic public-facing widget use, prefer the static file serving routes above.

ℹ️There is no public list endpoint — clients must know the profile ID (copy it from the admin dashboard or pass it from your backend).

Both endpoints return the same response shape. The only difference is which locale is resolved: /profiles/:id uses the profile's defaultLocale, while /profiles/:id/:locale uses the requested locale (falling back to defaultLocale if no translation exists).

Response shape

Both endpoints return a fully-resolved profile — banner text, modal copy, and button labels are already merged for the active locale. There is no raw translations map.

FieldTypeDescription
idstringProfile UUID — changes on every edit, so it alone signals a stale consent (no separate version field)
expiryDays?numberDays until consent expires and the visitor is asked again. Default: 365
defaultLocalestringBCP 47 locale used as the translation fallback
currentLocalestringLocale whose content is present in this response
localesstring[]All locale keys defined on the profile
cookiesCookieMapParameters managed by this profile, keyed by id
mainBannerMainBannerBanner config resolved for currentLocale
preferenceModalPreferenceModalModal config resolved for currentLocale
gpcBanner?GpcBannerGPC-specific banner variant (omitted if not configured)
resolvedComplianceGroup?ComplianceGroupIdPresent only on /profiles/auto/:locale — the geo-resolved compliance group

GET /profiles/:id

Returns the profile resolved in its defaultLocale. currentLocale will equal defaultLocale.

// GET /consenti/api/v1/profiles/my-profile-id HTTP/1.1

GET /profiles/:id/:locale

Same response shape as /profiles/:id, but currentLocale is set to :locale and all banner/modal strings are resolved for that locale. If the requested locale has no translation, the response falls back to defaultLocale (and currentLocale reflects the requested locale regardless).

// GET /consenti/api/v1/profiles/my-profile-id/fr HTTP/1.1
💡Use locales in the response to build a locale switcher — it lists every locale key the profile has translations for, so you can offer only the languages that are actually configured.

GET /profiles/auto/:locale Legacy

Legacy geo-resolution route — returns a fully resolved profile object from the database. Prefer GET /resolve-profile (above) for new integrations: it returns a file path so the widget fetches the locale JSON directly from disk or CDN, removing all DB overhead from the hot path.

Geo-resolves the visitor's compliance group from geo signals, finds the active profile tagged to that group, and returns it resolved for :locale. The response includes resolvedComplianceGroup. Falls back to the default profile when no active profile exists for the resolved group.

Requires compliance.type: 'auto' in createConsenti().

Query paramDescription
tzIANA timezone string (e.g. Europe/Paris). Used together with lang by the default (geoDataProvider: 'default') timezone+language heuristic.
langAccept-Language header value (e.g. fr-FR,fr;q=0.9). Used together with tz by the default timezone+language heuristic.
// GET /consenti/api/v1/profiles/auto/en?tz=Europe/Paris HTTP/1.1
ℹ️The 3-step jurisdiction lookup: (1) country + region override (e.g. California → CPRA), (2) country base group (e.g. US → opt-out), (3) country default (most strict) when region is unknown (US unknown state → opt-out-strict).

Consent

Consent records are keyed by visitorId. The visitor ID is persisted in a cookie (consenti_vid) set by the widget after the first submission.

POST /consent

Creates or updates a consent record. If visitorId is omitted, the server generates one and sets it as a cookie in the response.

// POST /consenti/api/v1/consent HTTP/1.1
// Content-Type: application/json

{
  "profileId": "my-profile-id",  // required
  "consentJson": {               // required
    "necessary": "granted",
    "analytics": "granted",
    "marketing": "denied"
  },
  "visitorId": "uuid-v4",        // optional — generated if omitted
  "locale": "en",                // optional — defaults to "en"
  "gpcDetected": false,          // optional — defaults to false
  "source": "banner",            // optional — "banner" | "api" | "import"
  "ageVerified": true,           // optional — set by the widget's age gate, see UI Events
  "parentalConsentToken": "pcon_..." // optional — set when age gate requires parental consent
}

GET /consent/:visitorId

// GET /consenti/api/v1/consent/visitor-uuid HTTP/1.1

PUT /consent/:visitorId

Updates an existing consent record. Only the supplied fields are applied.

// PUT /consenti/api/v1/consent/visitor-uuid HTTP/1.1
// Content-Type: application/json

{
  "consentJson": {          // required
    "necessary": "granted",
    "analytics": "denied",
    "marketing": "denied"
  },
  "locale": "fr",           // optional
  "gpcDetected": true       // optional
}

DELETE /consent/:visitorId

GDPR right-to-erasure. Deletes the consent record, consent history, and visitor record for the given visitor ID. Also expires the consent cookie in the response.

// DELETE /consenti/api/v1/consent/visitor-uuid HTTP/1.1

GET /consent/:visitorId/verify

Checks whether the stored consent record is still valid — i.e. the consent's profileIdstill matches the compliance group's currently active profile id, and the record has not expired. The UI widget calls this on page load to decide whether to re-show the banner.

// GET /consenti/api/v1/consent/visitor-uuid/verify HTTP/1.1
💡The widget automatically calls POST /consent after the visitor accepts, then calls GET /consent/:visitorId/verify on subsequent page loads to skip the banner if consent is still valid.

POST /consent/:visitorId/parental-consent-request

Stateless parental-consent hook — issues a token (signed with compliance.dataSigningHash, auto-generated in memory if not set) that a host's own eventBus.on('consent.parentalConsentRequired', ...) listener can use to email/notify the parent. Requires the same visitor-ownership cookie as the other visitor-scoped routes above.

// POST /consenti/api/v1/consent/visitor-uuid/parental-consent-request HTTP/1.1
{ "profileId": "my-profile-id" }

POST /consent/parental-consent-resolve

Recovers visitorId/profileId from a token issued by the route above and emits consent.parentalConsentGranted— your own backend listener decides what "granted" means for stored consent. No visitor-ownership cookie required (recovered from the verified token instead), since this is typically called from the parent's own device.

// POST /consenti/api/v1/consent/parental-consent-resolve HTTP/1.1
{ "token": "pcon_eyJ2aXNpdG9ySWQiOi...ab12cd34..." }
⚠️Stateless by design — token replay isn't prevented (no persistence to mark a token "used"). compliance.dataSigningHash is auto-generated in memory if unset, so tokens are always signed — but with an ephemeral, unpersisted key unless set explicitly, meaning a server restart between issuing and resolving a token invalidates it. This mechanism is plumbing for your own out-of-band verification, not by itself sufficient verifiable parental consent under COPPA or similar regimes — see the @consenti/apiREADME's "Parental consent request/resolve" section for the full design.