Migrate to Verify v3
API changes
The v3 API brings numerous breaking changes over the v2 API. It introduces a completely different API surface, both for requests and responses, aiming to simplify things for the end user, and to make responses more clear.
This document lists how the API has changed, and what you need to change in your v2 integration to start using v3.
For a full API reference and OpenAPI schema, see:
We will use the us-east server to illustrate with examples, but they all apply to other servers as well. Check your region.
New URL
The new verification endpoint is /api/v3/verify.
| v2 | v3 |
|---|---|
| https://us-east.verify.microblink.com/api/v2/docver | https://us-east.verify.microblink.com/api/v3/verify |
In addition to the verification endpoint, the API also has an extraction-only endpoint; see the Extraction API section.
Schema
Get the OpenAPI specification here.
Changes in the request
Only multipart
In v2, you had three ways to send an image: as a URL, as a base64-encoded string, or as a multipart/form-data upload.
The v3 API accepts only multipart uploads. It doesn't support uploading images as base64-encoded blobs, nor does it support passing images using a URL.
- cURL
- JavaScript
- Python
- Go
curl https://us-east.verify.microblink.com/api/v3/verify \
--request POST \
--header 'Authorization: Basic <credentials>' \
--form 'imageFirstSide=@front_id.jpg' \
--form 'imageSecondSide=@back_id.png'
const formData = new FormData()
formData.append('imageFirstSide', new Blob([]), 'front_id.jpg')
formData.append('imageSecondSide', new Blob([]), 'back_id.png')
fetch('https://us-east.verify.microblink.com/api/v3/verify', {
method: 'POST',
headers: {
Authorization: 'Basic <credentials>'
},
body: formData
})
requests.post(
"https://us-east.verify.microblink.com/api/v3/verify",
headers={
"Authorization": "Basic <credentials>"
},
files=[
("imageFirstSide", open("front_id.jpg", "rb")),
("imageSecondSide", open("back_id.png", "rb"))
]
)
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
requestUrl := "https://us-east.verify.microblink.com/api/v3/verify"
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
part, _ := writer.CreateFormFile("imageFirstSide", "front_id.jpg")
f, _ := os.Open("front_id.jpg")
defer f.Close()
_, _ = io.Copy(part, f)
part, _ = writer.CreateFormFile("imageSecondSide", "back_id.png")
f, _ = os.Open("back_id.png")
defer f.Close()
_, _ = io.Copy(part, f)
writer.Close()
req, _ := http.NewRequest("POST", requestUrl, payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Add("Authorization", "Basic <credentials>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
Changes in request parameters
JSON object in multipart upload
In v2, you used application/json for the request and uploaded a JSON object with the configuration.
In v3, you use multipart/form-data and pass a (JSON-encoded) configuration object as one of the form-data fields.
Using configuration.json as an example:
{
"verification": {
"useCase": {
"verificationPolicy": "HighAssurance",
"verificationContext": "Remote",
"manualReviewStrategy": "RejectedOnly"
},
"settings": {
"rejectExpiredDocuments": true,
"screenPresenceSensitivity": "Level5",
"dataMatchSensitivity": "Level7"
}
},
"extraction": {
"redactionSettings": {
"globalMode": "ImageOnly"
},
"documentCaptureModuleSettings": {
"documentImageReturnEnabled": true,
"faceImageExtractionEnabled": true
}
}
}
The request looks like this now:
- cURL
- JavaScript
- Python
- Go
curl https://us-east.verify.microblink.com/api/v3/verify \
--request POST \
--header 'Authorization: Basic <credentials>' \
--form 'imageFirstSide=@front_id.jpg' \
--form 'imageSecondSide=@back_id.png' \
--form 'configuration=@configuration.json;type=application/json'
const formData = new FormData()
formData.append('imageFirstSide', new Blob([]), 'front_id.jpg')
formData.append('imageSecondSide', new Blob([]), 'back_id.png')
formData.append('configuration', new Blob([]), 'configuration.json')
fetch('https://us-east.verify.microblink.com/api/v3/verify', {
method: 'POST',
headers: {
Authorization: 'Basic <credentials>'
},
body: formData
})
requests.post(
"https://us-east.verify.microblink.com/api/v3/verify",
headers={
"Authorization": "Basic <credentials>"
},
files=[
("imageFirstSide", open("front_id.jpg", "rb")),
("imageSecondSide", open("back_id.png", "rb")),
("configuration", ("configuration.json", open("configuration.json", "rb"), "application/json"))
]
)
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
requestUrl := "https://us-east.verify.microblink.com/api/v3/verify"
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
part, _ := writer.CreateFormFile("imageFirstSide", "front_id.jpg")
f, _ := os.Open("front_id.jpg")
defer f.Close()
_, _ = io.Copy(part, f)
part, _ = writer.CreateFormFile("imageSecondSide", "back_id.png")
f, _ = os.Open("back_id.png")
defer f.Close()
_, _ = io.Copy(part, f)
part, _ = writer.CreateFormFile("configuration", "configuration.json")
f, _ = os.Open("configuration.json")
defer f.Close()
_, _ = io.Copy(part, f)
writer.Close()
req, _ := http.NewRequest("POST", requestUrl, payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Add("Authorization", "Basic <credentials>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
Renamed and removed parameters
imageFrontis renamed toimageFirstSideimageBackis renamed toimageSecondSideimageBarcodehas the same name- a
configurationfield has been added;configuration.verificationandconfiguration.imageAssessmentare relevant for this migration guide optionsis renamed tosettingsand moved toconfiguration.verificationuseCaseis moved toconfiguration.verificationcaptureSessionIdandsessionIDare removed; usetraceIdto correlate a request with your own records
Changes in use cases
- All use cases no longer support
"Unknown"as a value. verificationContextis unchanged.manualReviewStrategyis unchanged.manualReviewSensitivityhas been removed.documentVerificationPolicyhas changed, see belowcaptureConditionshas changed, see below
Verification policy
documentVerificationPolicy is renamed to verificationPolicy.
Its allowed values are now:
"HighConversion"(previously"Permissive")"Balanced"(previously"Standard")"HighAssurance"(previously"Strict"/"VeryStrict")
Because the boundaries of verificationPolicy moved (3 values vs. 4 previously), re-tune rather than re-map.
Run a sample of known-good and known-fraudulent documents through your candidate policy and compare verdicts before you switch production traffic.
Capture conditions
captureConditions has been renamed to cropAffectsVerdict and moved to configuration.verification.settings.
It also changes shape, not just name.
In v2 it was an enum (NoControl, Basic, Hybrid).
In v3, cropAffectsVerdict is a boolean and answers one question: can a cropped document receive an "Accept" in its verdict?
If cropAffectsVerdict is true (default), a cropped document will not be accepted; it can only be rejected or sent to manual review.
If cropAffectsVerdict is false, a cropped document can be accepted.
Changes in options (settings)
The v2 options object splits in two.
Verification tuning goes to configuration.verification.settings; everything about extracted data and returned images goes under configuration.extraction.
Every MatchLevel becomes a Sensitivity.
The values are unchanged (Level1 through Level10, and Disabled), except that Unknown is no longer accepted.
All sensitivity values default to "Level5", except barcodeAuthenticityCheck which remains at a default of "Level7".
options.screenMatchLevelis nowverification.settings.screenPresenceSensitivityoptions.photocopyMatchLevelis nowverification.settings.photocopySensitivityoptions.barcodeAnomalyMatchLevelis nowverification.settings.barcodeAuthenticitySensitivityoptions.photoForgeryMatchLevelis nowverification.settings.portraitForgerySensitivityoptions.dataMatchMatchLevelis nowverification.settings.dataMatchSensitivityoptions.generativeAiMatchLevelis nowverification.settings.generativeAiSensitivity
The seven image-quality match levels collapse into one setting.
blurMatchLevel, glareMatchLevel, lightingMatchLevel, sharpnessMatchLevel, handOcclusionMatchLevel, dpiMatchLevel, and tiltMatchLevel are all replaced by imageAssessment.imageQualitySensitivity, which applies uniformly to every image-quality dimension.
Renamed:
treatExpirationAsFraudis nowverification.settings.rejectExpiredDocumentsimageQualityInterpretationis nowimageQualityRetryPolicyVeryHighConversionhas been removed- the rest of the values are renamed and describe when to ask for a retake:
NeverRetryBadQuality,RetryBadQualityAlways,RetryBadQualityForAcceptances, andRetryBadQualityForRejections.
Moved under extraction:
returnFullDocumentImageis nowextraction.documentCaptureModuleSettings.documentImageReturnEnabledreturnFaceImageis nowextraction.documentCaptureModuleSettings.faceImageExtractionEnabledreturnSignatureImageis nowextraction.vizModuleSettings.signatureImageExtractionEnabledanonymizationModeis nowextraction.redactionSettings.globalMode, with its values unchanged (None,ImageOnly,ResultFieldsOnly,FullResult)
Removed:
staticSecurityFeaturesMatchLevel: the check still runs and is still reported, atverification.checks.visualCheck.securityFeaturesCheck, but it is no longer tunablereturnImageFormat, and theImageFormatmultipart field: returned images are always base64-encoded JPG
Consent object
The consent object is mandatory if you use the cloud API. For self-hosted deployments, it isn't used.
v2 had no consent concept.
In v3, a cloud request registers the consent you collected from the end user, as a consent text form value alongside the images.
{
"userId": "<your-id-for-the-end-user>",
"durationDays": 365,
"givenOn": "2026-07-31T09:00:00Z",
"note": "User agreed to identity verification terms.",
"customerContext": {
"customerId": "<your-customer-id>",
"transactionId": "<your-transaction-id>"
}
}
- cURL
- JavaScript
- Python
- Go
curl https://us-east.verify.microblink.com/api/v3/verify \
--request POST \
--header 'Authorization: Basic <credentials>' \
--form 'imageFirstSide=@front_id.jpg' \
--form 'imageSecondSide=@back_id.png' \
--form 'consent={
"userId": "<your-id-for-the-end-user>",
"durationDays": 365
}'
const formData = new FormData()
formData.append('imageFirstSide', new Blob([]), 'front_id.jpg')
formData.append('imageSecondSide', new Blob([]), 'back_id.png')
formData.append('consent', '{
"userId": "<your-id-for-the-end-user>",
"durationDays": 365
}')
fetch('https://us-east.verify.microblink.com/api/v3/verify', {
method: 'POST',
headers: {
Authorization: 'Basic <credentials>'
},
body: formData
})
requests.post(
"https://us-east.verify.microblink.com/api/v3/verify",
headers={
"Authorization": "Basic <credentials>"
},
files=[
("imageFirstSide", open("front_id.jpg", "rb")),
("imageSecondSide", open("back_id.png", "rb"))
],
data={
"consent": "{\n \"userId\": \"<your-id-for-the-end-user>\",\n \"durationDays\": 365\n}"
}
)
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
requestUrl := "https://us-east.verify.microblink.com/api/v3/verify"
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
part, _ := writer.CreateFormFile("imageFirstSide", "front_id.jpg")
f, _ := os.Open("front_id.jpg")
defer f.Close()
_, _ = io.Copy(part, f)
part, _ = writer.CreateFormFile("imageSecondSide", "back_id.png")
f, _ = os.Open("back_id.png")
defer f.Close()
_, _ = io.Copy(part, f)
_ = writer.WriteField("consent", "{\n \"userId\": \"<your-id-for-the-end-user>\",\n \"durationDays\": 365\n}")
writer.Close()
req, _ := http.NewRequest("POST", requestUrl, payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Add("Authorization", "Basic <credentials>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))
}
Only two properties are required:
userId: your own unique identifier for the end user granting consentdurationDays: how long the consent stays valid, used to calculate its expiry
The rest are optional.
givenOn timestamps when consent was granted, note records the agreement text or statement the user accepted, and customerContext links the record to your own customerId and transactionId.
Because userId is your identifier, this is the field that ties consent records to a person in your system, so it should be stable across requests for the same end user.
Requests that don't send a consent object return a 400 response.
For the full field reference and the error responses consent can produce, see Get your users' consent.
Changes in the response
For a field-by-field walkthrough of the v3 response rather than a migration diff, see Interpret the response.
verdict replaces recommendedOutcome
In v2, verification.recommendedOutcome carried the overall decision.
In v3, the decision is verification.verdict.
Two values are renamed, and Unknown is gone:
Undeterminableis nowUnverifiableManuallyReviewis nowReviewAccept,Reject, andRetryare unchanged
certaintyLevel is no more
In v2, you would inspect verification.certaintyLevel to assess how certain the verification process was.
In v3, this field no longer exists, and all verifications can be considered to be of high certainty.
Checks are a structured object, not a flat array
In v2, checks was an array of Check objects, each identified by a name string and able to nest further checks in its own checks array.
Finding a specific check meant walking the array and matching on names.
In v3, verification.checks is a typed object with four named groups:
{
"verification": {
"verdict": "Reject",
"failedChecks": ["checks.extractedDataCheck.matchCheck.dateOfBirthCheck"],
"checks": {
"extractedDataCheck": {},
"documentLivenessCheck": {},
"visualCheck": {},
"documentValidityCheck": {}
}
}
}
Each group has named sub-checks, so a check you care about has a stable path instead of a position in an array.
result keeps its v2 values, Pass, Fail, and NotPerformed, minus Unknown.
verification.failedChecks is new and has no v2 equivalent.
It lists the flattened path of every check that ran and failed, which is the quickest way to find why a document was rejected without walking the tree.
Tiered checks report a threshold, not a level
In v2, a TieredCheck carried a matchLevel telling you the level at which the check was evaluated.
In v3, a tiered check carries passesAtOrBelowSensitivity: the highest sensitivity at which this check would still pass.
Instead of seeing which level you configured, you see what you would have had to configure for this document to pass, which is what you need in order to re-tune.
Process indicators become image assessment
V2 returned a processIndicators array of named indicators, resolving to Pass, Fail, Warn, or NotPerformed.
V3 replaces it with a top-level imageAssessment object holding three checks:
{
"imageAssessment": {
"imageQualityCheck": { "result": "Pass" },
"croppedDocumentCheck": {
"result": "Pass",
"firstSide": { "result": "Pass" },
"secondSide": { "result": "Pass" }
},
"handPresenceCheck": { "result": "Pass" }
}
}
The Warn result has no v3 equivalent: a check either passes, fails, or wasn't performed.
Images are keyed, not listed
v2 returned images as an array of { name, base64 } pairs, so you looked an image up by its name.
v3 returns an object with a key per image, and the value is the base64 string directly:
facesignaturefirstSideCroppedsecondSideCroppedbarcode
Extracted data is typed
This is the largest change in the response.
v2 returned extraction results as generic containers: extraction.overall, extraction.mrz, and extraction.barcode were arrays of Result objects, each a { type, field, details } bag that could nest more Result objects, with the field identified by a FieldValueType string.
Reading a first name meant finding the entry whose field was FirstName.
In v3, extracted data is a typed object with named properties, so for example a first name is at extraction.result.firstName.
Per-side and per-module results move under extraction.result.subResults.{side}.{viz,mrz,barcode}, and the document class moves to extraction.result.documentClassInfo.
The per-script values that v2 expressed with ExtractedScript (Latin, Cyrillic, Arabic, Greek) are now the lowercase keys of an AlphabetString:
{
"extraction": {
"result": {
"firstName": { "latin": "JOHN" }
}
}
}
extraction.recognitionStatus, which carried the v2 ResultState (Empty, Uncertain, Valid, StageValid), is removed.
Use extraction.processingStatus instead.
Configuration echo
V2 returned optionsUsed and useCaseUsed as two separate objects.
V3 returns a single configurationUsed that mirrors the structure of the configuration you sent, so the effective value of a setting is at the same path you would have used to set it.
Currently we don't indicate the defaults in the schema, so reading configurationUsed is also a reliable way to discover the default configuration.
Pipeline status replaces the root processing status
v2 had a root-level processingStatus describing the run as a whole, with values like Completed, PartiallyCompleted, ExtractionFailed, CompletedAfterFront, and ServerModelError.
v3 splits this into a pipeline object with one entry per stage, each carrying its own status:
{
"pipeline": {
"extraction": { "status": "Completed" },
"verification": { "status": "Completed" }
}
}
CompletedAfterFront has no v3 equivalent, because a v3 request carries all of its images at once and is processed as a whole.
Messages lose their status
- Messages were revised to be more consistent and informative, and they will also be appearing more often with the intention of guiding and providing additional feedback where individual checks/settings or their use in conjunction might be confusing.
- Clients should not build validations or business logic on messages.
messages.statusfield is gone.- Error messages are removed.
See the current supported messages.
New response codes
Three status codes are worth handling before you switch. These apply to both the cloud and self-hosted deployments.
503 is returned when the workers aren't ready to accept a request.
In v2, 503 came only from the /health/live and /health/ready endpoints, never from a verification request, so a client that treats it as "endpoint down" rather than "retry shortly" needs adjusting.
413 is new, for payloads over the size limit.
429 itself is not new, since v2 returned it when the rate limit was exceeded.
What's new is that v3 documents a Retry-After header carrying a suggested delay in seconds, and distinguishes two limiters: the memory-pressure limiter returns a JSON error body, while the concurrency limiter can return an empty one.
If you already handle 429, check that your client tolerates an empty body and reads Retry-After rather than using a fixed backoff.
Changes in self-hosted deployment
New image
The self-hosted deployment now bundles the self-hosted extraction API, and has changed its name.
Grab it here:
docker pull us-docker.pkg.dev/document-verification-public/on-prem/core:4000.0.0
The new GitHub repo with the Helm charts and other deployment-related information is here.
git clone https://github.com/microblink/on-prem-ops.git
Licensing
Your existing licenses and license environment variables are unchanged.
Single-image
If you're still on a release earlier than 3.19.0, you're moving from the multi-service stack at the same time.
Remove the old conf/doc-ver-api, conf/doc-ver-runner, and conf/bundle-doc-ver directories as part of that move.
See On-prem deployment for how the current image is deployed.
The container reports its own sizing in the runtime object of every response (workerCount, inflightLimit, cpus, ram, and others), which is useful when you are capacity planning against v2 numbers.
Extraction API
V2 was verification-only.
V3 adds an extraction-only endpoint, POST /api/v3/extract, which runs the extraction half of the pipeline and skips verification.
It's the right endpoint if you only need the data off the document and don't need a verdict.
The extraction results are identical in shape to the extraction object of a verification response, so you can move between the two endpoints without changing your parsing.
The extraction endpoint accepts only the extraction and imageAssessment sections of the configuration; verification settings are rejected.
There is also a barcode-only endpoint, POST /api/v3/extract-barcode.
If you are coming to this endpoint from the standalone self-hosted extraction API rather than from Verify v2, see Migrate from self-hosted extraction API, which covers that migration in full.