Home / Guides / Identity and consent
Product guide

Identity and consent

Every ContentFlow install has a device id. Identify links that device to your user id and records traits. Two separate consent decisions sit on top: analytics consent decides what ContentFlow may store about the person, and channel consent decides what may be sent to them on each channel. This page is the one place that explains both, plus sign-out and erasure.

In short
  • An install is anonymous until you identify it with your user id: setUserId in JavaScript and Flutter, identify(userId:) in Swift and Kotlin.
  • Analytics consent (setConsent, or setAnalyticsConsent in Swift and Kotlin) gates every event; until it is granted the SDK tracks nothing and does not replay events later.
  • Channel consent (setChannelConsent) is a separate decision per channel: push, SMS, WhatsApp, email, location and marketing. Registering a push token never grants it.
  • On sign-out call logout(); for the right to erasure call deleteIdentity(), which wipes local data only after the server confirms a complete erasure.
  • The most common trap: setting channel consent for a signed-in user before the device is identified as that user, which is refused with 403 DEVICE_NOT_IDENTIFIED.
Identity

Anonymous and signed-in

POST /identify sends the device id, the signed-in user id when there is one, traits, and the analytics consent decision when the app has made one. Traits, segments and the user link are only written while analytics consent is granted. Identify on app start and after sign-in.

  • Anonymous install: the JavaScript start(), the Kotlin start() and the Flutter ContentFlow.create() identify the device for you without a user id. The Swift SDK registers the device on the first identify or sync, so call identify(userId: nil) for an anonymous install.
  • Signed-in user: after sign-in, link the install to your user id. A consented anonymous profile then merges into the signed-in one; see Anonymous to signed-in.
  • Traits: an empty set means "leave traits unchanged", and sending traits: {} never erases stored traits. In Swift and Kotlin a non-empty set replaces the stored set, so send every trait you want to keep.
  • Consent on identify: omitting consent keeps the previous decision. A non-boolean value is refused with 400 consent must be a boolean when provided.
// 1. Anonymous: start() identifies the device without a user id.
await client.start();

// 2. After sign-in: link the install to your user id.
await client.setUserId(userId);

// 3. Send traits for targeting (consent omitted = unchanged).
await client.identify({ traits: { plan_tier: 'gold' } });
import { useEffect } from 'react';
import { useCF } from '@contentflow/sdk/react';

// 1. Anonymous: <CFProvider> calls start(), which identifies the device without a user id.

function AfterSignIn({ userId }: { userId: string }) {
  const cf = useCF();
  useEffect(() => {
    void (async () => {
      // 2. After sign-in: link the install to your user id.
      await cf.setUserId(userId);
      // 3. Send traits for targeting (consent omitted = unchanged).
      await cf.identify({ traits: { plan_tier: 'gold' } });
    })();
  }, [cf, userId]);
  return null;
}
import { useEffect } from 'react';
import { useCF } from '@contentflow/sdk/react-native';

// 1. Anonymous: <CFProvider> calls start(), which identifies the device without a user id.

function AfterSignIn({ userId }: { userId: string }) {
  const cf = useCF();
  useEffect(() => {
    void (async () => {
      // 2. After sign-in: link the install to your user id.
      await cf.setUserId(userId);
      // 3. Send traits for targeting (consent omitted = unchanged).
      await cf.identify({ traits: { plan_tier: 'gold' } });
    })();
  }, [cf, userId]);
  return null;
}
// 1. Anonymous: Swift registers the device on the first identify or sync.
try await client.identify(userId: nil)

// 2 and 3. After sign-in: link the install to your user id and send traits.
//    A non-empty traits set replaces the stored one.
try await client.identify(userId: userId, traits: ["plan_tier": "gold"])
// 1. Anonymous: start() identifies the device without a user id.
client.start()

// 2 and 3. After sign-in, from a coroutine: link the install and send traits.
//    A non-empty traits map replaces the stored one.
client.identify(userId, mapOf("plan_tier" to JsonValue.of("gold")))
// 1. Anonymous: ContentFlow.create() identifies the device without a user id.
await ContentFlow.create(const ContentFlowConfig(publicKey: CONTENTFLOW_SDK_KEY));
final cf = ContentFlow.instance;

// 2. After sign-in: link the install to your user id.
await cf.setUserId(userId);

// 3. Send traits for targeting.
await cf.identify(userId: userId, traits: {'plan_tier': 'gold'});
# 1. Anonymous: identify with the device id only.
curl -X POST https://api.contentflow.click/sdk/v1/identify \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\"}"

# 2 and 3. After sign-in: the same call with your user id and traits.
curl -X POST https://api.contentflow.click/sdk/v1/identify \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\",\"userId\":\"cust_123\",\"traits\":{\"plan_tier\":\"gold\"}}"

An identify that applied the identity but could not re-link the device's push token answers 207 with success: false and code: "PUSH_LINKAGE_INCOMPLETE": keep the new identity and retry identify. Full contract: REST guide: Identify.

Concept

Two kinds of consent

DecisionSent withGoverns
Analytics consent (one yes or no per install)identify({ consent }), setConsent() → POST /identifyWhether a profile, traits, segments, events and location are stored
Channel consent: push, SMS, WhatsApp, email, location, marketingsetChannelConsent() → POST /consentWhether each channel may be used to message the person. In-app messages use marketing consent.
Marketing preferencesFull consent API (requestConsent → POST /consent/full)Promotional, transactional, service updates, third-party offers
PDPL opt-outFull consent APIBlocks every outbound channel

Channel consent never turns analytics on, and analytics consent never grants a channel. Content delivery works without either. Always set both from a choice the person actually made, and never default them to granted.

Privacy by default

Analytics consent decides what's stored

Analytics consentDeviceProfile, traits, segmentsEvents and locationContent
Unknown (new device, no decision)StoredNot writtenDropped in the SDKUntargeted (All users) content
Denied (consent: false)StoredNot written; an account switch unlinks the old user and push tokenDropped; a revoke purges the SDK's queuesUntargeted content
Granted (consent: true)StoredWritten and kept freshAcceptedTargeted content

The SDK treats a grant as effective only once the server echoes consent: true. A revoke takes effect locally at once and stays in force even if the request fails. Every failed consent write rejects or throws, including an HTTP 200 carrying success: false, so keep showing the last confirmed setting until a write succeeds.

Messaging

Channel consent

Record each channel decision from your consent UI. A channel is used only when its flag is exactly true.

// 1. Analytics consent, from the person's choice.
await client.setConsent(userAcceptedAnalytics);

// 2. Signed in: identify as the user first (see above).
await client.setUserId(userId);

// 3. Channel consent, independent of analytics consent.
//    With a user id it is stored on the profile; without one, on this device only.
await client.setChannelConsent(userId, { push: true, sms: false });
import { useCF } from '@contentflow/sdk/react';

function ConsentForm({ userId }: { userId: string }) {
  const cf = useCF();
  async function save(userAcceptedAnalytics: boolean) {
    // 1. Analytics consent, from the person's choice.
    await cf.setConsent(userAcceptedAnalytics);
    // 2. Signed in: identify as the user first (see above).
    await cf.setUserId(userId);
    // 3. Channel consent, independent of analytics consent.
    //    With a user id it is stored on the profile; without one, on this device only.
    await cf.setChannelConsent(userId, { push: true, sms: false });
  }
  return <button onClick={() => save(true)}>Save choices</button>;
}
import { Button } from 'react-native';
import { useCF } from '@contentflow/sdk/react-native';

function ConsentForm({ userId }: { userId: string }) {
  const cf = useCF();
  async function save(userAcceptedAnalytics: boolean) {
    // 1. Analytics consent, from the person's choice.
    await cf.setConsent(userAcceptedAnalytics);
    // 2. Signed in: identify as the user first (see above).
    await cf.setUserId(userId);
    // 3. Channel consent, independent of analytics consent.
    //    With a user id it is stored on the profile; without one, on this device only.
    await cf.setChannelConsent(userId, { push: true, sms: false });
  }
  return <Button title="Save choices" onPress={() => save(true)} />;
}
// 1. Analytics consent, from the person's choice.
try await client.setAnalyticsConsent(.granted)

// 2. Signed in: identify as the user first (see above).
try await client.identify(userId: userId)

// 3. Channel consent, one channel per call. Sent with the linked user when
//    the client is identified, otherwise stored for this device only.
try await client.setChannelConsent(.granted, for: .push)
try await client.setChannelConsent(.denied, for: .sms)
// 1. Analytics consent, from the person's choice.
client.setAnalyticsConsent(ConsentStatus.Granted)

// 2. Signed in: identify as the user first (see above).
client.identify(userId)

// 3. Channel consent, one channel per call.
//    Kotlin requires an identified user: otherwise this throws InvalidConfiguration.
client.setChannelConsent(ConsentChannel.Push, ConsentStatus.Granted)
client.setChannelConsent(ConsentChannel.Sms, ConsentStatus.Denied)
final cf = ContentFlow.instance;

// 1. Analytics consent, from the person's choice.
await cf.setConsent(true);

// 2. Signed in: identify as the user first, or step 3 throws StateError.
await cf.setUserId(userId);

// 3. Channel consent. Every channel is sent: a flag you leave out is sent as false,
//    and locationTracking: false also stops location collection on the device.
await cf.setChannelConsent(userId, const CFChannelConsent(push: true, sms: false));
# 1 and 2. Identify as the user, with the analytics decision.
curl -X POST https://api.contentflow.click/sdk/v1/identify \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\",\"userId\":\"cust_123\",\"consent\":true}"

# 3. Channel consent: flags must be nested under channels.
#    The user is sent as username; userId is accepted as an alias (username wins if both are sent).
curl -X POST https://api.contentflow.click/sdk/v1/consent \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\",\"username\":\"cust_123\",\"channels\":{\"push\":true,\"sms\":false}}"
ChannelJavaScript, Flutter, RESTSwiftKotlin
Pushpush.pushConsentChannel.Push
SMSsms.smsConsentChannel.Sms
WhatsAppwhatsapp.whatsappConsentChannel.Whatsapp
Emailemail.emailConsentChannel.Email
LocationlocationTracking.locationConsentChannel.Location
Marketingmarketing.marketingConsentChannel.Marketing

Channel consent for a named user is accepted only once this device is identified as that user, with the same X-CF-Device:

StatuscodeMeaning
400DEVICE_REQUIREDA user is named but no device was sent.
400CHANNELS_REQUIREDChannel flags were sent at the top level of the body instead of under channels.
403DEVICE_NOT_IDENTIFIEDThe device is not signed in as a user. Identify first.
403USER_MISMATCHThe device is signed in as a different user.
409DEVICE_ENVIRONMENT_MISMATCHThe device was first identified with the other environment's key. See Test and live keys.

Registering a push token never grants push consent, and revoking push consent never removes the token. See A token is an address, not permission.

Regulated industries

Consent presets

A preset describes what a consent screen for a regulated industry should offer: required and optional items, default topics, and the legal framework. GET /sdk/v1/consent/config/{preset} needs the SDK key but no device header and no identify call, so you can show the screen before the person has agreed to anything. Reading a preset never changes anyone's consent; only an explicit write does, and the server validates what you send against the preset.

PresetFrameworkExample topics
banking_ksaSAMA + PDPLSecurity alerts (push required), transaction alerts, statement ready, offers and rewards, card updates
fintech_ksaSAMA + PDPLSecurity alerts (required), transaction alerts, payment reminders, product updates
insurance_ksaCCHI + PDPLPolicy updates, claim status
healthcare_ksaCBAHI + PDPLAppointment and medication reminders, lab results
travel_ksaPDPLBooking updates, flight status, gate changes, travel deals, loyalty updates
retail_ksaPDPLOrder updates, delivery status, promotions, back in stock, loyalty rewards
food_delivery_ksaPDPLOrder status (required), driver location, deals and discounts, recommendations
import { consentPathsToConfig } from '@contentflow/sdk/web';

// 1. Read the preset to build your consent screen.
//    preset.required / preset.optional are paths such as 'channels.push' or 'marketing.promotional'.
const preset = await client.getConsentConfig('banking_ksa');

// 2. Record what the person accepted, against the preset.
await client.requestConsent({ preset: preset.preset, ...consentPathsToConfig(userAcceptedPaths) });
import { useEffect, useState } from 'react';
import { useCF, consentPathsToConfig, type ConsentPreset } from '@contentflow/sdk/react';

function ConsentScreen({ userAcceptedPaths }: { userAcceptedPaths: string[] }) {
  const cf = useCF();
  const [preset, setPreset] = useState<ConsentPreset | null>(null);

  // 1. Read the preset to build your consent screen.
  //    preset.required / preset.optional are paths such as 'channels.push' or 'marketing.promotional';
  //    preset.availableTopics lists the push topics.
  useEffect(() => { void cf.getConsentConfig('banking_ksa').then(setPreset); }, [cf]);

  // 2. Record what the person accepted, against the preset.
  async function accept() {
    if (!preset) return;
    await cf.requestConsent({ preset: preset.preset, ...consentPathsToConfig(userAcceptedPaths) });
  }
  return <button disabled={!preset} onClick={accept}>Accept</button>;
}
import { useEffect, useState } from 'react';
import { Button } from 'react-native';
import { useCF, consentPathsToConfig, type ConsentPreset } from '@contentflow/sdk/react-native';

function ConsentScreen({ userAcceptedPaths }: { userAcceptedPaths: string[] }) {
  const cf = useCF();
  const [preset, setPreset] = useState<ConsentPreset | null>(null);

  // 1. Read the preset to build your consent screen.
  //    preset.required / preset.optional are paths such as 'channels.push' or 'marketing.promotional';
  //    preset.availableTopics lists the push topics.
  useEffect(() => { void cf.getConsentConfig('banking_ksa').then(setPreset); }, [cf]);

  // 2. Record what the person accepted, against the preset.
  async function accept() {
    if (!preset) return;
    await cf.requestConsent({ preset: preset.preset, ...consentPathsToConfig(userAcceptedPaths) });
  }
  return <Button title="Accept" disabled={!preset} onPress={accept} />;
}
// 1. Read the preset to build your consent screen.
let preset = try await client.getConsentConfig(preset: "banking_ksa")

// 2. Record what the person accepted, against the preset.
try await client.requestConsent(options: ConsentRequestOptions(
    preset: "banking_ksa",
    channels: .init(push: true, sms: false)))
// 1. Read the preset to build your consent screen (presets live on client.extensions).
val preset = client.extensions.getConsentConfig("banking_ksa")

// 2. Record what the person accepted, against the preset.
client.extensions.requestConsent(ConsentRequestOptions(
    preset = "banking_ksa",
    channels = FullConsentConfig.ChannelConsent(push = true, sms = false),
))
final cf = ContentFlow.instance;

// 1. Read the preset to build your consent screen. Returns null if the request fails.
final preset = await cf.getConsentConfig('banking_ksa');

// 2. Record what the person accepted, against the preset.
//    Every channel is sent: a flag you leave out is sent as false.
await cf.requestConsent(preset: 'banking_ksa', channels: const CFChannelConsent(push: true, sms: false));
# 1. Read the preset: the key alone, no device header needed.
curl https://api.contentflow.click/sdk/v1/consent/config/banking_ksa -H "X-CF-Key: $CF_SDK_KEY"

# 2. Record what the person accepted: channels, marketing and topics in one write.
curl -X POST https://api.contentflow.click/sdk/v1/consent/full \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\",\"preset\":\"banking_ksa\",\"channels\":{\"push\":true,\"sms\":false}}"

Contracts: GET /consent/config/{preset} and POST /consent/full.

Identity

Sign-out

logout() (POST /logout) unlinks the user from this install and removes the device's push token from the profile. Channel consent is kept. Local identity is cleared only after the server confirms, so a failed call can be retried. Never share one client between different people's requests on a server.

// 1. On sign-out: unlink the user and remove the push token.
//    A 207 PUSH_LINKAGE_INCOMPLETE throws a partial-write error: retry logout.
await client.logout();
import { useCF } from '@contentflow/sdk/react';

function SignOutButton() {
  const cf = useCF();
  // 1. On sign-out: unlink the user and remove the push token.
  //    A 207 PUSH_LINKAGE_INCOMPLETE throws a partial-write error: retry logout.
  return <button onClick={() => cf.logout()}>Sign out</button>;
}
import { Button } from 'react-native';
import { useCF } from '@contentflow/sdk/react-native';

function SignOutButton() {
  const cf = useCF();
  // 1. On sign-out: unlink the user and remove the push token.
  //    A 207 PUSH_LINKAGE_INCOMPLETE throws a partial-write error: retry logout.
  return <Button title="Sign out" onPress={() => cf.logout()} />;
}
// 1. On sign-out: unlink the user and remove the push token.
//    A partial push clear throws partialLogout: retry logout.
try await client.logout()
// 1. On sign-out, from a coroutine: unlink the user and remove the push token.
client.logout()
final cf = ContentFlow.instance;

// 1. On sign-out: unlink the user and remove the push token.
//    Throws on failure and leaves local state unchanged, so retry.
await cf.logout();
# 1. On sign-out: unlink the user and remove the push token.
curl -X POST https://api.contentflow.click/sdk/v1/logout \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" -H 'Content-Type: application/json' \
  -d "{\"deviceId\":\"$CF_DEVICE\"}"
Right to erasure

Erasure

deleteIdentity() (DELETE /identify) erases everything the device id addresses: the device record, its linked user profile, KYC, location history, geofence events, enrichment and analytics events. Analytics collection and location stop, and unsent events are discarded, before the request. Local identity, cache and consent are cleared only after the server confirms a complete erasure; otherwise the call throws and keeps the device id so you can retry.

// 1. Erase this device and its linked user's data.
//    Throws unless the server confirms a complete erasure; retry on failure.
await client.deleteIdentity();

// 2. On success the client is disposed: create a new one (new device id) if the app continues.
import { useState } from 'react';
import { CFProvider, useCF } from '@contentflow/sdk/react';

function EraseButton({ onErased }: { onErased: () => void }) {
  const cf = useCF();
  async function erase() {
    // 1. Erase this device and its linked user's data.
    //    Throws unless the server confirms a complete erasure; retry on failure.
    await cf.deleteIdentity();
    onErased();
  }
  return <button onClick={erase}>Delete my data</button>;
}

export function App() {
  // 2. On success the client is disposed: a new key remounts CFProvider with a new client (new device id).
  const [session, setSession] = useState(0);
  return (
    <CFProvider key={session} config={{ publicKey: CONTENTFLOW_SDK_KEY }}>
      <EraseButton onErased={() => setSession((n) => n + 1)} />
    </CFProvider>
  );
}
import { useState } from 'react';
import { Button } from 'react-native';
import { CFProvider, useCF } from '@contentflow/sdk/react-native';

function EraseButton({ onErased }: { onErased: () => void }) {
  const cf = useCF();
  async function erase() {
    // 1. Erase this device and its linked user's data.
    //    Throws unless the server confirms a complete erasure; retry on failure.
    await cf.deleteIdentity();
    onErased();
  }
  return <Button title="Delete my data" onPress={erase} />;
}

export function App() {
  // 2. On success the client is disposed: a new key remounts CFProvider with a new client (new device id).
  const [session, setSession] = useState(0);
  return (
    <CFProvider key={session} config={{ publicKey: CONTENTFLOW_SDK_KEY }}>
      <EraseButton onErased={() => setSession((n) => n + 1)} />
    </CFProvider>
  );
}
// 1. Erase this device and its linked user's data.
//    Throws when the server cannot confirm the erasure; retry on failure.
try await client.deleteIdentity()

// 2. On success, local identity, consent and cache are cleared.
// 1. Erase this device and its linked user's data.
//    Throws if the server cannot confirm the erasure; retry on failure.
client.deleteIdentity()

// 2. On success, local identity, cached segments and cached content are cleared.
final cf = ContentFlow.instance;

// 1. Erase this device and its linked user's data.
//    Throws (CFRequestException on a partial erasure) unless the server
//    confirms a complete erasure; local state is kept, so retry.
await cf.deleteIdentity();

// 2. On success, local data is cleared and the SDK starts over with a new device id.
# 1. Erase everything the device id addresses. The device id is read only from X-CF-Device.
curl -X DELETE https://api.contentflow.click/sdk/v1/identify \
  -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE"

# 2. Clear the device id on the client only after a 200; otherwise retry with the same id.
deleteIdentity(): the right to erasure
  1. The SDK sends the device id it is erasing.

    If this step is skipped: Without the X-CF-Device header the request is refused with 400 X-CF-Device header is required for identity deletion.

  2. ContentFlow deletes the device record.
  3. The profile, KYC records, location history, geofence events and enrichment for this device or user.
  4. Analytics events for the device.
  5. The device's popup frequency counters.
  6. Only on full success does the SDK wipe its local storage and dispose the client.

    If this step is skipped: If some parts failed, ContentFlow answers 207 with localWipeSafe: false and the parts still remaining, but only to an SDK that declares the erasure-partial-v1 capability, which promises to keep its local id on a 207. Every other SDK gets 502 instead, as it does when nothing was erased or the outcome is unknown, because an SDK without the capability may treat any 2xx as a completed erasure (Flutter 2.2.2 and earlier do). Either way the device record is kept so a retry can finish, and the SDK keeps its local id and data until a full success. Every SDK declares the capability from 2.3.0; earlier releases get the 502.

Partial erasure (207): success: false and localWipeSafe: false mean some data remains. Keep the device id, because it is the only key the server can use to reach what is left, and retry. Until the erasure completes, identify for that device answers 409 with code ERASURE_PENDING: retry deleteIdentity() first.

After an erasure: the erasure is recorded for audit, and the person's profile is not recreated, from this device or any other, until consent is granted again: an identify with consent: true sent after the erasure. Until then identify stores nothing about them (no user link, traits or profile) and reports results.identity as {applied: false, status: "erased"}. Only call identify with consent: true when the person has actually agreed again.

Saudi PDPL

PDPL notes

  • Nothing by default. Fresh installs are analytics-denied, and no identify call can grant consent. Over REST, omitting consent keeps the previous decision. The JavaScript, Swift and Kotlin SDKs omit it until the app records a choice, so a launch cannot silently revoke consent granted on another surface; Flutter always sends its last decision, false until you set it.
  • Every decision is a record. Each one is stamped with when it was made, how it was collected (app, web, SDK, import) and its legal basis (consent, contract, legitimate interest).
  • Suppression (bulk, from the dashboard or POST /users/bulk/suppress) switches marketing and the four outbound channels off while remembering the previous settings; unsuppress restores them.
  • PDPL opt-out blocks every outbound channel; sends are skipped with pdpl_opt_out. Neither suppression nor opt-out deletes data; for that, use erasure.
  • Retention. Events are kept for 365 days; erasure deletes a device's events. See Analytics: privacy and retention.
Troubleshooting

When it doesn't work

What you seeWhy, and the fix
Every event is dropped with consent_not_grantedAnalytics consent was never granted. Call setConsent(true) (Swift and Kotlin: setAnalyticsConsent) from your consent UI.
Channel consent refused with 403 DEVICE_NOT_IDENTIFIED or USER_MISMATCHThe device is not identified as that user. Call setUserId or identify(userId:) first. Flutter throws StateError and Kotlin InvalidConfiguration before sending.
400 CHANNELS_REQUIREDChannel flags were sent at the top level of a REST body. Nest them under channels.
409 DEVICE_ENVIRONMENT_MISMATCHThe device was first identified with the other environment's key. Use a separate install or a fresh device id for test builds.
409 DEVICE_STATE_CHANGEDThe device's identity changed while the request was in flight. Re-identify, then retry.
429 USER_RATE_LIMITEDMore than 10 consent changes for one user in an hour, counted across /consent and /consent/full. Wait for the Retry-After seconds, and don't write consent on every screen view.
getConsentConfig fails with an unknown presetThe name is not one of the valid presets (the JavaScript SDK throws CFUnknownConsentPresetError, the route answers 404). Use a name from the table above.
deleteIdentity() throwsThe server could not confirm a complete erasure (a 502, or a partial 207). The device id and local data are kept; retry the call.

Next