Back to the blog

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.

Extract Passport Data
passport APIpassport OCR integrationOCR APIextract passport dataJavaScriptPython

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.

HTTPcodeWhat happenedWhat to do
401MISSING_API_KEYHeader absentProgramming error
401INVALID_API_KEYKey invalid or revokedCheck credentials
402INSUFFICIENT_TOKENSBalance exhaustedTop up; see enroll_url
422LOW_IMAGE_QUALITYImage unreadableRecapture
429RATE_LIMITEDToo many requestsWait Retry-After
429TOO_MANY_FAILED_EXTRACTIONSFailure streakCheck your image source
500EXTRACTION_FAILEDEngine failedRetry 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

  1. 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.

  2. Store the extraction_id. It comes back on error responses too, and it's the reference for any support question.

  3. Watch tokens_remaining. It arrives on every successful response — the cheap way to alert before you run out mid-operation.

  4. 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