Ground truth:
cometchat_calls_sdk5.0.7 from pub.dev + catalogflutter-calls-v5.json(the closed symbol list — 59 COMPILER-VERIFIED symbols; every type below is in it). Catalog built from the PUBLISHED package; at 5.0.7 it matches thecalls-coremonorepo exactly — but never infer that from a version string (references/docs-map.md§ "Upstream"). Official docs:/calls/flutter/**= v5 · Docs MCP. Fetch exactSessionSettingsfields, event names and signatures from the docs (references/docs-map.md) — never memory. ⚠️ The live pages carry ~56 symbols that DO NOT COMPILE, including the entire documented token flow — see "Doc corrections" below; prefer THIS skill's names over the page's. APPEND to the user's app — additive wiring only. This is the HEADLESS path (no UI Kit); for the prebuilt drop-in call UI usecometchat-flutter-v6-callsinstead.
Companion skills (read first)
- Standalone/headless entry — this skill owns its own package (the Calls SDK) and has no
-coresibling; self-contained for meet-style calling. - For 1:1 RINGING only it also drives the Chat SDK (
cometchat_sdkv5) for signaling — signatures fetched viareferences/docs-map.md(§ "1:1 RINGING"). It does NOT depend on the Flutter UI Kit. - Prefer the UI Kit instead? If the app also wants chat and a prebuilt call UI, use
cometchat-flutter-v6-core+cometchat-flutter-v6-calls— not this skill.
Use this skill when
"add calling from scratch / without the UI Kit", "standalone voice/video on Flutter", "build my own call screen", "headless calls SDK", "meeting-room join by session id", "one-on-one ringing". Precondition: the caller chose the from-scratch / standalone path (the "add calling" router asks this). If they want the prebuilt call UI, route to cometchat-flutter-v6-calls.
Prerequisites & install
yaml# pubspec.yaml dependencies: cometchat_calls_sdk: ^5.0.7 # verified against 5.0.7
then flutter pub get.
1:1 ringing additionally needs the Chat SDK (cometchat_sdk v5) for signaling.
Permissions — REQUIRED or the call dies on the first permission prompt.
android/app/src/main/AndroidManifest.xml:INTERNET,CAMERA,RECORD_AUDIO,MODIFY_AUDIO_SETTINGS,ACCESS_NETWORK_STATE. On API 23+ you must ALSO request camera + mic at runtime — the manifest alone is not enough, and this is the single most common "the SDK is broken" report on Android.ios/Runner/Info.plist:NSCameraUsageDescription+NSMicrophoneUsageDescription.- Android
minSdkVersion 26, iOSplatform :ios, '15.1'— the real 5.0.7 floors. The docs say 24 and iOS 12; both fail at BUILD time. iOS moved 13.0 → 15.1 in 5.0.7, so an app can break on a routinepub upgrade.
Credentials: App ID · Region · Auth Key (dev only), via the CLI — npx @cometchat/skills-cli@3 auth login → provision use --app-id <id> --json (@3 pins the CLI major that matches the v5 skills). Mint auth tokens server-side for production; never ship the Auth Key.
Init & login ordering (BAKED — invariant)
Init (once, must succeed) → CometChatCalls.login(...) → then generateCallToken / joinSession / listeners. Nothing joins before init+login resolve. CometChatCalls.isInitialized is the guard.
-
DEFAULT to
CometChatCalls.initFromSettings(onSuccess:, onError:)— the ai-agent / telemetry-attributed path (persistsintegrationSource="ai-agent").It takes no settings argument: it reads
cometchat-settings.jsonthroughrootBundle, so the file must sit at the project root and be registered as a Flutter asset:yamlflutter: assets: - cometchat-settings.jsonRoot keys
appId+region; optionalcallsSDK→adminHost/clientHost/callsHost. No Auth Key — the Calls SDK reads nothing else from this file (the key is alogin()argument). Registering it underflutter: assets:is the ONLY step; no Xcode or Gradle change, because it is read throughrootBundle. Miss theassets:entry and init fails with settings-file-not-found — a quiet callback, not a crash. -
Fallback (public-doc path):
dartCallAppSettings s = (CallAppSettingBuilder()..appId = appId..region = region).build(); CometChatCalls.init(s, onSuccess: (String m) {}, onError: (CometChatCallsException e) {});Note the Dart cascade (
..appId =) — these are fields, notsetX()methods.CallAppSettingBuilderis singular (nos). -
⚠️ Never do both. A builder init AFTER
initFromSettingsre-attributes the app to"manual"and the ai-agent telemetry is lost. -
Login:
CometChatCalls.login(uid:, authKey:, onSuccess:, onError:)(dev) orCometChatCalls.loginWithAuthToken(authToken:, onSuccess:, onError:)(production). Checkawait CometChatCalls.getLoggedInUser()first — it returnsFuture<User?>and the SDK keeps the session, so logging in twice is wasted work. BothonSuccesscallbacks hand you a NULLABLEUser?— the docs dereference it unguarded; do not copy that.
Two build modes (BAKED — the router picks one)
- Meet-style (session room) — Calls SDK ONLY, no Chat SDK. Everyone joining the same session id lands in the same call.
- 1:1 ringing — Chat SDK signals, Calls SDK carries media. Needs
cometchat_sdkv5 as well. Fetch signatures viareferences/docs-map.md§ "1:1 RINGING".
⚠️ The Flutter fork: joinSession returns a WIDGET
Every other platform mounts the call into a container you hand it. Flutter hands the call back to
you as a Widget? and mounts nothing:
dartWidget? _callWidget; CometChatCalls.joinSession( sessionId: sessionId, sessionSettings: settings, onSuccess: (Widget? w) => setState(() => _callWidget = w), // ← THIS is the render step onError: (CometChatCallsException e) { /* surface it */ }, ); Widget build(BuildContext c) => Scaffold(body: _callWidget ?? const CircularProgressIndicator());
An emit that ignores the onSuccess argument joins the call successfully and shows nothing — audio flows, no video, no controls. It reads as a broken SDK and it is the #1 Flutter-specific failure. There is no container parameter and no zero-bounds trap; there is only "did you put the widget in the tree".
SDK method map (BAKED closed list — from the catalog; signatures → FETCH from docs)
- Lifecycle:
CometChatCalls.initFromSettings·CometChatCalls.init·CallAppSettingBuilder→CallAppSettings·CometChatCalls.isInitialized·login·loginWithAuthToken·logout·getLoggedInUser→User? - Session:
CometChatCalls.generateCallToken(sessionId, …)→CallToken·joinSession(…)→Widget?·SessionSettingsBuilder→SessionSettings·SessionType - ⚠️ TWO join paths, both real in 5.0.7 — pick one:
generateCallToken(sessionId)→joinSession(callToken:sessionSettings:), orjoinSession(sessionId:sessionSettings:)directly. - In-call actions — on
CallSession.getInstance(), for CUSTOM controls ONLY (the returned widget already renders them; see pitfall #1):leaveSession·muteAudio/unMuteAudio·pauseVideo/resumeVideo·setLayout·setAudioModeType·switchCamera·raiseHand/lowerHand·startRecording/stopRecording·startScreenShare/stopScreenShare/toggleScreenShare·pinParticipant/unPinParticipant·muteParticipant·pauseParticipantVideo·enablePictureInPictureLayout/disablePictureInPictureLayout·enterPipMode/isPipSupported·setChatButtonUnreadCount·isCallSessionActive- Toggles (Flutter-only, no iOS analogue):
toggleMuteAudio·togglePauseVideo·toggleCameraSource·toggleRaiseHand - Aliases for the same actions:
endCallSession·startCallRecording/stopCallRecording·setCallLayoutType - Settings panels:
hideSettingsPanel·openCallSettingsPanel·openVirtualBackgroundSettingsPanel - State getters (read, don't track):
isAudioMuted·isVideoPaused·isHandRaised·isRecording·isScreenSharing·isTranscribing
- Toggles (Flutter-only, no iOS analogue):
- Listeners (on
CallSession.getInstance(); four are add/remove,LayoutListenersis a single-slot property):SessionStatusListeners·ParticipantEventListeners·MediaEventListeners·ButtonClickListeners·LayoutListeners - Call logs:
CallLogRequestBuilder(top-level; FIELDS, nosetX()) →CallLogRequest→fetchNext/fetchPrevious→CallLog(+CallUser/CallGroup/CallEntity) - Background:
CometChatOngoingCallService.launch()/.abort()— Android foreground service, no-op on iOS - Enums:
AudioMode(.speaker/.earpiece/.bluetooth) ·SessionType·LayoutType(.tile/.spotlight/.sidebar) ·CameraFacing
A curated highlight, NOT the full surface. The authoritative closed list is the catalog (59 symbols, each proven by a
flutter analyzeprobe). A symbol is real iff it is in the catalog — confirm there, then fetch its signature from the page inreferences/docs-map.md.
Listener lifecycle (BAKED)
All five listener types are abstract classes you IMPLEMENT — they are NOT constructible.
The live docs build every one of them with named callbacks
(SessionStatusListeners(onSessionJoined: () {…})); an abstract class cannot be instantiated, so
none of those examples compile. flutter analyze reports instantiate_abstract_class.
dartclass _Status extends SessionStatusListeners { void onSessionJoined() { /* ... */ } void onSessionTimedOut() { /* distinct from onSessionLeft — handle BOTH */ } // `extends` instead of `implements` and the other four inherit their no-op bodies. } final _status = _Status(); // hoist into a FIELD CallSession.getInstance()?.addSessionStatusListener(_status); // BEFORE joining // teardown: CallSession.getInstance()?.removeSessionStatusListener(_status); // the SAME instance CallSession.getInstance()?.leaveSession();
- Every method already has a no-op body, so
extendslets you override only what you need;implementsobliges you to write them all. remove*Listenertakes the object you added. Constructing a fresh listener to remove one is a no-op that silently leaks the original — and it is what the live docs show (D4). Hoist into a field; there is no other shape in which removal works.- ⚠️ Registration names do NOT match the type names — full table in
references/docs-map.md. Two you cannot guess:MediaEvent**Listeners**registers viaaddMediaEvent**s**Listener, andLayoutListenershas no add/remove at all — it is a single-slotlayoutListener = lsetter, cleared with= null. CallSession.getInstance()is never actually null — the singleton is eagerly built, so registering BEFOREjoinSessionreally does attach (certified:onSessionJoinedfires). The type is stillCallSession?, so keep?.; useisCallSessionActive()to test for a live session.ButtonClickListenerscallbacks fire alongside the SDK's own action and do NOT suppress it. Reacting toonLeaveSessionButtonClickedmust NOT also callleaveSession()— the SDK is already leaving; doing both double-leaves. Use it to runCometChat.endCall(sessionId)on the ringing path.Participanthas TWELVE fields, all nullable:uid·name·avatar·mid·state·isJoined·deviceId·joinedAt·leftAt·totalAudioMinutes·totalVideoMinutes·totalDurationInMinutes. No mute/pin/hand/share flags — the seven the docs' "Participant Object Reference" lists (pid/role/audioMuted/videoPaused/isPinned/isPresenting/raisedHandTimestamp) do NOT exist and do not compile (seedoc-corrections.md); accumulate that live state fromParticipantEventListeners, as iOS must. PreferonParticipantListChanged, which delivers the WHOLE list. (Participants, plural, is a@Deprecatedtypedef alias ofParticipant.)
Least-code recipe (meet-style — ORDER, not signatures)
dart// Fetch every signature/field from references/docs-map.md. This is the ORDER, which is the part that bites. import 'package:cometchat_calls_sdk/cometchat_calls_sdk.dart'; 1. CometChatCalls.initFromSettings(onSuccess:, onError:) // ai-agent default; cometchat-settings.json as a Flutter ASSET 2. CometChatCalls.login(uid:, authKey:, onSuccess:, onError:) // or loginWithAuthToken in production 3. final settings = (SessionSettingsBuilder()..setDisplayName(name)..setType(SessionType.video)).build(); 4. CallSession.getInstance()?.addSessionStatusListener(_status); // register BEFORE joining 5. CometChatCalls.joinSession(sessionId:, sessionSettings:, onSuccess: (Widget? w) => setState(() => _call = w), onError:) // or: generateCallToken(sessionId) -> joinSession(callToken:sessionSettings:) 6. // return _call from build() — NOTHING renders until you do 7. // NO control buttons needed — the returned widget ALREADY renders mute/video/leave. 8. // teardown -> remove each listener (same instance); CallSession.getInstance()?.leaveSession()
Backgrounding & call notifications → references/platform-notes.md
Two things that are NOT on the join path and are easy to get wrong:
- Backgrounding — Android kills a call the moment the app backgrounds unless
CometChatOngoingCallService.launch()holds a foreground service (.abort()on teardown). Flutter-only; both calls are safe no-ops on iOS. The manifest permissions are not optional. - Ringing a closed app — there is no first-party CometChat Flutter push package (iOS has
one; Flutter does not), and no symbol in this skill's catalog implements it. It is FCM on
Android and APNs/PushKit/CallKit on iOS, wired by the app. Needs a dashboard push provider and a
physical device. An emit naming a
cometchat_push_notificationsFlutter package invented it.
Doc corrections (⚠️ the live pages do not compile — full table: references/doc-corrections.md)
23 classes of symbol on /calls/flutter/** fail flutter analyze against 5.0.7. Read
references/doc-corrections.md before copying off a page. The two that bite hardest:
| The docs write | It does not compile — use |
|---|---|
generateToken(sessionId: …) → CallToken (13 pages) | generateCallToken(sessionId, onSuccess:, onError:) → CallToken. generateToken is @Deprecated, takes two POSITIONAL args, and yields GenerateToken. No page mentions generateCallToken at all |
Participant.pid / .role / .audioMuted / .videoPaused / .isPinned / .isPresenting / .raisedHandTimestamp | None exist. Participant has 7 fields; track the rest from events |
Docs PR cometchat/docs#492 fixes the first seven classes (56 → 3 analyzer errors). Five more —
including the abstract-listener one — were found by running flutter analyze over this skill's
own harness AFTER #492 was open, and are not in it. 12 fences on those pages parse in no shape
and were never machine-checked, so the table is a floor, not a total.
Common pitfalls (BAKED)
- Forgetting to mount the returned widget (the #1 Flutter mistake) —
joinSessionrenders nothing itself. Store theWidget?fromonSuccessin State and return it frombuild(). - Don't duplicate the built-in call controls (the #1 mistake everywhere else). The joined widget renders a COMPLETE call UI — mute, camera, layout, participants, leave. Do NOT add your own buttons around it. The
CallSession.getInstance()action methods are for CUSTOM controls ONLY — reach for them just when the user EXPLICITLY asks to replace the defaults, and hide the built-ins first via the..hide*Buttonsetters onSessionSettingsBuilder. To merely REACT to the built-in leave button, useButtonClickListeners(onLeaveSessionButtonClicked:)— do not add a second button. - Removing a listener you did not keep — construct once into a field;
remove*Listenermatches by instance. Useris exported by BOTH barrels — ringing imports both SDKs;ambiguous_importuntil one hides it (… hide User;). No page says so.- Ringing teardown needs BOTH SDKs —
leaveSession()ends your media,CometChat.endCall(sessionId)tells the peer and completes the log. Only the first, and the peer still thinks the call is live. - There is no
cancelCall— cancel your own outgoing call withrejectCall(sessionId, CometChatCallStatus.cancelled). - Manifest permissions without the runtime request — on Android 6+ the manifest alone grants nothing; request camera/mic before joining or the call is black and silent.
minSdkVersionleft at Flutter's default — the floor is 26 (docs say 24), inandroid/app/build.gradle.ktson current Flutter, notandroid/build.gradle.- iOS Simulator on Apple Silicon:
CometChatWebRTCexcludes arm64, so the app builds x86_64 — fine on an iOS 18 simulator (Rosetta), impossible on iOS 26+. Device or iOS 18 sim; details inreferences/platform-notes.md. cometchat-settings.jsonnot influtter: assets:—initFromSettingsfails with settings-file-not-found. It is a quiet error callback, not a crash. Registering the asset is the whole job; no Xcode step.- Double init —
initafterinitFromSettingssilently re-attributes telemetry to"manual". - Handling
onSessionLeftbut notonSessionTimedOut— idle timeout (default 300s,0disables) fires a DIFFERENT callback; miss it and the user sits on a dead call screen. - Emitting an API Flutter lacks — no presentation mode, no first-party push package. Say so rather than emitting code that cannot resolve. The two PiPs are also different calls:
enablePictureInPictureLayout()is the in-app layout,enterPipMode()is Android system PiP. - Screen share — the control methods DO ship (docs lag them).
CallSession.getInstance()has realstartScreenShare()/stopScreenShare()/toggleScreenShare()+ anisScreenSharinggetter (barrel-reachable in 5.0.7; the method channel invokes a nativestartScreenShareand web routes togetDisplayMedia), andhideScreenSharingButtondefaults tofalseso the built-in button is visible. So an emit callingstartScreenShare()COMPILES and dispatches — do NOT tell the user local screen share is impossible or "invented." The/calls/flutter/screen-sharingDOC page still says "receive-only" — it documents only the RECEIVE side (onParticipantStartedScreenShare,participant.isPresenting, viewing a web participant's share); the shipped local-share control API is undocumented there. Reflect the shipped methods, and note the page lags them. - Joining before init+login resolve — chain from the success callbacks.
- Assuming a signature — event names,
SessionSettingsfields and action params are FETCHED from docs, never guessed.
Verify it works
- Tier-1 catalog: every
cometchat_calls_sdksymbol emitted appears influtter-calls-v5.json(node test-suite/scripts/verify-catalog.mjs --family flutter-calls-v5). - Tier-2 compile: the emit analyzes clean against the installed
cometchat_calls_sdk(5.0.7) withflutter analyzeintest-suite/harness/flutter-calls. - Tier-3 smoke: the round-trip (init→login→generateCallToken→join→widget-mounted→leaveSession) via the pack's dev calls-smoke harness (internal QA, not shipped) — certified on a physical Android device. Ringing runs as TWO concurrent devices, one process per role, and only counts if both succeed on the SAME session id — see
references/docs-map.md§ "1:1 RINGING".

