Get Started

This guide will take you through the process of setting up the Standalone Capture Module.

For a guide on setting up the full Sensibill SDK see Spend Manager SDK .

Before you begin

To arrange access to our SDK repositories please contact your Account Manager.

Installation

Adding Sensibill SDK to a Project

The Sensibill SDK for iOS is distributed in two formats: a binary package format (XCFramework), and a universal archive (legacy format). Both formats can be installed using CocoaPods dependency manager (a preferred method), or by adding the dependency manually to your project.

This section explains how to install Sensibill SDK for iOS in XCFramework format using CocoaPods dependency manager. For other options, please see Installation Options page.

Steps

  1. If your project isn’t using CocoaPods already, follow the Installation and Getting Started guides on the official CocoaPods website . Once your project is integrated with the CocoaPods, you should have a Podfile in the root directory of your project.

  2. Add the Sensibill SDK for iOS as a dependency to your project’s Podfile in the following format:

    pod 'Sensibill', :git => 'git@github.com:getsensibill/sdk-ios.git', :tag => 'vX.Y.Z'

    where vX.Y.Z represents to version of the SDK you want to use.

    Note: once Sensibill SDK was added to Podfile, don’t forget to run pod install from the root directory of your application’s project in your terminal. This will add Sensibill SDK for iOS to your project’s Pods.

  3. Validate the integration by importing the Sensibill module into one of your Swift classes:

    import Sensibill

    and try to build your project. If the project builds successfully, the Sensibill SDK for iOS was integrated correctly. Otherwise follow the Troubleshooting steps on Installation Options page.

Adding the Required Entitlements

  • When a user navigates to the Capture Document screen of the Sensibill SDK, SDK will use device’s camera to capture documents. In order to allow camera usage, your application must prompt the user for consent to use their device’s camera, and thus requires a Privacy - Camera Usage Description entitlement.

  • Sensibill SDK also allows the users to select an existing document images from the device’s photo gallery, which requires a Privacy - Photo Library Usage Description entitlement. Alternatively you can disable gallery access by setting the Capture.FeatureFlags.enableImageGallery to false.

  • If you would like to attach user’s device Location data to captured document, the Privacy - Location When In Use Usage Description entitlement is required. By default the location data is not collected, and you need to set Capture.FeatureFlags.attachLocationData to true to enable attaching the location data. Note that if user denies the access to the location, the location data will not be attached to a document, even if it was enabled for the app.

Steps

  1. Open Info.plist of your application.
  2. Locate or add each of the following entitlements, and provide a description of why the entitlement is required.

Required Entitlements

  • For the camera usage: NSCameraUsageDescription (displayed as Privacy - Camera Usage Description)
  • For the photo gallery access: NSPhotoLibraryUsageDescription (displayed as Privacy - Photo Library Usage Description).
  • For attaching user’s location data: NSLocationWhenInUseUsageDescription (displayed as Privacy - Location When In Use Usage Description).

Please contact your Account Manager for access to Maven.

The primary method of installing this module is via Sensibill’s private maven server. All stable artifacts are stored on a private maven server than can only be accessed using an authorized username and password. Contact Sensibill’s Client Services to acquire credentials. Once you have maven credentials, one of the two following changes to your gradle configuration will allow you to import Sensibill SDK dependencies.

// (old gradle style) IF Maven repositories are listed in the project level `build.gradle`
// file, Sensibill's Maven server should be added to the `allprojects.repositories` block
allprojects {
    repositories {
        // Other Maven Servers ...

        maven {
            // Sensibill SDK repository
            url 'https://maven.pkg.github.com/getsensibill/sdk-android'
            credentials {
                username 'to be provided by sensibill'
                password 'to be provided by sensibill'
            }
        }
    }
}

// (new gradle style) IF Maven repositories are listed in the `settings.gradle` file,
// Sensibill's Maven server should be added in the `dependencyResolutionManagement.repositories` block
dependencyResolutionManagement {
    repositories {
        // Other Maven Servers ...

        maven {
            // Sensibill SDK repository
            url 'https://maven.pkg.github.com/getsensibill/sdk-android'
            credentials {
                username 'to be provided by sensibill'
                password 'to be provided by sensibill'
            }
        }
    }
}
Maven Dependency

To add the Standalone Capture module to your Android project, add the following to the dependencies section of your main app module’s build.gradle file.

dependencies {
    // ...

    // See the rest of the documentation for the most up to date version
    def sensibillSdkVersion = 'X.X.X'

    // Standalone Capture Module
    implementation "com.getsensibill:sensibill-capture-standalone:$sensibillSdkVersion"

    // ...
}

Launch Capture

Capture entry point is a CaptureNavigationController class - a subclass of UINavigationController. When Capture starts, it displays a Camera Preview screen with options that allow the user to control how the documents are captured.

On instantiation, the caller can optionally provide an instance of Capture.RuntimeSettings, which allow to customize different aspects of capture experience. If this object is not provided, a Capture will be presented with default settings. See Global Theme page for more details on customization.

Upon its completion, the Capture needs to communiate the results to its caller. To do that, a caller has to implement CaptureNavigationControllerDelegate in one of the classes, and pass the reference to an instance of the delegate before presenting a Capture.

Steps

  1. Declare the conformance to CaptureNavigationControllerDelegate in one of the classes (for example as an extension of the class from where Capture is started), and add the required method. Note: the details on delegate implementation are provided in the next section, Using Captured Images .

  2. (Optional) Customize a Capture.RuntimeSettings. This step will not be covered in this example, and is detailed on Global Theme page.

  3. Instantiate CaptureNavigationController .

  4. Select a modalPresentationStyle for the CaptureNavigationController .

  5. Set a captureDelegate to an instance of the class you implemented in step 1.

  6. Present the CaptureNavigationController.

Note:

  • Code described in steps 3-6 is dealing with UI and hence must run on DispatchQueue.main (or using @MainActor).

Example

In the following example modalPresentationStyle was set to overFullScreen, and captureDelegate was set to the class that starts the Capture, which implements CaptureNavigationControllerDelegate:

import Sensibill

class YourClass {

    // This function must run on main thread
    func startCapture() {
        
        // 3. Instantiate
        let captureNavigationController = CaptureNavigationController()
        
        // 4. Select a modalPresentationStyle
        captureNavigationController.modalPresentationStyle = .overFullScreen
        
        // 5. Set a captureDelegate
        captureNavigationController.captureDelegate = self
        
        // 6. Present
        present(captureNavigationController, animated: true, completion: nil)
    }
}

// 1. Conformance to CaptureNavigationControllerDelegate
extension YourClass: CaptureNavigationControllerDelegate {
    
    func captureNavigationController(_ controller: CaptureNavigationController, didFinishCapture result: CaptureResult) {
    }
}

The only entry point to this module is via the CaptureStandaloneActivity (reference ). The Activity optionally takes a CaptureConfig (reference ) object for configuration as an intent extras. If none is provided, it will use the default CaptureConfig held at CaptureConfig.defaultConfig.

As Android has recently deprecated startActivityForResult, the below example will include both the classic startActivityForResult as well as the registerForActivityResult methods of starting the capture activity.
Our ActivityResultContract implementation for the capture flow is CaptureImages (reference ).

Example

Kotlin

class MyActivity : AppCompatActivity() {
    // ...

    // ---------- Using `startActivityForResult` ----------
    private fun launchCapture() {
        // Create configuration object if required
        val config = CaptureConfig(
            allowFlashToggling = false,
            defaultFlashMode = FlashMode.FLASH_MODE_OFF,
            maxPages = 5,
            enableLongCapture = false,
            enableSecureWindow = true
        )

        // Create intent and attach config
        val intent = Intent(this, CaptureStandaloneActivity::class.java).apply {
            putExtra(CaptureStandaloneActivity.EXTRA_CAPTURE_CONFIG, config)
        }

        // Start the capture activity
        startActivityForResult(intent, REQUEST_CODE_TO_CAPTURE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            REQUEST_CODE_TO_CAPTURE -> {
                // See next section for handling the result
            }
            else -> super.onActivityResult(requestCode, resultCode, data)
        }
    }

    companion object {
        private const val REQUEST_CODE_TO_CAPTURE = 0x123
    }

    // ---------- Using `registerForActivityResult` ----------
    private lateinit var launcher: ActivityResultLauncher<CaptureConfig>

    private fun registerForCaptureResult() {
        val contract = CaptureImages()
        launcher = registerForActivityResult(contract) { result ->
            // See next section for handling the result
        }
    }

    private fun launchCaptureWithRegistration() {
        launcher.launch(CaptureConfig())
    }

}

Java

public class MyActivity extends AppCompatActivity {
    private static final int REQUEST_CODE_TO_CAPTURE = 0x123;

    // ...

    // ---------- Using `startActivityForResult` ----------
    private void launchCapture() {
        // Create configuration object if required
        final CaptureConfig captureConfig = new CaptureConfig(
                true,
                true,
                FlashMode.FLASH_MODE_OFF,
                true,
                true,
                false,
                true,
                3,
                false,
                true,
                false,
                true,
                true,
                true,
                DocumentTypeStrings.Companion.getDefaultReceiptStrings(),
                CaptureDocumentType.RECEIPT
        );

        // Create intent and attach config
        final Intent intent = new Intent(this, CaptureStandaloneActivity.class);
        intent.putExtra(CaptureStandaloneActivity.EXTRA_CAPTURE_CONFIG, config);

        // Start the capture activity
        startActivityForResult(intent, REQUEST_CODE_TO_CAPTURE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) {
        if (requestCode == REQUEST_CODE_TO_CAPTURE) {
            // See next section for handling the result
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }

    }

    // ---------- Using `registerForActivityResult` ----------
    private ActivityResultLauncher<CaptureConfig> launcher;

    private void registerForCaptureResult() {
        CaptureImages contract = new CaptureImages();

        launcher = registerForActivityResult(contract, new ActivityResultCallback<BareCapturedReceiptData[]>() {
            @Override
            public void onActivityResult(BareCapturedReceiptData[] result) {
                // See next section for handling the result
            }
        });
    }

    private void launchCaptureWithRegistration() {
        launcher.launch(new CaptureConfig());
    }

    // ...
}

Using Captured Images

Implementing a Capture Delegate

The protocol CaptureNavigationControllerDelegate can be implemented as an extension to an existing class (for example in a class that instantiates capture, as shown in the examples on this page), or in a separate class.

Capture provides a CaptureNavigationControllerDelegate protocol to return the result of the capture back to the integrator’s app.

The result of the capture - a CaptureResult object - will contain all images user captured or selected from the gallery, as images array in Data format.

The data represents a JPEG-encoded image, and can be saved to file, or displayed. The image size and quality depends on settings, described in Captured Images Format section below.

The public Data extension, included with Sensibill SDK, also allows you to

  • Retrieve the image metadata, using metadata property,
  • Get image location (if enabled and captured) in CLLocation format, using getLocation() function.

If a user cancels the operation, or an error occurs during capture, the images array in the CaptureResult will be empty.

Example

extension YourClass: CaptureNavigationControllerDelegate {

     func captureNavigationController(_ controller: CaptureNavigationController, didFinishCapture result: CaptureResult) {

        guard !result.isEmpty else {
            print("User didn't capture any images, or an error occurred")
            return
        }

        print("Received \(result.images.count) photos")

        // Get image metadata
        for image in result.images {
            print(image.metadata)
        }
    }
}

Captured Images Format

All images in CaptureResult are JPEG-encoded. However the compression quality and size of the images depends on configuration, as explained below.

The most common use case is sending the captured images to Sensibill’s API for processing. To ensure the successful processing of the document by Sensibill’s API, the images must comply with the size and quality requirements specified in Sensibill API documentation. Therefore by default the captured images are compressed and resized to a format compatible with Sensibill API.

If you require the uncompressed images in their original size, you can customize Capture.FeatureFlags and set compressForSensibillApi flag to false. See Feature Switching page for more information on how to configure Capture feature flags.

Upon completion, the CaptureStandaloneActivity will provide the calling activity an array of BareCapturedReceiptData (reference ) objects representing the captured receipt images.
The BareCapturedReceiptData array will be attached to the activity result data Intent as a Serializable with the key CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS.

If the user cancels the flow or the flow ends unexpectedly, the resultCode will be Activity.RESULT_CANCELED, or the returned data will be null.

As above, examples have been provided for both the deprecated startActivityForResult flow, as well as the registerForActivityResult flow.

Kotlin

// ---------- Using `startActivityForResult` ----------
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
        REQUEST_CODE_TO_CAPTURE -> {
            // Result is OK when the flow was completed and images were captured
            if (resultCode == Activity.RESULT_OK && data != null) {
                // Retrieve captured images
                val capturedImages = data.getSerializableExtra(CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS)
                    as Array<BareCapturedReceiptData>

                // Process images
                // ...

                // *Important* clean up images
                capturedImages.forEach { it.cleanup() }
            } else {
                // Capture flow was cancelled
                Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show()
            }
        }
        else -> super.onActivityResult(requestCode, resultCode, data)
    }
}

// ---------- Using `registerForActivityResult` ----------
private fun registerForCaptureResult() {
    val contract = CaptureImages()
    launcher = registerForActivityResult(contract) { capturedImages ->
        if (capturedImages != null) {
            // Process images
            // ...

            // *Important* clean up images
            capturedImages.forEach { it.cleanup() }
        } else {
            // The user cancelled the capture flow
            Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show()
        }
    }
}

Java

// ---------- Using `startActivityForResult` ----------
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (requestCode == REQUEST_CODE_TO_CAPTURE) {
        if (resultCode == Activity.RESULT_OK && data != null) {
            // Retrieve captured images
            BareCapturedReceiptData[] capturedImages = (BareCapturedReceiptData[]) data.getSerializableExtra(CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS);

            // Process images
            // ...

            // *Important* clean up images
            for (BareCapturedReceiptData capturedImage : capturedImages) {
                capturedImage.cleanup();
            }
        } else {
            // Capture flow was cancelled
            Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show();
        }
    } else {
        super.onActivityResult(requestCode, resultCode, data);
    }
}

// ---------- Using `registerForActivityResult` ----------
private void registerForCaptureResult() {
    CaptureImages contract = new CaptureImages();
    launcher = registerForActivityResult(contract, new ActivityResultCallback<BareCapturedReceiptData[]>() {
        @Override
        public void onActivityResult(BareCapturedReceiptData[] result) {
            if (result != null) {
                // Process images
                // ...

                // *Important* clean up images
                for (BareCapturedReceiptData capturedImage : result) {
                    capturedImage.cleanup();
                }
            } else {
                // Capture flow was cancelled
                Toast.makeText(MyActivity.this, "No images captured", Toast.LENGTH_LONG).show();
            }
        }
    });
}

Next steps