callerapi 1.6.2
callerapi: ^1.6.2 copied to clipboard
CallerAPI Flutter plugin - privacy-preserving spam detection and caller labels (k-anonymity) for Android and iOS, plus automatic call screening on Android.
Changelog #
All notable changes to the CallerAPI SDKs are documented here. This project
adheres to Semantic Versioning: MAJOR.MINOR.PATCH.
- MAJOR - source-breaking public API changes.
- MINOR - backward-compatible additions.
- PATCH - backward-compatible fixes.
Every CallerAPI SDK shares one version number, held in sdk/VERSION and
enforced across every platform (and the server's advertised
LCID_LATEST_SDK_VERSION) by sdk/scripts/check-sdk-versions.sh. A release
publishes all four platforms from a single sdk-v<version> tag.
[Unreleased] #
1.6.2 #
The Android name card is what a customer sees on every labeled call. 1.6.1 still drew the old bar. Upgrade if you ship call screening.
Changed #
-
Android: the caller ID overlay is a card, not a bar across the screen. It was a full-width rectangle with square corners, drawn in a flat navy, with the name against the left edge and no vertical centring. It also set
FLAG_DIM_BEHIND, which greyed out the call screen underneath it, including the answer and reject buttons, so a caller's name read as a modal interruption rather than an annotation on the call.It is now inset from both edges, rounded, with a real shadow, the text block centred against the logo, a clearer size relationship between name and number, and no dimming. Nothing about the lookup changed.
This is the only place Android allows a name. Google reserves the call screen itself for the dialer, so a screening app draws over it, and the overlay either looks deliberate or it looks broken.
-
Android: a host can replace the name card. The SDK still has no theme API.
setIncomingCallerIdOverlayEnabled(false)only hides the SDK card. The screening service still receives every call, so the host never sees the number. To draw a custom card, removecom.callerapi.sdk.CallerScreeningServicewithtools:node="remove", write your own service, answer withcachedSpamVerdict, and calllookupLabelafterrespondToCall. The steps are inINTEGRATION.mdanddocs/public/sdk/android-call-screening.md.
Fixed #
- Android: the old strip no longer stacks on the new card. The screening
service launched
IncomingCallerIdActivityand the overlay together. The activity is the fallback for devices that never granted Display over other apps. Launching both put the old full-width strip on top of the new card. The service now draws the overlay when that permission is granted, and launches the activity only when it is not.
1.6.1 #
1.6.0 fixed Live Caller ID for CocoaPods and left Swift Package Manager broken, which is the install path the documentation recommends first. Upgrade straight past it.
Fixed #
-
The CallKit fix in 1.6.0 never reached Swift Package Manager. The public package resolves from a mirror repository, and the mirror has its own
Package.swift, because SwiftPM requires one at a repository root and the Live Caller ID sources are not one. 1.6.0 added the framework to the standalone manifest only, so the podspec carried the fix and the mirror did not, and every SwiftPM customer still had every call fail with Cocoa 4099.Nothing caught it. The mirror still built, because CallKit is only needed at run time, and the release gates check versions and slices rather than what a target links.
build-ios-mirror.shnow refuses to publish a mirror that does not link everything the standalone package links, which fails the release instead of shipping it.
1.6.0 #
Live Caller ID Lookup did not work outside an app that already happened to link CallKit. Everything below follows from finding that, on a device, against production.
Fixed #
-
iOS: Live Caller ID Lookup could not work at all in a host app that did not already link CallKit.
LiveCallerIDLookupManagerreaches the system through an XPC service that lives insideCallKit.framework/XPCServices/com.apple.CallKit.CallDirectory.xpc, and an XPCService name resolves only against the main bundle and the frameworks loaded in the process. A host that linkedCallerAPILiveCallerID, and soIdentityLookup, but never loaded CallKit failed every call before it began:activating connection: name=com.apple.CallKit.CallDirectory failed to do a bootstrap look-up: xpc_error=[3: No such process] invalidated after a failed initEverything downstream then had one symptom each and no common cause. The extension read as disabled however the user set the Settings toggle,
refreshPIRParametersandresetthrew Cocoa 4099, and no PIR or Privacy Pass request ever left the device, so the backend saw no traffic and looked healthy. Animport CallKitalone autolinks only the Swift overlay, and weakly, so the framework is now named inPackage.swiftand the podspec.An app that happened to link CallKit for its own reasons was unaffected, which is why this survived earlier testing.
-
iOS: the SDK no longer launches the extension to find out whether it is enabled. Probing readiness that way spawns an
extensionkitserviceprocess per attempt. A retry loop of them leavescom.apple.CallKit.CallDirectoryholding a dead handle, and the user then cannot turn the extension off or on: Settings reports "an error occurred while disabling the extension" until the device is rebooted. Reading the status is a plain query and is now the only thing the readiness path does. -
iOS: a failed
resetno longer aborts the refresh behind it. The first ExtensionKit call after an in-place app update fails withcom.apple.extensionKit.errorDomainerror 2, andresetis usually first. The refresh that follows it is the part the next call needs, so it now runs either way. -
The server told a device its evaluation key was current when it was not.
/configreported our own receive time inKeyStatus.timestamp. Apple defines that field as the generation time of the device's secret key, and the key identifier isSHA256(EvaluationKeyConfig), which does not change when a device rotates that secret. Our timestamp was always the later of the two, so a key derived from a superseded secret read as current, the device skipped/key, and its queries were evaluated under a key that could not decrypt them. The server saw nothing wrong: the config hash resolved, the key was found, and/queriesanswered 200 with a well formed reply. Only the device saw it, as a CipherML decrypt failure and an empty caller name.
Added #
-
iOS:
reset,refreshExtensionContextandreprovisionon the public surface.refreshPIRParametersalone does not drop a cached lookup, so a first lookup that returned empty stayed empty for the rest ofcache_expiry_minutes, which is 24 hours in production.reprovisionruns Apple's working order after a package publish: optional reset, extension context, then PIR parameters. -
iOS:
waitUntilEnabled, which polls the status until it settles. A singleisDisabledread at launch is not evidence the user turned the extension off, because CallDirectory answers Cocoa 4099 for the first few seconds of a process. Host apps were treating that as "off" and hiding their own controls. -
iOS:
isCallDirectoryUnavailableandisStaleExtensionConnection, so an integration can tell "the system is not ready yet, retry" apart from "the user has this switched off" instead of showing one misleading message for both. -
Flutter
resetLiveLookup()and React NativeresetLiveLookup(), plus an optionalclearCachedLookupson the Flutter prewarm.
1.5.1 #
Everything here comes from an external review of 1.5.0. Nothing in it changes what the SDK can do; it changes what the SDK tells you when it is not doing it.
Fixed #
-
An SDK with no credentials now says so, instead of reporting every caller as clean.
callerapi_init_exreturned success with no credential pair, because nothing needs one until the first lookup. That lookup then failed authentication,isSpamreported the failure asfalse, and the result was an integration that started cleanly, blocked nobody, and logged nothing. A reviewer needed a decompiler to work out why.Init now fails with
CALLERAPI_ERR_NOCREDS(-10) when neither a runtime pair nor a compiled-in one is available, and both platforms log what is missing and where to put it. A lookup that fails for any other reason, a rejected credential most likely, is logged once a minute rather than silently reported as "not spam".Nothing changes for an SDK that has credentials. If yours does not, it was already answering "not spam" to every call and now tells you.
-
iOS: the Call Directory extension no longer invents an at-rest key. On a keychain miss the extension minted a fresh key, which cannot decrypt a blob the host encrypted, so a
keychainAccessGroupmismatch surfaced as an empty block list. Only the host creates the key now; the extension reads it, and reports a miss. -
iOS:
CallerAPIErrordescribes -9 and -10 instead of falling through to "unknown error".
Documentation #
-
The guide the iOS docs called canonical was never published.
IOS-FULL-INTEGRATION.mdis now in the package, along with the 14 internal links across four documents that pointed at files customers never received.build-ios-mirror.shrepoints links renamed on the way out and refuses to publish a dangling one, so this cannot recur silently. -
The iOS quickstart told you to initialise without credentials. Its "complete" host
Info.plistomittedCallerAPISDKKeyandCallerAPISDKSecretentirely, which is what produced the silent failure above in the first place. Both are now in the plist example and in every launch snippet. The doc comment onconfigure(featureEnabled:)claimed the overload cannot authenticate under SwiftPM; it reads the plist, and now says so. -
The Call Directory guide put three
Info.plistkeys on the wrong target.CallerAPIKeychainAccessGroupis read by the extension, not the host.CallerAPIAppGroupis Live Caller ID's, andCallerAPICallDirectoryExtensionBundleIDis only read by the Flutter and React Native bootstraps; a native app sets both inAppConfig.swift. There is a table now saying which key belongs where. -
Android: the first call from an unknown number is not blocked, and the guide now says so in its own section rather than in one KDoc comment. Screening answers from cache because it cannot make the phone wait, so an uncached number is allowed through and blocked from the second call on. Worth checking against any marketing copy that says otherwise.
-
Android troubleshooting covers the case where every lookup returns
UNKNOWN, and no longer explains lookup code -2 as a native build problem when it usually means the server rejected the credentials. -
The published README's install snippets are generated from
VERSION. They had said 1.3.1 for three releases.
1.5.0 #
Added #
-
Android: verified Business Caller ID. A registered business can now prove it is the party placing a particular call, and only then does the incoming-call card show a verification mark and the business's logo.
This is a claim about a call, not about a number, and that distinction is the whole feature. A spoofer presenting a customer's calling number gets the same answer from any number-keyed lookup as the real owner does, so the k-anon index now carries a business's plain name and nothing else. The mark and the logo travel only inside a short-lived assertion the business publishes as it dials, signed ES256 by the server and verified on the device against keys from
/api/sdk/v1/config. There is no logo id in a bucket for any client to draw without checking a signature.The device also checks the assertion was issued for the call it is receiving, not merely that the business is dialling somebody, which is what stops a spoofer riding a real assertion by calling a different person in the same window. That check needs this handset's own number; where it is unavailable, verification is withheld.
CallerAPI.setRequireCalleeBinding(false)relaxes it and accepts the weaker guarantee.Verification cannot block a call and does not try to. Screening runs on the main thread and the phone does not ring until it returns, so the assertion fetch happens after the verdict — it decorates a call, it never decides one.
-
Logos are cached on disk by content id and fetched as a whole pack rather than per caller, because a request for one caller's logo would give away what the k-anon bucket exists to hide. Bounded to 64 MB, and every file is checked against its own content id before use.
-
iOS: Live Caller ID endpoints read from Info.plist. A customer whose Apple registration uses a dedicated host set can set
CallerAPILCIDServiceURLandCallerAPILCIDTokenIssuerURLinstead of callingsetEndpoints. The Swift call still works and still wins, so staging can override a shipped configuration. OnlyhttpsURLs are accepted, and surrounding whitespace is trimmed. -
Flutter and React Native reach the same setting through the environment. The podspec hook writes both keys from
CALLERAPI_LCID_SERVICE_URLandCALLERAPI_LCID_TOKEN_ISSUER_URL. Neither wrapper exposedsetEndpointsin Dart or TypeScript, so a dedicated-host customer on Flutter or React Native previously needed a per-customer build of the SDK. No build differs per customer now. -
bootstrapcopies a host Info.plist override into the App Group, so the extension reads it without a copy of its own. -
iOS: the heartbeat installs a tier token, so Live Caller ID issuance is attributed to your account. The server returns
lcid_tier_token, the SDK stores it in the App Group, and the extension presents it to the Privacy Pass issuer. The issuer then signs with a key of your own. Nothing to call and nothing to configure. Billing reconciliation reads your own numbers instead of a fleet aggregate.
1.4.0 #
Lookups did not work on iOS. The published pod carries no credentials, by design,
and the Swift API had no way to supply them, so every lookupLabel failed
authentication. This release adds the credential API that Android always had.
Upgrading is a version bump plus two lines telling the SDK who you are. See
UPGRADING.md.
Added #
- iOS:
CallerAPI.configure(_:)taking aCallerAPIConfig, the counterpart of Android'sCallerAPIConfig. SupplysdkKeyandsdkSecretfrom the dashboard. - iOS: credentials are also read from the app's Info.plist, from
CallerAPISDKKeyandCallerAPISDKSecret. This mirrors the Android manifest meta-data, and is what makes lookups work through Flutter and React Native. Those bridges call the lookup methods directly and have nowhere to pass a credential. - iOS:
CallerAPI.CallDirectory.hasBlocklistBlob(), to tell "the host app has not synced yet" from a real failure. The Call Directory template already called it. Cancelling the request instead makes iOS treat the extension as broken. - iOS: the bundle identifier is now reported when the SDK authenticates, so the server can refuse a credential presented by an app it was not issued to. Android has always reported its package name and signing digest.
Fixed #
- iOS: the Call Directory extension template did not compile. It was missing
import CallerAPIand called thehasBlocklistBlob()above, which did not exist. This was the first file a customer builds on that path. - iOS: the Live Caller ID extension template under
ExtensionTemplates/was missingimport CallerAPILiveCallerID. The copy published in the mirror was already correct, so no released package was affected. - Dashboard: every "start it" snippet on the SDK setup page called a function
that does not exist, on all four platforms:
CallerAPI.initon Android,CallerAPILiveCallerID.configure(clientKey:)on iOS, and aninitializeon Flutter and React Native. They now show the real API.
Internal #
sdk/scripts/check-extension-templates.shtype-checks every shipped iOS extension template in CI.Package.swiftcompilesSources/only, so nothing ever compiled the files customers copy into their own projects.- A public-surface test pins the Live Caller ID host API with a plain, non-testable import, so a symbol the integration guide tells customers to call cannot quietly become unreachable from outside the module.
1.3.1 #
The 1.3.0 React Native and Flutter packages still expected to find the native SDK copied into them by hand, the way the old partner archives delivered it. Installed from npm or pub.flutter-io.cn there is nothing to copy, so this release changes where they get it from: the same registries every other dependency comes from.
Upgrading is a version bump. No API changed.
Fixed #
- Flutter:
flutter pub add callerapiproduced a project that could not build for either platform. Android failed outright, telling you to "re-copy from a fresh partner zip"; iOS compiled the plugin against a core that was not in the package, so calls into it had no implementation. The plugin now resolvescom.callerapi:callerapi-sdkfrom Maven Central and theCallerAPIpod from CocoaPods. - React Native: the same Android failure, and on iOS a private 6.7 MB copy of the core bundled into the npm tarball. That copy was never rebuilt at release and no credential guard inspected it, so it drifted away from the core served elsewhere. Both platforms now resolve the published artifacts.
- iOS core: the shipped
CallerAPI.xcframeworkpredated SDK version reporting and was rebuilt. Devices running the core previously sent no version, which left the server unable to distinguish SDK versions or apply app-identity binding to that traffic.
Added #
- The iOS core is published for the first time, as the
CallerAPIpod and a SwiftPM product alongsideCallerAPILiveCallerID. One package, one tag, two products — see the README at github.com/dimondevceo/callerapi-sdk. It has an iOS 13 floor; Live Caller ID still needs iOS 18 and is unchanged.
1.3.0 #
Publishing to public registries made the SDK credential recoverable by anyone who downloads a customer's app — that is inherent to shipping a client, not a flaw in the packaging, and it was always true of the vendored archives too. This release makes possession of a credential insufficient on its own.
Nothing here is required to upgrade. All of it is opt-in per client and defaults to off, so an existing integration behaves exactly as it did on 1.2.0.
Added - app identity #
- Every SDK now reports the identity the operating system holds for the host app,
and the server can refuse a credential presented by an app it was not issued to.
Android reports its
applicationIdand signing-certificate SHA-256 (read fromPackageManager, not from configuration); iOS reports its bundle identifier. - Identity is checked on both
/api/mobile/authand/api/lcid/heartbeat.
Added - device attestation #
- Android: Play Integrity. Requires the host app to add
com.google.android.play:integrity— it needs a Play Console cloud project bound to the app, so it cannot be a transitive dependency of ours. Without it, attestation reports nothing and the server decides what that means. - iOS: App Attest. A Secure Enclave key is generated once per install, attested over a server-issued challenge, and used to sign every subsequent heartbeat. Unavailable on the simulator.
- Attestation material is only produced when the server asks for it, so a client with attestation off pays nothing for this.
Added - server controls #
- Per-client attestation modes:
off,monitor(record what enforcing would break) andenforce(refuse). - A per-client monthly active device ceiling, with alerting at 80%, 95% and 100%.
- Billing reconciliation against Privacy Pass issuance. A month whose token issuance does not agree with the active devices customers reported has its invoices generated but held unsent for review.
Changed #
app_versionon the authentication body now reports the real SDK version. It was previously hardcoded to1.0.0, which meant the server's version policy could never identify an outdated integration.callerapi_configgainedpackage_name,cert_sha256andapp_version. The struct is extended, not reordered, so existing initialisation is unaffected.
Fixed #
- The prebuilt Android native libraries are now checked against the modification time of the C sources, not only for the presence of the expected JNI symbols. A changed function signature keeps the symbol name, so the previous check could not see it and the mismatch surfaced as stack corruption at runtime.
sync-lcid-sources.shenumerates the canonical directory instead of a hardcoded file list. A new source file previously reached the SwiftPM package but not the React Native and Flutter copies.
1.2.0 #
Changed - distribution #
-
The SDKs are published to their ecosystems' registries. Upgrading is a version bump in your own manifest instead of a support email with an archive attached:
Platform Install Android implementation("com.callerapi:callerapi-sdk:1.2.0")(Maven Central)iOS pod 'CallerAPILiveCallerID', '~> 1.2', or SwiftPM at tagsdk-v1.2.0React Native npm install @callerapi/react-native@1.2.0Flutter flutter pub add callerapiThe published artifacts contain no per-customer secrets. See
sdk/UPGRADING.mdfor the migration from a vendored copy. -
Credentials are supplied at runtime rather than compiled in. One build of the SDK now serves every customer, which is what makes registry distribution possible. Pass them to
CallerAPI.register(...)(Android), or declare them in your manifest; seesdk/UPGRADING.md. Builds with credentials baked in continue to work.
Added #
callerapi_init_ex()/nativeInitEx— runtime configuration of API base, SDK key and secret, and per-request network timeouts.callerapi_is_spam_cached()/CallerAPI.cachedSpamVerdict()— a lookup that answers only from cache and never performs I/O, for use on latency-critical paths.callerapi_prewarm()/CallerAPI.prewarmAsync()— refreshes the cache off the call path.- iOS: published through CocoaPods in addition to Swift Package Manager, so CocoaPods-based apps no longer need a vendored copy.
Fixed - Android #
- Call screening no longer blocks on the network.
onScreenCallperformed a synchronous lookup, so on a slow or unreachable network the system's screening timeout elapsed and the call rang through unscreened. The verdict is now read from cache and the refresh happens afterwards, off the call path. - Blocked-call counts were reported more than once. The counter was cleared only after the next successful check-in, so a retry re-sent totals that had already been accepted and inflated the figures the account was billed on. Accounting is now exactly-once.
- The server's heartbeat interval was ignored, and the periodic worker was
enqueued with
KEEP, so an interval change never reached devices already in the field. The advertised cadence is now adopted viaUPDATE. - The heartbeat worker retries with backoff and gives up after a bounded number of attempts instead of retrying indefinitely.
Fixed - native core #
- Concurrent use could crash. The shared context and the libcurl handle had no lock, so a lookup from the call-screening thread racing a check-in from a worker corrupted state. All public entry points now serialize.
- Every HTTP request carries connect and total timeouts; previously a stalled connection could hang a caller indefinitely.
- The k-anonymity namespace configuration is cached to disk and served stale while revalidating, so a cold start with no network still answers from cache.
Fixed - iOS #
- Background heartbeat could lose the app's background-refresh budget. The
BGAppRefreshTaskexpiration handler cancelled the work but relied on the cancelled task to callsetTaskCompleted, which races and could complete the task twice or not at all. Completion is now exactly-once and reports failure when the window expires. The request also carries a 20 s timeout so it cannot outlive a refresh window. - The server's
heartbeat_interval_hourswas ignored; the next refresh was always scheduled 24 h out. The advertised cadence is now adopted and persisted, so it applies from the next cold launch too. registerBackgroundHeartbeat(taskIdentifier:)is now idempotent.BGTaskSchedulertreats a duplicate identifier as fatal, so calling it from both an app delegate and a SwiftUI scene (or after a state restore) crashed.- Metering configuration is now thread-safe. It is written on the main thread at launch and read from background refresh tasks; the shared store took no lock.
- A throttled heartbeat (HTTP 429) is reported as
MeteringError.throttledwith the server'sRetry-Afterinstead of a generic bad-response error. - An SDK below the server's minimum supported version now logs a clear upgrade
message rather than only exposing
isDeprecatedon the result.
Fixed - React Native / Flutter #
- Per-user tier tokens never reached the Live Caller ID Lookup extension. The
generated extension target had an empty entitlements file and no App Group, so
the extension — a separate process with its own sandbox — always fell back to
the production default and every subscriber got the default tier. The extension
now receives the App Group in its own entitlements and Info.plist, and
appGroupIdentifierresolves from the current bundle automatically.
Server #
- Monthly-active-user heartbeats are rate limited per device instead of per
client, so a large customer's devices no longer throttle each other and get
undercounted. Throttled responses carry
Retry-After.
1.1.0 #
Changed #
- Production endpoints moved to bare subdomains:
serviceURLis nowhttps://pir.callerapi.comandtokenIssuerURLishttps://pat.callerapi.com(previouslyhttps://api.callerapi.com/api/pir//api/pat). Apple removed support for path-style service / token issuer URLs in iOS/macOS 26.4; bare subdomains are mandatory for App Store / TestFlight builds and for the Apple relay onboarding form. No public API changed — rebuild against this version and the extension picks up the new defaults. The old path-style URLs keep working for Xcode dev installs and for already-deployed builds.
1.0.0 #
Initial release.
Added #
CallerAPILiveCallerIDnamespace:makeExtensionContext()for the extension@main, and host-app helpersisDisabled(forExtensionWithIdentifier:),openSettings(),refreshPIRParameters(forExtensionWithIdentifier:).CallerAPILiveCallerIDConfigurationwith zero-config production defaults (https://api.callerapi.com/api/pir+/api/pat).- App Group token sharing:
appGroupIdentifier,setUserTierToken(_:),setEndpoints(serviceURL:tokenIssuerURL:),resetSharedOverrides(). - Metering / MAU heartbeat (host app, iOS only):
configureMetering(clientKey:extensionIdentifier:endpoint:),sendHeartbeat(),registerBackgroundHeartbeat(taskIdentifier:),scheduleBackgroundHeartbeat(taskIdentifier:),disableMetering(). ExtensionTemplate/copy-in files (extension@main, Info.plist, entitlements) and a runnableExample/host app + extension.PrivacyInfo.xcprivacyprivacy manifest (declares the App Group UserDefaults required-reason API + device ID for app functionality; no tracking).- Unit tests for configuration + App Group token sharing (
swift test). sdkVersionconstant for support/diagnostics.