Self-hosted UPI payment verification with a provider-blind API, signed Android relay, direct SQLite persistence, and durable webhooks.
PayGate creates a payment instruction with a unique exact payable amount, watches supported incoming-payment notifications through one or more independently revocable Android relay phones, and changes payment state only when the server has an origin-bound confirmation it can trust.
The payer sends money directly to the configured UPI account. PayGate does not custody, route, or settle funds; notification matching is an evidence mechanism rather than a bank/acquirer settlement guarantee.
flowchart LR
M[Merchant backend] -->|POST /v1/payments| P[PayGate]
P -->|canonical UPI URI| M
A[PayGate Android] -->|signed notification events| P
P --> D[(SQLite)]
P -->|signed durable webhooks| W[Merchant webhook]
O[Operator] -->|Web / Android admin UI| P
PayGate v4 is deliberately small:
- one Go server process;
- one direct SQLite database using WAL,
synchronous=FULL, foreign keys and bounded busy timeout; - one Android app that combines operator UI with the background notification relay;
- one active collection profile for new payments;
- a durable webhook outbox;
- no PocketBase runtime, Redis, PostgreSQL, server-side Google Messages session, or external queue.
A merchant asks for a whole-INR amount. PayGate resolves the active collection profile and reserves a randomized non-.00 payable value.
For a ₹100 request, PayGate first selects among free ₹100.01 … ₹100.99 values. Only when that entire bucket is unavailable can it overflow into ₹101.01 … ₹101.99. The maximum v4.0 adjustment is therefore ₹1.99.
The default lifecycle is:
5 minutes active → 5 minutes grace → 5 minutes hard quarantine
Released values also receive soft recent-use avoidance when alternatives exist.
Matching is server-owned and uses the inferred collection profile, exact payable amount, reservation/lifecycle state, trustworthy occurrence time, and unique signed relay-event identity. The Android client never declares a payment successful.
v4.0 currently supports:
- Paytm for Business payment notifications;
- Kotak credit notifications delivered by Google Messages on the PayGate phone. Paytm package-bound notifications are currently the only automatic confirmation source. Kotak/Google Messages and package-agnostic notifications remain signed evidence for web-admin confirmation until an independent sender/provider proof is available.
The merchant does not select Paytm or Kotak. PayGate snapshots the active profile and destination when the payment is created, so later profile changes cannot alter an existing payment.
Create a payment:
POST /v1/payments
Authorization: Bearer <merchant-api-key>
Idempotency-Key: <unique-key>
Content-Type: application/json{
"amount": 100,
"name": "Asha Nair",
"external_id": "event_2026",
"metadata": { "registration_id": "reg_284" }
}The response is provider-blind and contains PayGate's payment ID, status, requested amount, exact payable amount, adjustment, lifecycle timestamps, deterministic transaction_note (PayGate <payment_id>) and canonical upi:// URI. The URI carries the same note in its RFC 3986-encoded tn parameter. Money is stored internally as integer paise.
Useful routes:
POST /v1/payments
GET /v1/payments/{id}
POST /v1/payments/{id}/cancel
POST /admin/session
GET /admin/overview
GET /admin/payments
GET /admin/activity
GET /admin/settings
See Public API and webhooks for field semantics, idempotency and signing rules.
Web and Android share the same PayGate charcoal + emerald/teal design system and the same four primary areas:
- Overview — money, payment status, active collection profile, relay and webhook health;
- Payments — search, filters, exact amounts, payer context, timeline and controlled corrections;
- Activity — payment detections, matches, webhook outcomes and system events;
- Settings — collection profiles, merchant API keys, webhook configuration, trusted phone and admin security.
The web UI is embedded in the v4 server image. The Android app remains a separate repository/artifact so its signing identity and in-place upgrade path stay independent of server deployment.
The PayGate phone uses NotificationListenerService, a durable local queue and a P-256 ECDSA key stored in Android Keystore.
Android performs only cheap source-agnostic notification capture. The server owns source-specific parsing, incoming-credit semantics, profile inference, source trust, matching, deduplication and payment mutation.
The foreground relay is intentionally independent of operator login and is designed to survive screen lock, Doze, process recreation, temporary network loss and normal Battery Saver when the app is exempt from battery optimization.
Production keeps exactly one PayGate process owning the live SQLite database. Writes use explicit transactions and the allocator/matcher fail closed when ownership is ambiguous.
The v4 container expects persistent storage at:
/app/data
/app/backups
Use SQLite's online backup path rather than copying a live database file. Keep rollback images, the previous data volume and the final verified migration archive until the new version has passed acceptance.
Copy .env.example as a reference for the canonical v4 environment variables. A fresh database requires PAYGATE_V4_ADMIN_PASSWORD for the initial bootstrap; merchant API and webhook bootstrap values are optional and can also be configured through the operator settings after startup. The container provides defaults for the data, backup and listen paths.
npm ci
npm run typecheck:v4
npm run build:v4
go test -count=1 ./...
go test -race -count=1 ./...
go vet ./...
docker build -f Dockerfile.v4 -t paygate:v4 .CI validates the v4 frontend, all retained Go packages, static analysis and the production v4 container.
- admin password, merchant API keys, webhook signing secrets and Android device keys are separate credentials;
- plaintext merchant/webhook secrets are never persisted when a verifier/hash is sufficient;
- Android private signing keys remain non-exportable in Android Keystore;
- payer identity and raw notification detail stay out of unauthenticated/public payment views;
- a false-positive confirmation is considered worse than a delayed/manual outcome, so only origin-bound evidence may transition payment state; generic notifications and Google Messages/SMS remain evidence for operator confirmation;
- never run a second PayGate process against the live SQLite volume.
Start with the v4 documentation index:
- Target architecture
- Notification ingestion and pairing
- Payment lifecycle and matching
- Public API and webhooks
- Android app
- Admin UI and design system
- Storage, security and operations
- Edge cases and invariants
Reusable PayGate assets live in docs/brand. The standalone mark is suitable for the web favicon, Android launcher icon and GitHub/social-preview artwork.