[C] Consentify

Widget Endpoints

Public API endpoints called by the embedded consent widget.

These endpoints are called by the widget running on customer websites. They require no authentication and support CORS from any origin.

Two config paths exist. Gateway endpoints take an opaque public token and are what the dashboard hands out; consent endpoints take the raw site UUID and are the legacy path, still used by the npm SDK. Both reject inactive sites. When a request carries an Origin or Referer header, its hostname must match the site's registered domain (exact or subdomain match); requests with neither header, such as server-to-server calls, are allowed.

Rate limits

Widget endpoints are rate limited per IP. Exceeding the limit returns 429 with a RATE_LIMITED error code. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

Get Config by Token

GET /api/gateway?token=ct_...

The recommended way to fetch widget config. Responds with the same body as GET /api/consent/config/:siteId below.

Query parameters

NameRequiredDescription
tokenYesPublic token, ct_ followed by 64 hex characters

Errors

StatusMeaning
400Missing token, or token does not match ct_[a-f0-9]{64}
404No site matches the token
403Site is not active, or Origin is not allowed

Successful responses are cached: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400.

Same 400 / 403 / 404 cases as the config endpoint above.

Get Config by Site ID (legacy)

GET /api/consent/config/:siteId
ParameterTypeLocationDescription
siteIdUUIDPathThe site identifier

200 OK

{
  "siteId": "550e8400-e29b-41d4-a716-446655440000",
  "siteName": "My Website",
  "layout": { "position": "bottom-right", "mode": "bar" },
  "colors": { "theme": "auto", "accent": "#3b82f6" },
  "content": { "privacyPolicyUrl": "https://example.com/privacy" },
  "buttons": { "acceptAll": "Accept", "rejectAll": "Reject" },
  "config": {
    "layout": { "position": "bottom-right", "mode": "bar" },
    "colors": { "theme": "auto", "accent": "#3b82f6" },
    "content": { "privacyPolicyUrl": "https://example.com/privacy" },
    "buttons": { "acceptAll": "Accept", "rejectAll": "Reject" }
  },
  "categories": [
    {
      "key": "necessary",
      "name": "Necessary",
      "description": "Essential cookies required for the website to function.",
      "required": true,
      "default": true
    },
    {
      "key": "analytics",
      "name": "Analytics",
      "description": "Help us understand how visitors interact with our website.",
      "required": false,
      "default": false
    }
  ]
}

The site's configuration is a free-form object written by the dashboard customizer; treat it as opaque. Its fields are spread across the top level of the response (where the widget reads them) and also returned as the nested config key. siteId, siteName, and categories always win over any same-named key in the configuration.

Errors: 400 Invalid UUID, 403 Site not active or origin not allowed, 404 Site not found


POST /api/consent/events

Request Body

{
  "siteId": "550e8400-e29b-41d4-a716-446655440000",
  "action": "accept_all",
  "categories": {
    "necessary": true,
    "analytics": true,
    "marketing": false
  },
  "visitorHash": "anon_7f3a9b2c4d5e"
}
FieldTypeRequiredDescription
siteIdUUIDYesThe site identifier
actionstringYesaccept_all, reject_all, or customize
categoriesobjectNoMap of category keys to boolean consent values
visitorHashstringYesAnonymous visitor identifier

200 OK

{
  "success": true,
  "consentId": "generated-uuid"
}

Errors: 400 Invalid JSON / missing fields / invalid action / invalid UUID, 403 Site not active, 404 Site not found

SDK Endpoints

@consentify/core in cloud mode (createConsentify({ siteId })) uses its own config format, served under /api/sdk. Point it here with endpoints:

const consentify = await createConsentify({
  siteId: 'your-site-id',
  endpoints: {
    config: 'https://consentify.dev/api/sdk',
    ingest: 'https://consentify.dev/api/sdk',
  },
});

Current policy pointer

GET /api/sdk/config/:siteId/latest.json
{ "current": "a1b2c3d4" }

Short-lived cache: this pointer must change promptly when you edit categories.

Policy body

GET /api/sdk/config/:siteId/<hash>.json
{
  "categories": ["analytics", "marketing"],
  "policyIdentifier": "a1b2c3d4",
  "mode": "opt-out",
  "consentMaxAgeDays": 180
}

Content-addressed, so the current hash is cached hard. categories lists only what the visitor chooses — required categories are implicit. mode and consentMaxAgeDays appear only when set on the site.

Record an event

POST /api/sdk/v1/events

Identical body and behaviour to Record Consent Event above — the SDK just expects it at this path.

On this page