Skip to main content

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.

MethodPathDescription
POST/register/send-otpBody: { "email": "user@example.com" }. Sends a 6-digit OTP to the email (via Resend). Returns 400 if email is already registered.
POST/register/verify-otpBody: { "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.

MethodPathDescription
POST/login/request-otpBody: { "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-otpBody: { "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 from POST /register/verify-otp after 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[] or file: File(s) to upload
  • print_type: "private" or "queue"
  • copies: Number of copies
  • print_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 INR
  • use_wallet (optional): Boolean to enable wallet payment if sufficient balance
  • print_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 ID
  • comment (optional): Customer notes
  • platform_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:

  1. If use_wallet is true, system checks wallet balance
  2. Sufficient balance: Deducts from wallet, sets payment_method: "wallet", skip_payment: true
  3. Insufficient balance: Creates Razorpay order, sets payment_method: "razorpay"
  4. 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, and cancelled_by are 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 from GET /queue and from order-files.
  • Returns 400 with a message if any file in the order is already printing.
  • Queue response includes first_file_id per 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:

  1. Creates Razorpay payment order for top-up
  2. After successful payment (via webhook), adds amount to wallet balance
  3. 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_transactions table
  • 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.