Skip to Content
SDK IntegrationDart / Flutter

Flutter SDK

Integrate TrustPin into your Flutter application for cross-platform certificate pinning on iOS, Android, and macOS.

Current version: trustpin_sdk 6.2.0 (bundles TrustPinKit 6.2.x for iOS/macOS and cloud.trustpin:kotlin-sdk 6.2.x for Android — native dependencies accept patch updates within the 6.2.x line)

Platform Requirements

PlatformMinimum Version
iOS15.0+
AndroidAPI 25+
macOS13.0+

Additional Requirements:

  • iOS/macOS: Swift 6.1+, Xcode 16.3+
  • Android: Kotlin 2.3.0+, Java 11+
  • macOS: network client entitlement required for sandboxed apps

Installation

Add TrustPin to your pubspec.yaml:

dependencies: trustpin_sdk: ^6.2.0

Then install:

flutter pub get

Quick Start

1. Get Your Credentials

Sign in to the TrustPin Dashboard  and retrieve:

  • Organization ID
  • Project ID
  • Public Key (Base64-encoded)

2. Initialize TrustPin

setupWithNativeBundle() tells each platform’s native SDK to read its own bundled configuration file, so credentials never enter the Dart isolate.

Drop platform-native config files into your app:

iOSios/Runner/TrustPin-Info.plist:

KeyTypeRequiredNotes
OrganizationIdStringYesNon-empty
ProjectIdStringYesNon-empty
PublicKeyStringYesBase64-encoded
ModeStringNo"strict" (default) or "permissive", lowercase
ConfigurationURLStringNoMust be HTTPS

After adding the file, open the project in Xcode and confirm Target Membership is checked for the Runner target — otherwise the plist won’t be copied into the app bundle and setupWithNativeBundle() will fail.

macOSmacos/Runner/TrustPin-Info.plist (same keys as iOS, same Target Membership requirement).

For sandboxed macOS apps, add the network client entitlement to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

<key>com.apple.security.network.client</key> <true/>

Androidandroid/app/src/main/assets/trustpin.json:

{ "organization_id": "your-org-id", "project_id": "your-project-id", "public_key": "LS0tLS1CRUdJTi...", "mode": "strict", "configuration_url": "https://your-server.com/pins.jws" }

Gradle includes the JSON in the APK automatically — no pubspec.yaml assets: entry is needed. The plugin’s own AndroidManifest.xml already declares the required network permission.

Make sure your consuming app declares minSdk 25 (or higher) in android/app/build.gradle:

android { defaultConfig { minSdk 25 } }

Then call setupWithNativeBundle() during app startup:

import 'package:flutter/material.dart'; import 'package:trustpin_sdk/trustpin_sdk.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); try { await TrustPin.shared.setupWithNativeBundle(); // setupWithNativeBundle() is non-blocking. Gate on // awaitConfiguration() to fail closed before the first pinned request. await TrustPin.shared.awaitConfiguration(); print('TrustPin initialized successfully'); } on TrustPinException catch (e) { print('TrustPin initialization failed: ${e.code} - ${e.message}'); } runApp(const MyApp()); }

Non-blocking setup: setup() and setupWithNativeBundle() now return as soon as configuration loading begins. await TrustPin.shared.awaitConfiguration() blocks until a validated configuration is available (and throws on failure/timeout); TrustPin.shared.isConfigurationLoaded is a non-throwing Future<bool> status check. Skip the gate and the first pinned request simply waits for the configuration to become ready. Setup is also one-shot: calling it a second time on the same instance throws ALREADY_INITIALIZED — create a separate named instance via TrustPin.instance(id) for a different pinning context.

Per-environment file names

setupWithNativeBundle() accepts optional per-platform filenames so you can ship different credentials per build flavor. null (the default) tells each native SDK to use its standard filename (TrustPin-Info.plist / trustpin.json):

await TrustPin.shared.setupWithNativeBundle( iosFileName: 'TrustPin-Staging.plist', macosFileName: 'TrustPin-Staging.plist', androidFileName: 'trustpin-staging.json', );

Validating Connections

TrustPin.shared.validateConnection() is the recommended way to check a host against the active configuration without going through your HTTP client. It performs a TLS handshake and verifies the leaf certificate against your pins.

Future<void> checkServer(String host) async { try { await TrustPin.shared.validateConnection( host, timeout: const Duration(seconds: 5), ); print('Connection is allowed by the configured pins.'); } on TrustPinException catch (e) { print('Validation failed: ${e.code} - ${e.message}'); } }
ParameterTypeDefault
hostString— (positional, required)
portint443
timeoutDurationconst Duration(seconds: 30)

Note: The older fetchCertificate() and verify() pair is deprecated and will be removed in a future major release. Migrate to validateConnection().


Integration Approaches

ApproachBest ForSetup Complexity
Dio HTTP Client (Recommended)Most Flutter apps🟢 Low
Standard http ClientApps using package:http🟢 Low
Manual validateConnection()Custom transports, pre-flight checks🟡 Medium

TrustPinDioInterceptor validates every request before it leaves the device:

import 'package:dio/dio.dart'; import 'package:trustpin_sdk/trustpin_sdk.dart'; final dio = Dio(); dio.interceptors.add(TrustPinDioInterceptor()); try { final response = await dio.get('https://api.example.com/data'); print('Request successful: ${response.statusCode}'); } on DioException catch (e) { if (e.error is TrustPinException) { final trustPinError = e.error as TrustPinException; print('Pinning failed: ${trustPinError.code}'); } }

Standard http Package

Create a pinned http.Client via TrustPinHttpClient.create():

import 'package:http/http.dart' as http; import 'package:trustpin_sdk/trustpin_sdk.dart'; final client = TrustPinHttpClient.create(); try { final response = await client.get(Uri.parse('https://api.example.com/data')); print('Request successful: ${response.statusCode}'); } on TrustPinException catch (e) { print('Pinning failed: ${e.code} - ${e.message}'); } finally { client.close(); }

Named Instances (Multi-Tenant)

If your app talks to multiple TrustPin projects — or if you’re shipping a library that needs its own isolated pinning configuration — ship one set of bundled config files per instance and load them via setupWithNativeBundle() on a named instance. Both TrustPinDioInterceptor and TrustPinHttpClient.create() accept an instance: parameter to bind to a specific named instance:

final pin = TrustPin.instance('com.mylib.networking'); await pin.setupWithNativeBundle( iosFileName: 'TrustPin-MyLib.plist', macosFileName: 'TrustPin-MyLib.plist', androidFileName: 'trustpin-mylib.json', ); // Dio bound to this instance dio.interceptors.add(TrustPinDioInterceptor(instance: pin)); // http.Client bound to this instance final client = TrustPinHttpClient.create(instance: pin);

Repeated calls to TrustPin.instance('id') with the same ID return the same instance.


Logging

Set the desired verbosity before calling setup() to capture initialization logs:

await TrustPin.shared.setLogLevel(TrustPinLogLevel.info);

Available levels: none, error, info, debug.

Log Stream

TrustPin.logs is a static broadcast stream of TrustPinLogEvent (level, instanceId, message) that routes SDK log output into your app’s logging pipeline. One stream covers all instances; the native sink is installed on first listen and removed on last cancel. While no one listens, the SDK keeps logging to its platform default. Per-instance verbosity remains controlled by setLogLevel:

final sub = TrustPin.logs.listen((event) { myLogger.log('[${event.instanceId}] ${event.message}'); });

Monitoring Pin Validation

TrustPin.validationEvents is a static broadcast stream of TrustPinValidationEvent surfacing the native SDKs’ validation telemetry — the signal to use for reporting suspected MITM attempts to your backend:

final sub = TrustPin.validationEvents.listen((event) { if (!event.isSuccess) { // Definitive failure verdicts only: PINS_MISMATCH, ALL_PINS_EXPIRED, // DOMAIN_NOT_REGISTERED (strict mode). Failure events carry the // presented leaf certificate as PEM — treat it as untrusted input. securityMonitor.report(event); } });

Events are observe-only: the verdict is decided before the event is emitted, so a listener cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no events. One stream covers all instances; the native listener is installed on first listen and removed on last cancel.


Error Handling

All SDK errors surface as TrustPinException, which exposes a code and a message plus convenience getters for the common cases:

GetterTrue when
isDomainNotRegisteredStrict mode and the host isn’t in the configuration
isPinsMismatchServer certificate doesn’t match any active pin
isAllPinsExpiredConfiguration is stale — rotate pins in the dashboard
isInvalidServerCertServer returned an unparseable certificate
isInvalidProjectConfigBad credentials or invalid configuration
isAlreadyInitializedsetup() was called a second time on the same instance
isErrorFetchingPinningInfoNetwork failure while loading the configuration
isConfigurationValidationFailedJWS signature didn’t verify against the project’s public key
isConfigIntegrityFailedConfiguration failed an integrity check — hard stop
isFetchCertificateTimeoutConnection timed out during certificate retrieval
isSetupInProgressAndroid only: an operation raced a setup() that hadn’t finished
isLockTimeoutAndroid only: an internal lock couldn’t be acquired in time
isSslContextSetupFailedAndroid only: the pinned SSL context couldn’t be created
isUnsupportedDeviceAndroid only: the runtime lacks required security primitives
try { await TrustPin.shared.validateConnection('api.example.com'); } on TrustPinException catch (e) { if (e.isPinsMismatch) { // Show a network-error UI and refuse the request. } else if (e.isDomainNotRegistered) { // Either the host is unexpected, or the configuration is out of date. } else { // Log everything else with e.code and e.message } }

Best Practices

Setup & Initialization

  1. Call WidgetsFlutterBinding.ensureInitialized() before initializing TrustPin.
  2. Initialize in main() before running the app.
  3. Set the log level first to capture initialization output.
  4. Handle setup errors gracefully — don’t block app launch.

Security

  1. Use TrustPinMode.strict in production.
  2. Prefer SPKI pinning; rotate pins in the dashboard before they expire.
  3. Monitor pin validation failures via the TrustPin.validationEvents stream.
  4. Keep credentials outside source control — prefer setupWithNativeBundle() with platform-native config files (TrustPin-Info.plist / trustpin.json), using iosFileName / androidFileName / macosFileName for per-flavor overrides. Credentials never enter the Dart isolate this way.
  5. Use HTTPS for all pinned domains.

Performance

  1. Configuration is cached for 10 minutes with automatic refresh.
  2. Reuse HTTP client instances rather than creating one per request.
  3. Use minimal log levels in production.

Complete Documentation

For the full API reference, advanced configuration, macOS sandbox setup, and the example app, visit:

TrustPin Flutter API Reference 


Resources