HomeSDK IntegrationDiscussions
Log In
SDK Integration

React Native Walkthrough

React Native Captur Events SDK

Integrate Captur’s verification capabilities into your React Native apps. Below you’ll find installation steps, initialization guidance, event handling, and a full example screen.

Prerequisites

  1. A valid Captur API key (provided by the Captur team).
  2. React Native >= 0.7.0 and >= 0.76.6.
  3. Kotlin Version >= 1.9.0 and < 2.0.0 ( 2.0.0+ not supported )
  4. iOS 15.4+
  5. Android 10+

Installation & Authentication

  1. Create or open your global npm config:
touch ~/.npmrc # create file if doesn't exist already
  1. Add your Access Token:
# ~/.npmrc
//registry.npmjs.org/:_authToken=ACCESS_TOKEN
  1. Install the Captur Events SDK:
# in your react-native project
npm install @captur-ai/captur-react-native-events

Platform setup

iOS Installation

  • From the ios/ folder:
cd ios && pod install
  • In Info.plist, add: ( or use XCode to add camera permissions )
<key>Privacy - Camera Usage Description</key>
<string>Your camera is used for verification</string>

Android Installation

  • In android/app/build.gradle, add the AAR repository:
repositories {
    google()
    mavenCentral()
    flatDir {
        dirs '../../node_modules/@captur-ai/captur-react-native-events/android/libs'
    }
}
  • In the same file, include the Captur AAR and its dependencies:
dependencies {
    ...
    implementation(name: 'capturMicroMobility-release', ext: 'aar') {
        transitive = true

        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
        implementation("androidx.camera:camera-core:1.3.4")
        implementation("androidx.camera:camera-camera2:1.3.4")
        implementation("androidx.camera:camera-lifecycle:1.3.4")
        implementation("androidx.camera:camera-view:1.3.4")
        api("org.tensorflow:tensorflow-lite-task-vision:0.4.0")
        implementation("com.squareup.retrofit2:adapter-rxjava3:2.9.0")

        implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
        implementation("com.squareup.retrofit2:retrofit:2.9.0")
        implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
        implementation("com.squareup.okhttp3:okhttp:4.11.0")
        implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
    }

  ...
}
  • In android/app/src/main/AndroidManifest.xml, add:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.camera.flash" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />
    <uses-permission android:name="android.permission.CAMERA" />

Initialization

  • Set up your config and initialize the SDK before rendering any camera views:
const DELAY = 1;
const TIMEOUT = 25;
const LOCATION_NAME = "Toronto";
const ASSET_TYPE = "package";
const API_KEY = "captur-*******";

async function initializeCaptur() {
  await setTimeout(TIMEOUT);
  await setDelay(DELAY);
  await setApiKey(API_KEY);
  await prepareModel(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
  await getConfig(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
}

Event Subscription

Use subscribeToEvents to receive both real-time guidance and final decision events from the Captur SDK. You register four callbacks:

  • capturDidGenerateGuidance: fires continuously as the camera analyzes frames, providing user instructions (e.g. “move closer”, “rotate left”).
  • capturDidRevokeGuidance: fires when the previous guidance no longer applies (e.g. user moved out of frame).
  • capturDidGenerateEvent: fires on state changes, including the final "cameraDecided" state which delivers the compliant or non-compliant decision along with a base64 image.
  • capturDidGenerateError: fires if any internal error occurs (e.g. network timeout, model load failure).
useEffect(() => {
  let unsubscriber: (() => void) | undefined;

  (async () => {
    await initializeCaptur();

    unsubscriber = subscribeToEvents({
      capturDidGenerateGuidance: (guidanceMeta) => {
        // guidanceMeta.guidanceTitle is a human-readable tip for the user
        guidanceInputRef.current?.setNativeProps({
          text: guidanceMeta.guidanceTitle,
        });
      },
      capturDidRevokeGuidance: () => {
        // clear or hide guidance when it’s no longer valid
        guidanceInputRef.current?.setNativeProps({ text: "" });
      },
      capturDidGenerateEvent: (state, metadata) => {
        // state === "cameraDecided" indicates the final result
        if (state === "cameraDecided") {
          // metadata.imageDataBase64 contains the decision image
          setThumbnail("data:image/png;base64," + metadata?.imageDataBase64);
        }
      },
      capturDidGenerateError: (error) => {
        // handle errors (e.g. show a message or retry)
        console.error("Captur error:", error);
      },
    });
  })();

  return () => {
    unsubscriber?.();
  };
}, []);

Camera Component

Render the camera preview and control flash/zoom:

<CapturCameraView
  key={referenceId}
  style={styles.camera}
  referenceId={referenceId ?? ""}
  startVerification={startVerification}
  isFlashOn={isFlashOn}
  isZoomedIn={isZoomedIn}
/>
  • referenceId: unique per session (e.g. rideId).
  • startVerification: on/off toggle for preview & analysis.
  • isFlashOn / isZoomedIn: control flash & zoom.
  • key: use when mounting/unmounting multiple instances. ( unnecessary if your flow dismounts the component on its own )

Attempts & Sessions

Start an Attempt

Starting an attempt tells the Captur SDK to begin capturing camera frames and running its compliance model in real time. If you already have a thumbnail from a prior attempt in the same session, call retake() first to clear previous state—otherwise you can simply set startVerification to true.

const handleStartVerification = async () => {
  try {
    if (thumbnail) {
      /**
       * Clears the image taken if exists.
       * Clears the prediction text.
       * Calls retake() to start a fresh attempt under the same referenceId.
       */
      setThumbnail(null);
      guidanceInputRef.current?.setNativeProps({ text: "" });
      await retake();
    }
    // Begin camera preview and analysis
    setStartVerification(true);
  } catch (error) {
    console.log(error);
  }
};

End an Attempt

Ending an attempt stops the camera analysis, resets any toggles (flash, zoom), and emits a final cameraDecided event containing the compliance decision and a base64-encoded image.

const handleStopVerification = async () => {
  if (startVerification) {
    // Stop analysis
    setStartVerification(false);
    // Reset flash and zoom to defaults
    setIsFlashOn(false);
    setIsZoomedIn(false);

    // ends the attempt and emits a camera decided event.
    return await endAttempt();
  }
};

New Session

If you need to start a completely new session (new referenceId), clear all UI state, call endAttempt(), then generate a new referenceId and begin verification again.

const newSession = async () => {
  setThumbnail(null);
  setIsFlashOn(false);
  setIsZoomedIn(false);
  guidanceInputRef.current?.setNativeProps({ text: "" });
  await endAttempt();
  setReferenceId(Date.now().toString());
  setStartVerification(true);
};
import React, { useState, useEffect, useRef } from "react";
import { View, StyleSheet, Button, Image, TextInput } from "react-native";
import {
  setDelay,
  setTimeout,
  setApiKey,
  prepareModel,
  getConfig,
  CapturCameraView,
  subscribeToEvents,
  endAttempt,
  retake,
} from "@captur-ai/captur-react-native-events";

const DELAY = 1;
const TIMEOUT = 25;
const LOCATION_NAME = "Toronto";
const ASSET_TYPE = "package";
const API_KEY = "captur-***-****-**-**-**-****";

async function initializeCamera() {
  await setTimeout(TIMEOUT);
  await setDelay(DELAY);
  await setApiKey(API_KEY);
  await prepareModel(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
  await getConfig(LOCATION_NAME, ASSET_TYPE, 0.0, 0.0);
}

export default function App() {
  const [startVerification, setStartVerification] = useState(false);
  const [isFlashOn, setIsFlashOn] = useState(false);
  const [isZoomedIn, setIsZoomedIn] = useState(false);
  const [thumbnail, setThumbnail] = useState<string | null>(null);
  const [referenceId, setReferenceId] = useState<string | null>(null);
  const guidanceInputRef = useRef<TextInput>(null);

  useEffect(() => {
    let unsubscriber: (() => void) | undefined;
    (async () => {
      await initializeCamera();

      unsubscriber = subscribeToEvents({
        capturDidGenerateEvent: (state, metadata) => {
          if (state === "cameraDecided") {
            setThumbnail("data:image/png;base64," + metadata?.imageDataBase64);
          }
        },
        capturDidGenerateError: (err) => {
          console.log("capturDidGenerateError", err);
        },
        capturDidGenerateGuidance: (meta) => {
          guidanceInputRef.current?.setNativeProps({
            text: meta.guidanceTitle,
          });
        },
        capturDidRevokeGuidance: () => {
          console.log("capturDidRevokeGuidance");
        },
      });
    })();

    return () => {
      unsubscriber?.();
    };
  }, []);

  useEffect(() => {
    setReferenceId(Date.now().toString());
  }, []);

  const handleStartVerification = async () => {
    try {
      if (thumbnail) {
        /**
         * Clears the image taken if exists.
         * Clears the prediction text
         * Start a new attempt ( thumbnail == true means that this is a subsequent attempt )
         * Retake has to be called if it's a subsequent attempt.
         */
        setThumbnail(null);
        guidanceInputRef.current?.setNativeProps({
          text: "",
        });
        await retake();
      }
      /**
       * Starts the camera, if it's not a retake there's no need to call retake and just start immediately.
       */
      setStartVerification(true);
    } catch (error) {
      console.log(error);
    }
  };

  const handleStopVerificiation = async () => {
    if (startVerification) {
      setStartVerification(false);
      setIsFlashOn(false);
      setIsZoomedIn(false);
      return await endAttempt();
    }
  };

  const handleFlash = () => {
    setIsFlashOn((prev) => !prev);
  };

  const handleZoom = () => {
    setIsZoomedIn((prev) => !prev);
  };

  const newSession = async () => {
    /**
     * Resets thumbnail, flash, zoom and prediction text.
     * Ends the attempt
     * Then starts the verification
     * This will start a new session with a new reference.
     */
    setThumbnail(null);
    setIsFlashOn(false);
    setIsZoomedIn(false);
    guidanceInputRef.current?.setNativeProps({
      text: "",
    });
    await endAttempt();

    setReferenceId(Date.now().toString());
    setStartVerification(true);
  };

  return (
    <View style={styles.container}>
      <View style={styles.cameraContainer}>
        <CapturCameraView
          key={referenceId}
          style={styles.camera}
          referenceId={referenceId ?? ""}
          startVerification={startVerification}
          isFlashOn={isFlashOn}
          isZoomedIn={isZoomedIn}
        />
      </View>

      {thumbnail && (
        <View style={styles.thumbnailContainer}>
          <Image style={styles.thumbnail} source={{ uri: thumbnail }} />
        </View>
      )}

      <View style={styles.actionButtons}>
        <Button title={"Start"} onPress={handleStartVerification} />
        <Button title={"Stop"} onPress={handleStopVerificiation} />
        <Button
          title={isFlashOn ? "Turn flash off" : "Turn flash on"}
          onPress={handleFlash}
        />
        <Button
          title={isZoomedIn ? "Zoom out" : "Zoom in"}
          onPress={handleZoom}
        />
        <Button title={"New session"} onPress={newSession} />
      </View>

      <View style={styles.guidance}>
        <TextInput
          style={styles.guidanceText}
          ref={guidanceInputRef}
          editable={false}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "flex-end",
  },
  cameraContainer: {
    flex: 1,
    position: "relative",
    justifyContent: "flex-end",
  },
  camera: {
    flex: 1,
  },
  thumbnailContainer: {
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    backgroundColor: "black",
  },
  thumbnail: {
    width: "100%",
    height: "100%",
    resizeMode: "cover",
  },
  guidance: {
    position: "absolute",
    top: "10%",
    width: "100%",
    zIndex: 14,
    alignItems: "center",
    backgroundColor: "rgba(0,0,0,0.4)",
    paddingVertical: 12,
  },
  guidanceText: {
    color: "white",
    fontSize: 16,
  },
  actionButtons: {
    position: "absolute",
    bottom: 0,
    left: 0,
    width: "100%",
    zIndex: 20,
    gap: 10,
  },
});