> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.uniledger.cc/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.uniledger.cc/_mcp/server.

# Eilith Public REST API — Overview

## Quick Start

```
Authorization: Bearer <YOUR_ACCESS_TOKEN>

Base URL (Production):
  https://krjtnboxzkrhejhqtwgm.supabase.co/functions/v1/platform-api/api/v1

Base URL (Staging):
  https://pmjidplmzxohyqsbajvg.supabase.co/functions/v1/platform-api/api/v1
```

All endpoints return `Content-Type: application/json`. CORS is enabled for all origins.

---

## Authentication

Every request requires an `Authorization: Bearer <api_key>` header.

**Key format:** `uk_live_` + 40 hex characters (64 chars total).

Keys are SHA-256 hashed before storage — the plaintext key is only shown once at creation time via the `generate_api_key` RPC from the Flutter UI.

### Key Management (via Flutter UI, not REST)

| Action     | RPC                                          | Returns                                              |
| ---------- | -------------------------------------------- | ---------------------------------------------------- |
| Create key | `generate_api_key(tenant_id, name, scopes?)` | `{ id, key, prefix, name, scopes }`                  |
| List keys  | `list_api_keys(tenant_id)`                   | `[{ id, name, key_prefix, scopes, is_active, ... }]` |
| Revoke key | `revoke_api_key(key_id)`                     | `boolean`                                            |

The full key is only returned at creation. After that, only `key_prefix` (first 16 chars + `...`) is stored.

---

## Plan Gating

API access is tiered by subscription plan:

| Plan                 | Monthly Limit    | Notes                                     |
| -------------------- | ---------------- | ----------------------------------------- |
| `unibasic`           | 0 (no access)    | Returns 403 with `upgrade_required: true` |
| `uniledger_plus`     | 10,000 requests  |                                           |
| `uniledger_plusplus` | 100,000 requests |                                           |

Exceeding the limit returns `429` with `{ error, limit, used, resets_at }`.

---

## Pagination

All list endpoints use **cursor-based pagination** with consistent parameters:

| Param    | Type   | Default      | Description                        |
| -------- | ------ | ------------ | ---------------------------------- |
| `limit`  | int    | 25           | Items per page (1–100)             |
| `cursor` | string | —            | ID of last item from previous page |
| `sort`   | string | `created_at` | Sort field                         |
| `order`  | string | `desc`       | `asc` or `desc`                    |

**Response shape:**

```json
{
  "data": [...],
  "pagination": {
    "next_cursor": "uuid-or-null",
    "has_more": true
  }
}
```

To paginate: pass `cursor` from `pagination.next_cursor` of the previous response.

---

## Endpoints

### Products (Inventory Items)

| Method   | Endpoint               | Description                      |
| -------- | ---------------------- | -------------------------------- |
| `GET`    | `/products`            | List products (paginated)        |
| `POST`   | `/products`            | Create a product                 |
| `GET`    | `/products/{id}`       | Get a product                    |
| `PUT`    | `/products/{id}`       | Update a product                 |
| `DELETE` | `/products/{id}`       | Soft-delete a product            |
| `POST`   | `/products/{id}/image` | Upload product image (multipart) |
| `DELETE` | `/products/{id}/image` | Delete product image             |

**List query params:** `status` (active/inactive/archived), `item_type` (stock/service/consumable/assembly), `search` (name or SKU).

**Create required fields:** `sku`, `name`, `item_type`, `default_uom`.

**Update allowed fields:** `sku`, `name`, `item_type`, `status`, `default_uom`, `cost_method`, `description`, `reorder_point`, `reorder_qty`, `is_stockable`, `is_purchasable`, `is_sellable`, `is_manufacturable`, `standard_cost`, `image_url`.

**Image upload:** `Content-Type: multipart/form-data` with a `file` field. Accepted types: JPEG, PNG, WebP, GIF. Max 5MB. Returns `{ image_url }` (signed URL, 7-day expiry).

**Soft-delete** sets `deleted_at` and `status: "archived"`.

---

### Stock

| Method | Endpoint           | Description                              |
| ------ | ------------------ | ---------------------------------------- |
| `GET`  | `/stock`           | List stock balances (paginated)          |
| `GET`  | `/stock/{item_id}` | Get stock for a product across locations |
| `POST` | `/stock/adjust`    | Create a stock adjustment                |

**List query params:** `location_id`, `item_id`.

**Adjust required fields:** `inventory_item_id`, `location_id`, `quantity` (positive = add, negative = subtract).

**Adjust optional fields:** `uom` (default `ea`), `unit_cost` (default 0), `notes`.

---

### Customers

| Method | Endpoint          | Description                |
| ------ | ----------------- | -------------------------- |
| `GET`  | `/customers`      | List customers (paginated) |
| `POST` | `/customers`      | Create a customer          |
| `GET`  | `/customers/{id}` | Get a customer             |
| `PUT`  | `/customers/{id}` | Update a customer          |

**List query params:** `search` (name, email, or phone).

**Create required fields:** `customer_code`, `name`.

**Update allowed fields:** `customer_code`, `name`, `email`, `phone`, `address_line1`, `address_line2`, `city`, `state`, `postal_code`, `country`, `is_active`.

---

### Orders (Sales Orders)

| Method | Endpoint       | Description                             |
| ------ | -------------- | --------------------------------------- |
| `GET`  | `/orders`      | List orders with line items (paginated) |
| `POST` | `/orders`      | Create an order with lines              |
| `GET`  | `/orders/{id}` | Get an order (includes lines)           |
| `PUT`  | `/orders/{id}` | Update order header fields              |

**List query params:** `status` (draft/confirmed/fulfilled/cancelled).

**Create required:** `lines` array (min 1 item), each with `quantity` and `unit_price`.

**Line item fields:** `inventory_item_id`, `description`, `quantity`, `unit_price`, `discount_pct` (default 0), `tax_pct` (default 0).

The API auto-calculates `subtotal`, `tax_total`, `grand_total`, and per-line `total`. Orders are created with `source: "integration"`.

**Update allowed fields:** `customer_name`, `customer_email`, `customer_phone`, `status`, `currency_code`.

---

### Usage

| Method | Endpoint | Description                     |
| ------ | -------- | ------------------------------- |
| `GET`  | `/usage` | Current month's API usage stats |

**Response:**

```json
{
  "plan": "uniledger_plus",
  "month": "2026-08",
  "used": 1234,
  "limit": 10000,
  "active_keys": 2
}
```

---

## Error Responses

All errors return `{ "error": "message" }` with an appropriate HTTP status:

| Status | Meaning                                                    |
| ------ | ---------------------------------------------------------- |
| 400    | Validation error (missing required field, invalid payload) |
| 401    | Missing/invalid API key, revoked key, or expired key       |
| 403    | Plan does not include API access                           |
| 404    | Resource not found                                         |
| 405    | HTTP method not allowed                                    |
| 429    | Monthly rate limit exceeded                                |
| 500    | Internal server error                                      |

---

## Architecture

```
Request → Supabase Edge Function (platform-api)
  → authenticateApiKey() — SHA-256 hash lookup, plan check, usage check
  → handlePublicApi() — route by path + method
  → logApiUsage() — async insert to api_usage_logs (fire-and-forget)
  → Response
```

**Key tables:**

| Table                                | Purpose                                               |
| ------------------------------------ | ----------------------------------------------------- |
| `uniledger.tenant_api_keys`          | API key hashes, scopes, plan limits                   |
| `uniledger.api_usage_logs`           | Per-request logging (endpoint, method, status\_code)  |
| `uniledger.tenant_webhook_endpoints` | Webhook URLs + secrets (Phase 2, not yet wired)       |
| `uniledger.inventory_items`          | Products (`image_url`, `image_gallery` columns added) |
| `uniledger.sales_orders`             | Orders (`source: "integration"` for API-created)      |
| `uniledger.customers`                | Customer master data                                  |

---

## Rate Limiting

Rate limits are checked per-tenant (not per-key). Each key on the same tenant shares the same monthly counter. The `monthly_limit` field on `tenant_api_keys` overrides the plan default if set.

Usage resets on the 1st of each month (UTC).

---

## Phase 2 (Planned)

* **Webhooks:**`tenant_webhook_endpoints` table is ready. Events will include `order.created`, `order.updated`, `stock.adjusted`, etc.
* **GraphQL:** After REST stabilizes.
* **Additional integrations:** Shopify, WooCommerce, Square.

---