API Documentation
Integrate Extraly's AI-powered document processing into your applications.
Quick Start
Get up and running with the Extraly API in four steps:
Create an account
Sign up at extraly.ai/register and choose a plan that includes API access.
Generate an API key
Navigate to Settings → API Keys in your dashboard and create a new key. Copy it — it will only be shown once.
Submit a document
Send a POST request to https://extraly.ai/api/v1/documents with your file attached.
Retrieve results
Poll the document status endpoint or configure a webhook to receive results automatically when processing completes.
curl -X POST https://extraly.ai/api/v1/documents \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY" \ -F "document=@bank_statement.pdf" \ -F "detection_type=bank_statement"
Authentication
All API requests require two authentication headers:
| Header | Description |
|---|---|
| Authorization: Bearer <token> | Your account access token, generated when you log in or via the API. |
| X-API-Key: <key> | Your API key, created from the dashboard under Settings → API Keys. |
Both headers must be present on every request. Requests with missing or invalid credentials will receive a 401 Unauthorized response.
Authorization: Bearer eyJhbGciOiJIUzI1NiIs... X-API-Key: xtr_live_a1b2c3d4e5f6...
Endpoints
Base URL: https://extraly.ai/api/v1
/api/v1/documents
Submit a document for AI processing. Send as multipart/form-data.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| document | file | Yes* | The document file to process. Supported formats: PDF, PNG, JPG, JPEG, TIFF, WEBP. Max 20 MB. |
| file | file | Yes* | Alias for document. Either document or file must be provided. |
| detection_type | string | No | Hint for the AI model. Options: bank_statement, invoice, receipt, auto. Defaults to auto. |
| custom_fields | JSON string | No | A JSON array of custom field names to extract, e.g. ["vat_number","po_number"]. |
| webhook_url | string (URL) | No | A URL to receive a POST callback when processing completes or fails. |
| callback_url | string (URL) | No | Alias for webhook_url. |
| metadata | JSON string | No | Arbitrary key-value pairs to attach to the document for your reference. |
Response 201 Created
{
"data": {
"id": "doc_8f3a1b2c4d5e",
"status": "processing",
"detection_type": "bank_statement",
"original_filename": "bank_statement.pdf",
"pages": 3,
"webhook_url": "https://yourapp.com/webhook",
"metadata": {},
"created_at": "2026-03-21T10:30:00Z",
"updated_at": "2026-03-21T10:30:00Z"
}
}
/api/v1/documents/{id}
Retrieve the status and results of a specific document. When processing is complete, the results field contains the extracted data.
Path Parameters
| Parameter | Description |
|---|---|
| id | The unique document identifier returned when the document was submitted. |
Response 200 OK
{
"data": {
"id": "doc_8f3a1b2c4d5e",
"status": "completed",
"detection_type": "bank_statement",
"original_filename": "bank_statement.pdf",
"pages": 3,
"webhook_url": "https://yourapp.com/webhook",
"metadata": {},
"results": {
"bank_name": "PrivatBank",
"account_number": "UA21 3223 1300 0002 6007 2335 6600 1",
"currency": "UAH",
"statement_period": {
"from": "2026-01-01",
"to": "2026-01-31"
},
"opening_balance": 15230.50,
"closing_balance": 18445.75,
"transactions": [
{
"date": "2026-01-03",
"description": "Payment from Client ABC",
"amount": 5200.00,
"type": "credit",
"balance": 20430.50
}
],
"total_credits": 42500.00,
"total_debits": 39284.75,
"transaction_count": 47
},
"created_at": "2026-03-21T10:30:00Z",
"updated_at": "2026-03-21T10:31:15Z"
}
}
Status Values
| Status | Description |
|---|---|
| pending | Document uploaded, waiting in queue. |
| processing | AI is currently analyzing the document. |
| completed | Processing finished successfully. Results are available. |
| failed | Processing encountered an error. Check the error field. |
/api/v1/documents
List all documents for the authenticated user. Results are paginated.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number. |
| per_page | integer | 20 | Results per page. Maximum 100. |
| status | string | — | Filter by status: pending, processing, completed, failed. |
| detection_type | string | — | Filter by detection type. |
| sort | string | created_at | Sort field. Options: created_at, updated_at. |
| order | string | desc | Sort order: asc or desc. |
Response 200 OK
{
"data": [
{
"id": "doc_8f3a1b2c4d5e",
"status": "completed",
"detection_type": "bank_statement",
"original_filename": "bank_statement.pdf",
"pages": 3,
"created_at": "2026-03-21T10:30:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 20,
"total": 94
}
}
/api/v1/documents/{id}
Permanently delete a document and all associated data (original file, extracted results, metadata). This action cannot be undone.
Response 200 OK
{
"message": "Document deleted successfully."
}
/api/v1/documents/{id}/export/{format}
Export the extracted results in a specific format. The document must have a completed status.
Path Parameters
| Parameter | Description |
|---|---|
| id | The unique document identifier. |
| format | Export format: csv, xlsx, json, or xml. |
Response
Returns a file download with the appropriate Content-Type and Content-Disposition headers. For json format, the response is a JSON object identical to the results field from the document detail endpoint.
Webhooks
If you provide a webhook_url when submitting a document, Extraly will send a POST request to that URL when processing completes or fails. Your endpoint must respond with a 2xx status code within 10 seconds. Failed deliveries are retried up to 3 times with exponential backoff.
Completion Payload
{
"event": "document.completed",
"document_id": "doc_8f3a1b2c4d5e",
"status": "completed",
"results": {
"bank_name": "PrivatBank",
"transaction_count": 47,
"opening_balance": 15230.50,
"closing_balance": 18445.75
},
"metadata": {},
"timestamp": "2026-03-21T10:31:15Z"
}
Failure Payload
{
"event": "document.failed",
"document_id": "doc_8f3a1b2c4d5e",
"status": "failed",
"error": {
"code": "PROCESSING_ERROR",
"message": "Unable to extract data from the provided document. The file may be corrupted or in an unsupported layout."
},
"metadata": {},
"timestamp": "2026-03-21T10:31:15Z"
}
Verifying Webhook Signatures
Every webhook request includes an X-Extraly-Signature header containing an HMAC-SHA256 signature of the request body. Verify this signature using your API key as the secret to ensure the request originated from Extraly.
X-Extraly-Signature: sha256=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 # Verification pseudocode: expected = HMAC-SHA256(request_body, your_api_key) is_valid = secure_compare(expected, signature_from_header)
Rate Limits
API requests are rate-limited to 1,000 requests per hour per API key. Rate limit information is included in every response via headers:
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 984 X-RateLimit-Reset: 1711015200
When the rate limit is exceeded, the API responds with 429 Too Many Requests. The Retry-After header indicates how many seconds to wait before making another request. Implement exponential backoff in your integration for best results.
Error Codes
All errors follow a consistent format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The document field is required.",
"details": {
"document": ["The document field is required."]
}
}
}
| HTTP Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body failed validation. Check the details field for specific errors. |
| 401 | UNAUTHORIZED | Missing or invalid authentication credentials. |
| 403 | FORBIDDEN | You do not have permission to access this resource. |
| 404 | NOT_FOUND | The requested document or resource does not exist. |
| 409 | CONFLICT | The document is still processing and cannot be modified. |
| 413 | FILE_TOO_LARGE | The uploaded file exceeds the 20 MB size limit. |
| 415 | UNSUPPORTED_FORMAT | The uploaded file format is not supported. |
| 422 | PROCESSING_ERROR | The document could not be processed by the AI model. |
| 429 | RATE_LIMITED | Rate limit exceeded. Retry after the time indicated in the Retry-After header. |
| 500 | INTERNAL_ERROR | An unexpected server error occurred. Contact support if the issue persists. |
| 503 | SERVICE_UNAVAILABLE | The service is temporarily unavailable due to maintenance or high load. |
Code Examples
Below are examples for submitting a document and retrieving results in popular languages.
cURL
# Submit a document curl -X POST https://extraly.ai/api/v1/documents \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY" \ -F "[email protected]" \ -F "detection_type=invoice" \ -F "webhook_url=https://yourapp.com/webhook" # Get document status and results curl https://extraly.ai/api/v1/documents/doc_8f3a1b2c4d5e \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY" # List all documents curl "https://extraly.ai/api/v1/documents?page=1&per_page=20&status=completed" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY" # Export results as CSV curl -OJ https://extraly.ai/api/v1/documents/doc_8f3a1b2c4d5e/export/csv \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY" # Delete a document curl -X DELETE https://extraly.ai/api/v1/documents/doc_8f3a1b2c4d5e \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "X-API-Key: YOUR_API_KEY"
Python
import requests
import time
BASE_URL = "https://extraly.ai/api/v1"
HEADERS = {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"X-API-Key": "YOUR_API_KEY",
}
# Submit a document
with open("bank_statement.pdf", "rb") as f:
response = requests.post(
f"{BASE_URL}/documents",
headers=HEADERS,
files={"document": f},
data={
"detection_type": "bank_statement",
"webhook_url": "https://yourapp.com/webhook",
},
)
doc = response.json()["data"]
doc_id = doc["id"]
print(f"Document submitted: {doc_id} (status: {doc['status']})")
# Poll for results
while True:
result = requests.get(
f"{BASE_URL}/documents/{doc_id}",
headers=HEADERS,
).json()["data"]
if result["status"] in ("completed", "failed"):
break
time.sleep(2)
if result["status"] == "completed":
print(f"Transactions found: {result['results']['transaction_count']}")
# Export as XLSX
export = requests.get(
f"{BASE_URL}/documents/{doc_id}/export/xlsx",
headers=HEADERS,
)
with open("results.xlsx", "wb") as f:
f.write(export.content)
else:
print(f"Processing failed: {result['error']}")
JavaScript (Node.js)
const fs = require("fs");
const FormData = require("form-data");
const BASE_URL = "https://extraly.ai/api/v1";
const headers = {
Authorization: "Bearer YOUR_ACCESS_TOKEN",
"X-API-Key": "YOUR_API_KEY",
};
async function processDocument(filePath) {
// Submit document
const form = new FormData();
form.append("document", fs.createReadStream(filePath));
form.append("detection_type", "bank_statement");
const submitRes = await fetch(`${BASE_URL}/documents`, {
method: "POST",
headers: { ...headers, ...form.getHeaders() },
body: form,
});
const { data: doc } = await submitRes.json();
console.log(`Submitted: ${doc.id}`);
// Poll for results
let result;
while (true) {
const res = await fetch(`${BASE_URL}/documents/${doc.id}`, { headers });
result = (await res.json()).data;
if (result.status === "completed" || result.status === "failed") break;
await new Promise((r) => setTimeout(r, 2000));
}
if (result.status === "completed") {
console.log(`Transactions: ${result.results.transaction_count}`);
// Export as JSON
const exportRes = await fetch(
`${BASE_URL}/documents/${doc.id}/export/json`,
{ headers }
);
const exportData = await exportRes.json();
console.log(exportData);
}
}
processDocument("./bank_statement.pdf");
PHP
<?php
$baseUrl = 'https://extraly.ai/api/v1';
$headers = [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'X-API-Key: YOUR_API_KEY',
];
// Submit a document
$ch = curl_init("$baseUrl/documents");
$postFields = [
'document' => new CURLFile('bank_statement.pdf', 'application/pdf'),
'detection_type' => 'bank_statement',
'webhook_url' => 'https://yourapp.com/webhook',
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
$docId = $response['data']['id'];
echo "Submitted: $docId\n";
// Poll for results
while (true) {
$ch = curl_init("$baseUrl/documents/$docId");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);
if (in_array($result['status'], ['completed', 'failed'])) {
break;
}
sleep(2);
}
if ($result['status'] === 'completed') {
echo "Transactions: " . $result['results']['transaction_count'] . "\n";
// Export as CSV
$ch = curl_init("$baseUrl/documents/$docId/export/csv");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
$csv = curl_exec($ch);
curl_close($ch);
file_put_contents('results.csv', $csv);
} else {
echo "Failed: " . $result['error']['message'] . "\n";
}
Webhook Signature Verification (PHP)
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EXTRALY_SIGNATURE'] ?? '';
$apiKey = 'YOUR_API_KEY';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $apiKey);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
if ($event['event'] === 'document.completed') {
// Process the completed document
$docId = $event['document_id'];
$results = $event['results'];
// ... your logic here
}
http_response_code(200);
echo 'OK';
Webhook Signature Verification (Python)
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
API_KEY = "YOUR_API_KEY"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Extraly-Signature", "")
expected = "sha256=" + hmac.new(
API_KEY.encode(), request.data, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
event = request.json
if event["event"] == "document.completed":
doc_id = event["document_id"]
results = event["results"]
# ... your logic here
return "OK", 200