API Documentation
Base URL
- Development:
http://localhost:8080 - Production:
https://your-api-domain.com
Authentication
Most endpoints require JWT authentication:
Authorization: Bearer <token>
Public Endpoints
GET /app-downloads
Returns app download links and "coming soon" flags for the public Download Apps page. No auth required.
Response:
{
"windows_shopkeeper_url": "https://...",
"android_customer_url": "https://...",
"ios_customer_url": "https://...",
"windows_coming_soon": false,
"android_coming_soon": false,
"ios_coming_soon": true
}
GET /delete-data
Serves a public HTML page for the Google Play "Delete data" link requirement (GDPR). Explains how users can delete their data (login and use Delete Account, or contact support).
Sign-up email OTP (required before registration)
Before calling POST /register, the client must verify the user's email with a 6-digit OTP. Email only (no SMS). See Sign-up Email OTP for the full flow.
| Method | Path | Description |
|---|---|---|
POST | /register/send-otp | Body: { "email": "user@example.com" }. Sends a 6-digit OTP to the email (via Resend). Returns 400 if email is already registered. |
POST | /register/verify-otp | Body: { "email", "code": "123456" }. Verifies the code and returns { "signup_token": "...", "email": "..." }. |
The signup_token must be included in the subsequent POST /register request. In development, set SKIP_SIGNUP_OTP=true to bypass OTP.
Login with email OTP (email only)
Users can log in with password (POST /login) or with email OTP. OTP login is email only (no SMS); if a phone number is sent, the API returns 400.
| Method | Path | Description |
|---|---|---|
POST | /login/request-otp | Body: { "login": "user@example.com" }. If the email is registered, sends a 6-digit OTP to that email (via Resend). Generic success message to avoid enumeration. Returns 400 if identifier is not an email (e.g. "OTP login is available for email only."). |
POST | /login/verify-otp | Body: { "login": "user@example.com", "code": "123456" }. Verifies the code and returns the same JWT response as POST /login: { "token": "...", "role": "...", "username": "..." }. |
Same auth rate-limit group as login/register. Used on web frontend, customer Android app, and shopkeeper Windows app.
POST /register
Register a new user. Requires a valid signup_token from POST /register/verify-otp (unless SKIP_SIGNUP_OTP=true).
Request:
{
"full_name": "John Doe",
"username": "johndoe",
"email": "john@example.com",
"phone": "+1234567890",
"password": "securepassword",
"role": "customer",
"signup_token": "eyJhbGciOiJIUzI1NiIs...",
"referral_code": "ABC12XYZ",
"shop_name": "My Shop",
"lat": 12.9716,
"long": 77.5946,
"address": "123 Main St"
}
signup_token(required in production): Obtained fromPOST /register/verify-otpafter the user enters the 6-digit code sent to their email. See Sign-up Email OTP.referral_code(optional): For customer signups, creates a referral link to the referrer; credit is applied when the referee completes first top-up or first print. See Referral (Refer & Earn).
Duplicate username/email/mobile (400): If username, email, or phone is already in use, the response body is a plain-text message indicating which field(s) are taken, e.g.:
- "This username is already in use."
- "This email is already in use."
- "This mobile number is already in use."
- "This username and email are already in use." (or any combination)
The customer app displays this message to the user.
POST /login
Login with username/email/mobile + password and receive JWT token. Alternatively, use Login with email OTP (POST /login/request-otp and POST /login/verify-otp).
Request:
{
"username": "johndoe",
"password": "securepassword"
}
Note: login can be username, email, or (for password login only) 10-digit mobile number.
Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"role": "customer",
"username": "johndoe"
}
POST /forgot-password
Request password reset email.
Request:
{
"email": "john@example.com"
}
POST /reset-password
Reset password using token.
Request:
{
"token": "reset-token-from-email",
"password": "newpassword"
}
Protected Endpoints
GET /profile
Get current user's profile.
Response:
{
"id": 1,
"username": "johndoe",
"full_name": "John Doe",
"email": "john@example.com",
"phone": "+1234567890",
"role": "customer",
"shop_name": null,
"address": null,
"is_open": false,
"referral_code": "ABC12XYZ",
"referral_link": "https://qprint.co.in/r/ABC12XYZ"
}
For customers, referral_code and referral_link are included when set. See Referral (Refer & Earn) for full referral docs.
PUT /profile
Update current user's profile.
Request:
{
"username": "newusername",
"address": "456 New St",
"password": "newpassword"
}
DELETE /profile
Delete the current user's account. Requires password confirmation.
Request:
{
"password": "currentpassword"
}
Response:
{
"message": "Account deleted successfully"
}
Notes:
- Requires password confirmation for security
- Deletes all associated data (files, reset tokens, payouts)
- Nullifies payment order references (preserves financial records)
- Clears authentication cookie
- Admin accounts cannot be deleted through this endpoint
Referral (Refer & Earn)
Referral endpoints are customer-only. Full behaviour, config, and policies: Referral (Refer & Earn).
GET /referral/summary
Returns referrer stats: total referred, total credited, total earnings, pending count.
Response:
{
"total_referred": 5,
"total_credited": 3,
"total_earnings": 150,
"pending_count": 2
}
GET /referral/history
Returns list of referrals (status, amount, dates). Query: limit (1–100), offset.
Response:
{
"referrals": [
{
"id": 1,
"status": "credited",
"amount": 50,
"credited_at": "2024-01-20T10:00:00Z",
"created_at": "2024-01-15T09:00:00Z"
}
]
}
POST /referral/invite
Send invite email with referral link (Resend). Body: { "email": "friend@example.com" }. Rate limited per referrer per 24h. Returns { "ok": true, "message": "Invite sent." } or 400/429.
POST /register (referral)
Optional body field referral_code. For customer signups, backend creates a referral row and credits when the referee completes first top-up or first print. See Referral.
POST /upload
Upload a file for printing.
Supported file types: PDF (.pdf), images (.png, .jpg, .jpeg), Word (.doc, .docx), PowerPoint (.ppt, .pptx). Max 20MB per file, 100MB total per batch. Billing uses real page/slide count per file type. If page/slide count cannot be determined (e.g. .doc without summary, empty or non-standard file), the API returns 400 with: "Page count not available for this file. Please convert to PDF and upload."
Headers: Authorization: Bearer <token>, Content-Type: multipart/form-data
Form Data:
files[]orfile: File(s) to uploadprint_type:"private"or"queue"copies: Number of copiesprint_mode:"single"or"double"color_mode:"bw"or"color"paper_size:"A4","Letter", etc.shop_id: Shopkeeper ID (required for queue prints)comment: Optional customer notes
Response:
{
"file_id": 123,
"code": "aB3xY9",
"num_pages": 5,
"total_cost": 25.00,
"queue_position": 3
}
GET /file/:code
Download file by unique code.
Parameters:
code(path): 6-character unique code
GET /file/:code/status
Check file status.
Parameters:
code(path): 6-character unique code
Response:
{
"status": "uploaded"
}
File status may be uploaded, printing (shop has started print; customer cannot withdraw), downloaded (printed), withdrawn, or cancelled (shop cancelled the order; customer is notified and refunded; cancel_reason and cancelled_at are set).
GET /shops
Get nearest shopkeepers. Requires authentication (Bearer or cookie); not public. Returns 401 without a valid token.
Response:
[
{
"id": 2,
"username": "shop1",
"shop_name": "Print Shop 1",
"lat": 12.9716,
"long": 77.5946,
"distance": 0.05,
"is_open": true,
"address": "123 Main St"
}
]
GET /queue
Get shopkeeper's print queue.
POST /calculate-cost
Calculate print cost before payment. Accepts the same file types as upload (PDF, images, Word, PPT). Send files as multipart/form-data with files[], copies, print_mode, color_mode, paper_size. Cost formula: pages × copies × price per page; the price per page depends on color_mode: black & white uses COST_PER_PAGE_BW (default 1), colour uses COST_PER_PAGE_COLOR (default 10). See Environment Variables. If page/slide count cannot be determined for a file, the API returns 400 with: "Page count not available for this file. Please convert to PDF and upload."
POST /create-payment-order
Create a payment order. Supports both Razorpay and Wallet payment methods.
Request:
{
"amount": 50.00,
"use_wallet": true,
"print_type": "private",
"copies": 1,
"print_mode": "single",
"color_mode": "bw",
"paper_size": "A4",
"shopkeeper_id": 123,
"comment": "Optional comment",
"platform_commission": 0.0
}
Request Fields:
amount(required): Payment amount in INRuse_wallet(optional): Boolean to enable wallet payment if sufficient balanceprint_type(required):"private"or"queue"copies(optional): Number of copies (default: 1)print_mode(optional):"single"or"double"(default: "single")color_mode(optional):"bw"or"color"(default: "bw")paper_size(optional): Paper size (default: "A4")shopkeeper_id(required for queue prints): Shopkeeper user IDcomment(optional): Customer notesplatform_commission(optional): Platform commission percentage (0-1, default: 0)
Response:
{
"id": 123,
"order_id": "order_xxx",
"amount": 50.00,
"status": "paid",
"payment_method": "wallet",
"wallet_amount": 50.00,
"razorpay_amount": 0.00,
"key_id": "rzp_test_xxx",
"shopkeeper_id": 123,
"shopkeeper_amount": 50.00,
"skip_payment": true
}
Payment Methods:
"wallet": Full payment from wallet (instant, no Razorpay needed)"razorpay": Payment via Razorpay gateway"hybrid": Partial wallet + Razorpay payment
Payment Flow:
- If
use_walletistrue, system checks wallet balance - Sufficient balance: Deducts from wallet, sets
payment_method: "wallet",skip_payment: true - Insufficient balance: Creates Razorpay order, sets
payment_method: "razorpay" - Partial balance: Deducts wallet portion, creates Razorpay order for remainder, sets
payment_method: "hybrid"
GET /payment-order/:orderId/status
Check payment status.
Parameters:
orderId(path): Payment order ID
Response:
{
"id": 123,
"user_id": 1,
"order_id": "order_xxx",
"payment_id": "pay_xxx",
"amount": 50.00,
"status": "paid",
"payment_method": "wallet",
"wallet_amount": 50.00,
"created_at": "2024-01-17T20:00:00Z",
"paid_at": "2024-01-17T20:00:01Z"
}
POST /payment/webhook
Razorpay webhook endpoint (public, signature verified). Handles payment confirmations and wallet top-ups.
GET /csrf-token
Get CSRF token for state-changing requests.
Response:
{
"csrfToken": "token-string"
}
POST /logout
Logout and clear authentication cookie.
POST /forgot-username
Request username recovery email.
Request:
{
"email": "john@example.com"
}
POST /file/:code/confirm
Confirm private print completion (shopkeeper use). Accepts file status uploaded or printing. Sets status to downloaded and deletes file from storage.
POST /file/:code/print-started
Mark private print as started (shopkeeper use). Sets file status to printing so customer cannot withdraw. Call when shopkeeper taps Print.
POST /file/:code/print-failed
Mark private print as failed or timed out (shopkeeper use). Sets file status back to uploaded so customer can withdraw again.
GET /queue/:orderGroupId/files
Get files for a specific order group in the queue.
GET /queue/download/:fileId
Download a specific file from the queue (shopkeeper use).
POST /queue/:fileId/confirm
Confirm queue print completion (shopkeeper use). Accepts file status uploaded or printing. Sets status to downloaded and deletes file from storage.
POST /queue/:fileId/print-started
Mark queue print as started (shopkeeper use). Sets file status to printing so customer cannot withdraw. Call when shopkeeper taps Print (for each file in a batch).
POST /queue/:fileId/print-failed
Mark queue print as failed or timed out (shopkeeper use). Sets file status back to uploaded so customer can withdraw again.
POST /queue/:fileId/cancel
Cancel a queue order before printing (shopkeeper use). Use when something is wrong (e.g. printer out of order). Not allowed if any file in the order has status printing.
Request:
{
"reason": "Printer out of order. Please try another shop."
}
- Effect: All files in the same payment order at this shop are set to status
cancelled;cancel_reason,cancelled_at, andcancelled_byare stored. If the order was paid, a refund is processed once per payment order. The customer receives an FCM notification with the reason. Cancelled orders are excluded fromGET /queueand from order-files. - Returns 400 with a message if any file in the order is already
printing. - Queue response includes
first_file_idper order; use that (or any file id in the order) as:fileId.
GET /my-files
Get current user's uploaded files.
GET /my-orders
Get current user's payment orders (for customer order history).
GET /shop/history
Get shop's print history (shopkeeper use).
POST /shop/status
Toggle shop open/closed status.
Request:
{
"is_open": true
}
POST /shop/heartbeat
Shopkeeper app sends a heartbeat to keep the shop marked as "open". Used with the backend sweeper: if both app heartbeat and web activity are stale, the shop is auto-closed. No request body required.
POST /withdraw/:fileId
Withdraw a print job and receive refund. Returns 409 when file status is printing (shop has started the print). Only allowed when status is uploaded.
GET /shopkeeper/payouts
Get shopkeeper's payout history.
Wallet Service Endpoints
The wallet service is implemented. Customers can pre-load money, pay instantly (wallet or hybrid with Razorpay), and receive instant refunds on withdrawal.
GET /wallet/balance
Get current wallet balance for the authenticated user.
Response:
{
"balance": 500.00
}
POST /wallet/topup
Add money to wallet via Razorpay.
Request:
{
"amount": 500.00
}
Request Fields:
amount(required): Top-up amount in INR (minimum: ₹10, maximum: ₹10,000)
Response:
{
"order_id": "order_xxx",
"key_id": "rzp_test_xxx",
"amount": 500.00
}
Flow:
- Creates Razorpay payment order for top-up
- After successful payment (via webhook), adds amount to wallet balance
- Creates wallet transaction record
GET /wallet/transactions
Get wallet transaction history.
Query Parameters:
limit(optional): Number of transactions to return (default: 50, max: 100)offset(optional): Pagination offset (default: 0)
Response:
{
"transactions": [
{
"id": 1,
"type": "topup",
"amount": 500.00,
"balance_before": 0.00,
"balance_after": 500.00,
"status": "completed",
"description": "Wallet top-up",
"created_at": "2024-01-17T20:00:00Z"
},
{
"id": 2,
"type": "payment",
"amount": -50.00,
"balance_before": 500.00,
"balance_after": 450.00,
"status": "completed",
"description": "Print payment",
"payment_order_id": 123,
"created_at": "2024-01-17T21:00:00Z"
},
{
"id": 3,
"type": "refund",
"amount": 50.00,
"balance_before": 450.00,
"balance_after": 500.00,
"status": "completed",
"description": "Refund for withdrawal",
"payment_order_id": 123,
"created_at": "2024-01-17T22:00:00Z"
}
],
"total": 25
}
Transaction Types:
"topup": Money added to wallet"payment": Money spent from wallet"refund": Money refunded to wallet"withdrawal": Money withdrawn from wallet (optional feature)"referral_bonus": Credit to referrer when a referred friend qualifies (see Referral)"referral_welcome": Welcome bonus to referee when they complete first qualifying action
POST /wallet/withdraw (Optional)
Withdraw wallet balance to bank account. Requires bank account verification.
Request:
{
"amount": 200.00,
"bank_account": "1234567890",
"ifsc_code": "BANK0001234",
"account_holder_name": "John Doe"
}
Response:
{
"withdrawal_id": 123,
"status": "pending",
"amount": 200.00,
"processing_time": "2-3 business days"
}
Wallet Service Implementation Details
Database Schema
Users Table Update
ALTER TABLE users ADD COLUMN wallet_balance DECIMAL(10,2) DEFAULT 0.00;
Wallet Transactions Table
CREATE TABLE wallet_transactions (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) NOT NULL,
transaction_type TEXT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
balance_before DECIMAL(10,2) NOT NULL,
balance_after DECIMAL(10,2) NOT NULL,
payment_order_id INT REFERENCES payment_orders(id),
razorpay_payment_id TEXT,
razorpay_refund_id TEXT,
status TEXT DEFAULT 'completed',
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Payment Orders Table Update
ALTER TABLE payment_orders ADD COLUMN payment_method TEXT DEFAULT 'razorpay';
ALTER TABLE payment_orders ADD COLUMN wallet_amount DECIMAL(10,2) DEFAULT 0.00;
Payment Flow with Wallet
Scenario A: Full Wallet Payment
1. Customer creates payment order with use_wallet: true
2. System checks wallet balance (e.g., ₹200)
3. Payment amount: ₹50
4. Balance sufficient → Deducts ₹50 from wallet
5. Payment order status: "paid" (immediate)
6. skip_payment: true (no Razorpay checkout needed)
7. File can be uploaded immediately
Scenario B: Insufficient Wallet Balance
1. Customer creates payment order with use_wallet: true
2. System checks wallet balance (e.g., ₹30)
3. Payment amount: ₹50
4. Balance insufficient → Creates Razorpay order for ₹50
5. Customer pays via Razorpay
6. Payment order status: "paid" (after webhook)
7. File can be uploaded
Scenario C: Hybrid Payment (Partial Wallet)
1. Customer creates payment order with use_wallet: true
2. System checks wallet balance (e.g., ₹30)
3. Payment amount: ₹50
4. Deducts ₹30 from wallet
5. Creates Razorpay order for remaining ₹20
6. Customer pays ₹20 via Razorpay
7. Payment order status: "paid" (after webhook)
8. payment_method: "hybrid", wallet_amount: 30.00, razorpay_amount: 20.00
Refund Flow with Wallet
When a customer withdraws a print job:
If payment was via wallet:
- Refund goes to wallet instantly
- Balance updated immediately
- No Razorpay refund needed
If payment was via Razorpay:
- Refund via Razorpay (5-7 business days)
- Or customer can choose wallet refund (instant)
Wallet Limits
- Minimum Top-up: ₹10
- Maximum Top-up: ₹10,000 per transaction
- Maximum Wallet Balance: ₹50,000 (optional limit)
- Minimum Withdrawal: ₹100 (if withdrawal enabled)
Security Considerations
- Wallet balance cannot go negative
- All transactions are logged in
wallet_transactionstable - Database transactions ensure atomicity
- Row-level locking prevents race conditions
- Rate limiting on top-ups (prevent abuse)
- Server-side validation of all amounts
Admin Endpoints
All admin endpoints require admin role and (optional) IP whitelist when ALLOWED_ADMIN_IPS is set. CSRF required for state-changing requests.
GET /admin/dashboard/stats
Get dashboard statistics.
GET /admin/users
Get all users (paginated).
GET /admin/users/details
Get detailed info for a specific user (e.g. for admin user management).
POST /admin/users/delete
Delete a user account (admin-initiated). Request body: { "user_id": 123 }.
GET /admin/orders
Get all payment orders.
GET /admin/payouts
Get all shopkeeper payouts.
PUT /admin/payouts/update
Update a single payout status.
PUT /admin/payouts/bulk-update
Bulk update payout statuses.
GET /admin/payouts/export
Export payouts (e.g. CSV).
PUT /admin/app-downloads
Update app download links and "coming soon" flags (Windows shopkeeper, Android customer, iOS customer). These appear on the public Download Apps page.
Request:
{
"windows_shopkeeper_url": "https://...",
"android_customer_url": "https://...",
"ios_customer_url": "https://...",
"windows_coming_soon": false,
"android_coming_soon": false,
"ios_coming_soon": true
}
For a quick reference, see the API Reference.