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 blocks list means nothing is published for your key's environment yet. That is a correct answer, not an error.

Before you start

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:

Terminal
cd vendor
shasum -a 256 -c --ignore-missing SHA256SUMS-2.4.1.txt   # Linux: sha256sum -c; never install an archive that fails
Terminal
# Web
npm install ./vendor/contentflow-sdk-2.4.1.tgz
Terminal
# React: the same archive. React 17+ comes from your app; import from @contentflow/sdk/react.
npm install ./vendor/contentflow-sdk-2.4.1.tgz
Terminal
# 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.
Swift
// 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")])]
Kotlin
// 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
YAML
# 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-flutter

Nothing 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.

TypeScript
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
}
TSX
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;
}
TSX
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>
  );
}
Swift
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
}
Kotlin
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)
}
Dart
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
}
Terminal
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}'
fi

Every 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

Generated from the ContentFlow repository by sdk-release/scripts/render-docs.mjs. Downloads and checksums are on Releases and downloads. The SDKs are licensed under the ContentFlow SDK License.