# Parts Platform Developer API Guide

Last updated: 2026-07-03

This guide documents the current `/api/v1` REST API, authentication model, token abilities, request conventions, response shapes, webhook behavior, and integration notes for the Parts Platform.

## 1. Base URL

All API routes are versioned under:

```text
/api/v1
```

Example:

```bash
curl -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  https://example.com/api/v1/products
```

### Public Web Routes

The customer-facing storefront is served by Inertia pages, not by the JSON API. Current public and account routes include:

```http
GET    /
GET    /catalog
GET    /products/{product-slug}
GET    /blog
GET    /blog/{article-slug}
GET    /cart
POST   /cart/items
PATCH  /cart/items/{product}
DELETE /cart/items/{product}
GET    /login
POST   /login
GET    /register
POST   /register
POST   /logout
GET    /checkout
POST   /checkout
GET    /account/orders
GET    /account/orders/{order}
GET    /sitemap.xml
GET    /robots.txt
GET    /media/{path}
```

Checkout and account order routes require an authenticated user. Admin users authenticate through `/admin/login`; non-admin users authenticate through `/login`.

## 2. Authentication

The API uses Laravel Sanctum bearer tokens created in:

```text
Admin > Webhooks & APIs > API Tokens
```

Send these headers with JSON requests:

```http
Authorization: Bearer parts_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json
Content-Type: application/json
```

For multipart product or article media uploads, send `multipart/form-data` instead of JSON.

Tokens can be:

- active or inactive
- expired or non-expiring
- scoped with granular abilities
- scoped with `*` for full API access

Plaintext tokens are shown only once after creation.

## 3. Abilities

| Ability | Access |
| --- | --- |
| `*` | Full API access |
| `products.read` | Read products |
| `products.create` | Create products |
| `products.update` | Update products |
| `products.delete` | Delete products |
| `categories.read` | Read categories |
| `categories.create` | Create categories |
| `categories.update` | Update categories |
| `categories.delete` | Delete categories |
| `attributes.read` | Read attribute groups and attributes |
| `attributes.create` | Create attribute groups and attributes |
| `attributes.update` | Update attribute groups and attributes |
| `attributes.delete` | Delete attribute groups and attributes |
| `vehicles.read` | Read vehicle settings and fitments |
| `vehicles.create` | Create vehicle settings and fitments |
| `vehicles.update` | Update vehicle settings and fitments |
| `vehicles.delete` | Delete vehicle settings and fitments |
| `blog.read` | Read blog content |
| `blog.create` | Create blog content |
| `blog.update` | Update blog content |
| `blog.delete` | Delete blog content |
| `customers.read` | Read customers |
| `customers.create` | Create customers |
| `customers.update` | Update customers |
| `customers.delete` | Delete customers |
| `orders.lookup` | Lookup orders by order number and customer email |
| `orders.read` | Read full order records |
| `orders.create` | Create orders |
| `orders.update` | Update orders |
| `orders.delete` | Delete orders |
| `webhooks.read` | Read webhook endpoints and deliveries |
| `webhooks.create` | Create webhook endpoints |
| `webhooks.update` | Update webhook endpoints |
| `webhooks.delete` | Delete webhook endpoints |
| `webhooks.retry` | Retry webhook deliveries |

## 4. Status Codes and Errors

Common responses:

| Status | Meaning |
| --- | --- |
| `200` | Success |
| `201` | Created |
| `204` | Deleted or empty success |
| `401` | Missing, invalid, inactive, or expired token |
| `403` | Token lacks the required ability |
| `404` | Record not found |
| `422` | Validation error |
| `429` | Throttled |

Validation errors use Laravel's standard JSON validation format.

## 5. Pagination

List endpoints use Laravel pagination:

```json
{
  "data": [],
  "links": {},
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 100
  }
}
```

Use `per_page` where supported. Maximums vary by endpoint.

## 6. Slug Reads vs Management Reads

Some resources support both public-safe slug reads and management numeric ID reads. Both still require API tokens.

Public-safe slug reads return only published or active records:

```http
GET /api/v1/products/{slug}
GET /api/v1/categories/{slug}
GET /api/v1/blog/articles/{slug}
GET /api/v1/vehicles/makes/{slug}
```

Numeric ID reads return management records:

```http
GET /api/v1/products/{id}
GET /api/v1/categories/{id}
GET /api/v1/blog/articles/{id}
GET /api/v1/orders/{id}
```

Because route matching uses numeric constraints for ID routes, numeric-looking slugs should be avoided.

## 7. Categories

Abilities:

- read: `categories.read`
- create: `categories.create`
- update: `categories.update`
- delete: `categories.delete`

Endpoints:

```http
GET    /api/v1/categories
GET    /api/v1/categories/{slug}
GET    /api/v1/categories/{id}
POST   /api/v1/categories
PUT    /api/v1/categories/{id}
DELETE /api/v1/categories/{id}
```

List query parameters:

| Query | Description |
| --- | --- |
| `include_inactive=1` | Include inactive categories |
| `per_page` | Default 50, max 100 |

Create/update payload:

```json
{
  "name": "Sensors",
  "slug": "sensors",
  "parent_id": null,
  "description": "Sensor parts",
  "seo_title": "Sensors",
  "meta_description": "Automotive sensors",
  "meta_keywords": "sensors,maf",
  "is_active": true,
  "sort_order": 10
}
```

Response fields include:

```text
id, parent_id, name, slug, description, seo_title, meta_description,
meta_keywords, is_active, sort_order, parent, children,
published_products_count, products_count, created_at, updated_at
```

## 8. Products

Abilities:

- read: `products.read`
- create: `products.create`
- update: `products.update`
- delete: `products.delete`

Endpoints:

```http
GET    /api/v1/products
GET    /api/v1/products/{slug}
GET    /api/v1/products/{id}
POST   /api/v1/products
PUT    /api/v1/products/{id}
DELETE /api/v1/products/{id}
```

List query parameters:

| Query | Description |
| --- | --- |
| `search` | Searches product name, slug, SKU, brand, OEM references, and aftermarket references |
| `category` | Active category slug |
| `make` | Active vehicle make slug |
| `fuel` | Active fuel type slug |
| `sort` | `latest`, `name_asc`, `name_desc` |
| `include_unpublished=1` | Include draft products |
| `per_page` | Default 16, max 50 |

Create/update payload:

```json
{
  "category_id": 1,
  "name": "Mass Air Flow Sensor",
  "slug": "mass-air-flow-sensor",
  "sku": "MAF-1001",
  "brand": "Bosch",
  "short_description": "Reliable airflow sensor.",
  "description": "Detailed technical description.",
  "seo_title": "Mass Air Flow Sensor",
  "meta_description": "OEM quality MAF sensor.",
  "meta_keywords": "maf sensor,bosch",
  "price": 149.9,
  "status": "published",
  "attribute_values": {
    "1": "4"
  },
  "vehicle_fitments": {
    "1": "compatible"
  },
  "references": [
    {
      "type": "oem",
      "number": "13 62 7 520 519",
      "brand": "BMW",
      "notes": null
    }
  ]
}
```

Allowed product statuses:

```text
draft, published
```

Reference types:

```text
oem, aftermarket, cross, alternative
```

Vehicle fitment types:

```text
compatible, incompatible
```

`vehicle_fitments` keys are vehicle variant IDs. Each value must be `compatible` or `incompatible`.

Media uploads:

- Use `new_media[]` for new files.
- On update, use `remove_media_ids[]` to remove existing media.
- Supported MIME types: `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `video/mp4`, `video/webm`.
- Maximum file size: 20 MB.

Example multipart create:

```bash
curl -X POST https://example.com/api/v1/products \
  -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  -F "name=Mass Air Flow Sensor" \
  -F "price=149.90" \
  -F "status=published" \
  -F "new_media[]=@/path/to/image.webp"
```

Product response fields include:

```text
id, name, slug, sku, brand, status, price_minor, currency,
short_description, description, seo_title, meta_description, meta_keywords,
category, media, references, technical_attributes, vehicle_fitments,
created_at, updated_at
```

`price` is accepted on writes as a decimal major-unit value. The API returns `price_minor` and `currency`. The default currency is `EUR` and can be configured with `PARTS_DEFAULT_CURRENCY`.

Creating, updating, and deleting products can dispatch `product.created`, `product.updated`, and `product.deleted` webhooks.

## 9. Dynamic Attributes

Abilities:

- read: `attributes.read`
- create: `attributes.create`
- update: `attributes.update`
- delete: `attributes.delete`

Attribute group endpoints:

```http
GET    /api/v1/attribute-groups
GET    /api/v1/attribute-groups/{id}
POST   /api/v1/attribute-groups
PUT    /api/v1/attribute-groups/{id}
DELETE /api/v1/attribute-groups/{id}
```

Attribute endpoints:

```http
GET    /api/v1/attributes
GET    /api/v1/attributes/{id}
POST   /api/v1/attributes
PUT    /api/v1/attributes/{id}
DELETE /api/v1/attributes/{id}
```

Attribute group payload:

```json
{
  "name": "Electrical",
  "slug": "electrical",
  "description": "Electrical specifications",
  "is_active": true,
  "sort_order": 1
}
```

Attribute payload:

```json
{
  "attribute_group_id": 1,
  "name": "Pin Count",
  "slug": "pin-count",
  "description": "Connector pin count.",
  "type": "number",
  "unit": "pins",
  "is_required": false,
  "is_active": true,
  "sort_order": 1
}
```

Allowed attribute types:

```text
text, number, boolean
```

Required active attributes are enforced when saving products.

## 10. Vehicles

Abilities:

- read: `vehicles.read`
- create: `vehicles.create`
- update: `vehicles.update`
- delete: `vehicles.delete`

Public-safe vehicle reads:

```http
GET /api/v1/vehicles/makes
GET /api/v1/vehicles/makes/{slug}
GET /api/v1/vehicles/variants
```

Vehicle management CRUD endpoints:

```http
GET|POST       /api/v1/fuel-types
GET|PUT|DELETE /api/v1/fuel-types/{id}

GET|POST       /api/v1/body-styles
GET|PUT|DELETE /api/v1/body-styles/{id}

GET|POST       /api/v1/drive-trains
GET|PUT|DELETE /api/v1/drive-trains/{id}

GET|POST       /api/v1/manufacturers
GET|PUT|DELETE /api/v1/manufacturers/{id}

GET|POST       /api/v1/vehicle-makes
GET|PUT|DELETE /api/v1/vehicle-makes/{id}

GET|POST       /api/v1/vehicle-models
GET|PUT|DELETE /api/v1/vehicle-models/{id}

GET|POST       /api/v1/vehicle-generations
GET|PUT|DELETE /api/v1/vehicle-generations/{id}

GET|POST       /api/v1/vehicle-engines
GET|PUT|DELETE /api/v1/vehicle-engines/{id}

GET|POST       /api/v1/vehicle-gearboxes
GET|PUT|DELETE /api/v1/vehicle-gearboxes/{id}

GET|POST       /api/v1/vehicle-variants
GET|PUT|DELETE /api/v1/vehicle-variants/{id}
```

Vehicle list query parameters:

| Endpoint | Query |
| --- | --- |
| `/vehicles/makes` | `include_inactive=1`, `per_page` default 50 max 100 |
| `/vehicles/variants` | `make`, `generation`, `model`, `fuel`, `body_type`, `drive`, `modification`, `engine_code`, `gearbox_code`, `gearbox_type`, `year`, `include_inactive=1`, `per_page` default 25 max 100 |

Base lookup payload for fuel types, body styles, and drive trains:

```json
{
  "name": "Diesel",
  "slug": "diesel",
  "description": "Diesel fuel",
  "is_active": true,
  "sort_order": 1
}
```

Manufacturer payload:

```json
{
  "name": "ZF",
  "slug": "zf",
  "country": "Germany"
}
```

Vehicle make payload:

```json
{
  "name": "BMW",
  "country": "Germany",
  "slug": "bmw",
  "description": "BMW vehicles",
  "is_active": true,
  "sort_order": 1
}
```

Vehicle model payload:

```json
{
  "vehicle_make_id": 1,
  "name": "5 Series",
  "slug": "5-series",
  "description": "BMW 5 Series",
  "is_active": true,
  "sort_order": 1
}
```

Vehicle generation payload:

```json
{
  "vehicle_model_id": 1,
  "code": "E34",
  "name": null,
  "slug": "e34",
  "year_from": 1987,
  "year_to": 1996,
  "notes": "Supports sedan and touring body configurations.",
  "is_active": true,
  "sort_order": 1
}
```

Vehicle engine payload:

```json
{
  "manufacturer_id": null,
  "code": "M51D25",
  "name": null,
  "slug": "m51d25",
  "fuel_type_id": 1,
  "displacement_cc": 2497,
  "displacement_liters": 2.5,
  "cylinders": 6,
  "valves": 12,
  "horsepower": 143,
  "horsepower_from": null,
  "horsepower_to": null,
  "torque_nm": 260,
  "aspiration_type": "Turbo",
  "turbo": true,
  "supercharged": false,
  "engine_layout": "Inline-6",
  "emission_standard": null,
  "production_year_from": 1991,
  "production_year_to": 2000,
  "notes": null,
  "is_active": true,
  "sort_order": 1
}
```

Vehicle gearbox payload:

```json
{
  "manufacturer_id": 1,
  "drive_train_id": 1,
  "code": "GS6-37DZ",
  "name": "GS6-37DZ",
  "slug": "gs6-37dz",
  "gearbox_type": "Manual",
  "gears_count": 6,
  "max_torque_nm": 600,
  "production_year_from": 2001,
  "production_year_to": 2008,
  "oil_capacity_liters": 1.3,
  "notes": "Manual six-speed gearbox.",
  "is_active": true,
  "sort_order": 7
}
```

Allowed gearbox types:

```text
Manual, Automatic, Dual Clutch, CVT
```

Manufacturer response fields include:

```text
id, name, slug, country, engines_count, gearboxes_count,
created_at, updated_at
```

Vehicle gearbox response fields include:

```text
id, manufacturer_id, drive_train_id, manufacturer, drive_train, code, name,
display_name, slug, gearbox_type, gears_count, max_torque_nm,
production_year_from, production_year_to, production_year_range,
oil_capacity_liters, notes, is_active, is_system, sort_order,
variants_count, created_at, updated_at
```

Vehicle variant payload:

```json
{
  "vehicle_make_id": 1,
  "vehicle_model_id": 1,
  "vehicle_generation_id": 1,
  "body_style_id": 1,
  "drive_train_id": 1,
  "vehicle_engine_id": 1,
  "vehicle_gearbox_ids": [1],
  "modification_name": "525tds",
  "year_from": 1991,
  "year_to": 1996,
  "doors": 5,
  "market": "EU",
  "notes": "Verify trim-specific equipment before purchase.",
  "is_active": true,
  "sort_order": 1
}
```

Vehicle variant response fields include:

```text
id, label, make, model, generation, body_style, fuel_type, drive_train,
engine, transmissions, vehicle_make_id, vehicle_model_id,
vehicle_generation_id, body_style_id, body_type, drive_train_id,
vehicle_engine_id, vehicle_gearbox_ids, modification_name, year_from, year_to, year_range,
doors, market, notes, is_active, sort_order, product_fitments_count,
created_at, updated_at
```

Vehicle variants can have multiple transmissions. Each transmission summary includes the transmission row ID, gearbox ID, code, name, slug, gearbox type, gear count, notes, and sort order.

## 11. Blog

Abilities:

- read: `blog.read`
- create: `blog.create`
- update: `blog.update`
- delete: `blog.delete`

Blog category endpoints:

```http
GET|POST       /api/v1/blog/categories
GET|PUT|DELETE /api/v1/blog/categories/{id}
```

Blog tag endpoints:

```http
GET|POST       /api/v1/blog/tags
GET|PUT|DELETE /api/v1/blog/tags/{id}
```

Blog article endpoints:

```http
GET    /api/v1/blog/articles
GET    /api/v1/blog/articles/{slug}
GET    /api/v1/blog/articles/{id}
POST   /api/v1/blog/articles
PUT    /api/v1/blog/articles/{id}
DELETE /api/v1/blog/articles/{id}
```

Article list query parameters:

| Query | Description |
| --- | --- |
| `search` | Searches title, excerpt, and body |
| `category` | Active blog category slug |
| `tag` | Active blog tag slug |
| `include_unpublished=1` | Include drafts and future articles |
| `per_page` | Default 12, max 50 |

Blog category/tag payload:

```json
{
  "name": "Diagnostics",
  "slug": "diagnostics",
  "description": "Troubleshooting guides",
  "is_active": true,
  "sort_order": 1
}
```

Blog article payload:

```json
{
  "blog_category_id": 1,
  "user_id": 1,
  "title": "Symptoms of a failing MAF sensor",
  "slug": "symptoms-of-a-failing-maf-sensor",
  "excerpt": "Diagnostic guide.",
  "body": "Article body.",
  "status": "draft",
  "content_font": "technical",
  "seo_title": "MAF Sensor Symptoms",
  "meta_description": "Learn common MAF sensor failure symptoms.",
  "published_at": null,
  "tag_ids": [1, 2]
}
```

Allowed article statuses:

```text
draft, published
```

Allowed article content fonts:

```text
sans, serif, technical
```

Article media uploads:

- Use `new_media[]` for uploaded files.
- On update, use `remove_media_ids[]` to remove existing media.
- Supported MIME types: `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `video/mp4`, `video/webm`.
- Maximum file size: 20 MB.

Published article slug reads require:

- `status` set to `published`
- `published_at` set
- `published_at` not in the future

Publishing an article can dispatch `article.published`.

## 12. Customers

Abilities:

- read: `customers.read`
- create: `customers.create`
- update: `customers.update`
- delete: `customers.delete`

Endpoints:

```http
GET    /api/v1/customers
GET    /api/v1/customers/{id}
POST   /api/v1/customers
PUT    /api/v1/customers/{id}
DELETE /api/v1/customers/{id}
```

List query parameters:

| Query | Description |
| --- | --- |
| `search` | Searches name, email, phone, city, and country |
| `per_page` | Default 25, max 100 |

Create/update payload:

```json
{
  "name": "Customer User",
  "email": "customer@example.com",
  "phone": "+1555123456",
  "address_line1": "Main St 12",
  "address_line2": "Floor 2",
  "city": "Sofia",
  "postal_code": "1000",
  "country": "Bulgaria",
  "password": "optional-password"
}
```

If password is omitted on create, the system generates a random hashed password. Customers with orders cannot be deleted.

Customer response fields include:

```text
id, name, email, phone, address, orders_count, orders_total_minor,
created_at, updated_at
```

## 13. Orders

Order lookup ability:

- `orders.lookup`

Full order management abilities:

- read: `orders.read`
- create: `orders.create`
- update: `orders.update`
- delete: `orders.delete`

Customer-safe lookup endpoint:

```http
GET /api/v1/orders/{order_number}?customer_email=customer@example.com
```

Full management endpoints:

```http
GET    /api/v1/orders
GET    /api/v1/orders/{id}
POST   /api/v1/orders
PUT    /api/v1/orders/{id}
DELETE /api/v1/orders/{id}
```

Order list query parameters:

| Query | Description |
| --- | --- |
| `search` | Searches order number, customer name, customer email, customer phone |
| `status` | One of the allowed order statuses |
| `per_page` | Default 25, max 100 |

Allowed statuses:

```text
placed, processing, completed, cancelled
```

Create payload:

```json
{
  "user_id": 1,
  "order_number": "ORD-20260701-0001",
  "status": "placed",
  "currency": "EUR",
  "total_minor": null,
  "customer_name": "Customer User",
  "customer_email": "customer@example.com",
  "customer_phone": "+1555123456",
  "address_line1": "Main St 12",
  "address_line2": null,
  "city": "Sofia",
  "postal_code": "1000",
  "country": "Bulgaria",
  "notes": "Call before delivery.",
  "placed_at": "2026-07-01T10:00:00Z",
  "items": [
    {
      "product_id": 1,
      "product_name": null,
      "product_slug": null,
      "sku": null,
      "quantity": 1,
      "unit_price_minor": 14990,
      "line_total_minor": null
    }
  ]
}
```

Update payload uses the same fields, but `order_number` and `status` are required on update.

Order behavior:

- If `order_number` is omitted on create, the system generates one.
- If `status` is omitted on create, it defaults to `placed`.
- If `currency` is omitted, it defaults to `EUR`.
- `subtotal_minor` is recalculated from item line totals.
- `total_minor` defaults to the calculated subtotal when omitted.
- If `line_total_minor` is omitted for an item, it is calculated as `quantity * unit_price_minor`.
- If `product_id` is provided, product name, slug, and SKU can be copied from the product.
- If no `product_id` is provided, `product_name` is required.

Creating and updating orders dispatches `order.created` and `order.updated` webhooks.

## 14. Webhook Management API

Abilities:

- read: `webhooks.read`
- create: `webhooks.create`
- update: `webhooks.update`
- delete: `webhooks.delete`
- retry: `webhooks.retry`

Endpoint management:

```http
GET    /api/v1/webhook-endpoints
GET    /api/v1/webhook-endpoints/{id}
POST   /api/v1/webhook-endpoints
PUT    /api/v1/webhook-endpoints/{id}
DELETE /api/v1/webhook-endpoints/{id}
```

Delivery logs:

```http
GET   /api/v1/webhook-deliveries
GET   /api/v1/webhook-deliveries/{id}
PATCH /api/v1/webhook-deliveries/{id}/retry
```

Webhook endpoint payload:

```json
{
  "name": "ERP Receiver",
  "url": "https://erp.example.com/webhooks/parts",
  "secret": "optional-secret-at-least-16-chars",
  "subscribed_events": ["product.created", "product.updated"],
  "headers": {
    "X-Integration": "erp"
  },
  "is_active": true,
  "max_attempts": 5,
  "timeout_seconds": 10
}
```

If `secret` is omitted on create, the system generates one. Store it securely because it is used to sign outgoing webhook requests.

Supported webhook events:

```text
product.created
product.updated
product.deleted
order.created
order.updated
article.published
```

## 15. Receiving Webhooks

Webhook delivery uses the queue system, so production must run a queue worker:

```bash
php artisan queue:work
```

Webhook request headers:

```http
Content-Type: application/json
User-Agent: Parts-Webhooks/1.0
X-Parts-Webhook-Event: product.created
X-Parts-Webhook-Delivery: 123
X-Parts-Webhook-Timestamp: 1782813600
X-Parts-Webhook-Signature: sha256=...
```

Webhook body:

```json
{
  "event": "product.created",
  "occurred_at": "2026-07-01T10:00:00+00:00",
  "data": {
    "id": 1,
    "name": "Mass Air Flow Sensor",
    "slug": "mass-air-flow-sensor"
  }
}
```

Signature verification:

```php
$signedPayload = $timestamp.'.'.$rawRequestBody;
$expected = 'sha256='.hash_hmac('sha256', $signedPayload, $secret);

if (! hash_equals($expected, $receivedSignature)) {
    abort(401);
}
```

Delivery behavior:

- Failed deliveries retry until the endpoint `max_attempts` is reached.
- Delivery attempts store response status, response body, error message, timestamps, and status.
- Manual retry is available through admin and `PATCH /api/v1/webhook-deliveries/{id}/retry`.

## 16. Rate Limits

Routes are throttled by group:

| Group | Limit |
| --- | --- |
| Read-heavy endpoints | `120` requests per minute |
| Most create/update/delete endpoints | `60` requests per minute |
| Product write endpoints | `30` requests per minute |
| Blog article write endpoints | `30` requests per minute |
| Order lookup | `60` requests per minute |

Clients should handle `429` responses with backoff.

## 17. System Records

The system creates protected "Unknown" records for fallback catalog and vehicle data. These records are marked as system records and cannot be updated or deleted like normal records.

Affected resources include:

```text
categories, products, attribute_groups, attributes, fuel_types,
body_styles, drive_trains, vehicle_makes, vehicle_models,
vehicle_generations, vehicle_engines, vehicle_gearboxes,
vehicle_variants
```

Use normal records for real catalog data. Treat Unknown records as migration and data-integrity fallbacks.

## 18. Plugin Extensions

The platform can load file-based plugins from the configured plugin path:

```env
PARTS_PLUGINS_PATH=/absolute/path/to/plugins
PARTS_ENABLED_PLUGINS=*
```

The default path is `plugins/`. `PARTS_ENABLED_PLUGINS=*` allows every plugin whose manifest has `"enabled": true`; otherwise set a comma-separated list of exact manifest names.

Scaffold a plugin with:

```bash
php artisan parts:make-plugin Vendor/InventorySync
```

Plugin discovery supports manifests one or two directory levels below the plugin path. A generated manifest looks like:

```json
{
  "name": "Vendor/InventorySync",
  "description": "Parts platform plugin.",
  "version": "0.1.0",
  "enabled": true,
  "provider": "PartsPlugins\\Vendor\\InventorySync\\PluginServiceProvider",
  "provider_file": "src/PluginServiceProvider.php",
  "routes": {
    "api": "routes/api.php",
    "web": "routes/web.php"
  },
  "migrations": "database/migrations",
  "api_prefix": "api/v1/plugins/vendor-inventory-sync",
  "web_prefix": "plugins/vendor-inventory-sync",
  "route_name_prefix": "plugins.vendor.inventory.sync."
}
```

Plugin route loading:

- Web routes are loaded with the `web` middleware under `/{web_prefix}`.
- API routes are loaded with the `api` middleware under `/{api_prefix}`.
- API route names receive `.api.` after the plugin route name prefix.
- Plugin migrations are loaded when the configured migrations path exists.
- The scaffolded API health route is `GET /api/v1/plugins/{plugin-slug}/health`.

Security expectations:

- Plugin API routes do not automatically inherit Sanctum token abilities. Add `auth:sanctum`, `abilities:*`, policies, and throttles where sensitive behavior requires them.
- Keep plugin secrets in `.env` or a secure runtime secret store, not in `plugin.json`.
- Treat plugin migrations as production-sensitive and review them before deployment.
- See [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) for the full plugin manifest, route, provider, migration, testing, and deployment guide.

## 19. Integration Examples

Search products by reference number:

```bash
curl -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  "https://example.com/api/v1/products?search=13627520519"
```

Filter products by vehicle make and fuel type:

```bash
curl -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  "https://example.com/api/v1/products?make=bmw&fuel=diesel"
```

Find compatible vehicles for a year and engine code:

```bash
curl -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  "https://example.com/api/v1/vehicles/variants?year=1995&engine_code=M51"
```

Create a webhook endpoint:

```bash
curl -X POST https://example.com/api/v1/webhook-endpoints \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  -d '{
    "name": "ERP Receiver",
    "url": "https://erp.example.com/webhooks/parts",
    "subscribed_events": ["product.created", "order.created"],
    "headers": {"X-Integration": "erp"},
    "is_active": true,
    "max_attempts": 5,
    "timeout_seconds": 10
  }'
```

Lookup an order by order number and customer email:

```bash
curl -H "Accept: application/json" \
  -H "Authorization: Bearer parts_xxx" \
  "https://example.com/api/v1/orders/ORD-20260701-0001?customer_email=customer@example.com"
```

## 20. Development and Verification

Useful local commands:

```bash
composer install
npm install
php artisan migrate
php artisan db:seed
php artisan queue:work
npm run dev
php artisan test
npm run build
```

Project completion checks required by the repository rules:

```bash
composer lint:check
php artisan test
npm run format:check
npm run lint:check
npm run types:check
npm run build
```

Architecture expectations:

- Keep controllers thin.
- Put business behavior in domain actions and services.
- Validate writes with Form Requests.
- Authorize sensitive admin actions with policies.
- Avoid N+1 queries by eager loading resources.
- Do not store secrets outside `.env`.
- Keep generated Wayfinder route/action files in sync with Laravel routes when routes change.
- Run `php artisan route:list` after API, admin, storefront, or plugin route changes.

## 21. Troubleshooting

If an API request returns `401`:

- Confirm the bearer token is present.
- Confirm the token is active.
- Confirm the token has not expired.
- Confirm the token was copied exactly when created.

If an API request returns `403`:

- Confirm the token has the required ability.
- Use `*` only for trusted full-access integrations.

If product writes fail validation:

- Confirm required active attributes are included.
- Confirm `price` is numeric.
- Confirm SKU and slug are unique.
- Confirm reference types are valid.
- Confirm vehicle fitment IDs exist and fitment values are `compatible` or `incompatible`.

If vehicle variant filters return no results:

- Confirm records are active unless using `include_inactive=1`.
- Confirm slug filters use active record slugs.
- Confirm `year` falls between `year_from` and `year_to`.

If webhooks do not arrive:

- Confirm the endpoint is active and subscribed to the event.
- Confirm the queue worker is running.
- Check `/api/v1/webhook-deliveries` or the admin Webhook Logs page.
- Verify receiver TLS, response status, timeout, and signature validation.

If plugin routes do not appear:

- Confirm the plugin manifest JSON is valid.
- Confirm `"enabled": true` in the manifest.
- Confirm `PARTS_ENABLED_PLUGINS` is `*` or includes the exact manifest `name`.
- Confirm the configured plugin path points to the directory that contains plugin manifests.
- Run `php artisan route:list` and check the configured web or API prefix.
