This documentation is designed for engineering teams integrating directly with the Secure CMS Cookie Consent Platform via REST APIs without using the JavaScript SDK.
Use direct API integration when building:
- Mobile Applications (iOS Swift, Android Kotlin, Flutter, React Native).
- Custom / Headless Web Frontends (Next.js Server Components, SvelteKit, Nuxt, custom design systems).
- Backend & Microservice Architectures (APIs enforcing privacy consent before processing personal data).
- Connected Devices & Smart TVs (OTT platforms, embedded web views).
- Platform Overview & Base URLs
- Direct API Integration Flow
- Authentication & Cryptographic Identity Model (RS256 & JWKS)
- Client Consent & Configuration REST APIs
- Admin & Management REST APIs
- Client-Side Implementation Guidelines (Without SDK)
- Code Integration Examples
- Client IdP Server Endpoints (JWKS Reference)
- Error Codes & Troubleshooting
The SecureCMS Consent Engine manages multi-tenant compliance policies, records immutable audit trails, and validates user identity claims using asymmetric public key cryptography.
| Environment | Base URL | Description |
|---|---|---|
| Production (Hosted) | https://cookie-be.securedapp.io/api |
Live production consent service |
| Local Development | http://localhost:3000/api |
Local backend instance |
| Demo Client IdP | https://cookie-demo-be.securedapp.io |
Reference client auth server (JWKS + Login) |
When building a custom client without the SDK, your application coordinates with both your own authentication service and the SecureCMS platform:
sequenceDiagram
autonumber
actor User as User / Device
participant ClientApp as Custom Client App (Web/Mobile/Backend)
participant ClientIdP as Client Auth Server (IdP)
participant SecureBE as SecureCMS Consent API (cookie-be.securedapp.io)
Note over User, SecureBE: Phase 1: Fetch Policy & Record Anonymous Consent
ClientApp->>SecureBE: GET /api/config/{tenantId}/{appId}
SecureBE-->>ClientApp: 200 OK (Purposes, Vendors, Policy Version)
ClientApp->>User: Displays Custom Privacy UI / Consent Screen
User->>ClientApp: Selects Preferences (e.g., Essential=true, Analytics=true, Marketing=false)
ClientApp->>ClientApp: Generate/Retrieve Device ID (e.g. dev_9x2k...)
ClientApp->>SecureBE: POST /api/cookie-consents
SecureBE-->>ClientApp: 200 OK (Returns consent_id: csnt_abc123)
ClientApp->>ClientApp: Store consent_id, policy_version, and expiry locally
Note over User, SecureBE: Phase 2: User Login & Cross-Device Identity Linking
User->>ClientApp: Logs in with username & password
ClientApp->>ClientIdP: POST /api/login
ClientIdP-->>ClientApp: 200 OK (Returns signed RS256 JWT Token)
ClientApp->>SecureBE: POST /api/cookie-consents/link (Header: Authorization: Bearer <JWT>)
SecureBE->>ClientIdP: GET /.well-known/jwks.json (Fetches public key)
ClientIdP-->>SecureBE: 200 OK (RSA Public JWK)
SecureBE->>SecureBE: Validates signature, audience (app_id), tenant_id, and sub (user_id)
SecureBE-->>ClientApp: 200 OK (Consent linked to User ID across all devices)
Note over User, SecureBE: Phase 3: Fetching Existing Consent on New Device
ClientApp->>SecureBE: GET /api/cookie-consents/user/{userId}?tenant_id={tenantId}&app_id={appId}
SecureBE-->>ClientApp: 200 OK (Returns active purposes & preferences)
ClientApp->>ClientApp: Applies synced consent to local services
To ensure zero shared secrets between organizations:
- Client IdP creates an RSA 2048-bit key pair.
- Client IdP signs user tokens using private key (
RS256). - Client IdP publishes public key at
GET /.well-known/jwks.json. - SecureCMS validates token signatures dynamically using the public JWK without needing private keys.
{
"alg": "RS256",
"typ": "JWT",
"kid": "demo-client-key-1"
}{
"iss": "https://cookie-demo-be.securedapp.io",
"aud": "hdfc",
"sub": "user_123",
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"name": "Alex Johnson",
"iat": 1724410000,
"nbf": 1724410000,
"exp": 1724413600
}| Field | Type | Description |
|---|---|---|
iss |
string |
Issuer URL: The base URL of your IdP / backend. |
aud |
string |
Audience: Must match the app_id. |
sub |
string |
Subject: The unique identifier of the user. |
tenant_id |
string |
The organization / tenant identifier registered in SecureCMS. |
app_id |
string |
The application identifier registered in SecureCMS. |
exp |
number |
Expiration timestamp in seconds (recommended TTL: 3600s / 1 hour). |
Retrieves the active policy version, banner content, purpose categories, and vendor script URLs for a specific application.
- HTTP Method:
GET - Path:
/api/config/{tenantId}/{appId} - Authentication: None (Public)
tenantId(path, string, required): Organization identifier (e.g.,ICICI Bank).appId(path, string, required): Application identifier (e.g.,hdfc).
curl -X GET "https://cookie-be.securedapp.io/api/config/ICICI%20Bank/hdfc" \
-H "Accept: application/json"{
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"policy_version": "v1.0.0",
"banner_title": "We Value Your Privacy",
"banner_description": "We use cookies and similar technologies to optimize your experience, analyze traffic, and personalize content in compliance with the DPDP Act 2023.",
"purposes": [
{
"purpose_id": "p_essential",
"name": "Essential",
"description": "Required for basic website functionality, authentication, and security.",
"is_essential": true
},
{
"purpose_id": "p_analytics",
"name": "Analytics",
"description": "Allows us to count visits and traffic sources to measure performance.",
"is_essential": false,
"vendors": [
{
"vendor_id": "v_ga4",
"name": "Google Analytics 4",
"scripts": [
"https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
]
}
]
},
{
"purpose_id": "p_marketing",
"name": "Marketing",
"description": "Used by advertising partners to build a profile of your interests.",
"is_essential": false,
"vendors": [
{
"vendor_id": "v_meta_pixel",
"name": "Meta Pixel",
"scripts": [
"https://connect.facebook.net/en_US/fbevents.js"
]
}
]
}
]
}Submits an anonymous or device-level consent decision when the user accepts, rejects, or customizes preferences.
- HTTP Method:
POST - Path:
/api/cookie-consents - Authentication: None (Public)
- Headers:
Content-Type: application/json
{
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"device_id": "dev_4k9x18a7b_1724410000",
"policy_version": "v1.0.0",
"purposes": [
{
"name": "Essential",
"status": "granted",
"timestamp": "2026-08-23T12:00:00.000Z"
},
{
"name": "Analytics",
"status": "granted",
"timestamp": "2026-08-23T12:00:00.000Z"
},
{
"name": "Marketing",
"status": "denied",
"timestamp": "2026-08-23T12:00:00.000Z"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
tenant_id |
string |
Yes | Organization identifier. |
app_id |
string |
Yes | Application identifier. |
device_id |
string |
Yes | Persistent device identifier (generated on client). |
policy_version |
string |
Yes | The policy version obtained from GET /api/config. |
purposes |
array[object] |
Yes | Array of purpose consent decisions. |
purposes[].name |
string |
Yes | Purpose name (Essential, Analytics, Marketing, etc.). |
purposes[].status |
string |
Yes | "granted" or "denied". (Essential must always be "granted"). |
purposes[].timestamp |
string |
Yes | ISO 8601 UTC timestamp of the user action. |
curl -X POST "https://cookie-be.securedapp.io/api/cookie-consents" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"device_id": "dev_4k9x18a7b_1724410000",
"policy_version": "v1.0.0",
"purposes": [
{ "name": "Essential", "status": "granted", "timestamp": "2026-08-23T12:00:00.000Z" },
{ "name": "Analytics", "status": "granted", "timestamp": "2026-08-23T12:00:00.000Z" },
{ "name": "Marketing", "status": "denied", "timestamp": "2026-08-23T12:00:00.000Z" }
]
}'{
"consent_id": "csnt_9876543210abcdef",
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"device_id": "dev_4k9x18a7b_1724410000",
"policy_version": "v1.0.0",
"purposes": [
{ "name": "Essential", "status": "granted", "timestamp": "2026-08-23T12:00:00.000Z" },
{ "name": "Analytics", "status": "granted", "timestamp": "2026-08-23T12:00:00.000Z" },
{ "name": "Marketing", "status": "denied", "timestamp": "2026-08-23T12:00:00.000Z" }
],
"created_at": "2026-08-23T12:00:01.120Z"
}Links an existing anonymous device consent_id to an authenticated user ID. The backend verifies the RS256 JWT using your JWKS.
- HTTP Method:
POST - Path:
/api/cookie-consents/link - Authentication: Bearer Token (
Authorization: Bearer <RS256_JWT>) - Headers:
Content-Type: application/jsonAuthorization: Bearer <JWT_TOKEN>
{
"consent_id": "csnt_9876543210abcdef",
"user_id": "user_123",
"device_id": "dev_4k9x18a7b_1724410000",
"tenant_id": "ICICI Bank",
"app_id": "hdfc"
}curl -X POST "https://cookie-be.securedapp.io/api/cookie-consents/link" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8tY2xpZW50LWtleS0xIn0..." \
-d '{
"consent_id": "csnt_9876543210abcdef",
"user_id": "user_123",
"device_id": "dev_4k9x18a7b_1724410000",
"tenant_id": "ICICI Bank",
"app_id": "hdfc"
}'{
"success": true,
"consent_id": "csnt_9876543210abcdef",
"user_id": "user_123",
"linked_at": "2026-08-23T12:05:00.000Z",
"purposes": [
{ "name": "Essential", "status": "granted" },
{ "name": "Analytics", "status": "granted" },
{ "name": "Marketing", "status": "denied" }
]
}Retrieves the latest synchronized consent preferences for a logged-in user (useful when logging in on a new device or browser).
- HTTP Method:
GET - Path:
/api/cookie-consents/user/{userId} - Query Parameters:
tenant_id(string, optional/recommended): Organization ID.app_id(string, optional/recommended): Application ID.
- Authentication: Bearer Token (
Authorization: Bearer <RS256_JWT>)
curl -X GET "https://cookie-be.securedapp.io/api/cookie-consents/user/user_123?tenant_id=ICICI%20Bank&app_id=hdfc" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8tY2xpZW50LWtleS0xIn0..." \
-H "Accept: application/json"{
"consent_id": "csnt_9876543210abcdef",
"user_id": "user_123",
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"policy_version": "v1.0.0",
"purposes": [
{ "name": "Essential", "status": "granted" },
{ "name": "Analytics", "status": "granted" },
{ "name": "Marketing", "status": "denied" }
],
"updated_at": "2026-08-23T12:05:00.000Z"
}These APIs are used by backend administrative dashboards to manage purpose taxonomies, third-party vendor scripts, and inspect consent audit trails.
GET /api/admin/purposes
- Response
200 OK: Returns array of all configured purposes.
POST /api/admin/purposes
- Request Body:
{
"name": "Personalization",
"description": "Enables enhanced functionality and customization.",
"is_essential": false
}GET /api/admin/vendors
- Response
200 OK: Returns list of all vendors and script tags.
POST /api/admin/vendors
- Request Body:
{
"vendor_id": "v_hotjar",
"name": "Hotjar",
"purpose_name": "Analytics",
"scripts": ["https://static.hotjar.com/c/hotjar-12345.js"]
}POST /api/config
- Request Body:
{
"tenant_id": "ICICI Bank",
"app_id": "hdfc",
"policy_version": "v1.1.0",
"banner_title": "Privacy Settings",
"banner_description": "We respect your data privacy under the DPDP Act 2023.",
"purposes": [ ... ]
}GET /api/admin/cookie-consents?page=1&limit=25
- Response
200 OK: Returns paginated immutable consent logs for compliance audits.
When you choose to implement your own frontend or mobile layer instead of using the JavaScript SDK, ensure you replicate these critical platform requirements:
Generate a persistent pseudo-unique ID stored in persistent storage (e.g., localStorage, Keychain, EncryptedSharedPreferences):
function getOrCreateDeviceId() {
let deviceId = localStorage.getItem('consent_device_id');
if (!deviceId) {
deviceId = 'dev_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now().toString(36);
localStorage.setItem('consent_device_id', deviceId);
}
return deviceId;
}Consent must be renewed if:
- No local record exists.
- The
policy_versionreturned fromGET /api/configdiffers from the local record. - The consent has expired (default validity: 6 months / 180 days).
function isConsentValid(localConsent, serverPolicyVersion) {
if (!localConsent) return false;
if (localConsent.policy_version !== serverPolicyVersion) return false;
if (localConsent.expiry && new Date(localConsent.expiry) < new Date()) return false;
return true;
}If the client is offline when the user makes a choice:
- Save the choice locally immediately to unblock the UI.
- Push the payload into a local offline queue (
consent_offline_queue). - Re-attempt transmission when the network is restored or at regular intervals (e.g., every 60s).
- Only execute vendor scripts or third-party SDKs when the user's
purposesstatus for that category equals"granted". - Essential scripts (
is_essential: true) may execute immediately. - Maintain a runtime cache/set of executed vendors to prevent duplicate script execution.
import axios from 'axios';
const SECURE_CMS_BASE = 'https://cookie-be.securedapp.io/api';
interface ConsentChoice {
name: string;
status: 'granted' | 'denied';
timestamp: string;
}
export class CookieConsentClient {
constructor(
private tenantId: string,
private appId: string,
private deviceId: string
) {}
// 1. Fetch Config
async fetchConfig() {
const res = await axios.get(`${SECURE_CMS_BASE}/config/${encodeURIComponent(this.tenantId)}/${encodeURIComponent(this.appId)}`);
return res.data;
}
// 2. Submit Anonymous Consent
async submitConsent(policyVersion: string, choices: ConsentChoice[]) {
const res = await axios.post(`${SECURE_CMS_BASE}/cookie-consents`, {
tenant_id: this.tenantId,
app_id: this.appId,
device_id: this.deviceId,
policy_version: policyVersion,
purposes: choices
});
return res.data;
}
// 3. Link with User JWT
async linkUser(consentId: string, userId: string, jwtToken: string) {
const res = await axios.post(
`${SECURE_CMS_BASE}/cookie-consents/link`,
{
consent_id: consentId,
user_id: userId,
device_id: this.deviceId,
tenant_id: this.tenantId,
app_id: this.appId
},
{
headers: {
Authorization: `Bearer ${jwtToken}`
}
}
);
return res.data;
}
// 4. Fetch User Consent
async getUserConsent(userId: string, jwtToken: string) {
const res = await axios.get(
`${SECURE_CMS_BASE}/cookie-consents/user/${encodeURIComponent(userId)}?tenant_id=${encodeURIComponent(this.tenantId)}&app_id=${encodeURIComponent(this.appId)}`,
{
headers: {
Authorization: `Bearer ${jwtToken}`
}
}
);
return res.data;
}
}import requests
from datetime import datetime, timezone
SECURE_CMS_BASE = "https://cookie-be.securedapp.io/api"
class CookieConsentAPI:
def __init__(self, tenant_id: str, app_id: str, device_id: str):
self.tenant_id = tenant_id
self.app_id = app_id
self.device_id = device_id
def get_config(self):
url = f"{SECURE_CMS_BASE}/config/{self.tenant_id}/{self.app_id}"
response = requests.get(url)
response.raise_for_status()
return response.json()
def record_consent(self, policy_version: str, analytics_granted: bool, marketing_granted: bool):
now_iso = datetime.now(timezone.utc).isoformat()
payload = {
"tenant_id": self.tenant_id,
"app_id": self.app_id,
"device_id": self.device_id,
"policy_version": policy_version,
"purposes": [
{"name": "Essential", "status": "granted", "timestamp": now_iso},
{"name": "Analytics", "status": "granted" if analytics_granted else "denied", "timestamp": now_iso},
{"name": "Marketing", "status": "granted" if marketing_granted else "denied", "timestamp": now_iso}
]
}
res = requests.post(f"{SECURE_CMS_BASE}/cookie-consents", json=payload)
res.raise_for_status()
return res.json()
def link_user(self, consent_id: str, user_id: str, jwt_token: str):
headers = {"Authorization": f"Bearer {jwt_token}"}
payload = {
"consent_id": consent_id,
"user_id": user_id,
"device_id": self.device_id,
"tenant_id": self.tenant_id,
"app_id": self.app_id
}
res = requests.post(f"{SECURE_CMS_BASE}/cookie-consents/link", json=payload, headers=headers)
res.raise_for_status()
return res.json()import Foundation
struct ConsentManager {
let tenantId = "ICICI Bank"
let appId = "hdfc"
let baseURL = "https://cookie-be.securedapp.io/api"
// Save Consent
func saveConsent(deviceId: String, policyVersion: String, analyticsAllowed: Bool, completion: @escaping (Result<String, Error>) -> Void) {
guard let url = URL(string: "\(baseURL)/cookie-consents") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let isoFormatter = ISO8601DateFormatter()
let now = isoFormatter.string(from: Date())
let payload: [String: Any] = [
"tenant_id": tenantId,
"app_id": appId,
"device_id": deviceId,
"policy_version": policyVersion,
"purposes": [
["name": "Essential", "status": "granted", "timestamp": now],
["name": "Analytics", "status": analyticsAllowed ? "granted" : "denied", "timestamp": now]
]
]
request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error { completion(.failure(error)); return }
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let consentId = json["consent_id"] as? String {
completion(.success(consentId))
}
}.resume()
}
}import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.time.Instant
class CookieConsentClient(
private val tenantId: String = "ICICI Bank",
private val appId: String = "hdfc",
private val client: OkHttpClient = OkHttpClient()
) {
private val baseUrl = "https://cookie-be.securedapp.io/api"
fun saveConsent(deviceId: String, policyVersion: String, analyticsGranted: Boolean, callback: (String?) -> Unit) {
val now = Instant.now().toString()
val json = JSONObject().apply {
put("tenant_id", tenantId)
put("app_id", appId)
put("device_id", deviceId)
put("policy_version", policyVersion)
put("purposes", JSONArray().apply {
put(JSONObject().apply {
put("name", "Essential")
put("status", "granted")
put("timestamp", now)
})
put(JSONObject().apply {
put("name", "Analytics")
put("status", if (analyticsGranted) "granted" else "denied")
put("timestamp", now)
})
})
}
val request = Request.Builder()
.url("$baseUrl/cookie-consents")
.post(json.toString().toRequestBody("application/json".toMediaType()))
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: java.io.IOException) = callback(null)
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string() ?: return callback(null)
val resObj = JSONObject(body)
callback(resObj.optString("consent_id"))
}
})
}
}Your Identity Provider must implement the following endpoints to support secure identity linking:
Returns the public JSON Web Key Set:
{
"keys": [
{
"kty": "RSA",
"kid": "demo-client-key-1",
"use": "sig",
"alg": "RS256",
"n": "u1w_...",
"e": "AQAB"
}
]
}Authenticates the user and returns the signed RS256 token:
{
"success": true,
"userId": "user_123",
"tenantId": "ICICI Bank",
"appId": "hdfc",
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8tY2xpZW50LWtleS0xIn0...",
"issuer": "https://cookie-demo-be.securedapp.io",
"jwks_url": "https://cookie-demo-be.securedapp.io/.well-known/jwks.json",
"expires_in": 3600
}| HTTP Status | Error Code / Message | Meaning & Resolution |
|---|---|---|
400 Bad Request |
Missing required fields |
Verify that tenant_id, app_id, device_id, and purposes are present. |
401 Unauthorized |
Invalid JWT Signature |
Verify that the token was signed with the private key matching your published JWKS kid. |
401 Unauthorized |
JWT expired |
Tokens must be fresh (exp > current time). Renew JWT upon login. |
404 Not Found |
Config not found |
Check that tenantId and appId are spelled accurately and configured in the Admin backend. |
500 Server Error |
Failed to reach JWKS |
Ensure your BASE_URL/.well-known/jwks.json is publicly reachable with valid SSL certificates. |
For further integration support or direct API credentials, contact SecuredApp support.