v1.0

API Documentation

Complete reference for integrating with the FLVXtract API for automated bank statement extraction and analysis.

Introduction

Everything you need to integrate with the FLVXtract API for automated bank statement extraction and analysis.

Upload & Extract

Upload bank statement PDFs and receive structured transaction data with AI-powered extraction.

Lending Analytics

Get lending evaluation metrics including salary regularity, debt-to-income ratio, and cash flow scores.

Real-time Webhooks

Receive instant notifications when extraction completes via secure webhook delivery.

Base URL

All API requests are made to the following base URL. All endpoints require HTTPS.

Base URL
https://api.flvxtract.com

Content Type

All request and response bodies use JSON unless otherwise specified (e.g., file uploads use multipart/form-data).

Request Headers
Content-Type: application/json
Accept: application/json

Quick Start

  1. Create an account or log in to get your JWT token
  2. Or generate an API Key pair from the API Keys section
  3. Upload a bank statement PDF via POST /api/statements/upload
  4. For consent flows, create a request via POST /api/consent/requests and share the join link
  5. Poll or configure a webhook to be notified on completion
  6. Retrieve results via GET /api/statements/{'{id}'}/analysis

Authentication

FLVXtract supports two authentication methods: JWT Bearer tokens and API Keys.

JWT Bearer Token

JWT authentication is ideal for user-facing applications. Tokens expire after 60 minutes and can be refreshed using the refresh token (valid for 7 days).

1. Obtain a token

bash
curl -X POST https://api.flvxtract.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "your-password"
  }'
javascript
const response = await fetch('https://api.flvxtract.com/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'your-password',
  }),
});
const { accessToken, refreshToken } = await response.json();
python
import requests

response = requests.post('https://api.flvxtract.com/api/auth/login', json={
    'email': 'user@example.com',
    'password': 'your-password',
})
data = response.json()
access_token = data['accessToken']

2. Use the token

Authorization Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

3. Refresh when expired

bash
curl -X POST https://api.flvxtract.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "your-refresh-token" }'

API Key Authentication

API keys are ideal for server-to-server integrations. Each key consists of a Consumer Key and Consumer Secret pair.

Include keys in headers

API Key Headers
X-Consumer-Key: flvx_prod_a1b2c3d4e5f6...
X-Consumer-Secret: sk_live_x9y8z7w6v5u4...
bash
curl https://api.flvxtract.com/api/statements \
  -H "X-Consumer-Key: flvx_prod_a1b2c3d4e5f6..." \
  -H "X-Consumer-Secret: sk_live_x9y8z7w6v5u4..."
javascript
const response = await fetch('https://api.flvxtract.com/api/statements', {
  headers: {
    'X-Consumer-Key': process.env.FLVXTRACT_CONSUMER_KEY,
    'X-Consumer-Secret': process.env.FLVXTRACT_CONSUMER_SECRET,
  },
});
python
import requests, os

response = requests.get('https://api.flvxtract.com/api/statements', headers={
    'X-Consumer-Key': os.environ['FLVXTRACT_CONSUMER_KEY'],
    'X-Consumer-Secret': os.environ['FLVXTRACT_CONSUMER_SECRET'],
})

Security Best Practices

  • Never expose API keys in client-side code or version control
  • Use environment variables to store keys
  • Rotate keys periodically using the regenerate endpoint
  • Use separate keys for development and production

Endpoints

All endpoints require authentication unless marked none.

Webhooks

Receive real-time notifications when extraction jobs complete, fail, or require attention.

Configure your webhook URL in Settings → Webhooks inside the dashboard. You can also test delivery from the settings page.

Events

Event TypeDescriptionTrigger
extraction.completedExtraction finished successfullyStatement processing complete
extraction.failedExtraction job failedProcessing error occurred
extraction.partialExtraction completed with warningsLow confidence results
webhook.testTest webhook eventManual test from dashboard

Webhook Headers

Webhook Delivery Headers
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Signature-Algorithm: sha256
X-Webhook-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000
X-Webhook-Event-Type: extraction.completed
X-Webhook-Attempt: 1
X-Webhook-Timestamp: 1704067200
User-Agent: FLVXtract-Webhook/1.0

extraction.completed payload

json
{
  "eventType": "extraction.completed",
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-01-26T12:00:00Z",
  "organizationId": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "statementId": "789e0123-e45b-67c8-d901-234567890abc",
    "fileName": "statement_january_2024.pdf",
    "status": "completed",
    "transactionCount": 45,
    "processingTimeMs": 3245,
    "confidence": { "overall": 0.95, "ocr": 0.97, "extraction": 0.93 },
    "detectedBank": "GTBank"
  }
}

Signature Verification

Every webhook includes an HMAC-SHA256 signature in the X-Webhook-Signature header. Always verify this to ensure requests are from FLVXtract.

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret)
      .update(JSON.stringify(payload))
      .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected)
  );
}

// Express.js
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  if (!verifyWebhookSignature(req.body, sig, process.env.FLVXTRACT_WEBHOOK_SECRET))
    return res.status(401).send('Invalid signature');
  res.status(200).send('OK');
});
python
import hmac, hashlib, os
from flask import Flask, request

def verify(body: bytes, sig: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected)

@app.route('/webhook', methods=['POST'])
def webhook():
    sig = request.headers.get('X-Webhook-Signature')
    if not verify(request.data, sig, os.environ['FLVXTRACT_WEBHOOK_SECRET']):
        return 'Invalid signature', 401
    return 'OK', 200
csharp
using System.Security.Cryptography;

public static bool Verify(string payload, string sig, string secret) {
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
    var expected = $"sha256={BitConverter.ToString(hash).Replace("-","").ToLower()}";
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(sig), Encoding.UTF8.GetBytes(expected));
}

Retry Behavior

AttemptDelayRetries On
1stImmediateN/A
2nd2 seconds5xx, 429, timeout, connection error
3rd4 seconds5xx, 429, timeout, connection error
4th8 seconds5xx, 429, timeout, connection error

Best Practices

  • Respond with 200 OK within 30 seconds
  • Process payloads asynchronously: queue for background processing
  • Use eventId for idempotent processing, as events may be delivered multiple times
  • 4xx responses (except 408, 429) are treated as permanent failures and will not be retried

Rate Limiting

API requests are rate-limited to ensure fair usage and platform stability.

TierLimitApplies To
Global1,000 req/minAll authenticated endpoints
Upload10 req/minStatement upload endpoint
Auth5 req/minLogin, register, forgot password
Strict3 req/minSensitive operations

Rate Limit Headers

Response Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1706270460

Requests that exceed the limit receive 429 Too Many Requests. Wait until the X-RateLimit-Reset timestamp before retrying.

Error Handling

All API errors follow a consistent JSON format.

Validation Error (400)
{
  "message": "Validation failed",
  "errors": {
    "email": ["The email field is required."],
    "password": ["Password must be at least 8 characters."]
  },
  "details": null,
  "traceId": "00-cf3d4c9f2353d95f7fbe4f4b2e04f8a6-..."
}
Quota Error (402)
{
  "message": "Monthly upload quota exceeded for your current plan.",
  "errors": null,
  "details": { "quotaType": "MonthlyUploads", "limit": 50, "used": 50 }
}

HTTP Status Codes

CodeMeaningCommon Cause
200OKRequest succeeded
201CreatedResource created successfully
400Bad RequestValidation error or malformed payload
401UnauthorizedMissing or invalid authentication
402Payment RequiredPlan quota exceeded
403ForbiddenInsufficient permissions
404Not FoundResource does not exist or is inaccessible
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected server error

Code Examples

Complete integration examples for common workflows.

Upload a Statement

Upload a bank statement PDF for extraction. The file must be sent as multipart/form-data.

bash
curl -X POST https://api.flvxtract.com/api/statements/upload \
  -H "X-Consumer-Key: ${CONSUMER_KEY}" \
  -H "X-Consumer-Secret: ${CONSUMER_SECRET}" \
  -F "file=@/path/to/statement.pdf"

# Response: { "id": "...", "status": "Processing", ... }
javascript
const form = new FormData();
form.append('file', fs.createReadStream('/path/to/statement.pdf'));

const res = await fetch('https://api.flvxtract.com/api/statements/upload', {
  method: 'POST',
  headers: {
    'X-Consumer-Key': process.env.FLVXTRACT_CONSUMER_KEY,
    'X-Consumer-Secret': process.env.FLVXTRACT_CONSUMER_SECRET,
  },
  body: form,
});
const stmt = await res.json();
console.log('ID:', stmt.id, 'Status:', stmt.status);
python
import requests, os

with open('/path/to/statement.pdf', 'rb') as f:
    res = requests.post(
        'https://api.flvxtract.com/api/statements/upload',
        headers={
            'X-Consumer-Key': os.environ['FLVXTRACT_CONSUMER_KEY'],
            'X-Consumer-Secret': os.environ['FLVXTRACT_CONSUMER_SECRET'],
        },
        files={'file': f},
    )
stmt = res.json()
print(stmt['id'], stmt['status'])

Poll for Results

After uploading, poll until status === 'Completed'. Or use webhooks to be notified automatically.

javascript
async function waitForCompletion(statementId, headers) {
  while (true) {
    const stmt = await (
      await fetch(`https://api.flvxtract.com/api/statements/${statementId}`, { headers })
    ).json();

    if (stmt.status === 'Completed' || stmt.status === 'Failed') return stmt;
    await new Promise(r => setTimeout(r, 5000));
  }
}

const result = await waitForCompletion(statementId, headers);
if (result.status === 'Completed') {
  const analysis = await (
    await fetch(`https://api.flvxtract.com/api/statements/${statementId}/analysis`, { headers })
  ).json();
  console.log('Transactions:', analysis.header.totalTransactions);
}
python
import requests, time

def wait_for_completion(statement_id, headers):
    while True:
        stmt = requests.get(
            f'https://api.flvxtract.com/api/statements/{statement_id}',
            headers=headers,
        ).json()
        if stmt['status'] in ('Completed', 'Failed'):
            return stmt
        time.sleep(5)

Full Integration Flow

Complete end-to-end: upload, wait for processing, retrieve results.

javascript
const fs = require('fs');
const HEADERS = {
  'X-Consumer-Key': process.env.FLVXTRACT_CONSUMER_KEY,
  'X-Consumer-Secret': process.env.FLVXTRACT_CONSUMER_SECRET,
};

async function processStatement(filePath) {
  // 1. Upload
  const form = new FormData();
  form.append('file', fs.createReadStream(filePath));
  const { id } = await (await fetch('https://api.flvxtract.com/api/statements/upload', {
    method: 'POST', headers: { ...HEADERS, ...form.getHeaders() }, body: form,
  })).json();

  // 2. Wait
  let status = 'Processing';
  while (status === 'Processing') {
    await new Promise(r => setTimeout(r, 5000));
    ({ status } = await (await fetch(`https://api.flvxtract.com/api/statements/${id}`, { headers: HEADERS })).json());
  }
  if (status === 'Failed') throw new Error('Processing failed');

  // 3. Get analysis
  const analysis = await (await fetch(`https://api.flvxtract.com/api/statements/${id}/analysis`, { headers: HEADERS })).json();
  return {
    id,
    bankName: analysis.header?.bankName,
    transactions: analysis.header?.totalTransactions,
    cashFlowScore: analysis.lendingMetrics?.cashFlowSufficiencyScore,
    debtToIncome: analysis.lendingMetrics?.debtToIncomeRatio,
  };
}

processStatement('./statement.pdf').then(console.log);
python
import requests, time, os

HEADERS = {
    'X-Consumer-Key': os.environ['FLVXTRACT_CONSUMER_KEY'],
    'X-Consumer-Secret': os.environ['FLVXTRACT_CONSUMER_SECRET'],
}

def process_statement(path):
    with open(path, 'rb') as f:
        stmt_id = requests.post(
            'https://api.flvxtract.com/api/statements/upload',
            headers=HEADERS, files={'file': f}
        ).json()['id']

    status = 'Processing'
    while status == 'Processing':
        time.sleep(5)
        status = requests.get(
            f'https://api.flvxtract.com/api/statements/{stmt_id}', headers=HEADERS
        ).json()['status']

    if status == 'Failed': raise Exception('Failed')

    return requests.get(
        f'https://api.flvxtract.com/api/statements/{stmt_id}/analysis', headers=HEADERS
    ).json()