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.

Header
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
Error Response Example
{
  "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.

Headers
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.

Example
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.

GET

Search Trips

Search available trips for the current tenant and date/route filters.

Endpoint
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

200 OK
{
  "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

Get Availability

Check seat availability for a specific route and date.

Endpoint
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

200 OK
{
  "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
POST

Create Booking

Create a new booking for one or more passengers. A hold will be placed on the inventory until payment is completed.

Endpoint
POST /v1/booking/orders

Headers

Authorization: Bearer qail_live_sk_...
Idempotency-Key: order:tenant_12:client_ref_8821
Content-Type: application/json

Request Body

application/json
{
  "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

201 Created
{
  "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

Get Booking

Retrieve details of an existing booking by ID or reference number.

Endpoint
GET /v1/orders/{order_id}

Response

200 OK
{
  "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
POST

Cancel Booking

Create a cancellation request for an existing order.

Endpoint
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

200 OK
{
  "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
POST

Issue Ticket

Issue e-tickets for a confirmed booking. Tickets are delivered via WhatsApp and email automatically.

Endpoint
POST /v1/bookings/{booking_id}/tickets

Headers

Authorization: Bearer qail_live_sk_...
Idempotency-Key: ticket:8762c9cd-3cf4-48de-b7e4-7ea96c224e47

Response

200 OK
{
  "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
GET

Verify Ticket

Verify a ticket by its number or QR code content. Used for boarding validation.

Endpoint
GET /v1/tickets/{ticket_number}/verify

Response

200 OK
{
  "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
POST

Check-in Passenger

Mark a passenger as checked-in for boarding. Used by crew on the captain dashboard.

Endpoint
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

200 OK
{
  "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
POST

Send WhatsApp Message

Send a freeform WhatsApp message to a customer. Use templates for better deliverability.

Endpoint
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

200 OK
{
  "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.

POST

Send Template Message

Send a pre-approved WhatsApp template message. Required for initiating conversations.

Endpoint
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

200 OK
{
  "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

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

booking.confirmed
{
  "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.