Version: 1.0
Base URL: https://yourdomain.com/api/v1
Format: All requests and responses use JSON.
All API endpoints are prefixed with: https://yourdomain.com/api/v1
- All endpoints (except
GET /health) require authentication. - All requests must include the header: Content-Type: application/json Accept: application/json
Verify the API is reachable before integrating:
GET /api/v1/healthResponse 200 OK:
{ "status": "ok" }The API uses token-based authentication via Devise. Tokens are issued on login and must be sent with every subsequent request.
POST /api/v1/auth/signupRequest body:
{
"user": {
"email": "dev@example.com",
"password": "securepassword",
"password_confirmation": "securepassword"
}
}Response 201 Created:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"user": {
"id": 1,
"email": "dev@example.com"
}
}POST /api/v1/auth/loginRequest body:
{
"user": {
"email": "dev@example.com",
"password": "securepassword"
}
}Response 200 OK:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"user": {
"id": 1,
"email": "dev@example.com"
}
}DELETE /api/v1/auth/logout
Authorization: Bearer <token>Response 200 OK:
{ "message": "Logged out successfully." }Include the token in the Authorization header on every authenticated request:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...Example with curl:
curl -X GET https://yourdomain.com/api/v1/agencies \
-H "Authorization: Bearer <your_token>" \
-H "Accept: application/json"Note: Agency endpoints are admin-only. Non-admin requests will receive
403 Forbidden.
GET /api/v1/agencies
Authorization: Bearer <token>Response 200 OK:
{
"id": 1,
"name": "Acme Travel",
"email": "admin@acme.com"
}PATCH /api/v1/agencies
Authorization: Bearer <token>
Content-Type: application/jsonRequest body:
{
"agency": {
"name": "Acme Travel Co.",
"email": "newadmin@acme.com"
}
}Response 200 OK:
{
"id": 1,
"name": "Acme Travel Co.",
"email": "newadmin@acme.com"
}Suppliers are the top level of the Suppliers → Services → Rates hierarchy.
GET /api/v1/suppliers
Authorization: Bearer <token>POST /api/v1/suppliers
Authorization: Bearer <token>Request body:
{
"supplier": {
"name": "Safari Camps Ltd.",
"contact_email": "info@safaricamps.com"
}
}GET /api/v1/suppliers/:id
Authorization: Bearer <token>PATCH /api/v1/suppliers/:id
Authorization: Bearer <token>DELETE /api/v1/suppliers/:id
Authorization: Bearer <token>Services are scoped to a parent supplier.
GET /api/v1/suppliers/:supplier_id/services
Authorization: Bearer <token>POST /api/v1/suppliers/:supplier_id/services
Authorization: Bearer <token>Request body:
{
"service": {
"name": "Game Drive",
"description": "Full-day game drive with guide"
}
}GET /api/v1/suppliers/:supplier_id/services/:id
PATCH /api/v1/suppliers/:supplier_id/services/:id
DELETE /api/v1/suppliers/:supplier_id/services/:idRates are scoped to a specific service under a supplier.
GET /api/v1/suppliers/:supplier_id/services/:service_id/rates
Authorization: Bearer <token>POST /api/v1/suppliers/:supplier_id/services/:service_id/rates
Authorization: Bearer <token>Request body:
{
"rate": {
"amount": 150.00,
"currency": "USD",
"valid_from": "2026-01-01",
"valid_to": "2026-12-31"
}
}GET /api/v1/suppliers/:supplier_id/services/:service_id/rates/:id
PATCH /api/v1/suppliers/:supplier_id/services/:service_id/rates/:id
DELETE /api/v1/suppliers/:supplier_id/services/:service_id/rates/:idGET /api/v1/quotations
Authorization: Bearer <token>POST /api/v1/quotations
Authorization: Bearer <token>Request body:
{
"quotation": {
"title": "Kenya Safari 7 Days",
"client_name": "John Doe",
"start_date": "2026-07-01"
}
}Triggers server-side repricing of all items in the quotation:
POST /api/v1/quotations/:id/recalculate
Authorization: Bearer <token>Response 200 OK:
{
"id": 42,
"total": 3200.00,
"status": "recalculated"
}Days represent individual itinerary days within a quotation.
GET /api/v1/quotations/:quotation_id/quotation_days
Authorization: Bearer <token>POST /api/v1/quotations/:quotation_id/quotation_days
Authorization: Bearer <token>Request body:
{
"quotation_day": {
"date": "2026-07-01",
"label": "Arrival & Transfer"
}
}Items are individual services or line entries within a quotation day.
GET /api/v1/quotations/:quotation_id/quotation_days/:quotation_day_id/quotation_items
Authorization: Bearer <token>POST /api/v1/quotations/:quotation_id/quotation_days/:quotation_day_id/quotation_items
Authorization: Bearer <token>Request body:
{
"quotation_item": {
"service_id": 5,
"rate_id": 12,
"quantity": 2,
"notes": "Airport pickup for 2 pax"
}
}All errors return a JSON body with a consistent structure. Use the HTTP status code as the primary signal and the body for debugging detail.
Malformed request syntax or missing required parameters.
{ "error": "param is missing or the value is empty: quotation" }Missing or invalid authentication token.
{ "error": "You need to sign in or sign up before continuing." }Authenticated but not permitted for this action.
{ "error": "You are not authorized to perform this action" }The requested resource does not exist (or does not belong to your agency).
{ "error": "Couldn't find Supplier with 'id'=999" }Validation failed. The errors array contains human-readable messages per field.
{
"errors": [
"Name can't be blank",
"Amount must be greater than 0"
]
}An unexpected server-side failure. Retry with exponential backoff; if the issue persists, contact support with the request ID if provided.
| Symptom | Likely cause | Fix |
|---|---|---|
401 on every request |
Token missing or expired | Re-authenticate via POST /auth/login |
403 on agency endpoints |
User is not an admin | Use an admin account |
404 on nested routes |
Wrong parent ID | Verify :supplier_id / :quotation_id belongs to your agency |
422 on create/update |
Validation failure | Read errors array and correct the payload |
500 intermittently |
Transient server error | Retry with backoff (see Best Practices) |
- Store tokens in memory or a secure store — never in
localStoragein browser environments. - Refresh tokens proactively: detect
401responses and re-authenticate before retrying the original request. - Log out explicitly when a session ends to invalidate the server-side token.
- Always send
Content-Type: application/jsonandAccept: application/json. - Wrap request bodies in the resource key (e.g.,
{ "supplier": { ... } }) to match Rails strong-parameters conventions. - Validate nested IDs before building deep URLs (
/suppliers/:supplier_id/services/:service_id/rates).
The API does not currently document pagination headers — default to requesting resources and implementing client-side limits. Monitor response sizes and coordinate with the backend team as data volumes grow.
GET,PATCH, andDELETEare safe to retry on network failure.POSTis not idempotent. Guard against duplicate submissions (e.g., disable the submit button after the first click, or check for an existing record before creating).
For 5xx errors or network timeouts, use truncated exponential backoff:
delay = min(base * 2^attempt + jitter, max_delay)
Example values: base = 500ms, max_delay = 10s, max_attempts = 4.
Do not retry 4xx errors — they indicate client-side issues that a retry will not resolve.
All data is scoped to the agency of the authenticated user. You will never see or be able to modify another agency's resources; a missing record returns 404, not a data leak.
The current API version is v1, reflected in the base path /api/v1/. Future breaking changes will be introduced under /api/v2/ with an advance deprecation notice. Pin your integrations to a specific version path to avoid unexpected breakage.