Consent Records
POST /io/consent — server-side consent ledger for GDPR Art. 7(1) proof of consent across all Misar products.
Every cookie-banner interaction across the Misar suite is recorded server-side as an immutable consent event. This is the audit trail that proves when a visitor granted, updated, or withdrew consent, for which categories, and under which policy version (GDPR Art. 7(1) burden of proof).
POST https://api.misar.io/io/consent
GET https://api.misar.io/io/consent
The shared @misar/ui cookie banner posts here automatically — most products need no integration work. Use the API directly (or the @misar/consent SDK) for custom consent surfaces.
Record a consent event
POST https://api.misar.io/io/consent
Public — no authentication required. Anonymous visitors must be able to record consent. If an SSO bearer token is sent (Authorization: Bearer <token>), the event is additionally linked to that user id.
The server derives two fields itself — they cannot be supplied by the client:
ip_hash— SHA-256 of the client IP with a server-side salt. The raw IP is never stored.user_agent— captured from the request.
Body parameters
productstringrequiredWhich Misar product the banner belongs to. One of io, mail, blog, dev, reach, post, tools, id.
domainstringrequiredHostname the consent was given on, e.g. www.misar.io or misarmail.com.
actionstringrequiredgranted (first opt-in), updated (preferences changed), or withdrawn (all optional categories revoked).
categoriesobjectrequiredCategory → boolean map of the visitor's choices, e.g. { "necessary": true, "analytics": true, "marketing": false, "functional": true }. 1–16 keys; keys must be simple identifiers.
policy_versionstringrequiredVersion of the cookie/privacy policy the visitor consented to, e.g. 2026-07-05.
anonymous_idstringBanner-generated identifier (a localStorage UUID) that correlates a signed-out visitor's grant with a later withdrawal. Max 64 chars, [a-z0-9-].
Request
curl -X POST "https://api.misar.io/io/consent" \
-H "content-type: application/json" \
-d '{
"product": "mail",
"domain": "misarmail.com",
"action": "granted",
"categories": { "necessary": true, "analytics": true, "marketing": false, "functional": true },
"policy_version": "2026-07-05",
"anonymous_id": "3f9c2a1e-8b4d-4c6a-9e2f-1a7b5d3c8e0f"
}'Response
Returns 201 Created:
{
"id": "5e0b2a4c-7d1f-4b9a-8c3e-2f6a9d1b4e7c",
"created_at": "2026-07-05T12:34:56.789Z"
}idstringId of the stored consent event.
created_atstringServer timestamp of the event (ISO 8601).
Get consent history
GET https://api.misar.io/io/consent
SSO bearer only — returns the token owner's own consent history, newest first, cursor-paginated in the same shape as /io/wallet/transactions.
Query parameters
limitnumberqueryItems per page. Default 20, max 100.
cursorstringqueryPagination cursor. Pass the nextCursor from the previous response to fetch the next page.
Request
curl "https://api.misar.io/io/consent?limit=20" \
-H "Authorization: Bearer $SSO_TOKEN"Response
{
"items": [
{
"id": "5e0b2a4c-7d1f-4b9a-8c3e-2f6a9d1b4e7c",
"created_at": "2026-07-05T12:34:56.789Z",
"product": "mail",
"domain": "misarmail.com",
"action": "granted",
"categories": { "necessary": true, "analytics": true, "marketing": false, "functional": true },
"policy_version": "2026-07-05"
}
],
"nextCursor": null
}itemsobject[]Consent events, newest first. ip_hash and user_agent are never returned.
nextCursorstring | nullCursor for the next page, or null when there are no more pages.
Rate limit
Writes are limited to 30 requests per IP per 60 s. Exceeding the limit returns 429 with a Retry-After header.
SDK
import { createConsentClient } from "@misar/consent";
const consent = createConsentClient(); // defaults to https://api.misar.io/io/consent
// Record (public; pass bearerToken to link a signed-in user)
await consent.recordConsent({
product: "mail",
domain: "misarmail.com",
action: "granted",
categories: { necessary: true, analytics: true, marketing: false },
policyVersion: "2026-07-05",
anonymousId: anonId,
});
// Own history (SSO bearer required)
const { items, nextCursor } = await consent.getConsentHistory(token, { limit: 20 });Notes
- The ledger is append-only: withdrawing consent adds a
withdrawnevent, it never deletes the original grant. - Events survive account deletion de-identified —
user_idis set tonull, the event itself is retained as evidence. - Errors from the banner never block the visitor: the shared banner posts fire-and-forget.