Docs

DSAR, preferences, ROPA & vendors

AdminUpdated Sep 15, 2026

DSAR, preferences, ROPA & vendors

The privacy/governance surface covers everything a DPO needs outside of the consent-decision log itself: Data Subject Access Requests (DSAR), a preference-center record store for marketing-style opt-ins, Records of Processing Activities (RoPA), and third-party vendor risk tracking. Every route requires an API key and is scoped to the calling key's org, so one org can never see another's DSARs, vendors, or RoPA entries.

Resource

Read scope

Write scope

/v1/dsar

dsar:read

dsar:write

/v1/vendors

vendors:read

vendors:write

/v1/ropa

ropa:read

ropa:write

/v1/preferences

consent:read

consent:write

A legacy key with no scopes list has full access to all four; scoping is opt-in (see API keys, members, webhooks & usage).

DSAR — Data Subject Access Requests

DSARs move through a fixed, one-way status graph: received → verifying → in_progress → completed, with rejected reachable from any non-terminal state. There's no going backward and no skipping steps.

List all DSARs

GET /v1/dsar

Scope: dsar:read. Returns every request in the org across all statuses (there's no per-status filter on this endpoint).

curl -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  https://api.cookiemunch.net/v1/dsar
[
  {
    "id": "dsar_7c1b",
    "type": "access",
    "subjectEmail": "[email protected]",
    "regulation": "gdpr",
    "status": "received",
    "createdAt": 1730332800000,
    "dueAt": 1732924800000
  }
]

dueAt is computed at creation from the regulation's statutory window: 30 days for gdpr, 45 days for ccpa.

const requests = await client.dsar.list();

Create a DSAR

POST /v1/dsar

Scope: dsar:write

Field

Type

Required

Description

type

access | deletion | rectification | portability | opt-out

yes

The right being exercised.

subjectEmail

string

yes

The data subject's email.

regulation

gdpr | ccpa

yes

Governing regulation; sets dueAt.

note

string

no

Free-text intake note.

curl -X POST https://api.cookiemunch.net/v1/dsar \
  -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "access", "subjectEmail": "[email protected]", "regulation": "gdpr" }'

201 Created:

{
  "request": {
    "id": "dsar_7c1b",
    "type": "access",
    "subjectEmail": "[email protected]",
    "regulation": "gdpr",
    "status": "received",
    "createdAt": 1730332800000,
    "dueAt": 1732924800000
  }
}

Fires a dsar.created webhook. Errors: 400 { "error": "invalid dsar intake: <reason>" } for a missing/malformed type, subjectEmail, or regulation.

const { request } = await client.dsar.create({
  type: 'access',
  subjectEmail: '[email protected]',
  regulation: 'gdpr',
});

Advance a DSAR

POST /v1/dsar/{id}/advance

Scope: dsar:write

Field

Type

Required

Description

toStatus

received | verifying | in_progress | completed | rejected

yes

Target status.

curl -X POST https://api.cookiemunch.net/v1/dsar/dsar_7c1b/advance \
  -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "toStatus": "verifying" }'
{ "request": { "id": "dsar_7c1b", "status": "verifying", "...": "..." } }

Fires a dsar.updated webhook. Errors: 400 { "error": "invalid toStatus" } if it isn't one of the five known statuses. 404 if id doesn't exist. 409 { "error": "illegal transition: received -> completed" } if you try to skip a step.

const { request } = await client.dsar.advance('dsar_7c1b', 'verifying');

Preference center

A lightweight, identity-keyed store of per-subject processing preferences — newsletter, SMS, profiling, etc. — separate from the cookie-consent ledger. Records support single- or double-opt-in; saving via this endpoint always uses single-opt-in, which is confirmed immediately (confirmed: true).

List preference records

GET /v1/preferences

Scope: consent:read

curl -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  https://api.cookiemunch.net/v1/preferences
[
  {
    "subjectId": "[email protected]",
    "purposes": { "newsletter": true, "sms": false },
    "method": "single-opt-in",
    "confirmed": true,
    "updatedAt": 1719858421000,
    "version": 1
  }
]

Errors: 501 { "error": "preference center not configured" } on a self-host without a preference store wired up.

const records = await client.preferences.list();

Save a preference record

POST /v1/preferences

Scope: consent:write

Field

Type

Required

Description

subjectId

string

yes

End-user identifier — email, internal id, etc.

purposes

object

yes

Map of purpose name → boolean. Merged into any existing record; each write bumps version.

curl -X POST https://api.cookiemunch.net/v1/preferences \
  -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "subjectId": "[email protected]", "purposes": { "newsletter": true, "sms": false } }'
{
  "record": {
    "subjectId": "[email protected]",
    "purposes": { "newsletter": true, "sms": false },
    "method": "single-opt-in",
    "confirmed": true,
    "updatedAt": 1719858421000,
    "version": 1
  }
}

Errors: 400 { "error": "subjectId required" } / { "error": "purposes required" }. 501 if the preference store isn't configured.

const { record } = await client.preferences.save('[email protected]', { newsletter: true, sms: false });

Vendors (processors)

Registering a vendor computes a transparent, deterministic 0–100 risk score: +40 with no DPA signed, +25 for sharing special-category data, up to +15 for subprocessor depth, +20 for a non-adequacy-decision region, minus up to 25 credit for ISO27001/SOC2 certifications. score >= 60 is high, >= 30 is medium, else low.

List vendors

GET /v1/vendors

Scope: vendors:read

curl -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  https://api.cookiemunch.net/v1/vendors
[
  {
    "id": "vnd_1",
    "name": "Segment",
    "category": "analytics",
    "dataShared": ["device_id", "email_hash"],
    "dpaSigned": true,
    "subprocessors": 3,
    "certifications": ["SOC2"],
    "region": "US",
    "risk": { "score": 34, "band": "medium" }
  }
]
const vendors = await client.vendors.list();

Create a vendor

POST /v1/vendors

Scope: vendors:write

Field

Type

Required

Description

name

string

yes

Vendor name.

category

string

yes

e.g. analytics, advertising.

dataShared

string[]

yes

Data categories shared with the vendor.

dpaSigned

boolean

yes

Whether a Data Processing Agreement is signed.

subprocessors

number

yes

Count of the vendor's own subprocessors.

certifications

string[]

yes

e.g. ["SOC2", "ISO27001"] (case-insensitive for scoring).

region

string

yes

Primary processing region, e.g. EU.

curl -X POST https://api.cookiemunch.net/v1/vendors \
  -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Segment", "category": "analytics", "dataShared": ["device_id"],
    "dpaSigned": true, "subprocessors": 3, "certifications": ["SOC2"], "region": "US"
  }'

201 Created: { "vendor": { "id": "vnd_1", "...": "..." }, "risk": { "score": 34, "band": "medium" } }.

Errors: 400 { "error": "<message>" } if a required field is missing or the wrong type.

const { vendor, risk } = await client.vendors.create({
  name: 'Segment', category: 'analytics', dataShared: ['device_id'],
  dpaSigned: true, subprocessors: 3, certifications: ['SOC2'], region: 'US',
});

Records of Processing Activities (RoPA)

List RoPA entries

GET /v1/ropa

Scope: ropa:read

curl -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  https://api.cookiemunch.net/v1/ropa
[
  {
    "id": "ropa_1",
    "name": "Email marketing",
    "purpose": "Send product updates",
    "legalBasis": "consent",
    "dataCategories": ["email"],
    "recipients": ["Mailchimp"],
    "retentionDays": 730,
    "crossBorderTransfer": true
  }
]
const entries = await client.ropa.list();

Create a RoPA entry

POST /v1/ropa

Scope: ropa:write

Field

Type

Required

Description

name

string

yes

Activity name.

purpose

string

yes

Purpose of processing.

legalBasis

consent | contract | legal-obligation | vital-interests | public-task | legitimate-interests

yes

GDPR Art. 6 legal basis.

dataCategories

string[]

yes

Personal-data categories processed.

recipients

string[]

yes

Who receives the data.

retentionDays

number

yes

Retention period, in days.

crossBorderTransfer

boolean

yes

Whether data crosses borders.

curl -X POST https://api.cookiemunch.net/v1/ropa \
  -H "Authorization: Bearer $COOKIEMUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Email marketing", "purpose": "Send product updates",
    "legalBasis": "consent", "dataCategories": ["email"],
    "recipients": ["Mailchimp"], "retentionDays": 730, "crossBorderTransfer": true
  }'

201 Created: { "entry": { "id": "ropa_1", "name": "Email marketing", "...": "..." } }.

Errors: 400 { "error": "invalid RoPA entry: <reason>" } for a missing/malformed field.

const { entry } = await client.ropa.create({
  name: 'Email marketing', purpose: 'Send product updates', legalBasis: 'consent',
  dataCategories: ['email'], recipients: ['Mailchimp'], retentionDays: 730,
  crossBorderTransfer: true,
});

Next: API keys, members, webhooks & usage for how dsar:*/vendors:*/ropa:* scopes are issued, or cookies, scan & verify for the consent-adjacent site data this page doesn't cover.

Was this page helpful?