Mahjoz Open API Documentation

Base URL: https://api.mahjoz.io/api/open/v1


Table of Contents


Getting Started

Authentication

The Open API uses OAuth2 Client Credentials flow.

Step 1: Obtain Credentials

Create an API client from your Mahjoz dashboard: Settings > API Integration

After creating a client, you will receive a client_id and client_secret. Save these immediately — the secret is only shown once.

Step 2: Get Access Token

POST /oauth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "<client_id>",
  "client_secret": "<client_secret>",
  "scope": ""
}

Response:

{
  "token_type": "Bearer",
  "expires_in": 31536000,
  "access_token": "eyJ0eXAiOiJKV1Q..."
}

Step 3: Use Token

Include the token in all requests:

Authorization: Bearer <access_token>

Rate Limiting

Requests are rate-limited per client. Default: 60 requests/minute. When exceeded, the API returns 429 Too Many Requests with a Retry-After header.


Scopes

Scope Description
orders:read Read orders
orders:write Create orders and check availability
customers:read Read customers
customers:write Create customers
items:read Read items (services and products)
staff:read Read staff (providers) and their services
branches:read Read branches
categories:read Read categories
payment_methods:read Read payment methods
payment_methods:write Create payment methods
transactions:read Read transactions
transactions:write Record order payments
* All scopes

Pagination

List endpoints return paginated results.

Parameter Default Max Description
per_page 25 100 Items per page
page 1 - Page number

Response format:

{
  "data": [...],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "last_page": 5, "per_page": 25, "total": 120 }
}

Error Handling

All errors follow a consistent format:

{
  "error": "error_code",
  "message": "Human-readable description"
}
HTTP Code Error Code Description
401 authentication_failed Missing/invalid/expired token
403 insufficient_scope Token lacks required scope
403 feature_disabled Open API not available on current plan
422 validation_error Invalid request data
422 timeslot_unavailable Requested time slot is not available
429 - Rate limit exceeded
502 availability_check_failed Availability could not be checked

Orders

List Orders

GET /orders

Scope: orders:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)
from date Filter orders from date (YYYY-MM-DD)
to date Filter orders to date (YYYY-MM-DD)
status string Filter by status

Response:

{
  "data": [
    {
      "id": "uuid",
      "order_number": "ORD-001",
      "status": "confirmed",
      "source": "api",
      "customer": { "id": "uuid", "first_name": "...", "last_name": "...", "phone": "..." },
      "branch": { "id": "uuid", "name": "Main Branch" },
      "items": [
        {
          "id": "uuid",
          "name": "Haircut",
          "type": "service",
          "price": 100.00,
          "quantity": 1,
          "discount_amount": 0,
          "total": 100.00
        }
      ],
      "currency": "SAR",
      "total": 150.00,
      "discount_amount": 0,
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Get Order

GET /orders/{uuid}

Scope: orders:read

Returns order with items, transactions, and full customer/branch details. Each item includes staff_id.


Create Order

POST /orders

Scope: orders:write

Note: If the order includes service items, the API validates the requested time slot against the provider's availability before creating the order. If the slot is unavailable, a 422 error is returned.

Base request (unpaid order):

{
  "team_id": "branch-uuid",
  "customer_id": "customer-uuid",
  "start": "2026-02-25T10:00:00",
  "items": [
    {
      "id": "service-uuid",
      "type": "service",
      "staff_id": "staff-uuid",
      "quantity": 1,
      "unit_amount": 100.00,
      "discount_amount": 0
    },
    {
      "id": "product-uuid",
      "type": "product",
      "quantity": 2,
      "unit_amount": 25.00
    }
  ],
  "note": "Optional note"
}

Base request fields:

Field Type Required Description
team_id uuid Yes Branch UUID
customer_id uuid Yes Customer UUID
start datetime Yes (if items contain a service) ISO 8601 appointment start time
items array Yes At least 1 item
items.*.id uuid Yes Service/Product/Package UUID
items.*.type string Yes service, product, or package
items.*.quantity integer Yes Minimum 1
items.*.staff_id uuid No Staff member UUID
items.*.unit_amount number No Override unit price
items.*.discount_amount number No Discount per item
note string No Order note
payment object No See Inline Payment below

Inline Payment

Add a payment object to create a confirmed transaction in the same request — the order is marked paid atomically. Requires scope transactions:write.

The payment method is picked in this order (first match wins):

  1. payment.payment_method_id (UUID) — explicit.
  2. payment.payment_method_type (slug) — e.g. cash, card, bank_transfer, other, third-party, voucher.
  3. Otherwise, if your client has a linked integration app, its payment method is used.

Payment fields:

Field Type Required Description
payment.amount number Yes (if payment present) Must not exceed order total
payment.payment_method_id uuid No Tenant payment method UUID (discover via GET /payment-methods)
payment.payment_method_type string No Method type slug — resolved against the tenant's active methods
payment.reference_number string No External transaction reference
payment.date datetime No Payment timestamp (defaults to now)
payment.note string No Free-form note attached to the transaction

Example A — cash payment by type:

{ "team_id": "...", "customer_id": "...", "items": [...],
  "payment": { "amount": 150.00, "payment_method_type": "cash" } }

Example B — explicit method UUID:

{ "team_id": "...", "customer_id": "...", "items": [...],
  "payment": { "amount": 150.00, "payment_method_id": "f3a1...-uuid" } }

Payment-related errors:

HTTP Error Condition
422 payment_method_not_found UUID is not a method of this tenant, or type slug has no active method
422 payment_method_type_ambiguous Tenant has multiple active methods of that type — use payment_method_id instead
422 payment_method_required payment.amount was given but no method could be resolved
422 payment_method_unavailable Linked integration app was uninstalled by the tenant

If the transaction fails validation, the order is also rolled back — you can safely retry.

Response (201) with inline payment:

{
  "data": {
    "id": "uuid",
    "order_number": "ORD-042",
    "status": "confirmed",
    "payment_status": "paid",
    "currency": "SAR",
    "total": 150.00,
    "created_at": "2026-02-25T10:30:00.000000Z",
    "transaction": {
      "id": "txn-uuid",
      "amount": "150.00",
      "currency": "SAR",
      "status": "confirmed",
      "reference_number": null,
      "date": "2026-02-25T10:30:00+00:00"
    }
  }
}

When no payment is sent, transaction is null and payment_status will typically be unpaid.

Change Order Status

POST /orders/{uuid}/status

Scope: orders:write

Request:

{
  "status": "completed",
  "note_for_status": "finished on-site"
}

Request Fields:

Field Type Required Description
status string Yes One of confirmed, in-progress, completed, canceled
note_for_status string No Free-text reason or note attached to the transition

Behavior:

Response: Same shape as GET /orders/{uuid} (full order detail with customer, branch, items, and transactions).


Availability

Check Availability

POST /availability

Scope: orders:write

Request:

{
  "service_id": "service-uuid",
  "date": "2026-02-25",
  "staff_id": "staff-uuid",
  "quantity": 1,
  "visible_days": 7,
  "next_availability": false
}

Request Fields:

Field Type Required Description
service_id uuid Yes Service to check
date date Yes Start date (YYYY-MM-DD)
staff_id uuid No Specific staff member
quantity integer No Default 1
visible_days integer No Days to check (1-30, default 7)
next_availability boolean No Find next available slot if none on given date

Response:

{
  "data": {
    "slots": {
      "2026-02-25": ["09:00", "09:30", "10:00", "10:30"],
      "2026-02-26": ["09:00", "11:00", "14:00"]
    }
  }
}

Customers

List Customers

GET /customers

Scope: customers:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)
name string Filter by name
phone string Filter by phone number
email string Filter by email

Response item fields: id, first_name, last_name, email, phone, type, city, created_at, updated_at


Get Customer

GET /customers/{uuid}

Scope: customers:read

Returns customer with address, note, blocked, tax_registration_number, tags.


Find Customer by Phone

GET /customers/phone?phone=<phone>

Scope: customers:read

Parameter Type Required Description
phone string Yes Phone number in international format, with or without the leading + — +966500000001, 966500000001 and 00966500000001 all match the same customer. If you send the +, URL-encode it as %2B. Separators (spaces, dashes, dots, parentheses) are ignored on both sides of the comparison.

Matching is done on digits only, so the stored formatting does not matter. Local (national) formats such as 0500000001 are not resolved — send the country code.

Response (found):

{
  "data": {
    "id": "uuid",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "phone": "+966500000001",
    "type": "individual",
    "city": "Riyadh",
    "address": "123 Main St",
    "note": "VIP customer",
    "blocked": false,
    "tax_registration_number": "300000000000003",
    "tags": [],
    "created_at": "2026-02-25T10:00:00.000000Z",
    "updated_at": "2026-02-25T10:00:00.000000Z"
  }
}

Response (not found):

{
  "data": null
}

Create Customer

POST /customers

Scope: customers:write

Request:

{
  "first_name": "John",
  "last_name": "Doe",
  "email": "john@example.com",
  "phone": "+966500000001",
  "phone_country": "SA",
  "type": "individual",
  "city": "Riyadh",
  "address": "123 Main St",
  "note": "VIP customer",
  "tax_registration_number": "300000000000003"
}

Request Fields:

Field Type Required Description
first_name string Yes Min 2 chars
last_name string No Min 2 chars
email string No Valid email (RFC + DNS)
phone string No Unique per tenant
phone_country string No Required with phone, 2-char country code (e.g., SA)
type string No individual or company (default: individual)
city string No Min 3 chars
address string No Min 3 chars
note string No Free text
tax_registration_number string No 15 digits

Response (201):

{
  "data": {
    "id": "uuid",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "phone": "+966500000001",
    "type": "individual",
    "city": "Riyadh",
    "address": "123 Main St",
    "note": "VIP customer",
    "blocked": false,
    "tax_registration_number": "300000000000003",
    "tags": [],
    "created_at": "2026-02-25T10:30:00.000000Z",
    "updated_at": "2026-02-25T10:30:00.000000Z"
  }
}

Items (Services & Products)

List Items

GET /items

Scope: items:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)
type string service or product
name string Filter by name

Response:

{
  "data": [
    {
      "id": "uuid",
      "type": "service",
      "sku": "SRV-001",
      "name": { "ar": "قص شعر", "en": "Haircut" },
      "slug": "haircut",
      "description": { "ar": "...", "en": "..." },
      "html_description": { "ar": "<p>...</p>", "en": "<p>...</p>" },
      "status": true,
      "price": 100.00,
      "offer_price": 80.00,
      "currency": "SAR",
      "duration": 30,
      "options": [
        {
          "id": "uuid",
          "name": "Red Small",
          "price": 120.00,
          "offer_price": 100.00,
          "duration": 45,
          "is_active": true,
          "values": [
            { "option_value_id": "uuid", "option_name": "Color", "value": "Red" },
            { "option_value_id": "uuid", "option_name": "Size", "value": "Small" }
          ]
        }
      ],
      "category": {
        "id": "uuid",
        "name": { "ar": "الشعر", "en": "Hair" },
        "status": true
      },
      "branch": { "id": "uuid", "name": "Main Branch" },
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    },
    {
      "id": "uuid",
      "type": "product",
      "sku": "PRD-001",
      "name": { "ar": "شامبو", "en": "Shampoo" },
      "slug": "shampoo",
      "description": { "ar": "...", "en": "..." },
      "html_description": { "ar": "<p>...</p>", "en": "<p>...</p>" },
      "status": true,
      "price": 25.00,
      "offer_price": null,
      "currency": "SAR",
      "cost": 10.00,
      "stock": 50,
      "options": [
        {
          "id": "uuid",
          "name": "500ml",
          "price": 30.00,
          "offer_price": 30.00,
          "quantity": 20,
          "is_active": true,
          "values": [
            { "option_value_id": "uuid", "option_name": "Size", "value": "500ml" }
          ]
        }
      ],
      "category": {
        "id": "uuid",
        "name": { "ar": "المنتجات", "en": "Products" },
        "status": true
      },
      "branch": { "id": "uuid", "name": "Main Branch" },
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Type-specific fields:

Options: Each item can have variants (options). Each option has a name, price, offer_price, and an array of values describing which option values make up the variant (e.g., Color=Red, Size=Small).


Get Item

GET /items/{uuid}

Scope: items:read

Returns item with full details. For services, includes staff array with each staff member's id, name, price, and duration.


Staff (Providers)

List Staff

GET /staff

Scope: staff:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)
name string Filter by name
branch_id uuid Filter by branch UUID

Response:

{
  "data": [
    {
      "id": "uuid",
      "name": "Ahmed Ali",
      "phone": "+966500000001",
      "phone_country": "SA",
      "email": "ahmed@example.com",
      "image": "https://...",
      "branch": { "id": "uuid", "name": "Main Branch" },
      "services": [
        {
          "id": "uuid",
          "name": "Haircut",
          "price": 100.00,
          "duration": 30
        }
      ],
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Get Staff

GET /staff/{uuid}

Scope: staff:read

Returns staff member with branch and full services list.


Get Staff Working Hour

GET /staff/{uuid}/working-hour

Scope: staff:read

Returns the staff member's default working hour with intervals. If the staff member has no working hour configured, returns the working hour of their branch (team) as a fallback. The source field indicates which one was returned.

Responds with 404 only when neither the staff nor their branch has a working hour.

Response:

{
  "data": {
    "id": "uuid",
    "source": "staff",
    "all_time": false,
    "active": true,
    "intervals": [
      {
        "id": "uuid",
        "day": "sun",
        "from": "09:00",
        "to": "17:00",
        "active": true
      }
    ]
  }
}
Field Type Description
source string staff if staff has a custom working hour, branch if falling back to the branch (team) working hour
intervals[].day string Lowercase three-letter day code (sun, mon, tue, wed, thu, fri, sat)
intervals[].from / intervals[].to string Time of day in HH:mm format, in the tenant's timezone

Branches

List Branches

GET /branches

Scope: branches:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)

Response:

{
  "data": [
    {
      "id": "uuid",
      "name": "Main Branch",
      "slug": "main-branch",
      "phone": "+966500000001",
      "phone_country": "SA",
      "address": "123 Main St, Riyadh",
      "latitude": 24.7136,
      "longitude": 46.6753,
      "location": "in-store",
      "status": true,
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Categories

List Categories

GET /categories

Scope: categories:read

Query Parameters:

Parameter Type Description
page integer Page number
per_page integer Items per page (max 100)
branch_id uuid Filter by branch UUID

Response:

{
  "data": [
    {
      "id": "uuid",
      "name": { "ar": "خدمات الشعر", "en": "Hair Services" },
      "slug": "hair-services",
      "description": { "ar": "جميع خدمات الشعر", "en": "All hair-related services" },
      "status": true,
      "sort_order": 1,
      "branch": { "id": "uuid", "name": "Main Branch" },
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Payment Methods

Use GET /payment-methods to discover which methods are available on a tenant (cash, card, bank transfer, and any installed app methods). The returned UUIDs can be passed as payment.payment_method_id on POST /orders.

POST /payment-methods still only supports creating cash and other types; other method types are managed by tenants in the Mahjoz dashboard.

List Payment Methods

GET /payment-methods

Scope: payment_methods:read

Query Parameters:

Parameter Type Description
per_page integer Items per page (default 25, max 100)
page integer Page number

Response:

{
  "data": [
    {
      "id": "uuid",
      "name": { "ar": "نقداً", "en": "Cash" },
      "slug": "on_arrival",
      "type": {
        "id": "uuid",
        "name": "Cash",
        "slug": "cash"
      },
      "created_at": "2026-02-25T10:00:00.000000Z",
      "updated_at": "2026-02-25T10:00:00.000000Z"
    }
  ]
}

Get Payment Method

GET /payment-methods/{id}

Scope: payment_methods:read

Returns a single payment method by UUID.


Transactions

List Transactions

GET /transactions

Scope: transactions:read

Query Parameters:

Parameter Type Description
status string Filter by transaction status (e.g. pending, confirmed, refused)
order_id uuid Filter by related order UUID
booking_id uuid Filter by related booking UUID
from date Start of date range (created_at)
to date End of date range (created_at)
per_page integer Items per page (default 25, max 100)
page integer Page number

Response:

{
  "data": [
    {
      "id": "uuid",
      "transaction_no": 1042,
      "amount": "150.00",
      "currency": "SAR",
      "status": "confirmed",
      "is_deposit": false,
      "note": null,
      "date": "2026-04-15T10:30:00+03:00",
      "payment_method": {
        "id": "uuid",
        "name": { "en": "Cash" },
        "slug": "on_arrival"
      },
      "order": { "id": "uuid", "order_number": 1015 },
      "booking": null,
      "created_at": "2026-04-15T10:30:00.000000Z",
      "updated_at": "2026-04-15T10:30:00.000000Z"
    }
  ]
}

Get Transaction

GET /transactions/{id}

Scope: transactions:read

Returns a single transaction by UUID.

Create Payment Method

POST /payment-methods

Scope: payment_methods:write

Creates a tenant-level payment method. Only the cash and other types are supported via the Open API.

Body:

Field Type Required Description
name string yes Display name
type string yes One of cash, other
status boolean no Defaults to true

Response: 201 Created with the created payment method.


Place Order Payment

POST /orders/{id}/payments

Scope: transactions:write

Records a payment (transaction) against a sale order. The amount cannot exceed the order's remaining unpaid amount and the order must not be in draft status.

Body:

Field Type Required Description
amount number yes Amount to pay (cannot exceed remaining)
payment_method_id uuid yes UUID of a tenant payment method
note string no Optional note
reference_number string no External reference
date datetime no Defaults to now

Response: 201 Created with the created transaction resource.