Self-hosted QRIS payment link platform for Indonesian merchants. Create payment links with pre-filled amounts — customers just scan and pay.
Bayaraja is an open-source platform for creating QRIS-based payment links that can be integrated into online stores, POS systems, or any application.
The problem it solves: Static QRIS codes require customers to type the amount manually — leading to incorrect payments. Bayaraja converts static QRIS into dynamic QRIS with the amount already embedded, so customers just scan and pay the exact amount.
You create a payment link → Send URL to customer → Customer scans QR → Pays the exact amount
| Feature | Description |
|---|---|
| 🔗 Payment Links | Generate unique links per transaction, share via WhatsApp/email |
| 📱 Dynamic QRIS | Amount auto-embedded in QR — no manual input needed |
| 📸 Payment Proof | Customers upload transfer screenshots, merchant confirms/rejects |
| 📊 Dashboard | Stats, 30-day revenue & transaction trends chart |
| 📋 Transaction History | Filterable, paginated list with CSV export |
| 🔀 Bulk Actions | Activate, deactivate, or delete multiple links at once |
| 🔔 Webhooks | Get notified on transaction.created/confirmed/rejected events |
| 🔑 REST API | Integrate with external systems using API keys |
| ⚡ Status Polling | Exponential backoff polling after payment proof upload |
| 🛡️ Rate Limiting | IP-based + per-link limits on all public endpoints |
| 🔐 Auth | Email/password + Google OAuth via Supabase Auth |
- Framework — Next.js 16 App Router, React Server Components, Suspense Streaming
- Backend — Supabase (PostgreSQL, Auth, Storage)
- Styling — Tailwind CSS v4
- Validation — Zod v4
- QR Code —
qrcode(generate) +jsqr(scan/parse) - Icons — Lucide React
- Language — TypeScript 5
- Node.js 18+
- A Supabase account (free tier works)
git clone https://github.com/akbarr13/bayaraja.git
cd bayaraja
npm install- Create a new project at supabase.com
- Open the SQL Editor and run the full contents of
supabase/schema.sql - Enable Storage and create a private bucket named
payment-proofs
cp .env.local.example .env.localFill in .env.local with your Supabase credentials:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
NEXT_PUBLIC_APP_URL=http://localhost:3000Important:
SUPABASE_SERVICE_ROLE_KEYis used server-side only. Never expose it to the client.
npm run devOpen http://localhost:3000, register an account, add your QRIS, and start creating payment links.
┌─────────────┐ POST /api/links ┌──────────────────┐
│ Merchant │ ──────────────────► │ Bayaraja API │
│ (Dashboard) │ ◄────────────────── │ │
└─────────────┘ { payment_url } └────────┬─────────┘
│
Send URL to customer │
▼
┌─────────────┐ GET /pay/{slug} ┌──────────────────┐
│ Customer │ ──────────────────► │ Payment Page │
│ (Browser) │ │ │
│ │ Scan QR & Upload │ Dynamic QRIS + │
└─────────────┘ Payment Proof │ Upload Form │
└──────────────────┘
- Merchant creates a payment link via dashboard or API
- Bayaraja generates a dynamic QRIS with the amount embedded
- Merchant sends the URL to the customer (WhatsApp, email, etc.)
- Customer opens the URL → scans QR → pays → uploads proof
- Merchant confirms in the dashboard → transaction complete
Base URL: https://your-domain.com
All API endpoints require:
Authorization: Bearer YOUR_API_KEY
Generate API keys in Dashboard → Settings → API Keys.
Generate a dynamic QRIS with a specific amount.
curl -X POST https://your-domain.com/api/qris/create-amount \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 75000}'Response
{
"success": true,
"data": {
"qris_dynamic": "000201...",
"qr_image_url": "data:image/png;base64,...",
"amount": 75000,
"merchant_name": "Toko Kopi Nusantara"
}
}Create a new payment link.
curl -X POST https://your-domain.com/api/links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"qris_account_id": "your-qris-uuid",
"title": "Order #42",
"amount": 75000,
"is_single_use": true
}'Response
{
"data": {
"id": "uuid",
"slug": "abc123xyz456abcd",
"title": "Order #42",
"amount": 75000,
"is_active": true,
"payment_url": "https://your-domain.com/pay/abc123xyz456abcd"
}
}Check the payment status of a payment link.
curl https://your-domain.com/api/pay/abc123xyz/status \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"slug": "abc123xyz",
"title": "Order #42",
"amount": 75000,
"paid": true,
"summary": { "confirmed": 1, "pending": 0, "total": 1 },
"latest_transaction": {
"id": "uuid",
"status": "confirmed",
"amount": 75000,
"payer_name": "Budi Santoso",
"created_at": "2026-03-21T10:00:00Z"
}
}| Code | Meaning |
|---|---|
200 |
Success |
400 |
Invalid request — check body format or parameters |
401 |
Missing, invalid, or disabled API key |
404 |
Resource not found |
429 |
Rate limit reached — wait and try again |
500 |
Server error |
All error responses follow the format: { "error": "error message" }
bayaraja/
├── app/
│ ├── (auth)/ # Login, register, forgot/reset password
│ ├── (dashboard)/ # Protected merchant dashboard
│ │ ├── dashboard/ # Stats, trends chart & recent transactions
│ │ ├── links/ # Manage payment links (bulk actions, search)
│ │ ├── qris/ # Manage QRIS accounts
│ │ ├── transactions/# Transaction history with filters & CSV export
│ │ ├── settings/ # Profile, API keys, webhooks
│ │ └── docs/ # Interactive API documentation
│ ├── api/ # API routes
│ ├── pay/[slug]/ # Public payment page
│ └── page.tsx # Landing page
├── components/
│ ├── ui/ # Base UI components (Modal, Toast, etc.)
│ ├── dashboard/ # Dashboard-specific components
│ ├── pay/ # Payment page components
│ └── layout/ # Sidebar, header, navigation
├── lib/
│ ├── qris.ts # EMV-CO QRIS conversion (static → dynamic)
│ ├── validations.ts # Zod schemas
│ ├── types.ts # TypeScript interfaces
│ └── supabase/ # Supabase clients (server & browser)
└── supabase/
└── schema.sql # Full database schema
| Item | Limit |
|---|---|
| QRIS accounts per user | 10 |
| Payment links per user | 100 |
| API keys per user | 5 |
| Upload file size | 5 MB |
| Upload file formats | JPEG, PNG, WebP |
- Fork this repo
- Import to Vercel
- Add environment variables
- Deploy — auto-deploys on every push to
main
Bayaraja is configured with output: standalone.
npm run build
# Copy static assets into standalone output
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
# Zip for upload (use forward slashes for Linux compatibility)
node -e "
const {execSync} = require('child_process');
const AdmZip = require('adm-zip'); // or use your preferred method
"
# Alternatively on Linux/WSL:
cd .next/standalone && zip -r ../../standalone.zip .
# Run: node server.jsWindows users: Use
ZipFile.CreateFromDirectoryvia PowerShell or zip on WSL — avoidCompress-Archiveas it produces Windows-style backslash paths that break on Linux extraction.
For cPanel, use Phusion Passenger pointed at a app.js entry file that requires server.js.
Pull requests are welcome. For major changes, please open an issue first to discuss what you'd like to change.
- Fork the repo
- Create a branch:
git checkout -b feat/your-feature - Commit:
git commit -m 'feat: add your feature' - Push:
git push origin feat/your-feature - Open a Pull Request
MIT — free to use, modify, and distribute.