ContentFlow developer docs
Server-driven content blocks for React Native, iOS, Android, and Web. Define block types in code, render them natively, then publish, localize, and roll back over the REST API, without an app release.
GET /sdk/sync, GET /sdk/blocks/:key and GET /sdk/strings all answer 200 with your workspace's live content when the only header you send is X-CF-Key: no device id, no identify call, no traits, no profile, nothing stored. If your app is privacy-sensitive, read Anonymous, content-only mode before you write a line of client code.Quickstart
Authenticate, sync, and render your first block over REST. About five minutes once you hold a workspace seat and a key.
Authentication
Workspace keys, the _app vs _test suffix, X-CF-Key, X-Tenant-Id, X-CF-Device.
Blocks & fields
The seven field types including image and collection, tag addressing, registering a block type.
Localization
String extraction, auto-translate, review-gated delivery via /sdk/strings.
Campaigns & webhooks
Segment dispatch, delivery channels, event subscriptions, audience gotchas.
REST API reference
Every /sdk/* endpoint, the response envelope, ETag caching, locale negotiation, errors.
Swift SDK
iOS, macOS, tvOS, watchOS. SwiftUI slots, renderers, caching, consent. Local package today.
Kotlin SDK
Android. Compose slots, typed field keys. Source only, never compiled yet.
Architecture wiki
Gateway and services, multi-tenancy, content lifecycle, glossary, FAQ.
Quickstart
Once the prerequisites below are met, the coding part takes about five minutes: you authenticate with a workspace key, sync live blocks over REST, and render one on screen. Getting the workspace, the seat, and the first published block is the part that is not five minutes, and it is not something this page can do for you.
0 · Before you start
The whole quickstart is gated on access you may not have yet. Check all four of these before you open an editor.
- A workspace and a seat in it. ContentFlow is workspace-scoped, and there is no self-serve path from this page. If you do not already belong to a workspace, the person who administers it has to invite you. Nothing below works without that.
- The SDK key and the tenant id. Both are visible to any workspace member at Dashboard, Developers, "Your keys" on app.contentflow.click: the Developers page, Overview tab, a card headed "Your keys" with rows for Base URL, Tenant ID, SDK key, Write key, and Read key.
- The write key and the read key, if you need them. They are shown in that same card to admin and editor roles only. On a viewer seat the card tells you to ask a workspace admin, and no amount of API calling changes that: the seed and read endpoints are the ones that need those keys, so a viewer cannot complete them alone.
- At least one block type registered and one live instance.
GET /sdk/syncanswers200for a brand new workspace, butdata.blockscomes back as an empty array until content exists. An empty array is a correct answer, not a failure, so do not spend the afternoon debugging your headers over it.
1 · Install
Nothing is published yet: no ContentFlow SDK can be installed from a package registry today. @contentflow/sdk, @contentflow/cli, @contentflow/cache, @contentflow/react-native and the bare name contentflow all return 404 from npm, and Maven Central holds zero artifacts under the group click.contentflow.
There is also nothing to vendor. There is no public repository URL, no downloadable tarball, no CDN build, and no Maven coordinate, and cdn.contentflow.click does not resolve. Where this site says a package is "usable only from a local or vendored copy" it means a copy held inside ContentFlow, not an object you can go and fetch. If you do not already have the source on disk, there is nothing to look for, so stop looking.
Hand writing a REST client is the intended and supported path today, on every platform including React Native, and it is not a workaround. The rest of this page and the full REST reference document that path end to end: the headers, the response envelope, ETag caching, locale negotiation, and every error code.
# npm install @contentflow/sdk → 404, not published # yarn add @contentflow/sdk → 404, not published # no git URL, no tarball, no CDN, no Maven coordinate either
React Native. There is no React Native package to wait for, and you do not need one. The supported path today is exactly the REST calls shown on this page: plain fetch over HTTPS, no native module, no linking, no pod install, no Gradle change. The only platform specific pieces are supplying a stable device id string and persisting it yourself between launches, and you only need that for the calls that are about a device (identify, register-push, events, track-event, consent). Fetching content needs neither.
2 · Initialize
Grab a workspace key from Dashboard, Developers, "Your keys". X-CF-Key is the only header a /sdk/* call actually requires, because the key already names the workspace. X-Tenant-Id is optional and must agree with the key if you send it, and X-CF-Device is needed only by the calls that are about a device: identify, register-push, events, track-event, consent.
2a · Use REST (Recommended)
Call the REST endpoints directly using standard HTTP headers. This is the canonical, working integration path.
$ curl https://app.contentflow.click/api/v1/sdk/sync \ -H "X-CF-Key: ws_a1b2c3d4_app" \ -H "X-Tenant-Id: ws_a1b2c3d4" \ -H "X-CF-Device: dev_9f2c41"
2b · Content only, with no device id and no identify
Fetching content is a read. If you do not need segment targeting, per-user analytics, or messaging, the snippet below is the entire integration, and it collects nothing about anybody.
$ curl "https://app.contentflow.click/api/v1/sdk/sync?locale=ar" \ -H "X-CF-Key: ws_a1b2c3d4_app"
GET /sdk/sync, GET /sdk/blocks/:key and GET /sdk/strings all answer 200 this way. No identify call, no persisted device id, no traits, no profile, and on a live _app key no write of any kind. You give up segment-targeted instances (an unidentified device resolves to no segments, so it receives the untargeted, global ones), engagement analytics, messaging campaigns, and per-device A/B assignment. You gain nothing to declare as a persistent identifier and nothing stored to retain or delete.
2c · Use an SDK (unpublished)
The Web and React Native packages are written and tested but unpublished, so the import below resolves only from a copy you already hold on disk. There is no registry entry, repository URL or tarball to obtain one from, so this path is not open to you unless ContentFlow has handed you the source. On iOS the Swift SDK builds today as a local package; on Android the Kotlin SDK is source that has never been compiled. Check the availability table before you plan around any of them.
// @contentflow/sdk is not on npm, this resolves only from a local copy // import { ContentFlow } from '@contentflow/sdk'
3 · Sync & render over REST
This runs today, as written, with no ContentFlow package imported. It is plain JavaScript and fetch, so the same file works in React Native, on the web, and in Node 18 or newer. It takes its two inputs, the key and a device id, from the environment, because neither belongs in the file as a literal.
Read the cfGet helper before you copy the rest of it. Four different failures and one empty workspace all come back as valid JSON, and without a status check and an envelope check they are indistinguishable from "there is simply no content yet". This snippet checks both, throws on all four failures, and sets a non-zero exit code, instead of rendering nothing and reporting success.
const BASE = 'https://app.contentflow.click/api/v1' // Both are inputs, not constants. A committed key cannot be rotated without an // app release, and a hard-coded device id merges every install into one device. function readConfig(name) { const v = globalThis.process?.env?.[name] if (!v) throw new Error('ContentFlow: ' + name + ' is not set') return v } const KEY = readConfig('CF_SDK_KEY') // shaped ws_a1b2c3d4_app const DEVICE = readConfig('CF_DEVICE_ID') // one stable string per install, persisted by you // Every /sdk/* reply is enveloped: { success, data }. Check the HTTP status AND // the envelope. Skipping either turns 401, 403, 404 and a wrong base URL into an // empty array indistinguishable from a workspace with nothing published yet. async function cfGet(path) { const res = await fetch(BASE + path, { headers: { 'X-CF-Key': KEY } }) const body = await res.json().catch(() => null) if (!body || typeof body.success !== 'boolean') { // No success member means this did not come from the current gateway. throw new Error('ContentFlow: unenveloped reply from ' + BASE + path + ' (HTTP ' + res.status + '). Check your base URL.') } if (!res.ok || !body.success) { const e = body.error || {} throw new Error('ContentFlow: ' + path + ' failed, HTTP ' + res.status + ' ' + (e.code || 'UNKNOWN') + ': ' + (e.message || '')) } return body.data } // Replace with your own renderer. Defined here so the file runs as written. function renderYourHero(title) { console.log('hero title:', title) } async function showHero() { const data = await cfGet('/sdk/sync') // throws on every failure above const blocks = data.blocks || [] // Reaching this line means the call genuinely succeeded, so an empty list is // a real answer: no live instance of this block, not a broken integration. const hero = blocks.find((b) => b.key === 'home_hero') if (!hero) { console.warn('ContentFlow: sync succeeded, no live home_hero instance') return } // values is keyed by field tag, never by index or order renderYourHero(hero.values['#title_main']) const ev = await fetch(BASE + '/sdk/events', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CF-Key': KEY, 'X-CF-Device': DEVICE }, body: JSON.stringify({ events: [{ type: 'impression', instanceId: hero.instanceId }] }) }) // Analytics must never break rendering, so warn here rather than throw. if (!ev.ok) console.warn('ContentFlow: impression not recorded, HTTP ' + ev.status) } showHero().catch((err) => { console.error(err.message) if (globalThis.process) globalThis.process.exitCode = 1 })
Each delivered block carries key, name, screen, instanceId, segment, version, values and fields. Read content out of values by field tag; fields describes the shape those tags belong to. The impression is posted against instanceId, which identifies the exact instance you rendered, not the block key.
blocks array, a quiet return and exit code 0. Four of them are failures.
| What you sent | What comes back | Unchecked | Checked, as above |
|---|---|---|---|
| A key naming a workspace that does not exist | 404 TENANT_NOT_FOUND | renders nothing, exit 0 | throws, names the code, exit 1 |
| A base URL pointing at the retired host | 404, and a body with no success member | renders nothing, exit 0 | throws, names the base URL, exit 1 |
| No key at all | 401 MISSING_SDK_KEY | renders nothing, exit 0 | throws, exit 1 |
| A key the gateway cannot parse | 401 INVALID_SDK_KEY | renders nothing, exit 0 | throws, exit 1 |
| A correct key, workspace with nothing published | 200, blocks: [] | renders nothing, exit 0 | warns, exit 0, which is the right answer |
CF_DEVICE_ID is one stable string per install, and persisting it is your jobThere is no correct literal value for it, which is why this snippet refuses to start without one. A constant shared by every install collapses your whole audience onto a single device. A value regenerated on each launch turns one person into a new device every time the app opens. Generate it once on first run, store it in AsyncStorage, the Keychain, SharedPreferences or localStorage, and read it back on every launch. It is needed only by the calls that are about a device: identify, register-push, events, track-event and consent. Fetching content needs none of them, and step 2b shows that path.X-CF-Read-Key, shaped rk_live_ plus 24 lowercase hex characters. Three routes exist: GET /api/v1/read/users/{userId}/devices, GET /api/v1/read/devices/{deviceId} and GET /api/v1/read/users/{userId}/segments. Use them from a server or a CI job to check what a given user or device actually resolved to; never ship a read key in client code. Full contract in the REST reference.In React, if you are running a local copy of the unpublished SDK, use the hooks instead. CFBlock is a TypeScript type describing a delivered block, not a component, so there is nothing to place in JSX.
import { CFProvider, useCFBlock } from '@contentflow/sdk/react' function Hero() { const hero = useCFBlock('home_hero') // auto-tracks the impression if (!hero) return null return <YourHero title={hero.values['#title_main']} /> }
cf.handshake() after init. That method does not exist, in this SDK or any other, so nothing written against it ever ran. To confirm your key, workspace, and environment resolve before you ship, call GET /sdk/sync with your key and check that the response is enveloped ({ success, data }) and carries data.blocks. That is the same check the SDK itself performs, and it doubles as the base-URL check.Authentication
Every /sdk/* request carries a short list of headers, and only one of them is required. There is no OAuth flow and no session to keep alive.
| Header | Required | Meaning | Format |
|---|---|---|---|
X-CF-Key | Yes | Publishable workspace key, the only credential the delivery API needs | <workspaceId>_app or <workspaceId>_test |
X-API-Key | No | Accepted alias for X-CF-Key on /sdk/*. Supported today and not deprecated. Send one or the other: if both are present X-CF-Key wins outright, see the precedence note below | Same key format as X-CF-Key |
X-Tenant-Id | No | Workspace id. Optional and redundant on /sdk/*, since the key already names the workspace. It is required on portal JWT calls | If sent, must match the workspace bound to the key, or 403 TENANT_KEY_MISMATCH |
X-CF-Device | No | Stable device id you generate. Needed only by identify, register-push, events, track-event and consent | Any stable opaque string, persisted per install |
The key goes in a header, with exactly one exception. ?key= in the query string is accepted on GET /sdk/stream only, because EventSource cannot set headers. On any other /sdk/* route it is refused with 401 SDK_KEY_IN_QUERY. Details for both this and X-API-Key are in Authentication in the REST reference.
X-CF-Key wins silentlyWhen a request carries X-CF-Key and X-API-Key naming two different workspaces, the workspace is taken from X-CF-Key, X-API-Key is ignored, and nothing in the response says a second key was sent. The precedence is positional, not quality-based: a malformed X-CF-Key alongside a valid X-API-Key answers 401 INVALID_SDK_KEY rather than falling back to the good one. This matters if you run a shared networking layer with a legacy header injector, because that is how a staging key ends up masking a production one with no error anywhere. Send one header. Check it both ways yourself:
$ curl -i https://app.contentflow.click/api/v1/sdk/sync \ -H "X-CF-Key: ws_alpha9_app" -H "X-API-Key: ws_beta7_app" # 404 Tenant not found: ws_alpha9 ← the X-CF-Key one $ curl -i https://app.contentflow.click/api/v1/sdk/sync \ -H "X-CF-Key: ws_beta7_app" -H "X-API-Key: ws_alpha9_app" # 404 Tenant not found: ws_beta7 ← still the X-CF-Key one
GET /sdk/sync, GET /sdk/blocks/:key and GET /sdk/strings need no device id and no prior identify. The examples on this page send all three headers because most integrations are personalized; that is a convention, not a requirement. See Anonymous, content-only mode.Environment comes from the key suffix. A key ending in _test resolves to the test environment and sees live plus test-staged content; anything else is live. Nothing in a request body or header can flip that.
$ curl https://app.contentflow.click/api/v1/sdk/sync \ -H "X-CF-Key: ws_a1b2c3d4_app" \ -H "X-Tenant-Id: ws_a1b2c3d4" \ -H "X-CF-Device: dev_9f2c41"
cf_live_ and cf_test_ examples resolve to a workspace that does not existThis site used to show keys shaped cf_live_… and cf_test_…. Here is what actually happens to them. The server strips a known _app or _test suffix; when neither is present it falls back to the text before the last underscore. So cf_live_xxx resolves to a workspace named cf_live, which does not exist, and the answer is 404 {"code":"TENANT_NOT_FOUND","message":"Tenant not found: cf_live"}. 401 INVALID_SDK_KEY is a different failure: it means no workspace id could be parsed out of the key at all, which happens when the key has no usable underscore, that is, no underscore anywhere after the first character, and no _app or _test suffix with at least one character in front of it. nounderscore, app, _, _app and _test all land there. __app does not: it leaves the workspace id _, which parses, so it answers 404 Tenant not found: _. cf_test_xxx also does not end in _test, so it would not select the test environment either. Copy your real key from the dashboard rather than adapting an old snippet. See Authentication in the REST reference.Core concepts
- Block type, a UI shape defined in code (banner, card, tile, hero, list, carousel, popup, tooltip, toast, panel), with a set of typed fields. Registered over the REST API today; the CLI that will version types in git is not published yet.
- Block instance, filled content of a given type, managed in the dashboard or seeded over the API with a stable
externalId. - Sync, the SDK pulls the live set of instances for your tenant. Idempotent and cache-friendly.
- Channel, where a block ships: push, WhatsApp, SMS, popup, or in-app.
- Segment, the audience a block or campaign targets.
See the Wiki glossary for the full vocabulary.
SDK · availability
Not one of these packages can be installed from a registry today. They are at genuinely different stages, and the differences matter more than the word "preview" ever conveyed, so the table says what each one actually is. The REST API is the canonical integration path for every platform until that changes.
| Platform | Package | What is actually true today |
|---|---|---|
| Web / React | @contentflow/sdk | Written and tested. Not published: npm returns 404 for the name. Usable only from a local or vendored copy. |
| React Native | @contentflow/react-native | Written and tested. Not published: npm returns 404 for the name. Usable only from a local or vendored copy. |
| iOS (Swift) | ContentFlow (SPM) | The package exists, builds cleanly, and passes its 146 tests, collection decoding included. Not published: no public Git URL and no SPM tag, so .package(url:from:) will not resolve. Consumable today as a local or vendored package. See the Swift SDK reference. |
| Android (Kotlin) | click.contentflow:* | Source exists and the tests are written, but the package has never been compiled: no JVM on the machine it was authored on, so zero build verification and zero test runs. Not on Maven Central, and the build declares no publishing at all. Weaker than the Swift package and not something to plan an Android release around. See the Kotlin SDK reference. |
Sync & render
sync() pulls every live instance for the tenant behind your key. The SDK strips unsupported url() wrappers from image values and normalizes field types, so what you render matches what you defined.
cf object it calls methods on cannot be constructed today. Read it as what the SDK will expose, and to check a method name against something real, but do not write code against it expecting it to run. The supported integration path today is REST: see the quickstart snippet above and the full REST reference.await cf.sync() // the winning instance for a key, from what sync already fetched cf.getBlock('home_hero') // every delivered instance of that key cf.getBlocks('card') // everything currently held cf.getAll() // fetch one block on demand, without a full sync await cf.fetchBlock('home_hero') // a collection field's cards, as whole blocks cf.getBlock('home_hero')?.getCollection('#slides') // subscribe to live updates (no redeploy) const stop = cf.subscribe((blocks) => rerender(blocks))
subscribe takes a listener and returns an unsubscribe function. It takes no event name.
cf.block(), cf.blocksOfType() and cf.on() were never realThis section previously showed all three, plus cf.on('content.published', …) as a client subscription. None of them exists in the SDK. The replacements are above: getBlock, getBlocks, and subscribe. content.published is a webhook event delivered to a server endpoint you register, not something a client listens for.Localization
ContentFlow extracts strings from your blocks, auto-translates them, and delivers only review-approved translations. Nothing half-translated reaches production.
cf.getStrings(namespace?), fetch approved strings (served from/sdk/strings). The locale comes from the client's configured locale, set at construction or withcf.setLocale(locale). The optional argument is a namespace, not a locale.cf.t(key, fallback?)reads one string from the loaded catalog,cf.getAllStrings()returns the whole map, andcf.subscribeStrings(listener)re-fires when the catalog refreshes. In React:useCFTranslations(namespace?)anduseCFString(key, fallback?).- Source language shows first in the dashboard; English is shown as reference when a source locale is selected.
- Hidden strings are soft-hidden, kept but never delivered.
cf.strings(locale) does not existThis page showed it for a long time. The real call is cf.getStrings(namespace?), and it takes a namespace rather than a locale, so code written against the old signature would have quietly requested a namespace named "ar" and received an empty catalog. Set the locale on the client with setLocale, or send ?locale= if you are calling /sdk/strings over HTTP.ok, which the dashboard displays as "Approved." The only other values are review and missing - there is no approved status anywhere in the API or the database. Only strings with status ok ship to /sdk/strings; draft and machine-translated strings are stored as review and stay in the dashboard.Sensors
Runtime Product Intelligence: turn existing product language into observation points. Every localization string is a dormant sensor. It renders normally and stays quiet until you enable observation remotely, with one toggle and no app release for activation. Enabled sensors emit consent-aware string_impression and string_interaction signals. Saved views are optional groupings for investigation across a surface, journey, or copy watch. Sensor analytics in the dashboard include Views, Interactions, Engagement rate, Reach, and Live now. Enabling sensors and managing saved views is free, analytics reads are available on Pro and Enterprise plans.
How it works
- Dormant sensors: every localization key is available as an observation point, but sends no sensor telemetry until observation is enabled.
- Enabled sensors: enabled keys are delivered as
smartKeysfromGET /api/v1/sdk/strings. Older integrations can ignore this field. The response ETag changes when the enabled sensor set changes. - Signals: send
string_impressionwhen an enabled string is used or viewed, andstring_interactionwhen the user clicks or taps marked copy. Saved views can group enabled strings for investigation, but are not required to start observation.
Emit signals over REST
The REST API is the canonical integration path today. Pass the same workspace, tenant, and device headers used for block events. Signals are accepted only when device-level analytics consent is granted.
$ curl -X POST https://app.contentflow.click/api/v1/sdk/events \ -H "Content-Type: application/json" \ -H "X-CF-Key: ws_a1b2c3d4_app" \ -H "X-Tenant-Id: ws_a1b2c3d4" \ -H "X-CF-Device: dev_9f2c41" \ -d '{ "consent": true, "events": [ { "type": "string_impression", "key": "checkout.title", "locale": "en", "source": "usage", "path": "/checkout" } ], "context": { "sessionId": "sess_x", "platform": "web" } }'
With the unpublished Web SDK
This path needs the @contentflow/sdk package, which is not on npm (see SDK availability), so it applies only if you are running a local copy. There, t() emits usage impressions automatically for enabled smartKeys. Viewport impressions and click interactions additionally require host markup with data-cf-t. Viewport tracking uses IntersectionObserver when at least 50% of the element is visible.
// @contentflow/sdk, local copy only, not on npm const title = cf.t("checkout.title") // emits a usage impression for enabled keys return ( <h1 data-cf-t="checkout.title"> {title} </h1> ) // data-cf-t enables viewport impressions and click interactions. // Old SDKs ignore smartKeys and emit no string signals.
smartKeys array delivered on the next strings refresh. It does not make your app emit anything by itself. If your code never calls t(key) for that key and never renders an element with data-cf-t="key", the sensor stays at zero forever, and that is expected, not a bug. The same happens if the key's namespace does not match the SDK session's active namespace: getStrings() only ever returns smartKeys for the namespace it was called with, so an enabled key in another namespace never reaches t() or data-cf-t at all.Signal types
| Type | When to send | Required fields |
|---|---|---|
string_impression | An enabled string is used in code or becomes visible in the UI. | key, locale, source as usage or viewport, optional namespace, path |
string_interaction | A user clicks or taps UI copy marked with data-cf-t. | key, locale, optional namespace, path |
:id, paths are lowercased, and values are capped at 128 characters.CLI · cards-as-code
Built, not published: the @contentflow/cli package is written but is not on npm, where both @contentflow/cli and the bare name contentflow return a 404. There is no public repository URL, no tarball and no CDN build either, so there is nothing to vendor and nothing to fetch. Nothing below runs today. You do not need it to ship: a block type can be registered today over the REST API, which is what the CLI will call anyway. See registering a block type.
# npx contentflow login → not on npm, 404 # npx contentflow init → not on npm, 404 # npx contentflow push → not on npm, 404 # no git URL and no tarball, so npx has nothing to resolve
Block types
Every dynamic block picks a design type. The renderer and dashboard badge follow the type automatically.
banner
Full-width strip.
card
Boxed content unit.
tile
Compact grid item.
hero
Large lead unit.
carousel
Swipeable set.
list
Stacked rows.
popup
Modal overlay.
tooltip / toast
Transient hints.
panel
Docked container.
The design type decides the shape. What a block holds is its fields: text, textarea, image, number, select, toggle, and collection. The first six are scalar. A collection holds an ordered list of other block instances, which delivery inlines in full under the field's tag, so a carousel or list is a wrapper around real card instances rather than one instance holding an array of values. Fields are addressed by tag, never by index or order. Blocks & fields covers every type, the block definition shape, how to register a type over REST, collections end to end, and a worked image-background banner.
repeater field type this replaces was withdrawn and never shipped.REST API
Base URL https://app.contentflow.click/api/v1. All requests are JSON and authenticated with the X-CF-Key header; X-Tenant-Id is optional and redundant on /sdk/*. Below are the most-used endpoints; the full REST reference documents every /sdk/* endpoint, anonymous content-only mode, the response envelope, ETag caching, locale negotiation, portal API differences, and errors.
https://app.contentflow.click/api/v1. The old https://api.contentflow.click/v1 host was never switched off: it still returns 200 with real-looking, frozen content to legacy keys, in the old unenveloped format, so nothing you publish ever reaches an app pointed at it and nothing errors. Do not test your base URL by checking for a 404, test that the response body is enveloped. See Base URL in the REST reference.Blocks
X-CF-Device and no prior identify is needed. An unidentified caller resolves to no segments and receives the untargeted, global instances, which is a valid answer rather than an error. See Anonymous, content-only mode.X-CF-Key. POST /cards/sync instead authenticates with a confidential X-CF-Write-Key header. Copy yours from the dashboard at Dashboard, Developers, "Your keys" - visible to admin and editor workspace roles only; a viewer role is told to ask a workspace admin instead of being shown the key. Never ship this key in client code; call /cards/sync from a server or CI script.An empty or malformed manifest is rejected with 400. To archive every block for a tenant in one call, pass "prune": true together with an empty blocks array, an empty array on its own is treated as malformed and rejected.
// POST /cards/sync, archive all blocks { "prune": true, "blocks": [] }
Audience
POST /sdk/consent manages messaging channels only: push, sms, whatsapp, email, locationTracking, marketing. Analytics consent is the consent boolean on /sdk/identify. Neither call affects the other, and both answer 200, so an analytics toggle wired to /sdk/consent silently records nothing. A single privacy screen has to write to both. See Consent & PDPL.Forwarding from identify to Audience and segmentation is asynchronous (fire-and-forget). A successful response means the ingest service accepted the update, not that every Audience count has refreshed yet.
Consented identifies (consent: true) typically show up in Audience People within a few seconds (target: p95 one minute). Segment reach updates on the segment's refresh schedule (Daily by default), or immediately via the segment's Refresh now action, not on identify.
consent: true, without it ContentFlow keeps the previous consent state and never stores traits pre-consent; (2) forwarding is asynchronous, allow up to a minute for propagation (segment reach additionally waits for the segment's scheduled or manual refresh); (3) without a userId the device is stored as anonymous presence, not a known profile.userId appears in the Anonymous tab: first seen, last seen, platform, and consent state only, under a pseudonymous id. Devices without granted consent never get traits stored, and non-consented visitors are purged after about 90 days of inactivity. Supplying a userId with granted consent upgrades the same record to a known profile in place, nothing is lost.Strings / Localization
GET /sdk/strings never reads a device at all, so X-CF-Device is ignored and identify is irrelevant to it. A key is the whole request.X-CF-Write-Key header, same Dashboard, Developers, "Your keys" location, same admin/editor-only visibility as /cards/sync above.Campaigns
Webhooks
Register an endpoint and subscribe to events to keep your systems in sync with what's live.
| Event | Fires when |
|---|---|
content.published | A block instance goes live |
content.rolledback | A block is reverted |
string.approved | A translation is review-approved |
campaign.sent | A campaign finishes dispatch |