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.

Next.jsTypeScriptARES APIPDF generatorQR codesVercel

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:

  1. Sign-up (email + password)
  2. Email verification
  3. First-run onboarding (company VAT, bank, template)
  4. 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:

StepTargetMeasured
Landing → fill VAT IDby user~5 s
ARES lookup< 1.5 s1.24 s p95
Filling line itemsby user~15 s
Click "Download" → PDF< 500 ms180 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

DokladOIDDokladBot
Goalone-off invoicesrecurring accounting
Registrationnoyes
Pricefree199 CZK/month
AInoyes (categorization, reports)
StorageURL statePostgres + history
Target usersporadic invoiceractive 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.
  • pdfkit is duller and more reliable than puppeteer-based PDF. No Chrome dependency, no cold-start drama.