Skip to Main Content
QsslyM3 Theme
Complete API Reference

Qssly Developer Hub

Everything you need to build a booking app. Complete endpoint documentation, real-world use cases, copy-paste code examples, and AI-ready prompts for vibe coding.

1. Getting Started

Qssly is a headless booking engine. You build the frontend — Qssly handles availability computation, double-booking prevention, payments, and real-time webhooks. Here's how to go from zero to a working booking flow in 5 minutes.

Quickstart Checklist

  1. Sign up — Register your business at the Qssly Portal to create your tenant account.
  2. Create a Location — Add your branch with its IANA timezone (e.g. "Asia/Kolkata").
  3. Add Resources — Create rooms, doctors, courts, or whatever is being booked.
  4. Define Services — Set up rate packages with pricing, duration, and payment rules.
  5. Bind Resources → Services — Link which services each resource offers.
  6. Set Availability Rules — Define weekly operating hours for each resource.
  7. Generate an API Key — Go to Business Portal → Settings → Generate API Key. Copy the qs_live_... key.
  8. Start Building — Use the endpoints below to query availability and create bookings.

Data Model

Tenant → Your business account. Automatically resolved from your API key.
Location → A branch with a timezone. All availability is computed in this timezone.
Resource → The bookable entity — a hotel room, a doctor, a tennis court.
Service → A rate/package defining price, duration, buffer time, and payment rules.
Booking → A reservation with start/end times, customer info, and payment status.

1.1. Interactive API Sandbox

Start an ephemeral sandbox session to verify standard API endpoints, check real-time availability, and test booking confirmation. Each sandbox session lives in the main database isolated by Row-Level Security, has hard entity limits, and is automatically destroyed after 5 hours.

Start a Live API Sandbox

Instantly spin up an isolated, pre-seeded developer environment. Get a sandbox API key valid for 5 hours with fully seeded demo data for your chosen use case.

No registration required. Epic and anonymous.

2. Authentication

Pass your API key in the X-API-Key header on every request. Your API key automatically scopes all data to your tenant — you'll only see your own locations, resources, and bookings.

GET /api/v1/locations HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-API-Key: qs_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Treat your API key as a secret. It grants full access to your tenant, so for a production browser app you should proxy requests through your own backend rather than shipping the raw qs_live_ key to end users. The client-side snippets on this page show the key inline for brevity — in a real frontend, call your proxy (which injects the key server-side). The OTP and credential rules below apply the same whether you call Qssly directly or through your proxy.

Bearer JWT Authentication

Certain endpoints (e.g. rescheduling or cancelling bookings via client-facing portals) require user authentication using JSON Web Tokens (JWT). Pass the JWT token in the Authorization header as a Bearer token.

PATCH /api/v1/bookings/2f7157ca-3edf-4ef4-90c6-bb0a5ed87848
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "start_time": "2026-06-29T03:30:00Z",
  "end_time": "2026-06-29T04:00:00Z"
}
Obtaining a JWT: Tokens can be retrieved by sending credentials to POST /auth/login (email and password) or by verifying a one-time passcode via POST /auth/otp/verify (passwordless OTP).
JWT Claims: Signed using the HS256 algorithm. The payload contains:
  • sub: The User's UUID
  • tenant_id: The Tenant's UUID
  • role: User role (TENANT_ADMIN | TENANT_STAFF | END_CUSTOMER)
  • type: Token type (access | refresh)
  • exp: Expiration timestamp (controlled by ACCESS_TOKEN_EXPIRE_MINUTES env var)
Role Enforcement: Staff/Admin roles can manage any booking. END_CUSTOMER users are strictly limited to rescheduling or cancelling their own bookings, where the authenticated user's email matches the booking's customer_email.

Token Acquisition Endpoints

POST/api/v1/auth/login

Authenticate staff/admins using email and password.

Request Body Schema

{
  "email": "[email protected]",
  "password": "secure_password"
}

Response (200 OK)

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer"
}

POST/api/v1/auth/otp/request

Trigger an email containing a 6-digit one-time passcode for passwordless login.

Request Body Schema

{
  "email": "[email protected]",
  "phone": "+919876543210"
}

Response (200 OK)

{
  "message": "OTP sent successfully"
}

POST/api/v1/auth/otp/verify

Verify a 6-digit OTP code requested via /api/v1/auth/otp/request. Creates an END_CUSTOMER user if none exists.

Request Body Schema

{
  "email": "[email protected]",
  "code": "123456"
}

Response (200 OK)

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer"
}

POST/api/v1/auth/refresh

Refresh expired JWT tokens using the HttpOnly refresh token cookie.

Request Headers

Cookie: refresh_token=eyJhbGciOi...

Response (200 OK)

{
  "access_token": "eyJhbGciOi...",
  "token_type": "bearer"
}

3. Integration Gotchas & Security

These four rules are the difference between a smooth integration and a support ticket. They were distilled from real end-to-end testing — read them before you write a single request.

1 · OTP login requires the X-API-Key header (RLS tenant mapping)

The passwordless OTP endpoints POST /auth/otp/request and POST /auth/otp/verify are not truly public. Database Row-Level Security needs to know which tenant the customer belongs to, and that identity comes from your API key. Every OTP request MUST include X-API-Key — without it the backend returns 401 "Missing X-API-Key or Bearer token", and the OTP code + user record are scoped to the correct organization only when the key is present.

// ✅ Correct — OTP request carries the API key so RLS resolves the tenant
await fetch('/api/v1/auth/otp/request', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': Qssly_API_KEY,   // ← REQUIRED, do not omit
  },
  body: JSON.stringify({ email: '[email protected]', phone: '+919876543210' }),
});

2 · Never send X-API-Key and Bearer JWT in the same request

The backend evaluates X-API-Key before Authorization: Bearer. If both are present and belong to different tenants, the API key's tenant silently wins — causing a tenant-context clash and stale/incorrect data. Send exactly one credential per request:

  • Customer is logged in → send only Authorization: Bearer <jwt> (drop the API key for that request).
  • Guest / not logged in → send only X-API-Key.
// Build headers with exactly ONE credential
function authHeaders({ jwt }) {
  const h = { 'Content-Type': 'application/json' };
  if (jwt) h['Authorization'] = 'Bearer ' + jwt;   // logged-in customer
  else h['X-API-Key'] = Qssly_API_KEY;          // guest
  return h;                                        // never both at once
}

3 · State hygiene — clear cached data on context change or failure

  • On tenant/organization switch: clear all customer tokens, cached entities (locations, resources, services), and in-flight form state. Never let one tenant's data bleed into another's session.
  • On API failure: if GET /locations, /resources, or /services fails, reset the corresponding UI state to empty rather than leaving stale dropdown options from a previous session.
  • On logout: purge tokens and any customer-scoped API keys so the next user starts clean.

4 · Overnight & day-boundary rules (the 20-hour threshold)

Qssly decides whether a booking may cross midnight based on the service's duration:

  • Duration < 20 hours (1200 minutes): the booking must start and end on the same calendar day in the resource's local timezone. A same-day consultation cannot span midnight.
  • Duration ≥ 20 hours (1200 minutes): the booking may cross day boundaries — this is how overnight hotel stays work (check-in 14:00, check-out 11:00 the next day).
  • Always make the booking's end_time − start_time equal the service's duration exactly, and send both as ISO-8601 UTC strings (e.g. 2026-07-01T14:00:00Z).
Double-booking is impossible by design. Even if you checked availability seconds ago, the slot may be taken at POST /bookings time. Treat HTTP 409 as a normal outcome: show "someone just booked this slot — please pick another time" and refresh availability.

4. API Reference

GET/api/v1/locations

List all branches for your business. Returns paginated results.

const res = await fetch('/api/v1/locations', {
  headers: { 'X-API-Key': 'qs_live_your_key' }
});
const { items, total, page, pages } = await res.json();
// items = [{ id, name, timezone, created_at, updated_at }]

GET/api/v1/resources

List bookable entities. Filter by location_id to get resources at a specific branch.

ParameterTypeRequiredDescription
location_idUUIDNoFilter by branch
pageintNoPage number (default: 1)
page_sizeintNoItems per page (default: 20)
// Response shape:
{
  "items": [
    {
      "id": "2f7157ca-3edf-4ef4-90c6-bb0a5ed87848",
      "name": "Deluxe Suite 101",
      "location_id": "a1b2c3d4-...",
      "capacity": 2,
      "metadata_schema": null,
      "tenant_id": "...",
      "created_at": "2026-06-20T10:00:00Z",
      "updated_at": "2026-06-20T10:00:00Z"
    }
  ],
  "total": 12,
  "page": 1,
  "page_size": 20,
  "pages": 1
}
metadata_schema (dynamic forms): A resource may carry a JSONSchema object in metadata_schema. When present, render a form from it and submit the collected values as booking_metadata on POST /bookings (e.g. a hotel's bed-type, a clinic's symptoms). When null, no extra fields are required.

GET/api/v1/services

List rate packages. Each service defines pricing, duration, buffer time between bookings, and payment rules.

// Response shape:
{
  "items": [
    {
      "id": "8b9e67d2-38ef-4171-aa31-50e5671192e8",
      "name": "Standard Night Stay",
      "duration_minutes": 1440,       // 24 hours = 1 night
      "buffer_minutes": 180,          // 3-hour gap between stays (cleaning)
      "price": 2500.00,
      "currency": "INR",
      "payment_type": "FULL_ADVANCE", // FULL_ADVANCE | PARTIAL_ADVANCE | PAY_LATER
      "advance_payment_amount": null, // Only set when PARTIAL_ADVANCE
      "cancellation_cutoff_hours": 24  // Can cancel up to 24h before
    }
  ]
}
Payment Types:
  • FULL_ADVANCE — Customer pays full amount online before confirmation
  • PARTIAL_ADVANCE — Customer pays a deposit (advance_payment_amount); rest is collected later
  • PAY_LATER — Booking is confirmed immediately; payment is collected in person

GET/api/v1/availability

Query available time slots for a resource. Supports single date or date range (up to 31 days).

ParameterTypeRequiredDescription
resource_idUUIDYesThe resource to check
service_idUUIDYesThe service (determines slot duration & buffer)
dateYYYY-MM-DD*Single date query
start_dateYYYY-MM-DD*Range start (use with end_date)
end_dateYYYY-MM-DD*Range end (max 31 days from start)

* Provide either date OR both start_date + end_date. Cannot combine both.

// Check a week of availability for a hotel room
const res = await fetch(
  '/api/v1/availability?' + new URLSearchParams({
    resource_id: roomId,
    service_id: serviceId,
    start_date: '2026-07-01',
    end_date: '2026-07-07'
  }),
  { headers: { 'X-API-Key': API_KEY } }
);
const { slots } = await res.json();
// slots = [
//   { date: "2026-07-01", times: ["14:00"] },
//   { date: "2026-07-02", times: ["14:00"] },
//   { date: "2026-07-03", times: [] },  // ← booked!
//   ...
// ]

POST/api/v1/bookings

Create a reservation. Returns a checkout_url if online payment is required.

FieldTypeRequiredDescription
resource_idUUIDYesThe room/doctor/entity to book
service_idUUIDYesThe rate package to apply
start_timeISO 8601YesBooking start (UTC)
end_timeISO 8601YesBooking end (UTC). Must be after start_time
customer_emailstringYesCustomer email address
customer_phonestringNoCustomer phone number
booking_metadataobjectNoCustom JSON data (e.g. guest count, notes)
is_manual_overridebooleanNoBypass payment flow for Staff/Admin
const booking = await fetch('/api/v1/bookings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': API_KEY,
  },
  body: JSON.stringify({
    resource_id: roomId,
    service_id: serviceId,
    start_time: '2026-07-01T14:00:00Z',
    end_time: '2026-07-04T11:00:00Z',
    customer_email: '[email protected]',
    booking_metadata: { guests: 2 }
  })
}).then(r => r.json());

if (booking.checkout_url) {
  // Redirect to payment page
  window.location.href = booking.checkout_url;
} else {
  // PAY_LATER: Booking is already confirmed
  console.log('Booking confirmed:', booking.id);
}

Other Booking Endpoints

MethodPathBody / ParamsDescription
GET/bookings?resource_id=&service_id=&date=&status=List bookings (all filters optional)
GET/bookings/{id}Get a single booking by ID
PATCH/bookings/{id}{ start_time, end_time }Reschedule (requires Bearer JWT)
POST/bookings/{id}/cancel{ cancellation_reason? }Cancel and release the time slot
POST/bookings/{id}/no-showMark as no-show (requires Bearer JWT)
POST/bookings/{id}/payments{ amount, method, reference?, notes? }Record offline payment (method: CASH | POS_CARD | BANK_TRANSFER)
POST/bookings/{id}/refunds{ amount, notes? }Record a manual refund
GET/bookings/{id}/paymentsList payment ledger for booking

Setup Endpoints (Admin Only)

These endpoints require a Bearer JWT token from a logged-in admin user, not just an API key.

MethodPathBodyDescription
POST/resource-services{ resource_id, service_id }Bind a service to a resource
GET/resource-services?resource_id=List all bindings
POST/availability-rules{ resource_id, day_of_week, start_time, end_time }Create weekly availability window
GET/availability-rules?resource_id=List rules for a resource
day_of_week values: 0 = Monday, 1 = Tuesday, 2 = Wednesday, 3 = Thursday, 4 = Friday, 5 = Saturday, 6 = Sunday

5. Availability & Scheduling

Understanding how Qssly computes available slots is key to building a great booking UI.

How Slot Generation Works

  1. Availability Rules define when a resource is available (e.g. "Monday 09:00–17:00").
  2. The system divides those windows into slots based on the service's duration_minutes + buffer_minutes.
  3. Existing bookings are subtracted — any slot that overlaps a confirmed booking is removed.
  4. The remaining slots are returned as available times.

Example: 30-min consultation with 10-min buffer

If a doctor works 09:00–12:00, the system generates: 09:00, 09:40, 10:20, 11:00, 11:40 (each slot = 30 + 10 = 40 min apart). If 09:00 is already booked, only 09:40, 10:20, 11:00, 11:40 are returned.

Example: Hotel room (1440-min = 1-night stay)

A room with 14:00 check-in rule generates one slot per day: 14:00. If the room is booked for July 3rd, that date returns an empty array. Use the start_date + end_date range query to check multiple days at once.

6. Use Case: Hotel Booking

A complete walkthrough for building a hotel room booking flow. This covers multi-night stays, date-range availability, and the payment redirect flow.

Step 1: Check room availability for a 3-night stay (July 1–4)

GET /api/v1/availability?resource_id={room_id}&service_id={stay_service_id}&start_date=2026-07-01&end_date=2026-07-04

Response:
{
  "resource_id": "2f7157ca-...",
  "service_id": "8b9e67d2-...",
  "timezone": "Asia/Kolkata",
  "slots": [
    { "date": "2026-07-01", "times": ["14:00"] },  ← Available (check-in at 2 PM)
    { "date": "2026-07-02", "times": ["14:00"] },  ← Available
    { "date": "2026-07-03", "times": ["14:00"] }   ← Available
  ]
}

✅ Jul 1–3 each show a 14:00 slot → the room is free for a Jul 1–4 stay.
Note: slot times are LOCAL. In Asia/Kolkata (UTC+5:30), 14:00 local = 08:30Z — convert before booking.

Step 2: Create the booking (one night)

POST /api/v1/bookings
X-API-Key: qs_live_xxxx
Content-Type: application/json

{
  "resource_id": "2f7157ca-3edf-4ef4-90c6-bb0a5ed87848",
  "service_id": "8b9e67d2-38ef-4171-aa31-50e5671192e8",
  "start_time": "2026-07-01T08:30:00Z",   // Check-in: Jul 1, 14:00 IST (08:30 UTC)
  "end_time": "2026-07-02T08:30:00Z",     // Check-out: Jul 2, 14:00 IST (08:30 UTC) — exactly 1440 min
  "customer_email": "[email protected]",
  "customer_phone": "+919876543210",
  "booking_metadata": {
    "guests": 2,
    "special_requests": "High floor room, extra pillows"
  }
}

Response:
{
  "id": "abc123-...",
  "status": "PENDING_PAYMENT",
  "payment_status": "PENDING",
  "total_amount": 2500.00,
  "checkout_url": "https://razorpay.com/pay/order_xxxxx"
}

→ Redirect guest to checkout_url to complete payment.
→ After payment, booking status auto-updates to CONFIRMED.
→ For a 3-night stay: make one booking per night, or use a 4320-min (3×1440) service booked Jul 1 14:00 → Jul 4 14:00.
How nights are modeled (important):

A booking must last exactly the service's duration_minutes. For a standard 1-night (1440-minute) service the stay is a full 24 hours, so check-out is at the same local clock time as check-in, on the next day (e.g. check-in 14:00 → check-out 14:00 the following day). end_time − start_time must equal 1440 minutes, and both are sent in UTC.

For a multi-night stay, either create one booking per night, or configure a service whose duration_minutes = nights × 1440 and book check-in day → check-out day in a single request. Either way, every local day the stay spans must have an availability rule, or the booking is rejected with a 400.

7. Use Case: Doctor Appointment

A complete walkthrough for building a doctor appointment booking system with short time slots and buffer gaps.

Step 1: Query Dr. Sharma's available slots on Monday

The service is a 30-minute consultation with a 10-minute buffer. Slots are generated every 40 minutes.

GET /api/v1/availability?resource_id={doctor_id}&service_id={consult_service_id}&date=2026-06-29

Response:
{
  "resource_id": "8a7c293c-...",
  "service_id": "5b12a9e3-...",
  "timezone": "Asia/Kolkata",
  "slots": [{
    "date": "2026-06-29",
    "times": [
      "09:00",   // 9:00 AM – 9:30 AM (appointment)
      "09:40",   // 9:40 AM – 10:10 AM
      "10:20",   // 10:20 AM – 10:50 AM
      "11:00",   // 11:00 AM – 11:30 AM
      "14:00",   // 2:00 PM – 2:30 PM (afternoon session)
      "14:40",   // 2:40 PM – 3:10 PM
      "15:20"    // 3:20 PM – 3:50 PM
    ]
  }]
}

Note: The gap from 11:40 to 14:00 is the doctor's lunch break
(no availability rule defined for 12:00–14:00).

Step 2: Book the 9:00 AM slot

POST /api/v1/bookings
X-API-Key: qs_live_xxxx
Content-Type: application/json

{
  "resource_id": "8a7c293c-21a4-44b2-8c29-de98e11a2fcd",
  "service_id": "5b12a9e3-2e3b-47e1-88fc-8f7a8109d9f2",
  "start_time": "2026-06-29T03:30:00Z",   // 9:00 AM IST (03:30 UTC)
  "end_time": "2026-06-29T04:00:00Z",     // 9:30 AM IST (04:00 UTC)
  "customer_email": "[email protected]",
  "customer_phone": "+919876543210",
  "booking_metadata": {
    "symptoms": "Recurring headaches and mild fever for 3 days",
    "allergies": "Penicillin",
    "blood_group": "B+"
  }
}

Important: end_time = start_time + duration_minutes (30 min).
Do NOT add buffer_minutes to end_time — buffer only affects slot generation.
Timezone Tip: Availability slots are returned in the location's local timezone (e.g. Asia/Kolkata), but bookings must always specify times in UTC ISO 8601 format. In the example above, the slot is at 09:00:00 local time (IST = UTC+5:30), which is converted to 03:30:00 UTC (2026-06-29T03:30:00Z) for the booking request.

8. Webhooks

Qssly sends real-time webhook notifications to your server when booking statuses change. Configure your webhook URL and secret in the Business Portal settings.

Webhook Payload Format

POST https://your-server.com/webhooks/Qssly
Content-Type: application/json
X-Qssly-Signature: sha256=a1b2c3d4e5f6...
X-Qssly-Idempotency-Key: booking-id:booking.confirmed:2026-07-01T14:00:00Z

{
  "event": "booking.confirmed",
  "timestamp": "2026-07-01T14:05:23Z",
  "data": {
    "id": "2f7157ca-3edf-4ef4-90c6-bb0a5ed87848",
    "resource_id": "room-uuid",
    "service_id": "service-uuid",
    "start_time": "2026-07-01T08:30:00Z",
    "end_time": "2026-07-04T05:30:00Z",
    "status": "CONFIRMED",
    "payment_status": "PAID",
    "total_amount": 7500.0,
    "amount_paid": 7500.0,
    "balance_due": 0.0,
    "location_timezone": "Asia/Kolkata",
    "customer_email": "[email protected]",
    "customer_phone": "+919876543210",
    "metadata": { "guests": 2 },
    "checkout_url": null,
    "provider_transaction_id": "pay_xyz12345"
  }
}

Events: booking.confirmed, booking.cancelled, booking.rescheduled, booking.no_show, booking.completed, booking.created

Verify Signature — Node.js

const crypto = require('crypto');
// Tip: Use a library like 'fast-json-stable-stringify' to properly sort keys recursively
const stringify = require('fast-json-stable-stringify'); 

function verifyWebhook(req, webhookSecret) {
  const signature = req.headers['x-Qssly-signature'];
  if (!signature) return false;

  const canonical = stringify(req.body);
  const expected = 'sha256=' + crypto
    .createHmac('sha256', webhookSecret)
    .update(canonical)
    .digest('hex');

  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch (e) {
    return false;
  }
}

Verify Signature — Python

import hmac, hashlib, json

def verify_Qssly_webhook(request_body: bytes, signature: str, secret: str) -> bool:
    """Verify the X-Qssly-Signature header."""
    payload = json.loads(request_body)
    canonical = json.dumps(payload, separators=(',', ':'), sort_keys=True).encode()
    expected = 'sha256=' + hmac.new(
        secret.encode(), canonical, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Usage in Python Web Framework:
@app.post("/webhooks/Qssly")
async def handle_webhook(request: Request):
    body = await request.body()
    sig = request.headers.get("X-Qssly-Signature", "")
    if not verify_Qssly_webhook(body, sig, WEBHOOK_SECRET):
        raise HTTPException(status_code=400, detail="Invalid signature")
    
    payload = json.loads(body)
    event = payload["event"]  # "booking.confirmed", etc.
    booking = payload["data"]
    # Process the event...

Retry Policy & Failures

If your webhook server returns a non-2xx response or times out (timeout limit is 10 seconds), Qssly will retry the delivery using an exponential backoff strategy:

  • Max Attempts: 5 retries (6 attempts total)
  • Backoff Interval: delay = 10 * (3 ** retry) + random(0, 5) seconds (approx. 10s, 35s, 95s, 275s, 815s)
  • Failure: If all 6 attempts fail, the webhook payload will be dropped.
  • Idempotency: Always check the X-Qssly-Idempotency-Key header to safely ignore duplicate webhook deliveries.

9. Error Handling

All errors return a JSON object with a detail field describing the issue.

StatusMeaningCommon Cause
400Bad RequestInvalid parameters, end_time before start_time, missing required fields
401UnauthorizedMissing or invalid API key / JWT token
403ForbiddenUser role doesn't have permission for this action
404Not FoundResource, booking, or service doesn't exist
409ConflictTime slot already booked (double-booking prevented)
422Validation ErrorInvalid UUID format, malformed JSON, schema mismatch
429Too Many RequestsRate limit exceeded (100 req/min per tenant; 3 OTP/min per IP)
// Error response format:
{
  "detail": "end_time must be greater than start_time"
}

// Double-booking conflict (409) format:
{
  "detail": "Resource is already booked for this slot",
  "conflicting_dates": ["2026-07-03"]
}

// Validation error (422) format:
{
  "detail": [
    {
      "loc": ["body", "customer_email"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

10. AI Coding Prompts

Copy these pre-engineered system prompts into your AI assistant (Claude, Gemini, ChatGPT) to instantly build booking integrations. Each prompt contains the full API context so the AI can write correct code on the first try.

Critical Integration Rules — paste this first

The canonical guardrails (OTP + RLS, credential isolation, state hygiene, day boundaries). Give this to your AI before any feature request.

🔧 Generic Qssly Integration Prompt

Full API reference for any booking use case. Paste this as a system prompt.

🏨 Hotel Booking App Prompt

Build a hotel room booking website with check-in/check-out, multi-night stays, and payment flow.

🩺 Doctor Appointment App Prompt

Build a clinic appointment system with time slots, buffer gaps, rescheduling, and cancellation policies.

Tip: After pasting the prompt, tell the AI what framework you're using (e.g. "Build this in Next.js with Tailwind" or "Create a React Native app"). The prompts include all the API details needed for the AI to write correct integration code.

11. Client-Side Payment Flow

For online payments (FULL_ADVANCE or PARTIAL_ADVANCE), Qssly seamlessly routes transactions to the tenant's preferred payment gateway (Razorpay or PayPal). Follow these steps to implement the client-side checkout experience.

Step 0: Branch on payment_type & checkout_url

The POST /bookings response tells you exactly what to do next. For FULL_ADVANCE and PARTIAL_ADVANCE services the status is PENDING_PAYMENT and a checkout_url is returned — redirect to it. For PAY_LATER the booking is already CONFIRMED with no checkout_url.

const booking = await createBooking(payload); // POST /bookings

switch (booking.status) {
  case 'PENDING_PAYMENT':
    // FULL_ADVANCE or PARTIAL_ADVANCE
    // Qssly automatically handles gateway routing (Razorpay vs PayPal).
    // Send the customer to the provided checkout_url to pay.
    window.location.href = booking.checkout_url;
    break;
  case 'CONFIRMED':
    // PAY_LATER — no online payment; collect in person later.
    showConfirmation(booking); // balance_due is collected offline
    break;
  default:
    showError('Unexpected booking status: ' + booking.status);
}
Gateway Specifics: The checkout_url will point to PayPal's hosted checkout or Razorpay's checkout depending on the tenant's configuration. You can just redirect to it, or if you prefer an in-page modal for Razorpay, follow the SDK instructions below.

Step 1: Get Checkout Config (Razorpay Modal Only)

If the tenant uses Razorpay and you want to use the in-page checkout modal instead of a redirect, retrieve the payment gateway configuration.

GET /api/v1/payments/checkout-config/{booking_id}
X-API-Key: qs_live_xxxx

Response (200 OK):
{
  "key_id": "rzp_live_XXXXX",
  "amount": 50000,               // Amount in the smallest currency unit (paise)
  "currency": "INR",
  "order_id": "order_XXXXXX",
  "business_name": "Your Business Name",
  "customer_email": "[email protected]",
  "customer_phone": "+919876543210"
}

Note: the booking must be in PENDING_PAYMENT state with a Razorpay order
already initialized, otherwise this returns 400.

Step 2: Initialize Razorpay SDK and Verify Payment

Open the Razorpay modal. When the customer completes the payment, send the verification payload back to Qssly.

// 1. Include Razorpay SDK: <script src="https://checkout.razorpay.com/v1/checkout.js"></script>

// 2. Fetch config from Qssly
const configResponse = await fetch(`/api/v1/payments/checkout-config/${bookingId}`, {
  headers: { 'X-API-Key': 'qs_live_your_key' }
});
const config = await configResponse.json();

// 3. Setup Razorpay options
const options = {
  key: config.key_id,
  amount: config.amount,
  currency: config.currency,
  name: config.business_name,
  order_id: config.order_id,
  prefill: {
    email: config.customer_email,
    contact: config.customer_phone,
  },
  handler: async function (response) {
    // 4. Send verification payload to Qssly
    const verifyRes = await fetch('/api/v1/payments/verify-razorpay', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': 'qs_live_your_key'
      },
      body: JSON.stringify({
        razorpay_order_id: response.razorpay_order_id,
        razorpay_payment_id: response.razorpay_payment_id,
        razorpay_signature: response.razorpay_signature
      })
    });

    if (verifyRes.ok) {
      alert('Payment successful and booking confirmed!');
    } else {
      alert('Payment verification failed.');
    }
  }
};

// 5. Open checkout
const rzp = new window.Razorpay(options);
rzp.open();