This document provides a comprehensive reference for the backend API endpoints in the Harakka App.
All API endpoints are relative to the base URL:
- Development:
http://localhost:3000 - Production:
https://booking-app-backend-duh9encbeme0awca.northeurope-01.azurewebsites.net/
All API endpoints require authentication via JWT token in the Authorization header:
Authorization: Bearer <jwt-token>
Successful responses follow this general structure:
{
"data": {
// Response data
},
"meta": {
// Metadata such as pagination
}
}Error responses follow this structure:
{
"statusCode": 400,
"message": "Error message",
"error": "Error type"
}| Header | Required | Description |
|---|---|---|
Authorization |
Yes* | Bearer token for authenticated requests |
Content-Type |
Yes | application/json for all requests |
x-user-id |
Yes* | User ID for certain endpoints |
Accept-Language |
No | Preferred language for localized responses |
*Required for authenticated endpoints
GET /Response:
{
"message": "Backend is running",
"timestamp": "2025-09-12T10:30:00Z"
}POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}Response:
{
"access_token": "jwt-token-here",
"refresh_token": "refresh-token-here",
"user": {
"id": "uuid",
"email": "user@example.com",
"role": "user"
}
}POST /auth/logout
Authorization: Bearer <token>POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "refresh-token-here"
}GET /users?page=1&limit=10&role=user&organizationId=uuidGET /users/profilePUT /users/profile
Content-Type: application/json
{
"first_name": "John",
"last_name": "Doe",
"phone": "+1234567890"
}GET /users/:idPATCH /users/:id/role
Content-Type: application/json
{
"roleId": "uuid",
"organizationId": "uuid"
}GET /rolesResponse:
{
"data": [
{
"id": "uuid",
"name": "super_admin",
"description": "Full system access"
},
{
"id": "uuid",
"name": "tenant_admin",
"description": "Organization admin"
}
]
}GET /organizations?page=1&limit=10GET /organizations/:idPOST /organizations
Content-Type: application/json
{
"name": "New Organization",
"description": "Organization description",
"website": "https://example.com",
"address": "123 Main St"
}PUT /organizations/:id
Content-Type: application/json
{
"name": "Updated Name"
}GET /items?page=1&limit=10&search=tent&categoryId=uuid&tagIds=uuid1,uuid2&locationIds=uuid3&orgIds=uuid4&isActive=trueQuery Parameters:
page: Page number (default: 1)limit: Items per page (default: 10)search: Search termcategoryId: Filter by categorytagIds: Filter by tags (comma-separated)locationIds: Filter by locations (comma-separated)orgIds: Filter by organizations (comma-separated)isActive: Filter active/inactive items
Response:
{
"data": [
{
"id": "uuid",
"name": "Camping Tent",
"description": "4-person tent",
"quantity": 5,
"available_quantity": 3,
"category_id": "uuid",
"organization_id": "uuid",
"location_id": "uuid",
"is_active": true,
"created_at": "2025-09-12T10:00:00Z",
"images": ["image-url-1", "image-url-2"],
"tags": ["outdoor", "camping"]
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 50,
"totalPages": 5
}
}GET /items/:idPOST /items
Content-Type: application/json
{
"name": "Camping Tent",
"description": "4-person tent",
"quantity": 5,
"category_id": "uuid",
"organization_id": "uuid",
"location_id": "uuid",
"tagIds": ["uuid1", "uuid2"]
}PUT /items/:id
Content-Type: application/json
{
"name": "Updated Tent Name",
"quantity": 8
}DELETE /items/:idGET /items/:id/availability?startDate=2025-09-20&endDate=2025-09-25Response:
{
"data": {
"item_id": "uuid",
"alreadyBookedQuantity": 2,
"availableQuantity": 3
}
}POST /items/bulk-create
Content-Type: multipart/form-data
{
"file": <excel-file>,
"organizationId": "uuid"
}POST /item-images/upload
Content-Type: multipart/form-data
{
"file": <image-file>,
"itemId": "uuid"
}Response:
{
"data": {
"id": "uuid",
"url": "https://storage-url/image.jpg",
"thumbnail_url": "https://storage-url/thumb.jpg"
}
}GET /item-images/:itemIdDELETE /item-images/:imageIdGET /categories?lang=enPOST /categories
Content-Type: application/json
{
"translations": {
"en": "Electronics",
"de": "Elektronik"
},
"sort_order": 1
}GET /tags
Returns a list of all tags.
Response 200
[
{
"id": "uuid",
"translations": {
"en": { "name": "Armor" },
"fi": { "name": "Haarniska" }
},
"created_at": "2023-01-01T00:00:00Z"
}
// ...
]POST /tags
Content-Type: application/json
{
"name": "outdoor",
"color": "#0d00ffff"
}GET /bookings?page=1&limit=10&status=pending&organizationId=uuid&userId=uuidQuery Parameters:
status: Filter by status (pending, confirmed, rejected, cancelled, completed)organizationId: Filter by organizationuserId: Filter by userstartDate: Filter bookings starting after dateendDate: Filter bookings ending before date
GET /bookings/:idPOST /bookings
Content-Type: application/json
{
"start_date": "2025-09-20",
"end_date": "2025-09-25",
"pickup_method": "pickup",
"notes": "Special instructions",
"booking_items": [
{
"storage_item_id": "uuid",
"quantity": 2
}
]
}PATCH /bookings/:id/status
Content-Type: application/json
{
"status": "confirmed",
"admin_notes": "Approved for pickup"
}DELETE /bookings/:id
Content-Type: application/json
{
"reason": "User cancelled"
}GET /booking-items?bookingId=uuidPATCH /booking-items/:id/status
Content-Type: application/json
{
"status": "picked_up"
}GET /storage-locations?organizationId=uuidPOST /storage-locations
Content-Type: application/json
{
"name": "Warehouse A",
"address": "123 Storage St",
"organization_id": "uuid"
}GET /mail/preview/booking-creation
GET /mail/preview/booking-confirmation
GET /mail/preview/welcomeGET /logs?page=1&limit=50&level=error&startDate=2025-09-01&endDate=2025-09-12POST /user-banning/ban
Content-Type: application/json
{
"userId": "uuid",
"reason": "Violation of terms",
"duration": 30 // days, omit for permanent
}POST /user-banning/unban
Content-Type: application/json
{
"userId": "uuid"
}All endpoints may return these error formats:
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request"
}{
"statusCode": 401,
"message": "Unauthorized",
"error": "Unauthorized"
}{
"statusCode": 403,
"message": "Insufficient permissions",
"error": "Forbidden"
}{
"statusCode": 404,
"message": "Resource not found",
"error": "Not Found"
}{
"statusCode": 500,
"message": "Internal server error",
"error": "Internal Server Error"
}{
"error": null,
"data": {}, // any data requested from backend or manually inserted
"count": null,
"status": 200,
"statusText": "OK"
}{
"error": {
"code": "22P02",
"details": null,
"hint": null,
"message": "invalid input syntax for type uuid: \"4dfd509e-a02d-493d-3a-e49dfddc6445\""
},
"data": null,
"count": null,
"status": 400,
"statusText": "Bad Request"
}The agreed-upon response types looks like the following:
export type ApiResponse<T> = PostgrestResponse<T> & {
metadata?: any;
};
export type ApiSingleResponse<T> = PostgrestSingleResponse<T> & {
metadata?: any;
};-
ApiResponse Returns an object with the same properties as a PostgrestResponse as well as an optional metadata. Will always return an array of the type, no need to explicitly say that the expected data will be an array.
-
ApiSingleResponse This type should be used the exact same way as the ApiResponse. Will always return a singular entity of the expected type, no arrays.
async getBookingItems(
supabase: SupabaseClient,
booking_id: string,
offset: number = 0,
limit: number = 20,
): Promise<ApiResponse<BookingItemsRow>> {
const result: PostgrestResponse<BookingItemsRow> = await supabase
.from("order_items")
.select("*")
.eq("order_id", booking_id)
.range(offset, offset + limit);
/**
* Example version of error-handling. If response is successful, result.error will be null
* */
if (result.error) {
throw new Error(result.error)
}
return result;
}- All timestamps are in ISO 8601 format (UTC)
- UUIDs are used for all entity IDs
- Pagination follows the format:
?page=1&limit=10 - File uploads support JPG, PNG, WebP formats (max 5MB)
- Role-based access control applies to all endpoints
- Rate limiting may apply to certain endpoints