Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ paths:
'200': { description: Replay of an earlier request with the same key and body }
'400': { $ref: '#/components/responses/Error' }
'401': { $ref: '#/components/responses/Error' }
'413': { $ref: '#/components/responses/Error' }
'422': { description: Idempotency-Key reused with a different body }
/v1/orders/{id}:
get:
Expand All @@ -36,6 +37,7 @@ paths:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: The order, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
'401': { $ref: '#/components/responses/Error' }
'404': { $ref: '#/components/responses/Error' }
/v1/orders/{id}/cancel:
post:
Expand All @@ -44,7 +46,8 @@ paths:
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: The cancelled order }
'200': { description: The cancelled order, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
'404': { $ref: '#/components/responses/Error' }
'409': { description: The order can no longer be cancelled }
components:
securitySchemes:
Expand Down
33 changes: 28 additions & 5 deletions web/src/content/docs/docs/connect/android-forwarder.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
---
title: Android forwarder
description: Pass bank SMS and app alerts to Kaasu.
description: Pass bank SMS and app alerts from an Android phone to Kaasu.
sidebar:
order: 1
---

The forwarder is a small Android app on a phone that receives your bank alerts. It signs each alert with a key unique to that phone and sends it to Kaasu.
> **Status:** the server side (pairing and signed ingest) is done and tested. The Android app itself is not built yet; this page describes what it will do and how its protocol works, so you can build against it.

- Pair it once with a code from the admin screen.
- You can revoke a phone at any time.
- Alerts older than 60 seconds or repeated are refused.
## What it will do

A small app on the phone that receives your bank's SMS and notifications. It forwards only messages from the bank sender ids you allow, signs each with a key that never leaves the phone, and sends them to Kaasu. It keeps a short list of what it forwarded so you can see it working, and a "send test" button.

## Pairing

1. Kaasu shows a one-time pairing code as a QR (valid ten minutes, usable once). It contains your ingest address.
2. The phone creates an Ed25519 key pair in the Android Keystore and sends the public key with the code.
3. Kaasu stores the public key against a device id. The private key never leaves the phone.

## Sending alerts

Each alert is a JSON body (`source`, `sender`, `text`, `received_at`) with four headers: device id, timestamp, a 16 to 64 character nonce, and the signature over all of it. Details in [API: device ingest](/docs/reference/api/).

- An alert whose timestamp is more than 60 seconds off is refused.
- A nonce can be used once, so a captured request cannot be replayed (the second try gets `409`).
- Every other failure is the same `401` with no detail.
- A revoked phone is refused at once.

## Battery savers

Phones kill background apps to save power. The app will include per-brand steps (Xiaomi, Oppo, Vivo, Samsung and others) and a foreground notification, because a phone that silently stops forwarding is the main way a shop loses confirmations. The dashboard shows **Phone offline** when it has not heard from the phone for 15 minutes.

## Until it ships

Use [bank email](/docs/connect/bank-email/) as a source, or build a forwarder to the protocol above.
22 changes: 20 additions & 2 deletions web/src/content/docs/docs/connect/bank-email.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
---
title: Bank email
description: Use bank email alerts as a second source.
description: Use bank email alerts as a second, independent source.
---

Forward bank emails to Kaasu. Each message is checked for a valid sender signature and must come from a bank domain on your allow list. Anything else is ignored.
> **Status:** the email checker is built and tested. It is not started by the server yet, and there is no setting screen for the mailbox, so it does not run in a normal install today.

## How it works

Kaasu reads an inbox that receives your bank's alert emails over IMAP with TLS, using an app password. For every unread message it:

1. Checks the **DKIM signature** on the raw message.
2. Requires the signing domain to be on the bank allow list (for example `hdfcbank.net`, `icicibank.com`, `axisbank.com`, `sbi.co.in`, `kotak.com`) **and** to match the From address.
3. Reads the body, decoding base64 and quoted-printable and looking inside nested parts, with a size limit.
4. Parses the credit with the bank's parser. Unknown formats are kept as evidence and never confirm anything.
5. Marks the message read so it is not fetched again.

A message that fails any step is logged as rejected with a reason (`no_dkim`, `bad_dkim`, `domain_not_allowed`, `from_not_aligned`) and ignored. A copy that arrives by SMS and by email is recognised as the **same payment**, which adds a second source and raises confidence.

## Tips

- Use a dedicated mailbox and an app password, never your main password.
- Forward only bank alerts to it.
- Email alone cannot be hurried by an attacker: a forged email fails DKIM, and an old one fails the time window.
17 changes: 15 additions & 2 deletions web/src/content/docs/docs/connect/mac-messages.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
---
title: Messages on Mac and iPhone
description: Use the Apple apps as an alert source.
description: Use an iPhone relayed to a Mac as an alert source (experimental).
---

The Apple apps can pass bank messages to Kaasu in the same signed way as the Android forwarder. Pair the device from the admin screen, then confirm it appears in the device list.
> **Status:** planned and experimental. The server side (signed ingest) is ready; the Mac agent is not built yet.

## Idea

An iPhone cannot let apps read its SMS. The workaround is to turn on **Text Message Forwarding** to a Mac you own. The Mac's Messages database then holds the bank SMS, and a small Kaasu agent on the Mac reads it and sends each allowed message to Kaasu.

## What it will need

- Full Disk Access for the agent, granted by you.
- Only messages from the bank sender ids you allow are read.
- The agent signs requests with a key held in the macOS Keychain, in exactly the same way as the [Android forwarder](/docs/connect/android-forwarder/).
- It is labelled experimental in the app, and warns you when it stops working, because it depends on how Apple stores messages.

Prefer [bank email](/docs/connect/bank-email/) or an Android phone when you can.
28 changes: 28 additions & 0 deletions web/src/content/docs/docs/connect/supported-banks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Supported banks
description: Which bank alerts Kaasu understands, and how new ones are added.
---

Kaasu reads **credits** (money received) from these banks. Debits, offers and anything unfamiliar are never treated as a payment.

| Bank | SMS sender ids | Email domain |
|---|---|---|
| HDFC Bank | `HDFCBK` | `hdfcbank.net` |
| State Bank of India | `SBIUPI`, `SBIINB`, `SBIPSG` | `sbi.co.in` |
| ICICI Bank | `ICICIB` | `icicibank.com` |
| Axis Bank | `AXISBK` | `axisbank.com` |
| Kotak Mahindra Bank | `KOTAKB` | `kotak.com` |

Sender ids are matched with their usual operator prefix and suffix, so `VM-HDFCBK-S` counts as `HDFCBK`. A look-alike such as `HDFCBK.evil` does not match.

## What gets read

The amount (in paise, parsed from the digits, never as a decimal number), the 12-digit UTR, the date, a masked payer and the last digits of your account. A reference that is not exactly 12 digits is rejected, so a longer number cannot be truncated into a valid-looking one.

## Not understood?

If a message is not recognised, Kaasu keeps it as evidence and never confirms from it. The optional [AI helper](/docs/use/ai-features/) can propose a reading, which always goes to you for approval.

## Add a bank

Parsers are small and have golden test files. See [Contribute](/docs/reference/contribute/): add a pattern, a **redacted** sample message (no real names, numbers or UTRs) and the expected result.
20 changes: 16 additions & 4 deletions web/src/content/docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@ Kaasu confirms UPI payments for small shops. Customers pay your bank directly, a

## Start here

1. [Install Kaasu](/docs/start/install/) and try it with demo data.
2. [Take your first payment](/docs/start/first-payment/).
3. [Connect a bank alert source](/docs/connect/android-forwarder/).
4. [Connect your shop](/docs/connect/woocommerce/): WooCommerce works today, [Shopify](/docs/connect/shopify/) is planned.
1. [How Kaasu works](/docs/start/how-it-works/): the flow and the rules it never breaks.
2. [Install Kaasu](/docs/start/install/) and run the first-time setup.
3. [Take your first payment](/docs/start/first-payment/).
4. [Connect an alert source](/docs/connect/android-forwarder/) and [your shop](/docs/connect/woocommerce/).

## Everyday use

[Dashboard](/docs/use/dashboard/), [Orders and pay links](/docs/use/orders-and-links/), [Needs your OK](/docs/use/needs-your-ok/), [Refunds](/docs/use/refunds/), [Reconciliation](/docs/use/reconciliation/), [Languages and voice](/docs/use/languages-and-voice/), [AI help](/docs/use/ai-features/), [Backups and keys](/docs/use/backup-and-keys/).

## Reference

[API](/docs/reference/api/), [Webhooks](/docs/reference/webhooks/), [Command line](/docs/reference/cli/), [Configuration](/docs/reference/configuration/), [Orders, states and the ledger](/docs/reference/ledger-and-states/), [Operations](/docs/reference/operations/), [Security](/docs/reference/security/), [Troubleshooting](/docs/reference/troubleshooting/), [FAQ](/docs/reference/faq/).

## What works today

Kaasu is early software. The core is built and tested: orders, exact amounts, the matcher, signed alerts, webhooks, the ledger, the pay and receipt pages, the dashboard and the command line. Phone and Mac apps, the email watcher's mailbox setup, backups and several screens are still being connected, and each page says so where it matters. Test with small amounts first.

## How these docs are kept

Expand Down
119 changes: 113 additions & 6 deletions web/src/content/docs/docs/reference/api.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,118 @@
---
title: API
description: Kaasu REST API basics, including the base path, API keys, idempotency and paise amounts.
description: "The Kaasu REST API: authentication, orders, idempotency, errors, limits, customer pages, webhooks and the device ingest API."
---

- Base path `/v1`.
- One API key per shop.
- `Idempotency-Key` header is required when creating an order.
- Money fields are integer paise, for example `49907` means ₹499.07.
Kaasu has three HTTP listeners. This page covers the three you may call from code: the **public** order API, the **customer pages**, and the **ingest** API used by bank-alert devices. The admin listener is for people and is never exposed to the internet. The machine-readable spec is `openapi.yaml` in the repository.

The full specification lives in `openapi.yaml` in the repository.
## Basics

- Base path `/v1`, JSON in and out, `Content-Type: application/json`.
- Money is an integer number of **paise**: `49907` means ₹499.07. Floats are rejected. Currency is always `INR`.
- Send `Authorization: Bearer <api key>`. Create a key on your server with `kaasu apikey new <label>`; it is shown once and only a hash is stored.
- Every response carries an `X-Request-Id`. Quote it when you report a problem.
- Request bodies are limited to 64 KB, and each listener rate-limits per client address (public: 200 requests a second, burst 400). Over the limit you get `429` with `Retry-After`.

## Create an order

`POST /v1/orders` with an `Idempotency-Key` header (required).

```json
{ "amount_paise": 49907, "reference": "INV-1042", "note": "Order 1042", "expires_in_seconds": 900 }
```

| Field | Meaning |
|---|---|
| `amount_paise` | Required. ₹1 to ₹1,00,000 (`100` to `10000000`). |
| `account_id` | Receiving account. Required only when the shop has more than one active account. |
| `reference` | Your own id, shown to you and on the receipt. |
| `note` | Shown in the customer's UPI app (first 50 characters). |
| `expires_in_seconds` | 60 to 86400, default 900 (15 minutes). |

The reply is `201` with the order. The `amount_paise` you get back is the exact amount the customer must pay: Kaasu adds a few paise (up to 99) so that every open order has a unique amount, and that is how a bank alert is matched to it. `base_amount_paise` is what you asked for.

### Idempotency

Repeat a request with the same key and the same body and you get the first order back with status `200` and the header `Idempotent-Replayed: true`. The same key with a different body is `422 idempotency_conflict`. Use one key per order attempt, for example your cart id.

## Read and cancel

- `GET /v1/orders/{id}` returns the order.
- `POST /v1/orders/{id}/cancel` cancels an unpaid order. An order that is already paid, expired or cancelled returns `409 not_cancellable`.

## The order object

```json
{
"id": "ord_x7k2m9q4w8r5t1y3u6p0",
"account_id": "acc_000001",
"state": "awaiting",
"base_amount_paise": 49900,
"amount_paise": 49907,
"currency": "INR",
"reference": "INV-1042",
"upi_link": "upi://pay?pa=shop%40okhdfcbank&pn=Sharma+Sweets&am=499.07&cu=INR&tn=Order+1042",
"expires_at": "2026-10-24T10:15:00Z",
"created_at": "2026-10-24T10:00:00Z",
"updated_at": "2026-10-24T10:00:00Z"
}
```

`upi_link` is present only while the order is `awaiting`. A `queued` order (the amount pool for that price is full) has no link until a slot frees up, so never show a pay button for it.

### States

| State | Meaning |
|---|---|
| `awaiting` | Waiting for the customer to pay. |
| `queued` | Waiting for a free exact amount. It moves to `awaiting` on its own. |
| `confirmed` | A verified bank alert matched the order. Safe to fulfil. |
| `review` | Kaasu is unsure (wrong amount, unknown sender, late). The owner decides. |
| `late_paid` | Paid after the order expired and accepted by the owner. |
| `rejected`, `cancelled`, `expired` | Closed without payment. |
| `refunded`, `disputed` | Set by the owner after the fact. |

Only `confirmed` and `late_paid` mean money arrived. Do not infer payment from anything the customer's browser tells you.

## Errors

Errors are `{"error": {"code": "...", "message": "..."}}`.

| Status | `code` | When |
|---|---|---|
| 400 | `invalid_request` | Bad JSON, a float amount, an amount or expiry out of range, a missing key. |
| 401 | `unauthorized` | Missing, invalid or revoked API key. |
| 404 | `not_found` | No such order. |
| 409 | `not_cancellable` | The order can no longer be cancelled. |
| 413 | `too_large` | Body over 64 KB. |
| 422 | `idempotency_conflict` | Same key, different body. |
| 429 | `rate_limited` | Too many requests; wait `Retry-After` seconds. |
| 500 | `internal` | Something went wrong. Details are in the server log under your `X-Request-Id`. |

## Customer pages

Send your customer to `/pay/{order id}` on your public address. These pages need no API key and load no third-party script.

- `GET /pay/{id}` is the pay page: exact amount, one button that opens the UPI app, and live waiting steps.
- `GET /pay/{id}/status` returns `{"state": "..."}` only. The page polls it every two seconds; you can too.
- `GET /pay/{id}/done` shows success only for a confirmed or late-paid order, and redirects to the pay page otherwise.
- `GET /pay/{id}/receipt` is a printable receipt (`?format=txt` for plain text) with the UTR and a six-character check code the shopkeeper can compare with their own app.
- Reusable payment links and a counter QR for walk-in customers exist in the engine but have no web routes yet, so create orders through this API for now.

## Webhooks

Kaasu pushes order changes to your server so you never have to poll. See [Webhooks](/docs/reference/webhooks/) for events, signatures and retries. Ready-made integrations: [WooCommerce](/docs/connect/woocommerce/); [Shopify](/docs/connect/shopify/) is planned.

## Device ingest API

Phones and the Mac agent forward bank alerts to the ingest listener. You only need this to build your own forwarder.

1. **Pair.** `POST /ingest/v1/pair` with the one-time pairing token from the QR code and the device's Ed25519 public key. The token works once and expires after ten minutes.
2. **Send alerts.** `POST /ingest/v1/alerts` with the raw alert JSON (`source`, `sender`, `text`, `received_at`) and four headers: `X-Kaasu-Device`, `X-Kaasu-Timestamp` (unix seconds), `X-Kaasu-Nonce` (16 to 64 characters) and `X-Kaasu-Signature`, the base64 Ed25519 signature over the version, device id, timestamp, nonce and SHA-256 of the body.
3. **Rules.** The timestamp must be within 60 seconds of the server's clock, and a nonce can be used once (a replay gets `409`). Any failed check is the same `401` with no detail. An alert from a sender Kaasu does not know is kept as evidence and never confirms anything.

Alerts from devices are authentic only because they are signed; text that merely looks like a bank message is never trusted.

## Try it without real money

Run `make dev` for demo data and `kaasu sim --list` to see the simulator's scenarios. Dev mode blocks outbound network calls, so you can test your integration offline.
35 changes: 35 additions & 0 deletions web/src/content/docs/docs/reference/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Command line
description: Every kaasu command, what it does and what it prints.
---

Commands read the same settings as the server: `KAASU_DATABASE_PATH`, `KAASU_MASTER_KEY` and the rest in [Configuration](/docs/reference/configuration/). Run them as the user that owns the database.

| Command | What it does |
|---|---|
| `kaasu serve` | Starts the three listeners and the background jobs. It takes no flags; configuration comes from the environment or `KAASU_CONFIG`. An unknown argument is refused. |
| `kaasu migrate` | Applies database migrations and exits. The server does this at start too. |
| `kaasu keygen` | Prints a fresh 32-byte master key. Does not touch the database. |
| `kaasu rotate-key` | Re-wraps every secret under `KAASU_NEW_MASTER_KEY`, all or nothing. |
| `kaasu account add --vpa <id> --bank <name> --name <display name> [--limit-rupees N]` | Adds a receiving UPI account. `--limit-rupees` is the auto-confirm ceiling; 0 means every payment waits for you. |
| `kaasu apikey new <label>` | Creates an API key and prints it once. Only a hash is stored. |
| `kaasu admin init --name <name> [--role owner\|staff]` | Creates a person and prints their authenticator link once. Default role is owner. |
| `kaasu verify` | Walks the ledger hash chain and reports the first broken row. |
| `kaasu doctor` | Checks config, master key, database, pending migrations and the ledger. It only reads. |
| `kaasu healthcheck` | Exits 0 if the admin listener answers `/healthz` (used by the Docker image). |
| `kaasu sim --list` | Lists the payment simulator scenarios. |
| `kaasu version` | Prints the version. |
| `kaasu backup`, `kaasu pair` | Placeholders; not implemented yet. |

## Exit codes

`0` success, `1` the command ran and failed, `2` the command was used wrongly (the usage line is printed).

## Examples

```bash
kaasu account add --vpa sharma.sweets@okhdfcbank --bank HDFC --name "Sharma Sweets" --limit-rupees 5000
kaasu apikey new website
kaasu admin init --name priya --role owner
kaasu doctor
```
Loading
Loading