# ENGARAGE Open API — v1.0.0

> Read and write your workshop data from your own systems.

**Base URL:** `https://app.engarage.io/api/v1`

The ENGARAGE Open API gives your own systems the same view of your workshop that
the app has: customers, vehicles, bookings, work orders, quotations, inventory and
procurement.

## Availability
The API is included with the **Pro** plan. Keys are issued per workshop from
**Account Settings → API Docs & Keys**, and every request is scoped to the workshop
that issued the key — there is no way to read another workshop's data.

## Authentication
Send your secret key on every request, either as an `X-API-Key` header or as
`Authorization: Bearer <key>`. Keys are shown once, at creation; store them
somewhere safe and never ship one in client-side code.

## Environments
Every key is either **live** (`eng_live_…`) or **sandbox** (`eng_test_…`). A sandbox
key runs against an isolated fixture dataset with identical validation, envelopes and
error codes, so you can build and test a client — including writes — without touching
production data. Reset the sandbox at any time from the portal or with
`POST /sandbox/reset`.

## Scopes
A key carries one of `read`, `write` or `admin`, and they nest: `admin` implies
`write`, which implies `read`. GET needs `read`, POST and PATCH need `write`, and
DELETE needs `admin`.

## Pagination
List endpoints take `page` and `limit` (max 200) and return a `meta` block with
`total`, `page`, `limit` and `total_pages`. Sort with `sort=field` or `sort=-field`.

## Rate limits
600 requests per minute per live key and 1200 per sandbox key.
Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`; a `429` carries
`retry_after` in seconds.

## Errors
Failures always return the same envelope, so one handler covers them all:

```json
{ "error": { "code": "validation_error", "message": "…", "field": "name" } }
```

---

## Endpoints

### Meta

#### `GET /ping`

**Health check**

Confirms the API is reachable and your key is valid. The cheapest call to wire up first.

Responses: `200`, `401`

```json
{
    "ok": true,
    "version": "1.0.0",
    "environment": "sandbox",
    "time": "2026-09-08T04:20:00Z"
}
```

#### `GET /me`

**Describe the calling key**

Who you are: the workshop the key belongs to, its environment, scopes and rate limit. Useful for showing "connected as…" in your own UI.

Responses: `200`, `401`

```json
{
    "workshop": {
        "id": 128,
        "name": "Workshop Engarage Demo"
    },
    "key": {
        "name": "Partner ERP Integration",
        "environment": "sandbox",
        "scopes": [
            "read",
            "write"
        ],
        "last_used_at": "2026-09-08T04:19:12Z"
    },
    "plan": "pro",
    "rate_limit": {
        "limit": 1200,
        "window": "1 minute"
    }
}
```

#### `POST /sandbox/reset`

**Reset the sandbox dataset**

Throws away everything your sandbox key has created or changed and reseeds the fixtures. Rejected with `409` if called with a live key — there is deliberately no way to reset production.

Responses: `200`, `409`

```json
{
    "ok": true,
    "message": "Sandbox dataset reset."
}
```

### Customers

#### `GET /customers`

**List customers**

A customer owns vehicles and work orders. Phone number is the natural key most workshops search on, so it is indexed and filterable.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `name`, `phone` and `email`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `phone` | query | string | Exact phone number match. |
| `email` | query | string | Exact email match. |
| `status` | query | integer | Record status. 1 = active. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "name": "Nurul Aisyah binti Rahman",
            "phone": "123456789",
            "country_code": "60",
            "email": "aisyah@example.com",
            "gender": "female",
            "dob": "1991-04-18",
            "note": "Prefers WhatsApp reminders.",
            "status": 1,
            "vehicle_count": 2,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /customers`

**Create a customer**

Creates a customer in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "name": "Nurul Aisyah binti Rahman",
    "phone": "123456789",
    "country_code": "60",
    "email": "aisyah@example.com",
    "gender": "female",
    "dob": "1991-04-18",
    "note": "Prefers WhatsApp reminders."
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Nurul Aisyah binti Rahman",
        "phone": "123456789",
        "country_code": "60",
        "email": "aisyah@example.com",
        "gender": "female",
        "dob": "1991-04-18",
        "note": "Prefers WhatsApp reminders.",
        "status": 1,
        "vehicle_count": 2,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /customers/{id}`

**Retrieve a customer**

Fetch a single customer by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "name": "Nurul Aisyah binti Rahman",
        "phone": "123456789",
        "country_code": "60",
        "email": "aisyah@example.com",
        "gender": "female",
        "dob": "1991-04-18",
        "note": "Prefers WhatsApp reminders.",
        "status": 1,
        "vehicle_count": 2,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /customers/{id}`

**Update a customer**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "name": "Nurul Aisyah binti Rahman",
    "phone": "123456789"
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Nurul Aisyah binti Rahman",
        "phone": "123456789",
        "country_code": "60",
        "email": "aisyah@example.com",
        "gender": "female",
        "dob": "1991-04-18",
        "note": "Prefers WhatsApp reminders.",
        "status": 1,
        "vehicle_count": 2,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

### Vehicles

#### `GET /vehicles`

**List vehicles**

Vehicles hang off a customer. `plate_no` is what every counter workflow looks up, so filter on it rather than scanning the list.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `plate_no` and `chassis_no`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `plate_no` | query | string | Exact registration-plate match. |
| `customer_id` | query | integer | Only vehicles owned by this customer. |
| `status` | query | integer | Record status. 1 = active. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "customer_id": 1,
            "customer_name": "Nurul Aisyah binti Rahman",
            "plate_no": "WXY 1234",
            "chassis_no": "JHMCM56557C404453",
            "ref_car_id": 1,
            "make": "Perodua",
            "model": "Myvi 1.5 AV",
            "segment": "Hatchback",
            "fuel_type": "Petrol",
            "year": "2021",
            "next_service": "2026-12-01",
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /vehicles`

**Create a vehicle**

Creates a vehicle in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "customer_id": 1,
    "plate_no": "WXY 1234",
    "ref_car_id": 1,
    "chassis_no": "JHMCM56557C404453",
    "year": "2021",
    "next_service": "2026-12-01"
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "plate_no": "WXY 1234",
        "chassis_no": "JHMCM56557C404453",
        "ref_car_id": 1,
        "make": "Perodua",
        "model": "Myvi 1.5 AV",
        "segment": "Hatchback",
        "fuel_type": "Petrol",
        "year": "2021",
        "next_service": "2026-12-01",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /vehicles/{id}`

**Retrieve a vehicle**

Fetch a single vehicle by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "plate_no": "WXY 1234",
        "chassis_no": "JHMCM56557C404453",
        "ref_car_id": 1,
        "make": "Perodua",
        "model": "Myvi 1.5 AV",
        "segment": "Hatchback",
        "fuel_type": "Petrol",
        "year": "2021",
        "next_service": "2026-12-01",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /vehicles/{id}`

**Update a vehicle**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "customer_id": 1,
    "plate_no": "WXY 1234"
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "plate_no": "WXY 1234",
        "chassis_no": "JHMCM56557C404453",
        "ref_car_id": 1,
        "make": "Perodua",
        "model": "Myvi 1.5 AV",
        "segment": "Hatchback",
        "fuel_type": "Petrol",
        "year": "2021",
        "next_service": "2026-12-01",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `DELETE /vehicles/{id}`

**Delete a vehicle**

Soft-deletes the record. Requires the `admin` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `204`, `401`, `403`, `404`

#### `GET /vehicle-models`

**List vehicle models**

Read-only reference data shared by every workshop. Look a model up here to get the `ref_car_id` that `POST /vehicles` requires.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `make` and `model`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `make` | query | string | Exact manufacturer match. |
| `segment` | query | string | Exact segment match, e.g. "SUV". |
| `fuel_type` | query | string | Exact fuel-type match. |
| `status` | query | integer | Record status. 1 = active. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "make": "Perodua",
            "model": "Myvi 1.5 AV",
            "segment": "Hatchback",
            "fuel_type": "Petrol",
            "oil_capacity": "3.2L",
            "car_battery": "NS60",
            "status": 1
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `GET /vehicle-models/{id}`

**Retrieve a vehicle model**

Fetch a single vehicle model by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "make": "Perodua",
        "model": "Myvi 1.5 AV",
        "segment": "Hatchback",
        "fuel_type": "Petrol",
        "oil_capacity": "3.2L",
        "car_battery": "NS60",
        "status": 1
    }
}
```

### Bookings

#### `GET /bookings`

**List bookings**

A booking is a request for a slot. Accepting one creates a work order; this endpoint covers the request itself.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `remark`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Booking status. 1 = pending, 2 = accepted, 3 = rejected. |
| `customer_id` | query | integer | Only bookings for this customer. |
| `from` | query | string (date) | Only bookings at or after this date. |
| `to` | query | string (date) | Only bookings at or before this date. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "customer_id": 1,
            "customer_name": "Nurul Aisyah binti Rahman",
            "vehicle_id": 1,
            "plate_no": "WXY 1234",
            "scheduled_at": "2026-09-15T09:30:00Z",
            "remark": "Engine oil + filter, brakes squealing.",
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /bookings`

**Create a booking**

Creates a booking in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "customer_id": 1,
    "vehicle_id": 1,
    "scheduled_at": "2026-09-15T09:30:00Z",
    "remark": "Engine oil + filter, brakes squealing.",
    "status": 1
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "vehicle_id": 1,
        "plate_no": "WXY 1234",
        "scheduled_at": "2026-09-15T09:30:00Z",
        "remark": "Engine oil + filter, brakes squealing.",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /bookings/{id}`

**Retrieve a booking**

Fetch a single booking by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "vehicle_id": 1,
        "plate_no": "WXY 1234",
        "scheduled_at": "2026-09-15T09:30:00Z",
        "remark": "Engine oil + filter, brakes squealing.",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /bookings/{id}`

**Update a booking**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "customer_id": 1,
    "vehicle_id": 1
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "vehicle_id": 1,
        "plate_no": "WXY 1234",
        "scheduled_at": "2026-09-15T09:30:00Z",
        "remark": "Engine oil + filter, brakes squealing.",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

### Work Orders

#### `GET /work-orders`

**List work orders**

A work order is the spine of the system: it carries the vehicle, the customer, the assigned mechanic and bay, and every line item. Its `status` walks 1 → 10; see the status table in the guide.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `wo_no`, `invoice_no`, `quotation_no` and `complaint`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Work-order status, 1–10. |
| `customer_id` | query | integer | Only jobs for this customer. |
| `wo_no` | query | string | Exact work-order number. |
| `invoice_no` | query | string | Exact invoice number. |
| `from` | query | string (date) | Created at or after this date. |
| `to` | query | string (date) | Created at or before this date. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "wo_no": "WO-2609-0042",
            "quotation_no": "QT-2609-0031",
            "invoice_no": "INV-2609-0028",
            "status": 7,
            "status_label": "Paid",
            "customer_id": 1,
            "customer_name": "Nurul Aisyah binti Rahman",
            "vehicle_id": 1,
            "plate_no": "WXY 1234",
            "mechanic_id": 3,
            "bay_id": 2,
            "complaint": "Brakes squealing on cold start.",
            "remark": "Front pads replaced, discs skimmed.",
            "mileage": "86540",
            "total_payment": 486.5,
            "payment_sst": 0,
            "method": "card",
            "booking_at": "2026-09-15T09:30:00Z",
            "completed_at": "2026-09-15T15:10:00Z",
            "invoiced_at": "2026-09-15T15:22:00Z",
            "paid_at": "2026-09-15T15:40:00Z",
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `GET /work-orders/{id}`

**Retrieve a work order**

Fetch a single work order by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "wo_no": "WO-2609-0042",
        "quotation_no": "QT-2609-0031",
        "invoice_no": "INV-2609-0028",
        "status": 7,
        "status_label": "Paid",
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "vehicle_id": 1,
        "plate_no": "WXY 1234",
        "mechanic_id": 3,
        "bay_id": 2,
        "complaint": "Brakes squealing on cold start.",
        "remark": "Front pads replaced, discs skimmed.",
        "mileage": "86540",
        "total_payment": 486.5,
        "payment_sst": 0,
        "method": "card",
        "booking_at": "2026-09-15T09:30:00Z",
        "completed_at": "2026-09-15T15:10:00Z",
        "invoiced_at": "2026-09-15T15:22:00Z",
        "paid_at": "2026-09-15T15:40:00Z",
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /work-orders/{id}`

**Update a work order**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "status": 7,
    "mechanic_id": 3
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "wo_no": "WO-2609-0042",
        "quotation_no": "QT-2609-0031",
        "invoice_no": "INV-2609-0028",
        "status": 7,
        "status_label": "Paid",
        "customer_id": 1,
        "customer_name": "Nurul Aisyah binti Rahman",
        "vehicle_id": 1,
        "plate_no": "WXY 1234",
        "mechanic_id": 3,
        "bay_id": 2,
        "complaint": "Brakes squealing on cold start.",
        "remark": "Front pads replaced, discs skimmed.",
        "mileage": "86540",
        "total_payment": 486.5,
        "payment_sst": 0,
        "method": "card",
        "booking_at": "2026-09-15T09:30:00Z",
        "completed_at": "2026-09-15T15:10:00Z",
        "invoiced_at": "2026-09-15T15:22:00Z",
        "paid_at": "2026-09-15T15:40:00Z",
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /work-orders/{id}/items`

**Line items on a work order**

Parts, services, packages and miscellaneous charges billed on this job.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": [
        {
            "id": 5501,
            "type": "part",
            "part_id": 812,
            "service_id": null,
            "package_id": null,
            "quantity": 4,
            "total_price": 168,
            "total_discount": 0,
            "tax_rate": 0
        }
    ]
}
```

### Quotations

#### `GET /quotations`

**List quotations**

A work order can carry several quotations across revisions; the one with `status` 2 is the accepted one.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `quotation_no`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Quotation status. 2 = accepted/converted. |
| `work_order_id` | query | integer | Only quotations on this work order. |
| `quotation_no` | query | string | Exact quotation number. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "quotation_no": "QT-2609-0031",
            "work_order_id": 42,
            "wo_no": "WO-2609-0042",
            "customer_id": 1,
            "status": 2,
            "discount": 20,
            "deposit": 100,
            "deposit_method": "cash",
            "deposit_at": "2026-09-15T10:05:00Z",
            "total": 486.5,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `GET /quotations/{id}`

**Retrieve a quotation**

Fetch a single quotation by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "quotation_no": "QT-2609-0031",
        "work_order_id": 42,
        "wo_no": "WO-2609-0042",
        "customer_id": 1,
        "status": 2,
        "discount": 20,
        "deposit": 100,
        "deposit_method": "cash",
        "deposit_at": "2026-09-15T10:05:00Z",
        "total": 486.5,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

### Inventory

#### `GET /parts`

**List parts**

Your inventory. `quantity` is on-hand stock; it moves when work orders are paid, so treat it as authoritative rather than caching it.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `name`, `part_no`, `brand` and `model`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `part_no` | query | string | Exact part number. |
| `brand` | query | string | Exact brand match. |
| `supplier_id` | query | integer | Only parts from this supplier. |
| `status` | query | integer | Record status. 1 = active. |
| `low_stock` | query | boolean | When true, only parts at or below their stock-alert threshold. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "name": "Engine Oil Filter",
            "part_no": "OF-15400-RTA",
            "brand": "Denso",
            "model": "Myvi",
            "variant": "1.5L",
            "description": "Spin-on oil filter.",
            "unit": "pcs",
            "bin_number": "A-04-2",
            "cost_price": 12.5,
            "price": 24,
            "quantity": 36,
            "stock_alert_threshold": 10,
            "supplier_id": 4,
            "is_low_stock": false,
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /parts`

**Create a part**

Creates a part in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "name": "Engine Oil Filter",
    "part_no": "OF-15400-RTA",
    "brand": "Denso",
    "model": "Myvi",
    "variant": "1.5L",
    "description": "Spin-on oil filter.",
    "unit": "pcs",
    "bin_number": "A-04-2",
    "cost_price": 12.5,
    "price": 24,
    "quantity": 36,
    "stock_alert_threshold": 10,
    "supplier_id": 4
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Filter",
        "part_no": "OF-15400-RTA",
        "brand": "Denso",
        "model": "Myvi",
        "variant": "1.5L",
        "description": "Spin-on oil filter.",
        "unit": "pcs",
        "bin_number": "A-04-2",
        "cost_price": 12.5,
        "price": 24,
        "quantity": 36,
        "stock_alert_threshold": 10,
        "supplier_id": 4,
        "is_low_stock": false,
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /parts/{id}`

**Retrieve a part**

Fetch a single part by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Filter",
        "part_no": "OF-15400-RTA",
        "brand": "Denso",
        "model": "Myvi",
        "variant": "1.5L",
        "description": "Spin-on oil filter.",
        "unit": "pcs",
        "bin_number": "A-04-2",
        "cost_price": 12.5,
        "price": 24,
        "quantity": 36,
        "stock_alert_threshold": 10,
        "supplier_id": 4,
        "is_low_stock": false,
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /parts/{id}`

**Update a part**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "name": "Engine Oil Filter",
    "part_no": "OF-15400-RTA"
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Filter",
        "part_no": "OF-15400-RTA",
        "brand": "Denso",
        "model": "Myvi",
        "variant": "1.5L",
        "description": "Spin-on oil filter.",
        "unit": "pcs",
        "bin_number": "A-04-2",
        "cost_price": 12.5,
        "price": 24,
        "quantity": 36,
        "stock_alert_threshold": 10,
        "supplier_id": 4,
        "is_low_stock": false,
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `DELETE /parts/{id}`

**Delete a part**

Soft-deletes the record. Requires the `admin` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `204`, `401`, `403`, `404`

#### `GET /suppliers`

**List suppliers**

Suppliers referenced by parts and purchase orders.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `name`, `company_reg`, `email` and `phone_no`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Record status. 1 = active. |
| `state` | query | string | Exact state match. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "name": "Southern Auto Parts Sdn Bhd",
            "company_reg": "201801023456",
            "sales_p": "Mr Tan",
            "phone_no": "0377881234",
            "email": "sales@southernauto.example",
            "address_1": "Lot 12, Jalan Perindustrian 3",
            "address_2": null,
            "postcode": "47100",
            "city": "Puchong",
            "state": "Selangor",
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /suppliers`

**Create a supplier**

Creates a supplier in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "name": "Southern Auto Parts Sdn Bhd",
    "company_reg": "201801023456",
    "sales_p": "Mr Tan",
    "phone_no": "0377881234",
    "email": "sales@southernauto.example",
    "address_1": "Lot 12, Jalan Perindustrian 3",
    "address_2": null,
    "postcode": "47100",
    "city": "Puchong",
    "state": "Selangor"
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Southern Auto Parts Sdn Bhd",
        "company_reg": "201801023456",
        "sales_p": "Mr Tan",
        "phone_no": "0377881234",
        "email": "sales@southernauto.example",
        "address_1": "Lot 12, Jalan Perindustrian 3",
        "address_2": null,
        "postcode": "47100",
        "city": "Puchong",
        "state": "Selangor",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /suppliers/{id}`

**Retrieve a supplier**

Fetch a single supplier by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "name": "Southern Auto Parts Sdn Bhd",
        "company_reg": "201801023456",
        "sales_p": "Mr Tan",
        "phone_no": "0377881234",
        "email": "sales@southernauto.example",
        "address_1": "Lot 12, Jalan Perindustrian 3",
        "address_2": null,
        "postcode": "47100",
        "city": "Puchong",
        "state": "Selangor",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /suppliers/{id}`

**Update a supplier**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "name": "Southern Auto Parts Sdn Bhd",
    "company_reg": "201801023456"
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Southern Auto Parts Sdn Bhd",
        "company_reg": "201801023456",
        "sales_p": "Mr Tan",
        "phone_no": "0377881234",
        "email": "sales@southernauto.example",
        "address_1": "Lot 12, Jalan Perindustrian 3",
        "address_2": null,
        "postcode": "47100",
        "city": "Puchong",
        "state": "Selangor",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `DELETE /suppliers/{id}`

**Delete a supplier**

Soft-deletes the record. Requires the `admin` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `204`, `401`, `403`, `404`

#### `GET /purchase-orders`

**List purchase orders**

Read-only view of procurement. Raising and receiving stock stays in the app so the stock ledger cannot be bypassed.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `po_no` and `supplier_invoice_no`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Purchase-order status. |
| `supplier_id` | query | integer | Only orders to this supplier. |
| `po_no` | query | string | Exact purchase-order number. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "po_no": "PO-2609-0011",
            "supplier_id": 4,
            "supplier_name": "Southern Auto Parts Sdn Bhd",
            "supplier_invoice_no": "SAP-88213",
            "total": 1420,
            "tax_rate": 0,
            "discount": 0,
            "shipping_cost": 25,
            "status": 3,
            "notes": "Monthly filter restock.",
            "ordered_at": "2026-09-01T08:00:00Z",
            "approved_at": "2026-09-01T09:12:00Z",
            "paid_at": null,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `GET /purchase-orders/{id}`

**Retrieve a purchase order**

Fetch a single purchase order by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "po_no": "PO-2609-0011",
        "supplier_id": 4,
        "supplier_name": "Southern Auto Parts Sdn Bhd",
        "supplier_invoice_no": "SAP-88213",
        "total": 1420,
        "tax_rate": 0,
        "discount": 0,
        "shipping_cost": 25,
        "status": 3,
        "notes": "Monthly filter restock.",
        "ordered_at": "2026-09-01T08:00:00Z",
        "approved_at": "2026-09-01T09:12:00Z",
        "paid_at": null,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

### Catalogue

#### `GET /services`

**List services**

The service catalogue billed on work orders and quotations.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `name`, `service_no` and `category`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `category` | query | string | Exact category match. |
| `status` | query | integer | Record status. 1 = active. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "name": "Engine Oil Service",
            "service_no": "SRV-001",
            "category": "Engine",
            "price": 80,
            "price_sst": 80,
            "sst": 0,
            "hours": 0.75,
            "skill_level": "junior",
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `POST /services`

**Create a service**

Creates a service in the workshop that owns the key. Requires the `write` scope.

Request body:

```json
{
    "name": "Engine Oil Service",
    "service_no": "SRV-001",
    "category": "Engine",
    "price": 80,
    "price_sst": 80,
    "sst": 0,
    "hours": 0.75,
    "skill_level": "junior"
}
```

Responses: `201`, `401`, `403`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Service",
        "service_no": "SRV-001",
        "category": "Engine",
        "price": 80,
        "price_sst": 80,
        "sst": 0,
        "hours": 0.75,
        "skill_level": "junior",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `GET /services/{id}`

**Retrieve a service**

Fetch a single service by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Service",
        "service_no": "SRV-001",
        "category": "Engine",
        "price": 80,
        "price_sst": 80,
        "sst": 0,
        "hours": 0.75,
        "skill_level": "junior",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `PATCH /services/{id}`

**Update a service**

Partial update — send only the fields you want to change. Requires the `write` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Request body:

```json
{
    "name": "Engine Oil Service",
    "service_no": "SRV-001"
}
```

Responses: `200`, `401`, `404`, `422`

```json
{
    "data": {
        "id": 1,
        "name": "Engine Oil Service",
        "service_no": "SRV-001",
        "category": "Engine",
        "price": 80,
        "price_sst": 80,
        "sst": 0,
        "hours": 0.75,
        "skill_level": "junior",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

#### `DELETE /services/{id}`

**Delete a service**

Soft-deletes the record. Requires the `admin` scope.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `204`, `401`, `403`, `404`

### Workshop

#### `GET /mechanics`

**List mechanics**

Read-only roster used to resolve `mechanic_id` on work orders.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `page` | query | integer | Page to fetch, 1-based. |
| `limit` | query | integer | Records per page. |
| `search` | query | string | Partial match across `name`, `nickname`, `email` and `reg_no`. |
| `sort` | query | string | Sort field. Prefix with `-` for descending. |
| `status` | query | integer | Record status. 1 = active. |

Responses: `200`, `401`, `403`, `422`, `429`

```json
{
    "data": [
        {
            "id": 1,
            "name": "Mohd Faizal bin Osman",
            "nickname": "Faizal",
            "email": "faizal@example.com",
            "phone_no": "0198887766",
            "reg_no": "MEC-003",
            "skill": "Brakes, suspension",
            "qualification": "SKM Level 3",
            "exp_year": "8",
            "status": 1,
            "created_at": "2026-09-01T08:15:00Z",
            "updated_at": "2026-09-06T11:42:00Z"
        }
    ],
    "meta": {
        "total": 8,
        "page": 1,
        "limit": 50,
        "total_pages": 1
    }
}
```

#### `GET /mechanics/{id}`

**Retrieve a mechanic**

Fetch a single mechanic by id. Ids from another workshop return `404`.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | integer | Record id. |

Responses: `200`, `401`, `404`

```json
{
    "data": {
        "id": 1,
        "name": "Mohd Faizal bin Osman",
        "nickname": "Faizal",
        "email": "faizal@example.com",
        "phone_no": "0198887766",
        "reg_no": "MEC-003",
        "skill": "Brakes, suspension",
        "qualification": "SKM Level 3",
        "exp_year": "8",
        "status": 1,
        "created_at": "2026-09-01T08:15:00Z",
        "updated_at": "2026-09-06T11:42:00Z"
    }
}
```

---

## Schemas

### PageMeta

Pagination block returned by every list endpoint.

| Field | Type | Description |
| --- | --- | --- |
| `total` | integer | Total records matching the query. |
| `page` | integer | Current page (1-based). |
| `limit` | integer | Records per page. |
| `total_pages` | integer | Number of pages available. |

### Error

Every failure uses this envelope.

| Field | Type | Description |
| --- | --- | --- |
| `error` | object |  |

### KeyContext

| Field | Type | Description |
| --- | --- | --- |
| `workshop` | object |  |
| `key` | object |  |
| `plan` | string |  |
| `rate_limit` | object |  |

### Customer

People and companies your workshop serves.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique customer id. |
| `name` | string | Full name or company name. |
| `phone` | string, null | Contact number, without the country code. |
| `country_code` | string, null | Dialling code for `phone`, e.g. "60". |
| `email` | string, null (email) | Contact email. |
| `gender` | string, null | Free-text gender as captured at the counter. |
| `dob` | string, null (date) | Date of birth. |
| `note` | string, null | Internal note shown to counter staff. |
| `status` | integer, null | Record status. 1 = active. |
| `vehicle_count` | integer | Vehicles currently registered to this customer. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### CustomerInput

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Full name or company name. |
| `phone` | string | Contact number, without the country code. |
| `country_code` | string | Dialling code for `phone`. |
| `email` | string | Contact email. |
| `gender` | string | Gender. |
| `dob` | string | Date of birth (YYYY-MM-DD). |
| `note` | string | Internal note. |

### Vehicle

Vehicles registered against your customers.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique vehicle id. |
| `customer_id` | integer | Owning customer. |
| `customer_name` | string, null | Owning customer name (read-only convenience). |
| `plate_no` | string | Registration plate. |
| `chassis_no` | string, null | VIN / chassis number. |
| `ref_car_id` | integer, null | Catalogue model id — see `GET /vehicle-models`. |
| `make` | string, null | Manufacturer, from the ENGARAGE vehicle catalogue. |
| `model` | string, null | Model, from the ENGARAGE vehicle catalogue. |
| `segment` | string, null | Vehicle segment, e.g. "SUV". |
| `fuel_type` | string, null | Petrol, diesel, hybrid or EV. |
| `year` | string, null | Model year. |
| `next_service` | string, null (date) | Next service due date. |
| `status` | integer, null | Record status. 1 = active. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### VehicleInput

| Field | Type | Description |
| --- | --- | --- |
| `customer_id` | integer | Owning customer id. |
| `plate_no` | string | Registration plate. |
| `ref_car_id` | integer | Catalogue model id — look it up with `GET /vehicle-models`. |
| `chassis_no` | string | VIN / chassis number. |
| `year` | string | Model year. |
| `next_service` | string | Next service due date. |

### VehicleModel

ENGARAGE's shared make/model catalogue.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Catalogue id — this is the `ref_car_id` used when creating a vehicle. |
| `make` | string, null | Manufacturer. |
| `model` | string, null | Model and variant. |
| `segment` | string, null | Vehicle segment, e.g. "SUV". |
| `fuel_type` | string, null | Petrol, diesel, hybrid or EV. |
| `oil_capacity` | string, null | Engine oil capacity, as published by the manufacturer. |
| `car_battery` | string, null | Recommended battery specification. |
| `status` | integer, null | Record status. 1 = active. |

### Booking

Appointments customers have made with your workshop.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique booking id. |
| `customer_id` | integer, null | Customer who booked. |
| `customer_name` | string, null | Customer name (read-only convenience). |
| `vehicle_id` | integer, null | Vehicle being brought in. |
| `plate_no` | string, null | Vehicle plate (read-only convenience). |
| `scheduled_at` | string, null (date-time) | Requested slot. |
| `remark` | string, null | What the customer wants done. |
| `status` | integer, null | Booking status. 1 = pending, 2 = accepted, 3 = rejected. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### BookingInput

| Field | Type | Description |
| --- | --- | --- |
| `customer_id` | integer | Customer id. |
| `vehicle_id` | integer | Vehicle id. |
| `scheduled_at` | string | Requested slot (ISO-8601). |
| `remark` | string | What the customer wants done. |
| `status` | integer | Booking status. |

### WorkOrder

Jobs on the shop floor, from booking through to paid.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique work-order id. |
| `wo_no` | string, null | Human-readable work-order number. |
| `quotation_no` | string, null | Quotation number, once quoted. |
| `invoice_no` | string, null | Invoice number, once invoiced. |
| `status` | integer | Status code 1–10. |
| `status_label` | string | Human label for `status`. |
| `customer_id` | integer, null | Customer the job is for. |
| `customer_name` | string, null | Customer name. |
| `vehicle_id` | integer, null | Vehicle in the bay. |
| `plate_no` | string, null | Vehicle plate. |
| `mechanic_id` | integer, null | Assigned mechanic. |
| `bay_id` | integer, null | Assigned bay. |
| `complaint` | string, null | The customer's reported problem. |
| `remark` | string, null | Internal note. |
| `mileage` | string, null | Odometer reading on arrival. |
| `total_payment` | number, null | Invoice total, including tax. |
| `payment_sst` | number, null | Tax portion of the total. |
| `method` | string, null | Payment method, once paid. |
| `booking_at` | string, null (date-time) | When the job was booked. |
| `completed_at` | string, null (date-time) | When work finished. |
| `invoiced_at` | string, null (date-time) | When the invoice was raised. |
| `paid_at` | string, null (date-time) | When payment was received. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### WorkOrderInput

| Field | Type | Description |
| --- | --- | --- |
| `status` | integer | Move the job along the status ladder. |
| `mechanic_id` | integer | Assign a mechanic. |
| `bay_id` | integer | Assign a bay. |
| `remark` | string | Internal note. |
| `complaint` | string | The customer's reported problem. |
| `mileage` | string | Odometer reading. |

### WorkOrderItems

Line items on a work order

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Line-item id. |
| `type` | string, null | What is being billed: `part`, `package`, `service` or `misc`. |
| `part_id` | integer, null | Inventory part, when `type` is part. |
| `service_id` | integer, null | Service, when `type` is service. |
| `package_id` | integer, null | Package, when `type` is package. |
| `quantity` | number, null | Quantity billed. |
| `total_price` | number, null | Line total before discount. |
| `total_discount` | number, null | Discount applied to the line. |
| `tax_rate` | number, null | Tax rate applied, as a percentage. |

### Quotation

Priced estimates issued against a work order.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique quotation id. |
| `quotation_no` | string, null | Human-readable quotation number. |
| `work_order_id` | integer, null | Work order this quotes. |
| `wo_no` | string, null | Work-order number. |
| `customer_id` | integer, null | Customer quoted. |
| `status` | integer, null | Quotation status. 2 = accepted. |
| `discount` | number, null | Discount value applied. |
| `deposit` | number, null | Deposit collected. |
| `deposit_method` | string, null | How the deposit was paid. |
| `deposit_at` | string, null (date-time) | When the deposit was collected. |
| `total` | number, null | Quoted total. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### Part

Stocked parts and consumables.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique part id. |
| `name` | string | Part name. |
| `part_no` | string, null | Manufacturer or internal part number. |
| `brand` | string, null | Brand. |
| `model` | string, null | Vehicle model this fits. |
| `variant` | string, null | Variant / specification. |
| `description` | string, null | Longer description. |
| `unit` | string, null | Unit of measure, e.g. "pcs", "litre". |
| `bin_number` | string, null | Storage location. |
| `cost_price` | number, null | What you pay your supplier. |
| `price` | number, null | What you charge the customer. |
| `quantity` | number, null | On-hand stock. |
| `stock_alert_threshold` | number, null | Level at which the part counts as low stock. |
| `supplier_id` | integer, null | Default supplier. |
| `is_low_stock` | boolean | True when on-hand stock is at or below the threshold. |
| `status` | integer, null | Record status. 1 = active. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### PartInput

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Part name. |
| `part_no` | string | Part number. Must be unique within your workshop. |
| `brand` | string | Brand. |
| `model` | string | Vehicle model this fits. |
| `variant` | string | Variant / specification. |
| `description` | string | Longer description. |
| `unit` | string | Unit of measure. |
| `bin_number` | string | Storage location. |
| `cost_price` | number | Supplier cost. |
| `price` | number | Selling price. |
| `quantity` | number | On-hand stock. |
| `stock_alert_threshold` | number | Low-stock threshold. |
| `supplier_id` | integer | Default supplier. |

### Service

Labour and service items you sell.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique service id. |
| `name` | string | Service name. |
| `service_no` | string, null | Internal service code. |
| `category` | string, null | Grouping, e.g. "Engine". |
| `price` | number, null | Price before tax. |
| `price_sst` | number, null | Price including tax. |
| `sst` | number, null | Tax rate applied, as a percentage. |
| `hours` | number, null | Standard labour hours. |
| `skill_level` | string, null | Skill required to perform it. |
| `status` | integer, null | Record status. 1 = active. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### ServiceInput

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Service name. |
| `service_no` | string | Internal service code. |
| `category` | string | Grouping. |
| `price` | number | Price before tax. |
| `price_sst` | number | Price including tax. |
| `sst` | number | Tax rate percentage. |
| `hours` | number | Standard labour hours. |
| `skill_level` | string | Skill required. |

### Supplier

Vendors you buy parts from.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique supplier id. |
| `name` | string | Supplier name. |
| `company_reg` | string, null | Company registration number. |
| `sales_p` | string, null | Sales contact person. |
| `phone_no` | string, null | Contact number. |
| `email` | string, null (email) | Contact email. |
| `address_1` | string, null | Address line 1. |
| `address_2` | string, null | Address line 2. |
| `postcode` | string, null | Postcode. |
| `city` | string, null | City. |
| `state` | string, null | State. |
| `status` | integer, null | Record status. 1 = active. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### SupplierInput

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Supplier name. |
| `company_reg` | string | Company registration number. |
| `sales_p` | string | Sales contact person. |
| `phone_no` | string | Contact number. |
| `email` | string | Contact email. |
| `address_1` | string | Address line 1. |
| `address_2` | string | Address line 2. |
| `postcode` | string | Postcode. |
| `city` | string | City. |
| `state` | string | State. |

### PurchaseOrder

Orders raised against your suppliers.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique purchase-order id. |
| `po_no` | string, null | Purchase-order number. |
| `supplier_id` | integer, null | Supplier ordered from. |
| `supplier_name` | string, null | Supplier name. |
| `supplier_invoice_no` | string, null | The supplier's own invoice number. |
| `total` | number, null | Order total. |
| `tax_rate` | number, null | Tax rate applied, as a percentage. |
| `discount` | number, null | Discount applied. |
| `shipping_cost` | number, null | Shipping charged. |
| `status` | integer, null | Purchase-order status. |
| `notes` | string, null | Internal note. |
| `ordered_at` | string, null (date-time) | When the order was raised. |
| `approved_at` | string, null (date-time) | When it was approved. |
| `paid_at` | string, null (date-time) | When it was paid. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

### Mechanic

Technicians on your team.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique mechanic id. |
| `name` | string, null | Full name. |
| `nickname` | string, null | Name shown on the shop-floor board. |
| `email` | string, null (email) | Email. |
| `phone_no` | string, null | Contact number. |
| `reg_no` | string, null | Staff / registration number. |
| `skill` | string, null | Skills, as captured on the profile. |
| `qualification` | string, null | Qualifications. |
| `exp_year` | string, null | Years of experience. |
| `status` | integer, null | Record status. 1 = active. |
| `created_at` | string (date-time) | When the record was created (ISO-8601, UTC). |
| `updated_at` | string (date-time) | When the record last changed (ISO-8601, UTC). |

---

_Generated from the ENGARAGE OpenAPI specification on Wed, Oct 21, 2026 5:03 PM._
