Get started
Goal: see your first block in about 5 minutes. Show your first ContentFlow block in your app and record that it was seen. Pick your platform on any code sample below; the choice applies to the whole site.
In short
- Create a publishable key in the dashboard under Settings → API keys (
<workspaceId>_cfpk<40 hex>) and copy it when it is shown.- Download and verify the SDK archive for your platform, then install it from a local folder. There is no public package registry.
- Start the client, set analytics consent from the person's real choice, read a block by key and track an impression.
- An empty
blockslist means nothing is published for your key's environment yet. That is a correct answer, not an error.
Before you start
- A workspace and a seat in it. ContentFlow is workspace-scoped. If you don't belong to a workspace yet, ask its administrator to invite you.
- Your SDK key. In the dashboard, open Settings → API keys and create a publishable key (
<workspaceId>_cfpk<40 hex>); the full value is shown once. Every request carries it inX-CF-Key. The older<workspaceId>_appand<workspaceId>_testkeys are deprecated but still accepted unless your admin has disabled them. Developers → Your keys shows the write key and read key to admin and editor seats; those two are for your server only and must never ship in an app. Details: Keys in the REST guide. - Something published. Publish a block instance to Test in the dashboard. Until something is published for the key's environment,
GET /syncanswers with an emptyblockslist.
1. Install the SDK
Requirements per platform, checking the installed version, upgrading and rolling back: Install and upgrade the SDK.
The SDKs are archives on this site, not packages on npm, CocoaPods, Maven or pub.dev. Download the one for your platform and its checksum file from Releases and downloads into a folder you commit (this guide uses vendor/), and verify it before installing:
cd vendor
shasum -a 256 -c --ignore-missing SHA256SUMS-2.4.1.txt # Linux: sha256sum -c; never install an archive that fails# Web
npm install ./vendor/contentflow-sdk-2.4.1.tgz# React: the same archive. React 17+ comes from your app; import from @contentflow/sdk/react.
npm install ./vendor/contentflow-sdk-2.4.1.tgz# React Native and Expo: the same archive, plus storage. react-native-sse is optional (live updates instead of polling).
npm install ./vendor/contentflow-sdk-2.4.1.tgz @react-native-async-storage/async-storage react-native-sse
# React Native 0.72 to 0.78 need one Metro setting; 0.79+ and Expo SDK 53+ need none. See the React Native and Expo guide.// iOS 15+, macOS 12+. Extract contentflow-sdk-swift-2.4.1.zip without changing its layout.
// Xcode: File → Add Package Dependencies → Add Local, and pick the extracted folder. Or in Package.swift:
dependencies: [.package(path: "../Vendor/contentflow-sdk-swift-2.4.1")],
targets: [.target(name: "App", dependencies: [.product(name: "ContentFlow", package: "contentflow-sdk-swift-2.4.1")])]// minSdk 24, JVM 17. Extract contentflow-sdk-kotlin-2.4.1.zip; it is included as a composite build.
// settings.gradle.kts
includeBuild("vendor/contentflow-sdk-kotlin")
// app/build.gradle.kts
dependencies { implementation("click.contentflow:compose") } // or :android without Compose# Dart 3.5+, Flutter 3.16+. Extract contentflow-sdk-flutter-2.4.1.zip, then in pubspec.yaml:
dependencies:
contentflow_sdk:
path: vendor/contentflow-sdk-flutterNothing to install: the API is plain HTTPS at https://api.contentflow.click/sdk/v1. Go to step 2.
2. Show your first block
Nothing renders until a block is published for your key's environment and, for content targeted at a segment, analytics consent is granted. Untargeted "All users" content still shows without consent.
Every sample does the same four things: start the client, set analytics consent from the person's actual choice, read the block home_banner (use a key published in your workspace), and track an impression once it is on screen. Nothing is tracked until analytics consent is granted.
import { createCFClient } from '@contentflow/sdk/web';
const cf = createCFClient({ publicKey: CONTENTFLOW_SDK_KEY, locale: 'ar' });
await cf.start(); // cached paint, sync, strings, live updates
await cf.setUserId(currentUser.id); // after sign-in; skip for anonymous visitors
await cf.setConsent(userAcceptedAnalytics); // the person's actual choice
const banner = cf.getBlock('home_banner');
if (banner) {
showBanner(banner.get('title'), banner.imageUrl);
cf.trackImpression(banner); // once it is actually visible
}import { useEffect } from 'react';
import { CFProvider, useCF, useCFBlock } from '@contentflow/sdk/react';
export function App({ userId, analyticsConsent }: { userId?: string; analyticsConsent: boolean }) {
return (
<CFProvider config={{ publicKey: CONTENTFLOW_SDK_KEY, locale: 'ar' }}>
<SessionSync userId={userId} analyticsConsent={analyticsConsent} />
<Home />
</CFProvider>
);
}
function SessionSync({ userId, analyticsConsent }: { userId?: string; analyticsConsent: boolean }) {
const cf = useCF();
useEffect(() => { if (userId) void cf.setUserId(userId); }, [cf, userId]);
useEffect(() => { void cf.setConsent(analyticsConsent); }, [cf, analyticsConsent]);
return null;
}
function Home() {
const banner = useCFBlock('home_banner'); // re-renders on live updates, tracks one impression
return banner ? <Banner title={String(banner.get('title') ?? '')} /> : null;
}import EventSource from 'react-native-sse';
import { CFProvider, CFBanner } from '@contentflow/sdk/react-native';
// Identify and set consent exactly as in the React tab (SessionSync), imported from react-native.
export function App() {
return (
<CFProvider config={{ publicKey: CONTENTFLOW_SDK_KEY, locale: 'ar' }} eventSource={EventSource}>
<CFBanner blockKey="home_banner" render={(block) => <Banner title={String(block.get('title') ?? '')} />} />
</CFProvider>
);
}import ContentFlow
let client = ContentFlow.start(configuration: ContentFlowConfiguration(
apiKey: AppConfig.contentFlowKey,
tenantId: AppConfig.contentFlowWorkspace,
locale: Locale(identifier: "ar")
))
_ = try await client.identify(userId: currentUser.id) // after sign-in
try await client.setAnalyticsConsent(userAcceptedAnalytics ? .granted : .denied) // the person's actual choice
if let banner = try await client.fetchBlock(key: "home_banner") {
let title = try banner.value(for: FieldKey<String>.text("#title"))
showBanner(title)
_ = try await client.trackEngagement(.impression(banner)) // once it is actually visible
}import click.contentflow.ConsentStatus
import click.contentflow.ContentFlow
import click.contentflow.FieldKeys
// Application.onCreate
ContentFlow.start(this, apiKey = BuildConfig.CONTENTFLOW_KEY) // your publishable key (test environment in debug builds)
// After sign-in and from your consent UI, inside a coroutine
val client = ContentFlow.client
client.identify(userId = currentUser.id)
client.setAnalyticsConsent(ConsentStatus.of(userAcceptedAnalytics)) // the person's actual choice
client.fetchBlock("home_banner")?.let { banner ->
showBanner(banner.value(FieldKeys.text("#title")))
client.trackEngagement(type = "impression", blockKey = banner.key, instanceId = banner.instanceId)
}import 'package:contentflow_sdk/contentflow_sdk.dart';
final cf = await ContentFlow.create(ContentFlowConfig(
publicKey: const String.fromEnvironment('CONTENTFLOW_KEY'), // your publishable key, test environment while developing
locale: 'ar',
));
await cf.identify(userId: currentUser.id); // after sign-in
await cf.setConsent(userAcceptedAnalytics); // the person's actual choice
await cf.sync();
final banner = cf.getBlock('home_banner');
if (banner != null) {
showBanner(banner.title);
await cf.trackEngagement(type: CFEventType.impression, blockKey: banner.key); // once it is actually visible
}B=https://api.contentflow.click/sdk/v1
# 1. Content is a read: the key alone, no device id, nothing stored.
curl "$B/sync" -H "X-CF-Key: $CF_SDK_KEY"
# 2. Record the person's actual analytics choice for this install (one stable id you persist).
# Identify either way, so a refusal is recorded too. Never hard-code true.
CF_CONSENT=$(yourConsentChoice) # "true" only if they agreed, otherwise "false"
curl -X POST "$B/identify" -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" \
-H 'Content-Type: application/json' -d "{\"deviceId\":\"$CF_DEVICE\",\"consent\":$CF_CONSENT}"
# 3. Only the event is gated on consent. It answers 202 even when refused, so read
# "accepted" and "dropReasons" (device_not_identified, consent_not_granted), not the envelope.
if [ "$CF_CONSENT" = true ]; then
curl -s -X POST "$B/events" -H "X-CF-Key: $CF_SDK_KEY" -H "X-CF-Device: $CF_DEVICE" \
-H 'Content-Type: application/json' -d '{"events":[{"type":"impression","blockKey":"home_banner"}]}' \
| jq '.data | {accepted, dropped, dropReasons}'
fiEvery platform's full API is on its own page: JavaScript, React Native and Expo, Swift, Kotlin, Flutter, and the REST guide.
3. Test it
Develop with a test-environment key and publish the block you are testing to Test in the dashboard. Check the key with curl https://api.contentflow.click/sdk/v1/sync -H "X-CF-Key: $CF_SDK_KEY", turn on SDK logging, and confirm that an event is accepted. Per-platform logging switches and the full checklist are in Testing your integration.
When it doesn't work
| What you see | Why, and the fix |
|---|---|
401 with MISSING_SDK_KEY |
No key on the request. Send it in X-CF-Key. |
401 with INVALID_SDK_KEY |
The key has the wrong shape, belongs to an unknown or suspended workspace, or is a derived _app / _test key that your admin has disabled. Create or copy a publishable key under Settings → API keys rather than typing one. |
200 with blocks: [] |
Nothing is published for the key's environment (publish to Test for a test key), or nothing targets this device. It does not mean the workspace is missing: an unknown workspace answers 401 INVALID_SDK_KEY. |
A reply without success |
The base URL is not the ContentFlow API. The default is https://api.contentflow.click/sdk/v1; see Configuring the base URL. |
Events accepted: 0 with consent_not_granted |
Analytics consent was not granted. Set it from the person's choice before tracking. |
Events accepted: 0 with device_not_identified |
The device has not been identified yet. The SDKs do this in start(); over REST, call POST /identify first. |
Next
- Guides: blocks, campaigns, push, consent, analytics and the rest, one task per page.
- Blocks and fields: the field types a block holds and how to read them by tag.