How to Integrate a Passport OCR API (Complete Guide with Code)
Step-by-step integration of a passport data extraction API: the four image upload methods, handling every error code, and ready-to-use examples in JavaScript, Python and PHP.
Integrating passport reading into your application is, in the simple case, one HTTP request. What separates an integration that works in the demo from one that survives production is how it handles the paths that aren't happy: the blurry photo, the exhausted balance, the retry.
This guide covers both.
The minimal request
curl -X POST https://extractpassportdata.com/api/v1/extract \
-H "X-API-Key: pass_your_api_key" \
-F "image_front=@./passport.jpg"
Your API key is generated in the dashboard when you create an account, which includes 20 free extractions with no card.
The response:
{
"success": true,
"extraction_id": "clx7f2k...",
"data": {
"passportNumber": "G12345678",
"surname": "GOMEZ VELAZQUEZ",
"givenNames": "MARGARITA",
"nationality": "MEX",
"issuingCountry": "MEX",
"dateOfBirth": "05/07/1980",
"dateOfIssue": "10/03/2023",
"dateOfExpiry": "05/07/2033",
"sex": "F",
"placeOfBirth": "CIUDAD DE MEXICO",
"issuingAuthority": "SRE",
"personalNumber": "GOVM800705MDFMLR09",
"mrzLine1": "P<MEXGOMEZ<VELAZQUEZ<<MARGARITA<<<<<<<<<<<<<",
"mrzLine2": "G123456786MEX8007050F3307054<<<<<<<<<<<<<<08"
},
"tokens_remaining": 19,
"upload_method": "multipart"
}
Note that dates come back as DD/MM/YYYY. Both MRZ lines arrive complete and already validated against their check digits. If you want to re-verify them yourself — a sound practice when the data feeds a regulated process — the algorithm is in ICAO 9303 check digits.
Four ways to send the image
The endpoint detects the method from the Content-Type, so you can use whichever fits your architecture without changing routes.
1. Multipart — the natural choice from a form or from curl:
const form = new FormData()
form.append('image_front', fileBuffer, 'passport.jpg')
const res = await fetch('https://extractpassportdata.com/api/v1/extract', {
method: 'POST',
headers: { 'X-API-Key': process.env.PASSPORT_API_KEY },
body: form,
})
2. Base64 in JSON — convenient when the image already travels inside a JSON payload:
await fetch(url, {
method: 'POST',
headers: {
'X-API-Key': key,
'Content-Type': 'application/json',
},
body: JSON.stringify({
image_front: `data:image/jpeg;base64,${buffer.toString('base64')}`,
}),
})
3. By URL — if your images already live in S3 or a public bucket, there's no need to download them just to re-upload:
{ "image_front_url": "https://your-bucket.s3.amazonaws.com/passport.jpg" }
4. Raw binary — the Content-Type is the image's own and the body is the bytes:
curl -X POST https://extractpassportdata.com/api/v1/extract \
-H "X-API-Key: pass_your_api_key" \
-H "Content-Type: image/jpeg" \
--data-binary @passport.jpg
Accepted formats: JPEG, PNG, WebP, HEIC and HEIF, up to 10 MB. HEIC matters more than it looks: it's what an iPhone produces by default, so a mobile app that uploads the photo as-is works with no conversion step.
Errors, which is where the real work is
Every error carries a stable code field. Branch on code, never on the error text, which can be rewritten.
| HTTP | code | What happened | What to do |
|---|---|---|---|
| 401 | MISSING_API_KEY | Header absent | Programming error |
| 401 | INVALID_API_KEY | Key invalid or revoked | Check credentials |
| 402 | INSUFFICIENT_TOKENS | Balance exhausted | Top up; see enroll_url |
| 422 | LOW_IMAGE_QUALITY | Image unreadable | Recapture |
| 429 | RATE_LIMITED | Too many requests | Wait Retry-After |
| 429 | TOO_MANY_FAILED_EXTRACTIONS | Failure streak | Check your image source |
| 500 | EXTRACTION_FAILED | Engine failed | Retry with backoff |
Two deserve their own note.
LOW_IMAGE_QUALITY (422) is not your bug
It's the response when the image arrived fine but doesn't permit a reading that verifies. It carries missing_fields listing what couldn't be read:
{
"success": false,
"code": "LOW_IMAGE_QUALITY",
"error": "Image quality does not permit a reliable reading",
"missing_fields": ["passportNumber", "mrzLine2"],
"extraction_id": "clx7f2k..."
}
We prefer this 422 to returning doubtful data, and the token is refunded: an unreadable image costs you nothing. In your UI, translate it into a concrete instruction — "the bottom strip couldn't be read, retake the photo including both lines at the foot" is infinitely more useful than "processing error".
429 with Retry-After
Both 429s carry a Retry-After header in seconds. Respect it:
async function extractWithRetry(image, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await send(image)
if (res.ok) return res.json()
const { code } = await res.json()
// Retrying fixes neither an unreadable image nor a bad key.
if (code === 'LOW_IMAGE_QUALITY' || code?.includes('API_KEY')) {
throw new Error(code)
}
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 2 ** i)
await new Promise((r) => setTimeout(r, wait * 1000))
continue
}
if (res.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** i * 1000))
continue
}
throw new Error(code)
}
throw new Error('Retries exhausted')
}
The rule that avoids most trouble: don't retry what a retry can't fix. A blurry image is still blurry on the fourth attempt; all you achieve is burning quota and tripping the failure-streak guard.
Python
import requests
def extract(path, api_key):
with open(path, 'rb') as f:
r = requests.post(
'https://extractpassportdata.com/api/v1/extract',
headers={'X-API-Key': api_key},
files={'image_front': f},
timeout=30,
)
body = r.json()
if not body.get('success'):
raise RuntimeError(f"{body.get('code')}: {body.get('error')}")
return body['data']
Always set a timeout. Without one, a hung request blocks the worker indefinitely.
PHP
$ch = curl_init('https://extractpassportdata.com/api/v1/extract');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $apiKey],
CURLOPT_POSTFIELDS => [
'image_front' => new CURLFile($path, 'image/jpeg'),
],
CURLOPT_TIMEOUT => 30,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($response['success'])) {
throw new RuntimeException($response['code'] ?? 'UNKNOWN_ERROR');
}
Four things before production
-
The API key belongs on the server. Never in a browser bundle or a mobile app: anyone can extract it and spend your balance. Upload the image to your backend and let it call the API.
-
Store the
extraction_id. It comes back on error responses too, and it's the reference for any support question. -
Watch
tokens_remaining. It arrives on every successful response — the cheap way to alert before you run out mid-operation. -
Test with deliberately bad photos. Glare, skew, low light. The happy path always works; what decides your users' actual experience is how your code behaves on the 422.
The capture patterns that prevent most of those 422s are in Common passport scanning errors.
Get started
The full documentation has the complete field reference, and the free demo lets you try a real passport without signing up. Creating an account gives you 20 extractions to integrate against real data; pricing starts after that.
Need to extract passport data automatically?
Try our API with 20 free extractions. Integrate in minutes, get results in seconds.
Start for free