Integration examples
Copyable server-side Node.js, Python and cURL examples with one POST per attempt.
These examples send one POST. They do not generate an idempotency key, retry POST automatically, follow redirects or print extracted data. Set INE_API_KEY and a saved INE_IDEMPOTENCY_KEY in your server environment before running them.
If the request returns 202, use the same company's key for GET after Retry-After. If it times out, preserve the same key and exact image bytes; the outcome is unconfirmed. See errors and retries.
Node.js 22+
Save this as ine-request.mjs. It uses native APIs and needs no SDK or package installation. The example assumes front.jpg and back.jpg are actual JPEG files.
import { readFile } from 'node:fs/promises';
const apiKey = process.env.INE_API_KEY;
const transactionKey = process.env.INE_IDEMPOTENCY_KEY;
if (!apiKey) throw new Error('Set INE_API_KEY on the server.');
if (!/^[A-Za-z0-9._:-]{8,128}$/.test(transactionKey || '')) {
throw new Error('Set a saved, stable INE_IDEMPOTENCY_KEY.');
}
const form = new FormData();
for (const side of ['front', 'back']) {
const bytes = await readFile(`${side}.jpg`);
form.set(side, new Blob([bytes], { type: 'image/jpeg' }), `${side}.jpg`);
}
let response;
try {
response = await fetch('https://ocr.ecostudios.dev/api/v3/ine', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Idempotency-Key': transactionKey,
Accept: 'application/json',
'User-Agent': 'ine-api-backend-example/1.0',
},
body: form,
redirect: 'manual',
signal: AbortSignal.timeout(90_000),
});
} catch {
throw new Error('Outcome unconfirmed. Keep the same transaction key and images.');
}
const result = await response.json().catch(() => null);
console.log(JSON.stringify({
http_status: response.status,
request_id: result?.request_id ?? response.headers.get('x-request-id'),
status: result?.status,
error_code: result?.error?.code,
action: result?.error?.action,
retry_after: response.headers.get('retry-after'),
}));
// Consume result.data privately when HTTP 200 and status is succeeded.
// Do not log the entire response or automatically resubmit it.
node ine-request.mjs
A response outside the expected 200/202 envelopes needs explicit error handling, including non-JSON responses from the network edge. A redirect is not followed and must not be treated as success.
Python 3.10+
Save this as ine_request.py. It uses only the standard library. The random multipart boundary is transport formatting, not a new transaction key.
import json
import os
import re
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import HTTPRedirectHandler, Request, build_opener
from uuid import uuid4
api_key = os.environ.get('INE_API_KEY')
transaction_key = os.environ.get('INE_IDEMPOTENCY_KEY', '')
if not api_key or not re.fullmatch(r'[A-Za-z0-9._:-]{8,128}', transaction_key):
raise SystemExit('Set the server API key and a saved transaction key.')
class NoRedirects(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
boundary = 'ine-upload-' + uuid4().hex
parts = []
for side in ('front', 'back'):
parts.extend([
(f'--{boundary}\r\nContent-Disposition: form-data; '
f'name="{side}"; filename="{side}.jpg"\r\n'
'Content-Type: image/jpeg\r\n\r\n').encode(),
Path(f'{side}.jpg').read_bytes(), b'\r\n',
])
parts.append(f'--{boundary}--\r\n'.encode())
request = Request(
'https://ocr.ecostudios.dev/api/v3/ine',
data=b''.join(parts), method='POST',
headers={
'X-API-Key': api_key,
'Idempotency-Key': transaction_key,
'Content-Type': f'multipart/form-data; boundary={boundary}',
'Accept': 'application/json',
'User-Agent': 'ine-api-backend-example/1.0',
},
)
try:
try:
response = build_opener(NoRedirects()).open(request, timeout=90)
except HTTPError as error:
response = error
with response:
status = response.status
headers = response.headers
try:
result = json.loads(response.read(128 * 1024))
except ValueError:
result = {}
except (URLError, TimeoutError, OSError):
raise SystemExit('Outcome unconfirmed. Keep the same transaction key and images.')
if not isinstance(result, dict):
result = {}
error = result.get('error')
error = error if isinstance(error, dict) else {}
print(json.dumps({
'http_status': status,
'request_id': result.get('request_id') or headers.get('x-request-id'),
'status': result.get('status'),
'error_code': error.get('code'),
'action': error.get('action'),
'retry_after': headers.get('retry-after'),
}))
# Consume result['data'] privately only for a succeeded HTTP 200 response.
python3 ine_request.py
cURL
cURL prints the response, so these commands can expose personal data in your terminal. Use a private environment and avoid logging the output.
curl --max-time 90 --include \
'https://ocr.ecostudios.dev/api/v3/ine' \
-H "X-API-Key: $INE_API_KEY" \
-H "Idempotency-Key: $INE_IDEMPOTENCY_KEY" \
-H 'Accept: application/json' \
-H 'User-Agent: ine-api-backend-example/1.0' \
-F 'front=@front.jpg' \
-F 'back=@back.jpg'
To retrieve a known job, set REQUEST_ID to its UUID and send GET:
curl --max-time 90 --include \
"https://ocr.ecostudios.dev/api/v3/ine/$REQUEST_ID" \
-H "X-API-Key: $INE_API_KEY" \
-H 'Accept: application/json' \
-H 'User-Agent: ine-api-backend-example/1.0'
Do not add --retry or --location. Respect Retry-After and use a bounded polling period. Keep the known request identifier for a later GET if the wait ends.
Several users at once
Your backend can submit separate transactions concurrently. Each transaction contains one credential; the company key is shared by your backend, never by end users. The current default is two active readings per company. Arrange higher concurrency with us before sending larger bursts.
The Node.js batch helper accepts up to 100 items and runs at most two at a time. Save a unique, stable idempotency key for each transaction in batch.json:
[
{ "idempotency_key": "transaction-0001", "front": "first-front.jpg", "back": "first-back.jpg" },
{ "idempotency_key": "transaction-0002", "front": "second-front.jpg", "back": "second-back.jpg" }
]
node ine-batch.mjs batch.json --output private-batch-state.json
Set INE_API_KEY privately on the server. Image paths are relative to the manifest. The helper saves request metadata in a new private file and excludes extracted data from its default output. To consume successful data in your backend, import runBatch and provide its onResult handler.
Only a structured admission 429 with an eligible error code is retried, at most twice, after Retry-After, using the same bytes and key. Technical failures, timeouts and uncertain outcomes are not automatically resubmitted. Authentication, quota or uncertain-outcome errors stop new submissions; already active requests finish. Preserve the state file and resolve known requests through GET before deciding what to submit next.