API Reference
The Qail API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes.
Versioned paths. Endpoints below use versioned route paths so you can use the same contract across environments.
Authentication
The Qail API uses API keys to authenticate requests. You can view and manage your API keys in the Dashboard.
Authentication is performed via the Authorization header using
Bearer token format.
Authorization: Bearer qail_live_sk_a1b2c3d4e5f6g7h8i9j0
Keep your API keys secure! Do not share your secret API keys in publicly accessible areas such as GitHub or client-side code.
Base Path
Use this base path on your configured gateway host:
/v1
Common route groups:
| Module | Path Prefix |
|---|---|
| Search | /v1/search |
| Booking | /v1/booking |
| Orders | /v1/orders |
| Payments | /v1/xendit, /v1/doku |
| Provider webhooks | /v1/webhooks/xendit, /v1/webhooks/doku |
Error Handling
Qail uses conventional HTTP response codes to indicate the success or failure of an API request.
| Code | Description |
|---|---|
200 |
OK - Request succeeded |
201 |
Created - Resource created successfully |
400 |
Bad Request - Invalid parameters |
401 |
Unauthorized - Invalid or missing API key |
404 |
Not Found - Resource doesn't exist |
409 |
Conflict - Resource already exists or booking conflict |
500 |
Server Error - Something went wrong on our end |
{
"success": false,
"error": {
"code": "BOOKING_CONFLICT",
"message": "Selected departure is fully booked",
"details": {
"departure_id": "dep_7x8y9z",
"available_seats": 0,
"requested_seats": 2
}
}
}
Request Conventions
Use these conventions consistently for stable integrations across environments.
Idempotency for mutation endpoints
For create or charge-like operations, send an Idempotency-Key header to prevent duplicate execution during client retries.
Authorization: Bearer qail_live_sk_...
Idempotency-Key: order:tenant_12:ref_8762c9cd
Pagination defaults
Collection endpoints support page and per_page. Keep per_page conservative to reduce API and database pressure.
GET /v1/orders?page=1&per_page=20
Tenant context and security
Tenant scope is derived from credentials. Do not send tenant identity from public clients as a trust signal. For provider callbacks, always validate webhook signatures before processing.
Webhook safety. Verify provider signature headers, reject stale timestamps, and keep handler logic idempotent.
Search Trips
Search available trips for the current tenant and date/route filters.
GET /v1/search
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
origin |
string | Optional | Filter by origin port code |
destination |
string | Optional | Filter by destination port code |
date |
string | Optional | Filter by departure date in YYYY-MM-DD |
page |
integer | Optional | Pagination index (default: 1) |
per_page |
integer | Optional | Items per page (default: 20, max: 100) |
Response
{
"success": true,
"data": [
{
"id": "route_sanur_penida",
"origin": { "code": "SNR", "name": "Sanur Harbor" },
"destination": { "code": "NPN", "name": "Nusa Penida (Buyuk)" },
"duration_minutes": 45,
"vessels": ["vessel_fastcat_1", "vessel_fastcat_2"],
"active": true
}
],
"meta": { "total": 2, "page": 1, "per_page": 20 }
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
400 |
INVALID_FILTER |
Invalid date format or unsupported filter value |
401 |
UNAUTHORIZED |
Missing or invalid bearer token |
429 |
RATE_LIMITED |
Too many requests within the active limit window |
Get Availability
Check seat availability for a specific route and date.
GET /v1/search/availability
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
route_id |
string | Required | Route identifier |
date |
string | Required | Date in YYYY-MM-DD format |
passengers |
integer | Optional | Number of passengers (default: 1) |
Response
{
"success": true,
"data": {
"route_id": "route_sanur_penida",
"date": "2026-02-15",
"departures": [
{
"id": "dep_abc123",
"departure_time": "08:30",
"vessel": { "name": "Fast Cat I", "capacity": 50 },
"availability": { "total_seats": 50, "available": 18, "status": "available" },
"pricing": { "adult": 150000, "child": 100000, "currency": "IDR" }
}
]
}
}
Inventory timing. Availability can change quickly due to payment holds. Re-check availability before creating a booking.
Common Errors
| Status | Code | When it happens |
|---|---|---|
400 |
INVALID_DATE |
Date format is invalid or outside bookable range |
404 |
ROUTE_NOT_FOUND |
Route does not exist for current tenant scope |
409 |
NO_CAPACITY |
Requested passenger count exceeds available seats |
Create Booking
Create a new booking for one or more passengers. A hold will be placed on the inventory until payment is completed.
POST /v1/booking/orders
Headers
Authorization: Bearer qail_live_sk_...
Idempotency-Key: order:tenant_12:client_ref_8821
Content-Type: application/json
Request Body
{
"departure_id": "dep_abc123",
"contact": {
"name": "John Smith",
"email": "john.smith@example.com",
"phone": "+6281234567890"
},
"passengers": [
{ "type": "adult", "name": "John Smith", "id_number": "AB1234567" },
{ "type": "child", "name": "Tommy Smith", "date_of_birth": "2018-05-12" }
]
}
Validation Notes
| Field | Constraint | Reason |
|---|---|---|
departure_id |
Must be active and bookable | Prevents booking closed departures |
passengers[] |
At least 1 passenger, max per vessel policy | Enforces capacity and policy limits |
contact.phone |
E.164 recommended | Improves WhatsApp and notification deliverability |
Response
{
"success": true,
"data": {
"id": "8762c9cd-3cf4-48de-b7e4-7ea96c224e47",
"reference": "QAIL-8821",
"status": "pending_payment",
"pricing": { "subtotal": 400000, "fees": 15000, "total": 415000, "currency": "IDR" },
"payment": { "status": "pending", "payment_url": "/payment/checkout?ref=QAIL-8821" }
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
409 |
BOOKING_CONFLICT |
Capacity changed between availability check and booking request |
422 |
VALIDATION_FAILED |
Missing required passenger/contact fields |
500 |
PAYMENT_SESSION_ERROR |
Order created but payment session initialization failed |
Idempotency behavior. Repeating the same request with the same
Idempotency-Key returns the original booking response
instead of creating a duplicate order.
Get Booking
Retrieve details of an existing booking by ID or reference number.
GET /v1/orders/{order_id}
Response
{
"success": true,
"data": {
"id": "8762c9cd-3cf4-48de-b7e4-7ea96c224e47",
"reference": "QAIL-8821",
"status": "paid",
"departure": { "id": "dep_abc123", "date": "2026-02-15", "time": "08:30" },
"passengers_count": 2,
"payment": { "status": "paid", "paid_at": "2026-02-14T09:55:42Z" }
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
404 |
ORDER_NOT_FOUND |
Order ID is unknown or outside tenant scope |
401 |
UNAUTHORIZED |
Token missing/invalid or expired |
Cancel Booking
Create a cancellation request for an existing order.
POST /v1/order-requests/cancellation
Headers
Authorization: Bearer qail_live_sk_...
Idempotency-Key: cancel:8762c9cd-3cf4-48de-b7e4-7ea96c224e47
Content-Type: application/json
Request Body
{ "order_id": "8762c9cd-3cf4-48de-b7e4-7ea96c224e47", "reason": "customer_request", "notes": "Customer changed travel plans" }
Response
{
"success": true,
"data": {
"request_id": "cancel_req_72f8",
"order_id": "8762c9cd-3cf4-48de-b7e4-7ea96c224e47",
"status": "submitted"
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
404 |
ORDER_NOT_FOUND |
Order ID is invalid or inaccessible in tenant scope |
409 |
CANCELLATION_NOT_ALLOWED |
Order state cannot be cancelled under active policy |
422 |
INVALID_REASON |
Reason value is unsupported by policy |
Issue Ticket
Issue e-tickets for a confirmed booking. Tickets are delivered via WhatsApp and email automatically.
POST /v1/bookings/{booking_id}/tickets
Headers
Authorization: Bearer qail_live_sk_...
Idempotency-Key: ticket:8762c9cd-3cf4-48de-b7e4-7ea96c224e47
Response
{
"success": true,
"data": {
"booking_id": "8762c9cd-3cf4-48de-b7e4-7ea96c224e47",
"tickets_issued": 2,
"delivery": {
"email": "queued",
"whatsapp": "queued"
}
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
409 |
ORDER_NOT_PAID |
Ticket issue attempted before payment confirmation |
409 |
TICKETS_ALREADY_ISSUED |
Tickets are already generated for this booking |
500 |
TICKET_GENERATION_FAILED |
PDF generation or dispatch pipeline failed |
Verify Ticket
Verify a ticket by its number or QR code content. Used for boarding validation.
GET /v1/tickets/{ticket_number}/verify
Response
{
"success": true,
"data": {
"ticket_number": "QAIL-TKT-000912",
"status": "valid",
"passenger": { "name": "John Smith" },
"departure": { "id": "dep_abc123", "date": "2026-02-15", "time": "08:30" },
"check_in": { "is_checked_in": false }
}
}
Status Values
| Status | Meaning |
|---|---|
valid |
Ticket is eligible for boarding check-in |
checked_in |
Ticket already used for boarding |
invalid |
Ticket format or signature is not valid |
Check-in Passenger
Mark a passenger as checked-in for boarding. Used by crew on the captain dashboard.
POST /v1/tickets/{ticket_number}/check-in
Headers
Authorization: Bearer qail_live_sk_...
Idempotency-Key: checkin:QAIL-TKT-000912
Request Body
{
"device_id": "captain_device_03",
"location": { "lat": -8.6692, "lng": 115.2568 },
"notes": "Boarded at Pier 3"
}
Response
{
"success": true,
"data": {
"ticket_number": "QAIL-TKT-000912",
"status": "checked_in",
"checked_in_at": "2026-02-15T01:21:12Z"
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
404 |
TICKET_NOT_FOUND |
Ticket number is invalid or inaccessible in tenant scope |
409 |
ALREADY_CHECKED_IN |
Ticket was already checked in previously |
409 |
DEPARTURE_CLOSED |
Check-in attempted outside allowed boarding window |
Send WhatsApp Message
Send a freeform WhatsApp message to a customer. Use templates for better deliverability.
POST /v1/whatsapp/messages
Headers
Authorization: Bearer qail_live_sk_...
Idempotency-Key: wa:msg:order_8821:1
Content-Type: application/json
Request Body
{
"to": "+6281234567890",
"type": "text",
"text": { "body": "Hi John! Your departure has been rescheduled to 09:00." }
}
Response
{
"success": true,
"data": {
"message_id": "wamid.HBgL...",
"status": "queued"
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
400 |
INVALID_PAYLOAD |
Unsupported message type or invalid body format |
422 |
INVALID_RECIPIENT |
Destination number format is invalid |
502 |
PROVIDER_REJECTED |
Upstream WhatsApp provider rejected request |
Conversation policy. Use templates to initiate conversations outside the customer service window.
Send Template Message
Send a pre-approved WhatsApp template message. Required for initiating conversations.
POST /v1/whatsapp/templates
Request Body
{
"to": "+6281234567890",
"template": {
"name": "booking_confirmation_v1",
"language": "id",
"components": [
{ "type": "body", "parameters": ["QAIL-8821", "15 Feb 2026", "08:30"] }
]
}
}
Response
{
"success": true,
"data": {
"message_id": "wamid.HBgM...",
"template_name": "booking_confirmation_v1",
"status": "queued"
}
}
Common Errors
| Status | Code | When it happens |
|---|---|---|
404 |
TEMPLATE_NOT_FOUND |
Template name/language pair is unknown |
422 |
TEMPLATE_PARAM_MISMATCH |
Provided component parameters do not match template schema |
502 |
PROVIDER_REJECTED |
Provider rejected due to template policy state |
Webhook Events
Configure webhooks to receive real-time notifications about booking and message events.
Delivery Semantics
| Property | Behavior |
|---|---|
| Delivery model | At-least-once; duplicate deliveries are possible |
| Retry policy | Automatic retry with backoff on non-2xx responses |
| Idempotency key | Use event id as dedupe key in your consumer |
| Timeout expectation | Return 200 quickly and process async in your worker |
Webhook Headers
X-Qail-Signature: sha256=...
X-Qail-Timestamp: 2026-03-10T12:31:45Z
Content-Type: application/json
Available Events
| Event | Description |
|---|---|
booking.created |
New booking created (pending payment) |
booking.confirmed |
Payment successful, booking confirmed |
booking.cancelled |
Booking was cancelled |
ticket.issued |
E-tickets were generated and sent |
passenger.checked_in |
Passenger boarded the vessel |
message.received |
Incoming WhatsApp message from customer |
message.status |
Message delivery status update |
Webhook Payload Example
{
"id": "evt_abc123xyz",
"type": "booking.confirmed",
"data": {
"booking": {
"id": "book_xyz789abc",
"reference": "QAIL-8821",
"status": "confirmed",
"passengers_count": 3,
"total_amount": 415000,
"currency": "IDR"
}
}
}
Security: Verify webhook signatures using the X-Qail-Signature header to ensure requests are from Qail.
Receiver pattern: verify signature and timestamp, store event id, enqueue job, return 200.