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.