A document scanner is one call now, and scanDocumentAsync is the call.
You add a document scanner to a React Native app by calling CameraView.scanDocumentAsync() from expo-camera. On iPhone that opens Apple's own scanning screen from VisionKit. On Android it opens Google's ML Kit document scanner. Back comes an ordered array of file URIs, one per page, and a PDF if you asked for one. The person gets the camera viewfinder, the edge detection, the crop handles and the cleanup that Apple and Google already wrote, and you get the files. If you have not built the app the scanner sits inside yet, start with make an app.
Two things about that sentence are newer than most advice you will find. The method is new in Expo SDK 58, and the SDK 57 copy of the same reference page has no mention of it. And the permission answer is lopsided: iOS requires a usage description string, while Google states that its scanner needs no camera permission from your app at all. Every claim below comes from Apple, Google or Expo documentation, read on 23 October 2026.
Start hereThe short version
One call, two native scanners, and a camera permission you ship without needing it.
The scanning experience people expect from a phone, where you hold the camera over a page and the app finds the edges, straightens the perspective and drops the shadow of your hand, is a system feature on both platforms. Nobody has to build it. The work in front of you is choosing the call, handling the null, and deciding where the files live after the scan.
The permission line is the one piece worth planning around. Google's scanner runs inside Google Play services and needs no camera permission from your app. Apple's runs inside your app and therefore needs a camera usage description. But expo-camera adds the Android camera permission to your manifest anyway, because the package also gives you a live camera preview. So the permissions list on your Play Store listing will mention the camera even if the only camera code in your app is the document scanner.
What does Apple document in VisionKit for scanning a document?
A single class. Apple calls VNDocumentCameraViewController an object that presents UI for a camera pass-through that helps people scan physical documents. Its availability line reads iOS 13.0 and later, iPadOS 13.0, Mac Catalyst 13.1 and visionOS 1.0, so this has been in the system since 2019 and nothing about it is experimental. The overview says a person scans a physical document page by page by tapping a camera interface in the controller's view, and that the results of a scan include images, by page number.
The shape of the API is three parts. You create the controller, set a delegate and present it. The delegate protocol has three methods: one for a successful save, one for a cancel and one for a failure with an error. Apple adds a line that native developers forget and then file bugs about, which is that your app is responsible for dismissing the document camera in all delegate callback methods. The controller also carries a class variable, isSupported, described as a Boolean that indicates whether or not the current device supports document scanning. Check it before you offer the button.
What comes back on success is a VNDocumentCameraScan, which Apple describes as a single document scanned in the document camera. It has three members worth knowing: a title, a pageCount, and imageOfPage(at:) to request the image of a page at a specified index. That is the whole result. There is no PDF handed to you: the overview says your app can create a digital version of the physical document and export the scanned images to PDF, which puts the export on your side of the line. Nothing in the controller page, the delegate page or the scan page mentions a page limit, a gallery import or a permission key.
Apple, VisionKit reference, the pages for VNDocumentCameraViewController, VNDocumentCameraViewControllerDelegate and VNDocumentCameraScan under developer.apple.com/documentation/visionkit, read 23 October 2026
Does Expo expose VisionKit's document camera?
Yes, and this is the part most guides get wrong, because it is recent. expo-camera documents a static method, scanDocumentAsync, on Android and iOS. Expo's own words: it presents the system document scanner and returns the captured pages once the user saves, on iOS it uses VisionKit's VNDocumentCameraViewController, and on Android it uses the ML Kit document scanner. The documented example is one line, an await on CameraView.scanDocumentAsync with requestPdf set to true, followed by reading pages and pdfUri off the result.
Check your SDK version before you plan around it. The same reference page for SDK 57 contains no occurrence of scanDocumentAsync, so the method arrived in SDK 58. That matters because advice written even a few months ago tells you to add a community library or write a config plugin for something the SDK now does. Both versions of the page were read on 23 October 2026.
The option surface is narrow, which is the honest trade for a one line call. DocumentScanningOptions documents two properties. quality is a number between 0 and 1 for the JPEG compression of the saved page images, default 1, and Expo notes it has no effect on Android because the ML Kit scanner controls image compression. requestPdf is a boolean, default false, for producing a multi-page PDF alongside the per-page images. The result, DocumentScanningResult, has pages, an ordered array of file URIs with one per scanned page, and an optional pdfUri that is only present when you asked for the PDF. Handle null: the promise resolves to null if the user cancels the scan or the scanner is unavailable on the device.
One line in that reference deserves a sticky note. The page images and the optional PDF are written to the app's cache directory, and Expo tells you to copy them to a permanent location with expo-file-system if you need to keep them. The cache is a place the operating system may empty. Decide on the spot whether a scan is a draft or a record, and if it is a record, move it or upload it before the screen closes. That decision is the same one behind add offline mode, because a scan taken in a basement has to wait somewhere safe until there is signal to send it.
Check yours
What the one call actually gives you
Tick what your scan has to do. The iOS and Android notes are the ones in Apple's VisionKit reference and Google's ML Kit document scanner pages. The third note is what Expo exposes on top of them. All three read on 23 October 2026.
Nothing ticked. If all you need is a photo of a receipt that comes back straight and readable, the one call does that on both platforms with no options at all, and you can stop reading here.
What does Android document as the equivalent?
The ML Kit document scanner API, delivered as an SDK Google names play-services-mlkit-document-scanner. Google is explicit about how it arrives: the models, scanning logic and UI flow are dynamically downloaded by Google Play services, the app size impact is around 300KB, and users might have to wait for that download before first use. The Android guide lists the Gradle dependency at version 16.0.0 as of 23 October 2026, and two device floors: API level 21 or above, and a minimum of 1.7GB of total device RAM, below which the API returns an MlKitException with error code UNSUPPORTED.
The flow itself belongs to Google. The scanner provides a dedicated viewfinder screen and a preview screen, and Google lists what the person can do inside them: crop, apply filters, remove shadows, clean stains and reorder pages. Three customisable controls exist: importing from the photo gallery, a limit on the number of pages scanned, and a scanner mode. The modes are SCANNER_MODE_BASE for basic editing, SCANNER_MODE_BASE_WITH_FILTER which adds image filters, and SCANNER_MODE_FULL, which Google marks as the default and which adds the cleaning that erases stains and fingers. Google also notes that full mode will pick up future features automatically through Play services updates while the other two keep their current feature sets.
A native implementation builds GmsDocumentScannerOptions, gets a client from GmsDocumentScanning, then launches the scanner through getStartScanIntent and the AndroidX Activity Result APIs. The result object gives the number of pages scanned, the URIs of the JPEG images and the PDF, depending on the result formats you asked for. Google adds one performance note that applies through Expo as well: only request the output formats you actually need, because generating document files takes time and processing power. That is the argument for leaving requestPdf off unless a PDF is the thing you are shipping.
Google, ML Kit document scanner, the overview and the Android guide, read 23 October 2026
Does either one need a camera permission string?
iOS does. Apple's Information Property List page for NSCameraUsageDescription describes it as a message that tells people why the app is requesting access to the device's camera, and carries an Important note saying the key is required if your app uses APIs that access the device's camera. VisionKit's document camera is a camera pass-through running inside your process, so the key applies. In an Expo project the config plugin sets it: the cameraPermission option writes NSCameraUsageDescription, with a default string of Allow $(PRODUCT_NAME) to access your camera. To translate it, Expo says to add NSCameraUsageDescription to the ios object in each locale file, keep cameraPermission at its default, and Expo writes the localised values into InfoPlist.strings during prebuild.
Android does not, and Google says so twice on the same page. No camera permission is required, because the document scanner leverages Google Play services' camera permission, and the user controls which files come back to your app. The capabilities list repeats it: no camera permission is needed from your app. The whole flow runs on-device, which Google also states. That is a genuinely unusual arrangement and it is the reason the Android scanner is cheaper to ship than most camera features.
Then comes the catch, and it is worth knowing before somebody on your team reads the Play listing and asks why. expo-camera automatically adds the CAMERA permission to your app's AndroidManifest.xml, because the package's main job is a live camera preview. So a project that installs expo-camera purely to call scanDocumentAsync still declares android.permission.CAMERA. Google's scanner does not need it, your manifest has it, and the two facts sit next to each other without contradiction. On iOS the equivalent keys the library uses are NSCameraUsageDescription and NSMicrophoneUsageDescription, the second one because expo-camera records video too. Write both strings as if a cautious person will read them, since that is exactly who does. If you want to know how many people open the scanner and back out before saving, that is a two-event question for add analytics, and the null return from the promise is the cancel signal.
Google, ML Kit document scanner overview, the no camera permission line, read 23 October 2026
The same scan on two platforms, side by side
Both columns are what each vendor documents, read on 23 October 2026. Read it as the list of differences you have to design around, because your users will not all be on the same phone and the scanner behaves differently depending on which one they picked.
| The question | iOS, VisionKit | Android, ML Kit |
|---|---|---|
| The scanning screen is written by the platform | Yes | Yes |
| Hands back a PDF without you building one | No. Apple says your app exports the images to PDF | Yes, through RESULT_FORMAT_PDF |
| Your app declares a camera permission | Yes, NSCameraUsageDescription | No, Google says none is required |
| Earliest version in the documentation | iOS 13.0 | API level 21 |
| A shipped device can still refuse | Yes, check the isSupported class variable | Yes, below 1.7GB of total RAM it returns UNSUPPORTED |
| Models and UI download before the first scan | No download step appears in Apple's pages | Yes, Google Play services delivers them |
| A page limit you can set | No | Yes, setPageLimit |
| Gallery import inside the flow | No | Yes, setGalleryImportAllowed |
What is left to build once the scanner works?
Four small jobs, and none of them are the camera. Move the files out of the cache the moment a scan is kept. Show the pages back to the person so they can throw one away and scan it again, because a bad page is discovered at the desk and not at the shelf. Handle the null return as a cancel rather than an error, since Expo uses the same value for a cancel and for a device that cannot scan. And give the scan somewhere to go: a job, an invoice, a delivery, a tenancy. A scanner with no destination is a camera roll with extra steps.
Newly builds this kind of app from a description. You say what the screens do and it writes a real React Native and Expo project you own, with two-way GitHub sync, so expo-camera and a call to scanDocumentAsync are code you can read and change rather than a setting in somebody's dashboard. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It is $25 a month, with no free tier.
It will not press submit on the App Store for you, and that is on purpose: Apple guideline 4.2.6 says apps created from a commercialized template or app generation service are rejected unless they are submitted directly by the provider of the app's content, so the account and the submission stay yours. The steps for that are in publish to app store. Before you get there, run the scanner on an old cheap Android phone, because the 1.7GB RAM floor is a real device and not a footnote, and the null branch you wrote in five seconds is the only thing standing between that user and a button that does nothing.
Questions people ask about adding a document scanner
Call CameraView.scanDocumentAsync() from expo-camera. Expo's Camera reference says the method presents the system document scanner and returns the captured pages once the user saves, that on iOS it uses VisionKit's VNDocumentCameraViewController, and that on Android it uses the ML Kit document scanner. The promise resolves to a result with a pages array of file URIs, one per page, plus a pdfUri when you passed requestPdf. It resolves to null if the user cancels or the scanner is unavailable on the device. Read from the Expo SDK 58 Camera page on 23 October 2026.
Apple and Google wrote the scanner. Write the rest.
Decide three things before you build: what a scan attaches to, where the file lives after the person taps save, and what the screen says on a phone too small to scan. That list is the brief, and the camera work is already done.
Start building