Oligon Receipts is in private beta — request access.
Guides
Expense app integration

Building an expense app on Oligon

A complete reference architecture for the most common Oligon use case: your users take a photo, you store a structured receipt, your finance team gets a clean CSV.

Architecture

┌──────────┐   1. signed URL     ┌──────────┐
│  Mobile  │ ───────────────────▶│  Your    │
│  / Web   │                     │  backend │
└──────────┘                     └────┬─────┘
     ▲                                │
     │ 4. WebSocket "done"            │ 2. POST /v1/extract
     │                                ▼
┌────────────┐  3. webhook    ┌──────────────┐
│ Your portal│◀───────────────│ Oligon API   │
└────────────┘                └──────────────┘

Step 1 — Mobile uploads to your backend

Plain multipart POST. Your backend is the only thing with the Oligon secret. Mobile clients do not touch it.

Step 2 — Backend forwards to Oligon

from fastapi import FastAPI, UploadFile, Header
from oligon_receipts import AsyncOligonReceipts
 
app = FastAPI()
oligon = AsyncOligonReceipts()
 
@app.post("/expenses")
async def create_expense(file: UploadFile, user_id: str = Header(...)):
    content = await file.read()
    receipt = await oligon.extract(
        file=content,
        metadata={"user_id": user_id},
        webhook_url=f"https://api.acme.com/oligon/{user_id}",
    )
    return {"receipt_id": receipt.id, "status": receipt.status}

Step 3 — Oligon emits a webhook

When extraction completes, we POST to webhook_url (or your default endpoint). Verify, persist, push to the user.

Step 4 — Push to the mobile / web client

WebSocket, SSE, or APNs/FCM. The key is to wait for receipt.completed before showing the user the parsed data — extract may return pending on heavy load.

Browser direct upload (alternative)

If you want to skip the backend hop, mint pk_publishable_… keys per user session:

// server
app.get("/api/oligon-token", auth, async (req, res) => {
  const key = await oligon.apiKeys.create({
    name: `session:${req.userId}`,
    scopes: ["write"],
    expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
  });
  res.json({ publishableKey: key.secret });
});
 
// browser
const { publishableKey } = await fetch("/api/oligon-token").then((r) => r.json());
const client = new OligonReceipts({ apiKey: publishableKey });
const receipt = await client.extract({ file: inputEl.files![0] });

Trade-off — one fewer hop, but you cannot enrich the receipt with backend context (user_id, project_id, etc.) before storage. For most expense apps, the backend hop is worth it.

Storing receipts

Whatever schema you already use for expenses. The Oligon receipt.id is your foreign key. You do not need to denormalise the full receipt into your DB — it is always one GET /v1/receipts/:id away, and the SDK is cached aggressively.

Reconciling with accounting

For Brazil, NFe chave de acesso is the canonical ID — store it unique-indexed and you'll automatically reject duplicates. For US/EU, combine merchant_tax_id + document_number + issued_at as a composite dedup key.