Public API Routes
Base path: /consenti/api/v1 (default — set via basePath in createConsenti()). No authentication required.
| Method | Path | Description |
|---|---|---|
| GET | /profiles/:tenantId/:complianceGroup/:locale | Hot-serve — serve locale JSON directly from disk (zero DB) |
| GET | /profiles/:tenantId/:profileId/:entryId/:locale | Serve a specific archived edit's locale JSON (dashboard preview) |
| GET | /resolve-profile | Geo-resolve compliance group, return file path + locale |
| GET | /profiles/:id | Get profile by ID (DB-backed, default locale) |
| GET | /profiles/:id/:locale | Get profile by ID and locale (DB-backed) |
| GET | /profiles/auto/:locale | Legacy: geo-resolve and return matching active profile |
| POST | /consent | Submit a consent record |
| GET | /consent/:visitorId | Get current consent for a visitor |
| PUT | /consent/:visitorId | Update consent for a visitor |
| DELETE | /consent/:visitorId | GDPR erasure — delete all data for visitor |
| GET | /consent/:visitorId/verify | Verify 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.1GET /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.1Resolve 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 param | Required | Description |
|---|---|---|
data | Preferred | Base64-encoded JSON with keys timezone, languages, language, locale. Sent automatically by the widget via encodeGeoHints(). |
tz | Legacy | IANA timezone string (e.g. Europe/Paris). Used when data is absent. |
lang | Legacy | Accept-Language value (e.g. fr-FR,fr;q=0.9). |
locale | Legacy | Preferred 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)}`)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.
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.
| Field | Type | Description |
|---|---|---|
| id | string | Profile UUID — changes on every edit, so it alone signals a stale consent (no separate version field) |
| expiryDays? | number | Days until consent expires and the visitor is asked again. Default: 365 |
| defaultLocale | string | BCP 47 locale used as the translation fallback |
| currentLocale | string | Locale whose content is present in this response |
| locales | string[] | All locale keys defined on the profile |
| cookies | CookieMap | Parameters managed by this profile, keyed by id |
| mainBanner | MainBanner | Banner config resolved for currentLocale |
| preferenceModal | PreferenceModal | Modal config resolved for currentLocale |
| gpcBanner? | GpcBanner | GPC-specific banner variant (omitted if not configured) |
| resolvedComplianceGroup? | ComplianceGroupId | Present 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.1GET /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.1locales 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 param | Description |
|---|---|
tz | IANA timezone string (e.g. Europe/Paris). Used together with lang by the default (geoDataProvider: 'default') timezone+language heuristic. |
lang | Accept-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.1Consent
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.1PUT /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.1GET /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.1POST /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..." }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.