Maruška - Human Accountant for Freelancers via WhatsApp
Accounting service for freelancers and small businesses. Users photograph receipts and send them via WhatsApp; Maruška handles documentation, invoicing, deadlines, reporting. "Person, not software" - deliberate counterweight to AI-first tools.
3 pricing tiers, 24/7 support
Brief
Maruška is an accounting service for freelancers and micro-businesses. The differentiator is one thing - on the other end of the line is a real accountant, not AI. The user takes a photo of a receipt, sends it via WhatsApp, and Maruška categorizes it, invoices, watches tax deadlines, and sends a monthly report. That's the whole product.
The site I built is a marketing + onboarding interface: explain the positioning, show the workflow, sell the subscription, integrate Stripe and the WhatsApp Business API, stay GDPR-compliant.
Positioning vs DokladBot and DokladOID
Three accounting products in my portfolio for the same user type, each solving a different pain:
| DokladOID | DokladBot | Maruška | |
|---|---|---|---|
| Goal | one-off invoice | recurring SaaS accounting | full human service |
| Who does the work | the user | AI + the user | accountant (human) |
| Price | free | 199 CZK/mo | 399 / 599 / 799 CZK/mo |
| ROI | 5 min/mo | hour/mo | zero time, just pay |
| Audience | sporadic freelancer | digital-native freelancer | non-digital freelancer with deadlines |
Maruška is deliberately anti-AI-first. There's a large slice of freelancers who refuse to import receipts via web app, dump them into Excel, or learn new software. But they'll send a photo over WhatsApp to anyone. That distribution gap is the whole business.
WhatsApp Business API - webhook architecture
The WhatsApp Business API is webhook-based. Meta posts to our endpoint every time a user sends a message. The architecture wraps two flows: the marketing site (where Maruška onboards and signs up the user via Stripe) and the operational backend (where Maruška actually processes messages).
The marketing site is pure Next.js, but it has one Server Action subscribeAndProvisionWhatsApp that:
- Creates a Stripe Customer + Subscription
- Saves the user to Postgres (
tier,whatsapp_phone,stripe_customer_id) - Sends a welcome WhatsApp message via the Business API
- Notifies Maruška in Slack
// src/app/actions/subscribe.ts
'use server';
import { stripe } from '@/lib/stripe';
import { sendWhatsAppMessage } from '@/lib/whatsapp';
import { db } from '@/lib/db';
interface SubscribePayload {
email: string;
phone: string; // E.164, e.g. +420777123456
tier: 'basic' | 'standard' | 'premium';
paymentMethodId: string;
}
export async function subscribeAndProvisionWhatsApp(payload: SubscribePayload) {
// 1) Stripe customer + subscription
const customer = await stripe.customers.create({
email: payload.email,
payment_method: payload.paymentMethodId,
invoice_settings: { default_payment_method: payload.paymentMethodId },
metadata: { whatsapp_phone: payload.phone },
});
const sub = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: PRICE_IDS[payload.tier] }],
});
// 2) DB
await db.user.create({
data: {
email: payload.email,
phone: payload.phone,
tier: payload.tier,
stripeCustomerId: customer.id,
stripeSubscriptionId: sub.id,
activatedAt: new Date(),
},
});
// 3) WhatsApp welcome
await sendWhatsAppMessage({
to: payload.phone,
template: 'welcome',
variables: { tier: payload.tier },
});
// 4) Slack notification for Maruška
await notifySlack(`New client: ${payload.email} (${payload.tier})`);
return { ok: true, subscriptionId: sub.id };
}The WhatsApp Business API webhook handler lives in a separate Vercel function (longer execution timeout to fit OCR + DB writes):
// src/app/api/whatsapp/webhook/route.ts
export async function POST(req: Request) {
const body = await req.json();
const signature = req.headers.get('x-hub-signature-256');
if (!verifyMetaSignature(body, signature, process.env.WA_APP_SECRET!)) {
return new Response('invalid signature', { status: 401 });
}
for (const entry of body.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.value.messages) {
for (const msg of change.value.messages) {
await enqueueIncomingMessage({
from: msg.from,
type: msg.type, // image | text | document
mediaId: msg.image?.id ?? msg.document?.id,
text: msg.text?.body,
timestamp: msg.timestamp,
});
}
}
}
}
return new Response('ok');
}Messages queue into a Postgres incoming_messages table; Maruška sees a dashboard with everything that came in, and clicking "done" fires a confirmation WhatsApp message back to the user.
Stripe subscriptions, 3 plans
The Stripe checkout is standard, but plan-gating UI is the important detail:
const FEATURES = {
basic: ['receipts', '20 docs/mo', 'monthly report'],
standard: ['+ unlimited docs', '+ VAT management', '+ 24/7 chat'],
premium: ['+ tax filing', '+ advisory', '+ weekend coverage'],
} as const;The comparison table on the pricing page renders features dynamically from config, so when Maruška adds a tier it's one line in pricing.ts, not a new page.
Receipt → invoice pipeline (visual workflow)
The site has to show what happens after. I built an animated diagram (Motion):
[1. Snap a photo] → [2. Send WhatsApp] → [3. Maruška files it] → [4. Invoice/report]
📷 💬 🧾 📊
These four steps are scroll-triggered animations showing the document's path. No heavy parallax, just motion opacity + transform. Trust signal up - landing page conversion went from 2.1 % (before) to 4.7 % (after).
Privacy / GDPR
This was the most sensitive piece. A receipt contains a VAT ID, financial data, and often geolocation (EXIF in the photo). Data flow:
- WhatsApp Business API receives the photo (Meta servers, EU region)
- Webhook handler downloads via the Media Download API
- Stored in Vercel Blob in an EU region, encrypted at rest
- EXIF stripped before storage (geolocation removed)
- After 7 years (statutory retention for tax documents) it's deleted
- Users can request export or deletion via WhatsApp commands
!export/!delete
GDPR docs live at /gdpr, rendered from MDX. The DPA with Meta is signed separately (Maruška as data controller, Meta as processor for WA traffic).
Lessons
- "The human touch" is a marketing gold mine for non-tech audiences. 60+ users responded 3× better to "real accountant" than to "AI-powered".
- The WhatsApp Business API is bureaucratic hell. Business account verification took 2 weeks, template messages need Meta approval (60 % pass first try, the rest is copy editing).
- Stripe metadata is gold.
metadata.whatsapp_phoneon the customer = no joins required for lookups, it's inline in the Stripe webhook payload. - Visual workflow > a text description. The 4-step animated diagram lifted conversion from 2.1 % to 4.7 % (A/B tested on ~6,000 visitors).
- The privacy page is a sales tool, not a necessary evil. Non-tech-savvy audiences want to see where their data lives. Explicit "EU region, encrypted, deleted after 7 years" cut abandonment in the onboarding flow by 22 %.