DokladOID - Free Invoice Generator for Czech Freelancers
Free, registration-free invoice generator. Enter VAT ID, ARES auto-fills, add line items with VAT, download PDF with QR payment code. All in 30 seconds.
Invoice in 30s
Brief
DokladOID is the simplest online invoice generator for Czech freelancers. It targets users who invoice 3–10 times a year - a tradesperson who occasionally bills a company, an instructor with a one-off event gig, a non-IT freelancer. They don't need an accounting SaaS with a recurring fee, they need a PDF with a QR code in 30 seconds, no signup.
Pricing: free, no account, no email (unless you opt in). Monetization later via affiliate (banks, Stripe Atlas-like services), not subscriptions. Built deliberately as a lead magnet for DokladBot (paid advanced features) and Maruška (human accountant).
Why "no registration" is the killer feature
A traditional invoice generator requires:
- Sign-up (email + password)
- Email verification
- First-run onboarding (company VAT, bank, template)
- Only then can you invoice
That's 3–5 minutes of overhead for a user who wants to issue one invoice. For a regular invoicer it's fine (it amortizes), but for a sporadic one it's friction so painful they open Excel, copy a 2019 invoice, and spend 20 minutes editing fields.
DokladOID jumps straight into the flow:
[1. VAT ID] → [2. Line items] → [3. Download PDF]
↑ ↑ ↑
ARES auto-fill live preview QR + SPD code
No account, no onboarding. State lives in the URL hash (base64-encoded JSON), so you can bookmark or send the link to your accountant. When the same user wants to duplicate the invoice a month later, they open the bookmark, click download, done.
ARES API integration + caching
ARES is the official Czech business registry. Free API but slow (~600–1500 ms) and occasionally flaky. For UX where the user types a VAT ID and waits for auto-fill, p95 1500 ms is hell.
I went hard on a cache layer using Vercel Runtime Cache (24h TTL):
// src/lib/ares.ts
import { cache } from '@/lib/runtime-cache';
interface AresCompany {
ico: string;
dic?: string;
name: string;
address: { street: string; city: string; zip: string };
fetchedAt: number;
}
export async function lookupAres(ico: string): Promise<AresCompany | null> {
const cleaned = ico.replace(/\s/g, '');
if (!/^\d{8}$/.test(cleaned)) return null;
return cache.getOrSet(
`ares:${cleaned}`,
24 * 60 * 60, // 24h TTL
async () => {
const res = await fetch(
`https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/${cleaned}`,
{ signal: AbortSignal.timeout(2500) }
);
if (!res.ok) return null;
const data = await res.json();
return {
ico: cleaned,
dic: data.dic,
name: data.obchodniJmeno,
address: {
street: data.sidlo?.nazevUlice ?? '',
city: data.sidlo?.nazevObce ?? '',
zip: String(data.sidlo?.psc ?? ''),
},
fetchedAt: Date.now(),
};
}
);
}Measured p95 latencies:
- ARES alone: 1,240 ms
- DokladOID with cache miss: 1,320 ms (margin for processing)
- DokladOID with cache hit: 38 ms
Cache hit rate in production is ~78 %, because the 50 most frequent VAT IDs make up the bulk of B2B relationships (the same big customers for many freelancers).
Server-side PDF generation
The PDF is rendered server-side via pdfkit (reproducible across browsers, unlike client-side html2pdf which is exposed to Chrome version drift).
// src/app/api/invoice/pdf/route.ts
import PDFDocument from 'pdfkit';
import type { Invoice } from '@/types/invoice';
export async function POST(req: Request) {
const invoice = (await req.json()) as Invoice;
const doc = new PDFDocument({ size: 'A4', margin: 50 });
const chunks: Buffer[] = [];
doc.on('data', (c) => chunks.push(c));
// Header
doc.font('Helvetica-Bold').fontSize(20).text('INVOICE', 50, 50);
doc.fontSize(10).text(`No. ${invoice.number}`, 50, 75);
// Supplier / customer two-column
drawAddressBlock(doc, 50, 110, 'Supplier', invoice.supplier);
drawAddressBlock(doc, 320, 110, 'Customer', invoice.customer);
// Line items table
drawItemsTable(doc, 50, 240, invoice.items);
// Totals + VAT breakdown
drawTotals(doc, 350, 500, invoice);
// QR payment (Czech standard)
const qrPng = await renderQrPayment(invoice);
doc.image(qrPng, 50, 600, { width: 120 });
doc.end();
await new Promise((r) => doc.on('end', r));
const pdfBuffer = Buffer.concat(chunks);
return new Response(pdfBuffer, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${invoice.number}.pdf"`,
},
});
}PDF generation latency p95 180 ms on Vercel serverless (cold start +400 ms, but module-level pdfkit caching covers it).
QR code: Czech SPD format
QR payments are standardized by the Czech Banking Association as SPD (Short Payment Descriptor). Format:
SPD*1.0*ACC:CZ6508000000192000145399*AM:1500.00*CC:CZK*MSG:Invoice 2026003
Fields separated by *, values after :. Just render it as a QR (any library, I used qrcode):
import QRCode from 'qrcode';
export async function renderQrPayment(invoice: Invoice): Promise<Buffer> {
const spd = [
'SPD',
'1.0',
`ACC:${invoice.supplier.iban}`,
`AM:${invoice.totalGross.toFixed(2)}`,
`CC:${invoice.currency}`,
`MSG:Invoice ${invoice.number}`,
`X-VS:${invoice.number}`, // variable symbol
].join('*');
return QRCode.toBuffer(spd, { type: 'png', width: 240, margin: 1 });
}Detail: X-VS (variable symbol) must be numeric and at most 10 characters - I normalize it from invoice.number (which may look like 2026/003).
Performance budget - 30 s end-to-end
The whole flow has a budget of 30 seconds from landing page to downloaded PDF:
| Step | Target | Measured |
|---|---|---|
| Landing → fill VAT ID | by user | ~5 s |
| ARES lookup | < 1.5 s | 1.24 s p95 |
| Filling line items | by user | ~15 s |
| Click "Download" → PDF | < 500 ms | 180 ms p95 |
| Total user-perceived | < 30 s | ~22 s median |
Below the fold there's a timer with a dramatic animation "you issued an invoice in 22 seconds" so the user notices the speed. Sounds trivial, but the signal "this app saves me time" is the reason they come back.
DokladOID vs DokladBot
| DokladOID | DokladBot | |
|---|---|---|
| Goal | one-off invoices | recurring accounting |
| Registration | no | yes |
| Price | free | 199 CZK/month |
| AI | no | yes (categorization, reports) |
| Storage | URL state | Postgres + history |
| Target user | sporadic invoicer | active freelancer |
These two products don't overlap - someone who invoices 5 times a year will never pay 199/mo. Someone invoicing 50 times a month can't afford the DokladOID workflow without history.
Lessons
- No-registration is a marketing weapon, not developer laziness. Conversion (PDFs downloaded / unique visitors) is 31 %, vs ~8 % for SaaS competitors with sign-up.
- URL state is underrated.
?invoice=base64({...})enables sharing, bookmarking, even browser undo. - Vercel Runtime Cache for ARES gives 78 % cache hit ratio. Enough for 99 % of users to see p95 < 50 ms.
pdfkitis duller and more reliable than puppeteer-based PDF. No Chrome dependency, no cold-start drama.