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
- Sign up — Register your business at the Qssly Portal to create your tenant account.
- Create a Location — Add your branch with its IANA timezone (e.g. "Asia/Kolkata").
- Add Resources — Create rooms, doctors, courts, or whatever is being booked.
- Define Services — Set up rate packages with pricing, duration, and payment rules.
- Bind Resources → Services — Link which services each resource offers.
- Set Availability Rules — Define weekly operating hours for each resource.
- Generate an API Key — Go to Business Portal → Settings → Generate API Key. Copy the
qs_live_...key. - Start Building — Use the endpoints below to query availability and create bookings.
Data Model
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
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"
}POST /auth/login (email and password) or by verifying a one-time passcode via POST /auth/otp/verify (passwordless OTP).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)
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/servicesfails, 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_timeequal the service's duration exactly, and send both as ISO-8601 UTC strings (e.g.2026-07-01T14:00:00Z).
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| location_id | UUID | No | Filter by branch |
| page | int | No | Page number (default: 1) |
| page_size | int | No | Items 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. 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
}
]
}FULL_ADVANCE— Customer pays full amount online before confirmationPARTIAL_ADVANCE— Customer pays a deposit (advance_payment_amount); rest is collected laterPAY_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).
| Parameter | Type | Required | Description |
|---|---|---|---|
| resource_id | UUID | Yes | The resource to check |
| service_id | UUID | Yes | The service (determines slot duration & buffer) |
| date | YYYY-MM-DD | * | Single date query |
| start_date | YYYY-MM-DD | * | Range start (use with end_date) |
| end_date | YYYY-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.
| Field | Type | Required | Description |
|---|---|---|---|
| resource_id | UUID | Yes | The room/doctor/entity to book |
| service_id | UUID | Yes | The rate package to apply |
| start_time | ISO 8601 | Yes | Booking start (UTC) |
| end_time | ISO 8601 | Yes | Booking end (UTC). Must be after start_time |
| customer_email | string | Yes | Customer email address |
| customer_phone | string | No | Customer phone number |
| booking_metadata | object | No | Custom JSON data (e.g. guest count, notes) |
| is_manual_override | boolean | No | Bypass 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
| Method | Path | Body / Params | Description |
|---|---|---|---|
| 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-show | — | Mark 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}/payments | — | List 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.
| Method | Path | Body | Description |
|---|---|---|---|
| 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 |
5. Availability & Scheduling
Understanding how Qssly computes available slots is key to building a great booking UI.
How Slot Generation Works
- Availability Rules define when a resource is available (e.g. "Monday 09:00–17:00").
- The system divides those windows into slots based on the service's
duration_minutes+buffer_minutes. - Existing bookings are subtracted — any slot that overlaps a confirmed booking is removed.
- 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.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.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.createdVerify 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-Keyheader to safely ignore duplicate webhook deliveries.
9. Error Handling
All errors return a JSON object with a detail field describing the issue.
| Status | Meaning | Common Cause |
|---|---|---|
| 400 | Bad Request | Invalid parameters, end_time before start_time, missing required fields |
| 401 | Unauthorized | Missing or invalid API key / JWT token |
| 403 | Forbidden | User role doesn't have permission for this action |
| 404 | Not Found | Resource, booking, or service doesn't exist |
| 409 | Conflict | Time slot already booked (double-booking prevented) |
| 422 | Validation Error | Invalid UUID format, malformed JSON, schema mismatch |
| 429 | Too Many Requests | Rate 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.
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);
}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();