# Stockpilot API - Full Reference Version 1.1.0. Generated from the OpenAPI schema at `/openapi.json`. # Stockpilot API Documentation REST API for multi-channel e-commerce operations. ## Authentication All API endpoints require authentication via API credentials. ### Authentication Method - **Type**: Custom API Key Authentication - **Headers Required**: - `X-CLIENT-ID`: Your client ID - `X-CLIENT-SECRET`: Your client secret ### Error Responses - **401 Unauthorized**: Invalid or missing credentials ## API Overview ### Core Features - **Inventory Management** - CRUD operations with location and threshold management - **Order Processing** - Complete order lifecycle from creation to fulfillment - **Returns Management** - Paginated returns with status and channel filtering - **Product Management** - Product catalog and image management - **Sales Analytics** - Comprehensive sales reporting and forecasting - **Channel Integrations** - Multi-platform sales channel management - **Shipping Operations** - Label generation and fulfillment tracking - **Purchase Orders** - Supplier management and procurement workflows - **Webhooks** - Outbound event delivery with signed payloads ## Data Formats ### Pagination Most list endpoints use page-based pagination: - **Default page size**: 100 items - **Max page size**: 100 items - **Query parameters**: `page` (default: 1), `page_size` (default: 100) ### Date Formats - **Date fields**: ISO format (YYYY-MM-DD) - **DateTime fields**: ISO format with timezone (YYYY-MM-DDTHH:MM:SSZ) ### Location Format Bin locations use hierarchical format: - `"A1-001-01"` - Path A1, Rack 001, Shelf 01 - `"B2-003-05-02"` - Path B2, Rack 003, Shelf 05, Bin 02 ### Threshold Format Stock thresholds support multiple formats: - `"5u"` - Alert when stock drops below 5 units - `"33w"` - Alert when stock estimated to run out in 33 weeks ## Product Identification Multiple endpoints support flexible product lookup: - **id**: Database primary key (integer) - **sku**: Stock Keeping Unit (string) - **barcode**: Product barcode/EAN (string) **Note**: Exactly one identifier must be provided per request. ## Error Handling ### Standard HTTP Status Codes - **200**: Success - **201**: Created - **400**: Bad Request (validation error) - **401**: Unauthorized (authentication failed) - **404**: Not Found - **500**: Internal Server Error ### Error Response Format ```json { "detail": "Error description", "error": "Error message" } ``` ## Rate Limiting Limits are applied **per API key** (per `X-CLIENT-ID` / `X-CLIENT-SECRET` pair) in fixed 60 second windows: - **Single-resource reads** (`/inventory/get`, `/orders/get-single`): 300 requests per minute - **List endpoints** (`/inventory`, `/orders`, `/products`): 120 requests per minute - **Create/update/delete operations**: 60 requests per minute - **Heavy operations** (analytics, label generation, channel sync, invoice send): 20 requests per minute ### Rate Limit Headers Every response reports your position in the current window: - `X-RateLimit-Limit`: Requests allowed per window - `X-RateLimit-Remaining`: Requests remaining in current window - `X-RateLimit-Reset`: Unix timestamp (UTC seconds) when the window resets - `X-RateLimit-Policy`: Which bucket the request was counted against **These limits are advisory today** - no request is rejected with `429`. The headers let you pace an integration ahead of enforcement, which will be announced in advance. ### Idempotency There is no `Idempotency-Key` header. `GET`s and absolute-value updates are safe to retry; creates are not. See **Rate Limits & Idempotency** for the full table. ### Additional Considerations - External API rate limits (Shopify, bol.com, etc.) may further restrict operations - Bulk operations and large datasets may have additional performance considerations - Background task processing respects queue capacity limits ## Async Operations Several endpoints use background processing for: - Purchase order recommendations - Shipping label generation - Analytics calculations - Inventory synchronization Use provided task IDs to monitor completion status. ## Field Organization ### Inventory Items Responses organized in logical groups: - **Product Identification**: id, product_id, item_name, sku, barcode, image - **Location & Threshold**: bin_location, threshold, stock_threshold - **Product Details**: condition, weight, dimensions, hs_code, country_origin, vat_class - **Pricing**: purchase_price, wholesale_price, base_price, retail_price - **Stock Management**: quantity, moq, reserved_quantity, incoming_quantity, backorder_amount - **Status & Timestamps**: is_active, created_at, updated_at ### Order Management - **Order Status**: open, pending, on-hold, completed, cancelled - **Order Deletion**: Remove individual items or entire orders with inventory book-back - **Fulfillment**: shipping carriers, tracking codes, delivery confirmations - **Customer Details**: shipping addresses, contact information, special instructions ### Returns - **Return Status**: `REQUESTED` (nothing handled), `PARTLY_ACCEPTED` (some lines handled), `RETURN_ACCEPTED` (fully handled) - **Status Filtering**: filter values are lowercase - `requested`, `partly_accepted`, `accepted` (maps to `RETURN_ACCEPTED`) - **Channel Filtering**: `handle` and `channel_id` must be supplied together - **Pagination**: `next`/`previous` are booleans - use `current_page` and `total_pages` to page through results ### Analytics Metrics - **Sales Data**: total_orders, total_items, revenue - **Forecasting**: daily, weekly, monthly projections - **Pricing**: average selling price, price ranges - **Channel Breakdown**: per-platform sales analysis ## Integration Examples ### Basic Authentication ```python headers = { "X-CLIENT-ID": "your_client_id", "X-CLIENT-SECRET": "your_client_secret" } ``` ### Pagination ```python params = { "page": 1, "page_size": 100 } ``` ### Product Lookup ```python # By ID params = {"id": 123} # By SKU params = {"sku": "PROD-123"} # By Barcode params = {"barcode": "1234567890"} ``` Visit `/redoc` for detailed interactive documentation. ## Authentication Verify API credentials. ### GET /auth/who-is **Verify API credentials** Verify the provided API credentials and return organization details and feature flags. This endpoint can be used to test if your API credentials are valid and retrieve organization information and enabled features. ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/auth/who-is" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "id": 123, "organization_name": "Acme Corp", "unique_id": "acme-corp-uuid", "features": { "best_before_alerts": true, "purchase_order_management": true, "warehouse_management": false, "create_picking_batch": true, "api_access": true, "b2b_portal": false, "email_campaign": true, "product_feed": false, "accounting_connector": true, "odoo_connector": false } } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Successful authentication - `401` - Missing API credentials - `403` - Invalid credentials - `422` - Validation Error `200` example: ```json { "id": 123, "organization_name": "Acme Corp", "unique_id": "acme-corp-uuid", "features": { "best_before_alerts": true, "purchase_order_management": true, "warehouse_management": false, "create_picking_batch": true, "api_access": true, "b2b_portal": false, "email_campaign": true, "product_feed": false, "accounting_connector": true, "odoo_connector": false } } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `403` example: ```json { "error": "Invalid credentials" } ``` ## Rate Limits & Idempotency Limits are applied **per API key** - per `X-CLIENT-ID` / `X-CLIENT-SECRET` pair - in fixed 60 second windows. An organization running several integrations on separate keys gets these allowances on each of them, and a key can be raised or cut off on its own without disturbing the others. ## Limits | Bucket | Limit | What is in it | | --- | --- | --- | | `read-single` | **300 / min** | Fetching one resource by `id`, `sku` or `barcode` - one upstream call. | | `read-list` | **120 / min** | Paginated collections - one upstream call per page of rows. | | `write` | **60 / min** | Any `POST`, `PUT`, `PATCH` or `DELETE` that is not in `heavy`. | | `heavy` | **20 / min** | Operations that fan out to a system Stockpilot does not control (carriers, marketplaces, mail) or run a large aggregation. These sit behind third-party limits of their own. | A request is bucketed by what it costs upstream, not by its HTTP verb alone. `GET`s that return one resource are cheap and get the highest allowance; anything that reaches a carrier, a marketplace or a mail provider gets the lowest, because those systems impose limits of their own that Stockpilot cannot raise for you. ### Endpoints in `heavy` - `/analytics/items/sales` - `/analytics/product-order-history` - `/analytics/sales-summary` - `/invoices/send` - `/purchase-orders/recommendations` - `/sales-channels/sync-listings` - `/shipping/label-suggestion` - `/shipping/request-label` - `/shipping/retrieve-label` - `/webhooks/{webhook_id}/test` ## Headers Every documented endpoint returns its position in the current window: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per window for this bucket | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Unix timestamp (UTC seconds) when the window resets | | `X-RateLimit-Policy` | Which bucket the request was counted against | ``` X-RateLimit-Limit: 120 X-RateLimit-Remaining: 117 X-RateLimit-Reset: 1756819200 X-RateLimit-Policy: read-list ``` ## Enforcement **The limits are advisory today.** Stockpilot does not return `429` and does not drop requests over the line - the headers report your usage so you can pace an integration before enforcement begins. Enforcement will be announced in advance, and per-account ceilings are agreed during onboarding; the numbers above are the defaults. Because the counters are held per API instance, `X-RateLimit-Remaining` is a **lower bound** on what you actually have left, never an overstatement. ### Pacing an integration - Read `X-RateLimit-Remaining` and slow down as it approaches zero, rather than discovering a limit by hitting it. - Sleep until `X-RateLimit-Reset` instead of retrying immediately. - Prefer one paginated `read-list` call over N `read-single` calls when you need many records - it is cheaper for both sides, whatever the bucket allowances suggest. - Back off on `5xx` with jittered exponential delays. A retry storm is the failure mode these limits exist to contain. ## Idempotency There is **no `Idempotency-Key` header today.** Do not send one and assume it is honoured. What is safe to retry depends on the operation: | Operation | Safe to blind-retry? | | --- | --- | | Any `GET` | Yes. | | `POST /inventory/update`, and the `PATCH` updates on orders, customers and bundles | Yes. These set absolute values rather than applying deltas, so replaying one converges on the same state. | | `POST /orders/create`, `POST /purchase-orders`, `POST /bundles/create` | **No.** A retry creates a second record. | | `POST /orders/{order_pk}/items/add`, `move-to-backorder`, `move-from-backorder` | **No.** These apply a change relative to current state. | | `POST /shipping/request-label` | **No.** A retry can buy a second label. | If a create times out or fails ambiguously, **poll before retrying** - `GET /orders` filtered to the customer and window, or `GET /purchase-orders` - and only resend if the record is genuinely absent. Webhook deliveries carry a `delivery_id` for exactly this reason; the REST create endpoints have no equivalent handle yet. Idempotency keys and account-specific limits are part of the onboarding conversation - if your integration needs guaranteed-once creates, raise it there and we will size it with you. ## Products Product catalog and product images. ### GET /products **Get paginated list of products** Returns a paginated list of products (Parent objects). ## Query Parameters: - **page**: Page number for pagination (starts at 1) - **page_size**: Number of items per page (max 100) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/products?page=1&page_size=50" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "count": 50, "next": "https://api.stockpilot.dev/products?page=2", "previous": null, "results": [ { "id": 123, "title": "Premium Wireless Headphones", "description": "High-quality wireless headphones", "brand": 1, "brand_name": "TechBrand", "category": 2, "category_name": "Electronics", "is_active": true, "image_url": "https://example.com/product.jpg" } ] } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number | | `page_size` | query | integer | no | Items per page | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Products list retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 50, "next": "https://api.stockpilot.dev/products?page=2", "results": [ { "id": 123, "title": "Premium Wireless Headphones", "description": "High-quality wireless headphones with noise cancellation", "brand": 1, "brand_name": "TechBrand", "category": 2, "category_name": "Electronics", "is_active": true, "image_url": "https://example.com/product.jpg", "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-20T14:45:00Z" } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### POST /products/create **Create a new product** Create a new product in your organization. **Default Behavior for Optional Fields:** - **Brand**: If `brand` is not provided, null, or 0, the system will automatically create or use a default brand named "Brandless" for your organization - **Category**: If `category` is not provided, null, or 0, the system will automatically create or use a default category named "No category" for your organization - **Description**: Can be left empty if not needed - **is_active**: Defaults to `true` if not specified **Examples:** - Minimal payload: `{"title": "My Product"}` - will use default brand and category - Full payload: `{"title": "My Product", "brand": 123, "category": 456, "description": "Great product", "is_active": true}` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID for authentication | | `x-client-secret` | header | string | yes | Your API client secret for authentication | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | Product title | | `description` | string | no | Product description | | `brand` | integer | no | Brand ID. If not provided or empty, a default 'Brandless' brand will be created for your organization | | `category` | integer | no | Category ID. If not provided or empty, a default 'No category' category will be created for your organization | | `is_active` | boolean | no | Whether the product is active | **Responses** - `200` - Returns the newly created product with ID and title - `201` - Product created successfully - `400` - Validation error - `401` - Authentication failed - Invalid client ID or secret - `422` - Validation Error - `500` - Internal server error or upstream service error `201` example: ```json { "product_id": 789, "title": "Premium Wireless Headphones" } ``` `400` example: ```json { "title": [ "This field is required." ] } ``` ### GET /products/get **Get product by ID** Retrieve a single product (Parent) by its internal ID. ## Query Parameters: - **id**: Internal product ID (required) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/products/get?id=123" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "id": 123, "title": "Premium Wireless Headphones", "description": "High-quality wireless headphones", "brand": 1, "brand_name": "TechBrand", "category": 2, "category_name": "Electronics", "is_active": true, "image_url": "https://example.com/product.jpg" } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | query | integer | yes | Internal product ID | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Product found - `400` - Missing product ID - `401` - Missing API credentials - `404` - Product not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 123, "title": "Premium Wireless Headphones", "description": "High-quality wireless headphones with noise cancellation", "brand": 1, "brand_name": "TechBrand", "category": 2, "category_name": "Electronics", "is_active": true, "image_url": "https://example.com/product.jpg", "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-20T14:45:00Z" } ``` `400` example: ```json { "detail": "Missing product ID" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Product not found." } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### POST /products/{product_id}/set-image **Set image for a product** Set the image of a product (parent) via direct upload or remote URL. You can: - Upload an image file via `multipart/form-data` - Provide an `image_url` as JSON Only one method should be used per request. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `product_id` | path | integer | yes | Product (Parent) ID | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (optional) **Responses** - `200` - Uploads or fetches the product image - `422` - Validation Error ## Categories & Brands Product categories and brands. ### GET /brands **List all brands** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Responses** - `200` - Returns a list of brands - `422` - Validation Error ### POST /brands/create **Create a brand** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | **Responses** - `200` - Returns the created brand ID - `422` - Validation Error ### GET /categories **List all categories** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Responses** - `200` - Returns a list of product categories - `422` - Validation Error ### POST /categories/create **Create a category** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | **Responses** - `200` - Returns the created category ID - `422` - Validation Error ## Inventory Inventory items, stock levels, locations and thresholds. ### GET /inventory **List inventory items** Retrieve a paginated list of inventory items with location and threshold features. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of items per page (max 100) * **created_at**: Filter by creation date (optional, YYYY-MM-DD format) ## Paging to the end Page until `current_page == total_pages`. Requesting a page beyond `total_pages` returns `404`, so read `total_pages` from the first response rather than probing. Note that `next` and `previous` are **booleans**, not URLs - the DRF convention implies URLs, but this API does not return them. Increment `page` yourself. ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/inventory?page=1&page_size=50&created_at=2023-01-15" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "count": 2150, "next": true, "previous": false, "current_page": 1, "total_pages": 22, "results": [ { "id": 12345, "product_id": 456, "item_name": "Premium Wireless Headphones", "sku": "WH-PREM-001", "barcode": "5901234123457", "barcode_type": "EAN", "image": "https://example.com/image.jpg", "bin_location": ["A1-001-01", "B2-003-05"], "threshold": "5u", "condition": "new", "hs_code": "8518300095", "country_origin": "CN", "weight": "0.300", "item_length": "20.00", "item_width": "15.00", "item_height": "8.00", "purchase_price": "45.00", "wholesale_price": "75.00", "base_price": "99.99", "retail_price": "99.99", "vat_class": "standard_rate", "quantity": 150, "moq": 1, "stock_threshold": 5, "backorder_amount": 0, "reserved_quantity": 3, "incoming_quantity": 50, "is_active": true, "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-20T14:45:00Z" } ] } ``` ## Field Groups Each inventory item includes fields organized in logical groups: - **Product Identification**: id, product_id, item_name, sku, barcode, barcode_type, image - **Location & Threshold**: bin_location, threshold, stock_threshold - **Product Details**: condition, hs_code, country_origin, weight, dimensions, vat_class - **Pricing**: purchase_price, wholesale_price, base_price, retail_price - **Stock Management**: quantity, moq, reserved_quantity, incoming_quantity, backorder_amount - **Status & Timestamps**: is_active, created_at, updated_at **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `created_at` | query | string | no | Filter by creation date (YYYY-MM-DD) | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Inventory list retrieved successfully - `400` - Invalid pagination parameter - `401` - Missing API credentials - `404` - Requested page is beyond the last page - compare current_page with total_pages before requesting the next one - `422` - Validation Error - `500` - Internal server error - `502` - Upstream service could not be reached - `504` - Upstream service did not respond within 10 seconds `200` example: ```json { "results": [ { "id": 12345, "product_id": 456, "item_name": "Premium Wireless Headphones", "sku": "WH-PREM-001", "barcode": "5901234123457", "barcode_type": "EAN", "image": "https://example.com/image.jpg", "bin_location": [ "A1-001-01", "B2-003-05" ], "threshold": "5u", "condition": "NEW", "quantity": 150, "base_price": 99.99, "is_active": true } ], "count": 2150, "next": true, "previous": false, "current_page": 1, "total_pages": 22 } ``` `400` example: ```json { "error": "Failed to fetch inventory" } ``` `404` example: ```json { "error": "Failed to fetch inventory" } ``` `502` example: ```json { "error": "Upstream service error" } ``` `504` example: ```json { "error": "Inventory service temporarily unavailable", "detail": "Request timed out, please retry" } ``` ### POST /inventory/create **Create a new inventory item** Create a new inventory item in the system. ## Request Body Fields: - **product_id**: Database ID of parent product (required) - **item_name**: Product display name (required) - **sku**: Stock Keeping Unit (required, must be unique) - **barcode**: Product barcode/EAN (optional) - **barcode_type**: Type of barcode (e.g., "EAN", "UPC") - **quantity**: Initial stock quantity (default: 0) - **stock_threshold**: Minimum stock alert level - **moq**: Minimum order quantity (default: 1) - **base_price**: Selling price (required) - **purchase_price**: Cost price (optional) - **wholesale_price**: B2B price (optional) - **weight**: Product weight in kg (optional) - **length/width/height**: Dimensions in cm (optional) - **condition**: Product condition (default: "new") - **vat_class**: VAT classification (default: "standard_rate") - **is_active**: Active status (default: true) ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/inventory/create" -H "Content-Type: application/json" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -d '{"product_id": 456, "item_name": "Premium Wireless Headphones", "sku": "WH-PREM-001", "barcode": "5901234123457", "barcode_type": "EAN", "quantity": 100, "stock_threshold": 10, "moq": 1, "base_price": "99.99", "purchase_price": "45.00", "wholesale_price": "75.00", "weight": "0.3", "length": "20.00", "width": "15.00", "height": "8.00", "condition": "new", "vat_class": "standard_rate", "is_active": true}' ``` ## Response Example: ```json { "item_id": 12346, "sku": "WH-PREM-001", "message": "Inventory item created successfully" } ``` ## Error Responses: - **400**: Invalid request payload, duplicate SKU, missing required fields - **401**: Missing or invalid authentication credentials - **500**: Internal server error **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `product_id` | integer | yes | | | `sku` | string | yes | | | `item_name` | string | yes | | | `barcode` | string | yes | | | `barcode_type` | string | no | | | `quantity` | integer | no | | | `moq` | integer | no | | | `stock_threshold` | integer | no | | | `purchase_price` | number | no | | | `wholesale_price` | number | no | | | `base_price` | number | no | | | `weight` | string | no | | | `length` | number | no | | | `width` | number | no | | | `height` | number | no | | | `condition` | string | no | | | `vat_class` | string | no | | | `is_active` | boolean | no | | **Responses** - `200` - Returns the newly created inventory item ID and details - `201` - Inventory item created successfully - `400` - Invalid request payload - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `201` example: ```json { "id": 12346, "product_id": 457, "sku": "NEW-PROD-001", "message": "Inventory item created successfully" } ``` `400` example: ```json { "detail": "Invalid product data provided" } ``` ### GET /inventory/get **Get single inventory item by barcode, SKU, or ID** Fetch a single inventory item by ID, SKU, or barcode with location and threshold features. ## Query Parameters (choose exactly one): - **id**: Internal Stockpilot product ID - **sku**: SKU of the product - **barcode**: EAN/barcode of the product At least one of the above must be provided. ## cURL Examples: ```bash # Get by SKU curl -X GET "https://api.stockpilot.dev/inventory/get?sku=WH-PREM-001" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" # Get by ID curl -X GET "https://api.stockpilot.dev/inventory/get?id=12345" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" # Get by Barcode curl -X GET "https://api.stockpilot.dev/inventory/get?barcode=5901234123457" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "id": 12345, "product_id": 456, "item_name": "Premium Wireless Headphones", "sku": "WH-PREM-001", "barcode": "5901234123457", "barcode_type": "EAN", "image": "https://example.com/image.jpg", "bin_location": ["A1-001-01", "B2-003-05"], "threshold": "5u", "condition": "new", "hs_code": "8518300095", "country_origin": "CN", "weight": "0.300", "item_length": "20.00", "item_width": "15.00", "item_height": "8.00", "purchase_price": "45.00", "wholesale_price": "75.00", "base_price": "99.99", "retail_price": "99.99", "vat_class": "standard_rate", "quantity": 150, "moq": 1, "stock_threshold": 5, "backorder_amount": 0, "reserved_quantity": 3, "incoming_quantity": 50, "is_active": true, "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-20T14:45:00Z" } ``` ## Response Structure Returns a comprehensive inventory item with fields organized in logical groups: - **Product Identification**: Basic product info and identifiers - **Location & Threshold**: bin location and threshold management - **Product Details**: Physical characteristics and classification - **Pricing**: All pricing tiers (purchase, wholesale, base, retail) - **Stock Management**: Current stock levels and management settings - **Status & Timestamps**: Active status and audit timestamps ## Error Responses: - **400**: Missing or invalid identifier - **404**: Inventory item not found **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | query | integer | no | Internal product ID | | `sku` | query | string | no | Stock Keeping Unit | | `barcode` | query | string | no | Barcode / EAN | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Item retrieved successfully - `400` - Invalid request - missing identifier - `404` - Item not found - `422` - Validation Error `200` example: ```json { "backorder_amount": 0, "barcode": "5901234123457", "barcode_type": "EAN", "base_price": 99.99, "bin_location": [ "A1-001-01", "B2-003-05" ], "condition": "NEW", "country_origin": "CN", "created_at": "2026-01-15T10:30:00Z", "hs_code": "8518300000", "id": 12345, "image": "https://example.com/image.jpg", "incoming_quantity": 100, "is_active": true, "item_height": 80.0, "item_length": 200.0, "item_name": "Premium Wireless Headphones", "item_width": 180.0, "moq": 1, "product_id": 456, "purchase_price": 45.0, "quantity": 150, "reserved_quantity": 25, "retail_price": 129.99, "sku": "WH-PREM-001", "stock_threshold": 10, "threshold": "5u", "updated_at": "2026-02-19T14:45:00Z", "vat_class": "standard_rate", "weight": "250.0", "wholesale_price": 75.0 } ``` `400` example: ```json { "detail": "At least one of id, sku or barcode must be provided." } ``` `404` example: ```json { "detail": "Inventory item not found" } ``` ### POST /inventory/update **Update inventory item** Update an inventory item's details with location and threshold management. ## Required Identifier At least one identifier (id, sku, or barcode) must be provided. ## Standard Update Fields * **id**: Internal product ID (optional) * **sku**: Stock Keeping Unit (optional) * **barcode**: EAN/barcode (optional) * **quantity**: New stock quantity (optional) * **base_price**: New base price (optional) * **weight**: Product weight (optional) * **condition**: Product condition (optional) * **is_active**: Product status (optional) ## Location & Threshold Management * **assign_bin_location**: Assign new bin location using format like "A1-001-01" (path-rack-shelf or path-rack-shelf-bin) * **remove_bin_location**: Remove existing bin location using format like "A1-001-01" * **threshold**: Set new threshold using format like "5u" for 5 units or "33w" for 33 weeks ## Location Format Examples * "A1-001-01" - Path A1, Rack 001, Shelf 01 * "B2-003-05-02" - Path B2, Rack 003, Shelf 05, Bin 02 ## Threshold Format Examples * "5u" - Alert when stock drops below 5 units * "33w" - Alert when stock is estimated to run out in 33 weeks * "10u" - Alert when stock drops below 10 units ## Implementation Notes * Bin locations use LocationEntry model for multiple locations per product * Threshold format validates against unit-based ("Nu") and weeks-based ("Nw") patterns * Location assignments are additive - use remove_bin_location to remove specific locations * Invalid formats return appropriate error responses with guidance **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | integer | no | Internal Stockpilot product ID | | `item_name` | string | no | Name of the item | | `sku` | string | no | Stock Keeping Unit | | `barcode` | string | no | Product barcode (EAN) | | `quantity` | integer | no | Stock quantity | | `base_price` | number | no | Base price | | `retail_price` | number | no | Retail price | | `purchase_price` | number | no | Purchase price | | `wholesale_price` | number | no | Wholesale price | | `sale_price` | number | no | Sale price | | `weight` | string | no | Weight as string, (default in grams)' | | `length` | number | no | Length (default in mm) | | `width` | number | no | Width (default in mm) | | `height` | number | no | Height (default in mm) | | `vat_class` | string | no | VAT class name (e.g., 'standard_rate') | | `condition` | string | no | Condition (e.g., 'NEW') | | `stock_threshold` | integer | no | Minimum stock threshold for alerts | | `moq` | integer | no | Minimum order quantity | | `assign_bin_location` | string | no | Assign bin location using format like 'A1-001-01' (path-rack-shelf or path-rack-shelf-bin) | | `remove_bin_location` | string | no | Remove bin location using format like 'A1-001-01' (path-rack-shelf or path-rack-shelf-bin) | | `threshold` | string | no | Set threshold using format like '5u' for 5 units or '33w' for 33 weeks | | `is_active` | boolean | no | Is the product active | Example: ```json { "assign_bin_location": "A1-001-01", "base_price": 19.95, "condition": "NEW", "height": 2.5, "is_active": true, "item_name": "My Blue Shirt", "length": 30.0, "moq": 1, "purchase_price": 10.0, "quantity": 25, "sku": "TSHIRT-BLUE-M", "stock_threshold": 3, "threshold": "33w", "vat_class": "standard_rate", "weight": "0.3", "width": 20.0 } ``` **Responses** - `200` - Inventory item updated successfully - `400` - Invalid request payload - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "product_id": 101, "sku": "TSHIRT-BLUE-M", "updated_fields": [ "quantity", "base_price", "assign_bin_location", "threshold" ] } ``` `400` example: ```json { "detail": "Must provide at least one identifier: id, sku, or barcode" } ``` ### POST /inventory/{item_id}/set-image **Set image for an inventory item** Set an image for a given inventory item by either: - Uploading an image file (`image_file`) - Providing a remote image URL (`image_url`) Both methods are supported, but only one should be used per request. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item_id` | path | integer | yes | Inventory item ID | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (optional) **Responses** - `200` - Uploads or fetches an image for the given inventory item - `422` - Validation Error ## Bundles Bundled products and their component items. ### GET /bundles/ **Get Bundles** Get paginated list of bundles. Returns all bundles with their items included. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number | | `page_size` | query | integer | no | Number of bundles per page | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error ### POST /bundles/create **Create Bundle** Create a new bundle. Creates a new bundle with the provided information. Items can be added separately. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Bundle name | | `sku` | string | yes | Bundle SKU | | `barcode` | string | yes | Bundle barcode | | `description` | string | no | Bundle description | | `purchase_price` | number | no | Bundle purchase price | | `wholesale_price` | number | no | Bundle wholesale price | | `is_active` | boolean | no | Whether the bundle is active | | `is_b2b` | boolean | no | Whether the bundle is B2B | Example: ```json { "barcode": "1234567890123", "description": "Complete gaming setup with headphones and mouse", "is_active": true, "is_b2b": false, "name": "Gaming Setup Bundle", "purchase_price": 75.0, "sku": "BUNDLE-GAMING-001", "wholesale_price": 120.0 } ``` **Responses** - `200` - Successful Response - `422` - Validation Error `200` example: ```json { "barcode": "1234567890123", "description": "Complete gaming setup with headphones and mouse", "id": 123, "image_url": "https://example.com/bundle-image.jpg", "is_active": true, "is_b2b": false, "items": [ { "product_barcode": "1111111111111", "product_id": 456, "product_name": "Gaming Headphones", "product_sku": "HEADPHONES-GAMING", "quantity_in_bundle": 1 }, { "product_barcode": "2222222222222", "product_id": 789, "product_name": "Gaming Mouse", "product_sku": "MOUSE-GAMING", "quantity_in_bundle": 1 } ], "name": "Gaming Setup Bundle", "purchase_price": 75.0, "quantity": 50, "sku": "BUNDLE-GAMING-001", "ws_price": 120.0 } ``` ### GET /bundles/{bundle_id} **Get Bundle Detail** Get detailed information about a specific bundle. Returns bundle details including all items in the bundle. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error `200` example: ```json { "barcode": "1234567890123", "description": "Complete gaming setup with headphones and mouse", "id": 123, "image_url": "https://example.com/bundle-image.jpg", "is_active": true, "is_b2b": false, "items": [ { "product_barcode": "1111111111111", "product_id": 456, "product_name": "Gaming Headphones", "product_sku": "HEADPHONES-GAMING", "quantity_in_bundle": 1 }, { "product_barcode": "2222222222222", "product_id": 789, "product_name": "Gaming Mouse", "product_sku": "MOUSE-GAMING", "quantity_in_bundle": 1 } ], "name": "Gaming Setup Bundle", "purchase_price": 75.0, "quantity": 50, "sku": "BUNDLE-GAMING-001", "ws_price": 120.0 } ``` ### PATCH /bundles/{bundle_id} **Update Bundle** Update an existing bundle. Partially updates bundle information with the provided fields. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | Bundle name | | `sku` | string | no | Bundle SKU | | `barcode` | string | no | Bundle barcode | | `description` | string | no | Bundle description | | `purchase_price` | number | no | Bundle purchase price | | `ws_price` | number | no | Bundle wholesale price | | `is_active` | boolean | no | Whether the bundle is active | | `is_b2b` | boolean | no | Whether the bundle is B2B | Example: ```json { "description": "Updated complete gaming setup", "is_active": true, "name": "Updated Gaming Bundle", "purchase_price": 80.0, "ws_price": 130.0 } ``` **Responses** - `200` - Successful Response - `422` - Validation Error ### DELETE /bundles/{bundle_id} **Delete Bundle** Delete a bundle. Permanently deletes a bundle and all its associated items and relationships. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error ### POST /bundles/{bundle_id}/items/add **Add Bundle Items** Add multiple products to a bundle. Adds or updates the quantities of multiple products in the specified bundle. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | yes | List of items to add to the bundle | Example: ```json { "items": [ { "product_id": 456, "quantity": 2 }, { "product_id": 789, "quantity": 1 } ] } ``` **Responses** - `200` - Successful Response - `422` - Validation Error ### DELETE /bundles/{bundle_id}/items/{product_id}/delete **Remove Bundle Item** Remove a product from a bundle. Removes the specified product from the bundle completely. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `product_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error ### PUT /bundles/{bundle_id}/items/{product_id}/update **Update Bundle Item** Update the quantity of a product in a bundle. Updates the quantity of an existing product in the bundle. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `bundle_id` | path | integer | yes | | | `product_id` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | | | `X-CLIENT-SECRET` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `quantity` | integer | yes | New quantity of the product in the bundle | Example: ```json { "quantity": 3 } ``` **Responses** - `200` - Successful Response - `422` - Validation Error `200` example: ```json { "barcode": "1234567890123", "description": "Complete gaming setup with headphones and mouse", "id": 123, "image_url": "https://example.com/bundle-image.jpg", "is_active": true, "is_b2b": false, "items": [ { "product_barcode": "1111111111111", "product_id": 456, "product_name": "Gaming Headphones", "product_sku": "HEADPHONES-GAMING", "quantity_in_bundle": 1 }, { "product_barcode": "2222222222222", "product_id": 789, "product_name": "Gaming Mouse", "product_sku": "MOUSE-GAMING", "quantity_in_bundle": 1 } ], "name": "Gaming Setup Bundle", "purchase_price": 75.0, "quantity": 50, "sku": "BUNDLE-GAMING-001", "ws_price": 120.0 } ``` ## Warehouses Warehouses and their stock. ### GET /warehouses/get **List warehouses** Retrieve a paginated list of warehouses. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of warehouses per page (max 100) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/warehouses/get?page=1&page_size=10" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "name": "Main Warehouse", "unique_id": "main-wh-uuid-123", "location_code": "NL-AMS", "country": "NL", "default": true, "inventory_source": "default", "sales_channel_handle": "shopify", "sales_channel_id": 123 } ] } ``` ## Returns A JSON object containing: * List of warehouses with location and configuration details * Pagination information (count, next, previous) * Total count of warehouses **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Warehouses list retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 3, "results": [ { "id": 1, "name": "Main Warehouse", "unique_id": "main-wh-uuid-123", "location_code": "NL-AMS", "country": "NL", "default": true, "inventory_source": "default", "sales_channel_handle": "shopify", "sales_channel_id": 123, "address": "123 Warehouse Street", "city": "Amsterdam", "zipcode": "1000AA", "created_at": "2023-01-15T10:30:00Z" }, { "id": 2, "name": "Secondary Warehouse", "unique_id": "sec-wh-uuid-456", "location_code": "NL-RTM", "country": "NL", "default": false, "inventory_source": "external", "sales_channel_handle": "woocommerce", "sales_channel_id": 456, "address": "456 Storage Ave", "city": "Rotterdam", "zipcode": "3000BB", "created_at": "2023-02-01T14:20:00Z" } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### GET /warehouses/{unique_id}/items **List warehouse items** Retrieve a paginated list of warehouse items. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of items per page (max 100) * **unique_id**: Unique ID for this warehouse * **sku**: Optional filter by SKU (product identifier) * **barcode**: Optional filter by barcode (product identifier) ## Returns A JSON object containing: * List of warehouse items * Pagination information * Total count ## Note You can filter by either SKU or barcode, but not both. Both parameters are optional. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `unique_id` | path | string | yes | Unique ID for this warehouse | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `sku` | query | string | no | Filter by SKU (product identifier) | | `barcode` | query | string | no | Filter by barcode (product identifier) | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Warehouses items retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error ## Orders Order lifecycle from creation through fulfilment. ### GET /orders **List orders** Retrieve a paginated list of orders with optional status filtering. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of orders per page (max 100) * **status**: Comma-separated list of order statuses to filter by (e.g., "open,pending,on-hold,cancelled,completed") * **is_forwarded**: Include forwarded orders (default true) * **commission**: Include commission data per line item (default false) ## Returns A JSON object containing: * List of orders * Pagination information * Total count ## Commissions `has_commissions` (order level) and `commissions` (line item level) are always present. Without `commission=true` they are always `false` / `null`. * **fee**: string, per-unit commission in EUR. Line total is `fee` x `quantity`. * **percentage**: integer, so a 14.5% rate shows as 14. Treat as approximate, not exact. * **commissions: null**: no commission resolved for that line. Render blank, not 0.00. * **has_commissions**: true if any line resolved. Use it to decide whether to show a commission column. Only Bol returns data for now, all other channels return `null`. Field names differ per channel (Bol sends `fee`/`percentage`), so key off the order's `handle` and don't assume `fee` exists on every commission object. The value is an estimate: it is the commission on the product's listing price at last sync, not on what the order actually sold for. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `status` | query | string | no | Filter orders by status (comma-separated for multiple statuses) | | `is_forwarded` | query | string | no | Include forwarded orders (default True) | | `commission` | query | boolean | no | Commission per line item (default False) | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Orders list retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 100, "next": "https://api.stockpilot.dev/orders?page=2", "results": [ { "id": 123, "order_id": "ORD-2023-001", "customer_name": "John Doe", "customer_email": "john@example.com", "is_processing": true, "is_completed": false, "is_cancelled": false, "is_forwarded": false, "created_at": "2023-01-15T10:30:00Z", "channel": "Shopify", "order_total": "209.97", "order_details": { "ship_street": "123 Main St", "ship_city": "Amsterdam", "ship_country": "NL", "order_items": [ { "product_name": "Premium Headphones", "quantity": 2, "retail_price": "99.99" } ] } } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### PUT /orders/cancel-order **Cancel order** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_pk` | integer | yes | Order primary key | | `reason_code` | string | no | Cancellation reason code | **Responses** - `200` - Successful Response - `422` - Validation Error ### GET /orders/cancellation-request **Check cancellation request** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | query | integer | yes | | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error ### GET /orders/cancellation-requests **Get orders with cancellation requests** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Responses** - `200` - Successful Response - `422` - Validation Error ### POST /orders/create **Create a new order** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `billing` | object | yes | | | `shipping` | object | yes | | | `customer_email` | string | yes | | | `customer_phone` | string | no | | | `vat_number` | string | no | | | `shipping_total` | string | no | | | `shipping_method` | string | no | | | `customer_note` | string | no | | | `line_items` | array of object | yes | | **Responses** - `200` - Returns the created order ID and primary key - `422` - Validation Error ### POST /orders/fulfil **Fulfil an order** Register fulfillment for an order with shipping and tracking details. ## Request Body * **order_pk**: Order primary key as integer (provide either this OR order_number) * **order_number**: Order number as string (provide either this OR order_pk) * **fulfilled_at**: ISO datetime string of fulfillment (optional) * **service**: Shipping service provider name (optional) * **carrier_code**: Carrier code (optional) * **carrier_name**: Human-readable carrier name (optional) * **shipping_method**: Shipping method (optional) * **shipment_type**: Shipment type (optional) * **tracking_code**: Tracking number (optional) * **tracking_url**: Tracking URL (optional, defaults to "https://www.no-tracking-url.com/") * **items**: List of fulfilled items with SKU and quantity (optional) ## Example Requests Using order_pk: ```json { "order_pk": 12345, "fulfilled_at": "2024-03-20T14:30:00Z", "carrier_name": "PostNL", "tracking_code": "3SDEUT987654321", "items": [ {"sku": "PROD-001", "quantity": 1} ] } ``` Using order_number: ```json { "order_number": "ORD-12345", "fulfilled_at": "2024-03-20T14:30:00Z", "carrier_name": "PostNL", "tracking_code": "3SDEUT987654321", "items": [ {"sku": "PROD-001", "quantity": 1} ] } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `order_pk` | integer | no | Order primary key (integer) | | `order_number` | string | no | Order number (string) | | `fulfilled_at` | string | no | ISO datetime string | | `service` | string | no | Shipping service provider name (e.g., Sendcloud) | | `carrier_code` | string | no | Carrier code (e.g., POSTNL) | | `carrier_name` | string | no | Human-readable carrier name (e.g., PostNL) | | `shipping_method` | string | no | Shipping method (e.g., Standard, Evening) | | `shipment_type` | string | no | Shipment type (e.g., Parcel, Mailbox) | | `tracking_code` | string | no | Tracking number | | `tracking_url` | string | no | Tracking URL | | `items` | array of object | no | | **Responses** - `200` - Order fulfilled successfully - `400` - Invalid request payload - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error ### GET /orders/fulfillment **Get fulfillment details** Retrieve fulfillment info for a specific order. ### Query Parameters - `order_pk`: Internal Stockpilot order PK (Primary Key) - Can be found in the url of order details page. ### Response example ```json { "order_id": 123, "carrier": "PostNL", "tracking_number": "3SYZ123456789", "service": "postnl-42" } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | query | integer | yes | Internal order primary key | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns fulfillment metadata for the given order - `422` - Validation Error ### GET /orders/get-single **Get single order** Retrieve a single order by either order_pk or order_number. ## Parameters * **order_pk**: Order primary key (integer) - provide either this OR order_number * **order_number**: Order number (string) - provide either this OR order_pk ## cURL Examples: ```bash # Get by order_pk curl -X GET "https://api.stockpilot.dev/orders/get-single?order_pk=12345" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" # Get by order_number curl -X GET "https://api.stockpilot.dev/orders/get-single?order_number=ORD-12345" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "id": 123,o "order_id": "ORD-2023-001", "customer_name": "John Doe", "customer_email": "john@example.com", "is_processing": true, "is_completed": false, "is_cancelled": false, "created_at": "2023-01-15T10:30:00Z", "order_details": { "ship_street": "123 Main St", "ship_city": "Amsterdam", "ship_country": "NL", "order_items": [ { "product_name": "Premium Headphones", "quantity": 2, "retail_price": "99.99" } ] } } ``` ## Returns A JSON object containing the complete order details including customer information, items, and status. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | query | integer | no | Order primary key | | `order_number` | query | string | no | Order number | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Order retrieved successfully - `400` - Must provide either order_pk or order_number - `401` - Missing API credentials - `404` - Order not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 123, "order_id": "ORD-2023-001", "customer_name": "John Doe", "customer_email": "john@example.com", "is_processing": true, "is_completed": false, "is_cancelled": false, "created_at": "2023-01-15T10:30:00Z", "order_details": { "ship_street": "123 Main St", "ship_city": "Amsterdam", "ship_country": "NL", "ship_zipcode": "1000AA", "order_items": [ { "product_name": "Premium Headphones", "sku": "WH-PREM-001", "quantity": 2, "retail_price": "99.99", "total_price": "199.98" } ], "order_total": "199.98", "shipping_cost": "9.99", "total_with_shipping": "209.97" } } ``` `400` example: ```json { "detail": "Must provide either order_pk or order_number" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Order not found" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### PATCH /orders/ordered-items/{item_id}/update **Update ordered item details** Update details of a specific ordered item (quantity, refund etc.). ## Parameters * **item_id**: ID of the ordered item to update * **payload**: JSON payload with item details to update **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `item_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) **Responses** - `200` - Ordered item updated successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error ### DELETE /orders/{order_id} **Delete entire order** Delete an entire order and all its items with optional inventory book-back. This endpoint removes the complete order including all items, order details, and order call records. Optionally returns all item quantities to inventory. ## Parameters * **order_id**: Order primary key (integer) * **book_back**: Whether to return inventory to stock (default True) ## Request Body Example: ```json { "book_back": true } ``` ## cURL Example: ```bash curl -X DELETE "https://api.stockpilot.dev/orders/12345" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"book_back": true}' ``` ## Response Example: ```json { "message": "Order deleted successfully", "order_id": "ORD-12345", "order_pk": 12345, "book_back_quantity": 15 } ``` ## ⚠️ Warning This operation is **irreversible**. The order and all associated data will be permanently deleted. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `book_back` | boolean | no | Whether to book back inventory (default True) | Example: ```json { "book_back": true } ``` **Responses** - `200` - Order deleted successfully - `400` - Bad request - invalid parameters - `401` - Missing API credentials - `404` - Order not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "book_back_quantity": 15, "message": "Order deleted successfully", "order_id": "ORD-12345", "order_pk": 67890 } ``` ### DELETE /orders/{order_id}/items/{item_id} **Delete order item** Delete a specific item from an order with optional inventory book-back. This endpoint removes an order item and optionally returns inventory to stock. It also recalculates order totals, updates backorder status, and creates a timeline entry. ## Parameters * **order_id**: Order primary key (integer) * **item_id**: Order item ID to delete * **book_back_quantity**: Quantity to return to inventory (defaults to item quantity) * **book_back**: Whether to return inventory to stock (default True) ## Request Body Example: ```json { "book_back_quantity": 5, "book_back": true } ``` ## cURL Example: ```bash curl -X DELETE "https://api.stockpilot.dev/orders/12345/items/67890" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"book_back_quantity": 2, "book_back": true}' ``` ## Response Example: ```json { "message": "Order item deleted successfully", "item_id": 67890, "book_back_quantity": 2 } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `item_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `book_back_quantity` | integer | no | Quantity to book back to inventory (defaults to item quantity) | | `book_back` | boolean | no | Whether to book back inventory (default True) | Example: ```json { "book_back": true, "book_back_quantity": 5 } ``` **Responses** - `200` - Order item deleted successfully - `400` - Bad request - invalid parameters - `401` - Missing API credentials - `404` - Order or item not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "book_back_quantity": 5, "item_id": 12345, "message": "Order item deleted successfully" } ``` ### POST /orders/{order_id}/move-from-backorder **Move order items from backorder** Move specific order items from backorder status back to normal fulfillment. This endpoint removes items from backorder when stock becomes available. It validates that sufficient stock exists before moving items and updates inventory levels and backorder amounts accordingly. ## Parameters * **order_id**: Order primary key (integer) * **get_out_items**: List of items to move from backorder with quantities ## Request Body Example: ```json { "get_out_items": [ {"item_id": 123, "quantity": 3}, {"item_id": 456, "quantity": 1} ] } ``` ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/orders/12345/move-from-backorder" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"get_out_items": [{"item_id": 123, "quantity": 3}]}' ``` ## Response Example: ```json { "message": "Successfully moved 0 item(s) out of backorder", "total_moved": 0, "backorder_status": "full", "results": [ {"item_id": 123, "success": False, "error": "No stock available"} ] } ``` ## Notes - Items can only be moved from backorder if sufficient stock is available - The system will validate stock levels before processing the request - Backorder amounts and inventory levels are updated automatically **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `get_out_items` | array of object | yes | List of items to move from backorder | Example: ```json { "get_out_items": [ { "item_id": 123, "quantity": 3 }, { "item_id": 456, "quantity": 1 } ] } ``` **Responses** - `200` - Items moved from backorder successfully - `400` - Bad request - invalid parameters or insufficient stock - `401` - Missing API credentials - `404` - Order not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "backorder_status": "none", "message": "Successfully moved 2 item(s) out of backorder", "results": [ { "item_id": 123, "quantity_moved": 2, "remaining_backorder": 0, "success": true } ], "total_moved": 2 } ``` ### POST /orders/{order_id}/move-to-backorder **Move order items to backorder** Move specific order items to backorder status. This endpoint moves order items to backorder when they cannot be fulfilled due to insufficient stock. The system will update backorder amounts for affected products and create appropriate timeline entries for tracking. ## Parameters * **order_id**: Order primary key (integer) * **backorder_items**: List of items to move to backorder with quantities ## Request Body Example: ```json { "backorder_items": [ {"item_id": 123, "quantity": 5}, {"item_id": 456, "quantity": 2} ] } ``` ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/orders/12345/move-to-backorder" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"backorder_items": [{"item_id": 123, "quantity": 5}]}' ``` ## Response Example: ```json { "message": "Successfully moved 2 item(s) to backorder", "items_processed": 2, "backorder_status": "partial", "backorder_results": [ {"item_id": 123, "quantity_moved": 5, "new_backorder_qty": 5}, {"item_id": 456, "quantity_moved": 2, "new_backorder_qty": 2} ] } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `backorder_items` | array of object | yes | List of items to move to backorder | Example: ```json { "backorder_items": [ { "item_id": 123, "quantity": 5 }, { "item_id": 456, "quantity": 2 } ] } ``` **Responses** - `200` - Items moved to backorder successfully - `400` - Bad request - invalid parameters - `401` - Missing API credentials - `404` - Order not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "backorder_results": [ { "item_id": 123, "new_backorder_qty": 2, "quantity_moved": 2 } ], "backorder_status": "partial", "items_processed": 2, "message": "Successfully moved 2 item(s) to backorder" } ``` ### PATCH /orders/{order_id}/update-customer-details **Update order customer details** Update billing, shipping and contact information for a specific order. ## Parameters * **order_id**: ID of the order to update * **payload**: JSON payload with customer details to update **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) **Responses** - `200` - Customer details updated successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error ### PATCH /orders/{order_id}/update-forwarding **Update order forwarding status** Update order forwarding status and metadata. This endpoint marks an order as forwarded and prevents Stockpilot from fetching duplicate orders from the marketplace. The register_order_id will be prefixed with 'FWD-' in the system. ## Parameters * **order_id**: ID of the order to update * **register_order_id**: Order number from marketplace (prevents duplicate fetching) * **source**: Source identifier (typically 'api') **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `register_order_id` | string | yes | Order number from marketplace (will be prefixed with 'FWD-') | | `source` | string | yes | Source identifier (typically 'api') | Example: ```json { "register_order_id": "MP-12345", "source": "api" } ``` **Responses** - `200` - Order forwarding status updated successfully - `400` - Bad request - missing required fields - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "forwarded_order_id": "FWD-MP-12345", "message": "Forwarding details updated.", "to_forwarded_channel": "api" } ``` ### PATCH /orders/{order_id}/update-status **Update order status** **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | | | `x-client-secret` | header | string | yes | | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `open` \| `pending` \| `completed` \| `cancelled` | yes | New status for the order | **Responses** - `200` - Order status updated successfully - `422` - Validation Error ### POST /orders/{order_pk}/items/add **Add item to order** Add a new item to an existing order. This endpoint adds a product to an order, updates inventory levels (reduces available quantity, increases reserved), recalculates order totals and taxes, creates timeline entry for audit trail, and updates picklist automatically. ## Parameters * **order_pk**: Order primary key (integer) * **product_id**: ID of the product to add * **quantity**: Number of items to add (must be > 0) * **retail_price**: Price per item * **vat_rate**: VAT rate percentage (defaults to 21) ## Request Body Example: ```json { "product_id": 123, "quantity": 2, "retail_price": 29.99, "vat_rate": 21 } ``` ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/orders/12345/items/add" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"product_id": 123, "quantity": 2, "retail_price": 29.99, "vat_rate": 21}' ``` ## Response Example: ```json { "message": "Order item added successfully", "item_id": 456, "product_name": "Product Name - Variant", "quantity": 2, "retail_price": 29.99, "total_price": 59.98 } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `product_id` | integer | yes | Product ID to add to order | | `quantity` | integer | yes | Number of items to add (must be > 0) | | `retail_price` | number | yes | Price per item | | `vat_rate` | integer | no | VAT rate percentage (defaults to 21) | Example: ```json { "product_id": 123, "quantity": 2, "retail_price": 29.99, "vat_rate": 21 } ``` **Responses** - `200` - Successful Response - `201` - Order item added successfully - `400` - Bad request - invalid parameters - `401` - Missing API credentials - `404` - Order or product not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "item_id": 456, "message": "Order item added successfully", "product_name": "Product Name - Variant", "quantity": 2, "retail_price": 29.99, "total_price": 59.98 } ``` `201` example: ```json { "item_id": 456, "message": "Order item added successfully", "product_name": "Product Name - Variant", "quantity": 2, "retail_price": 29.99, "total_price": 59.98 } ``` ### POST /orders/{order_pk}/items/swap **Swap order item product** Replace a product in an existing order item with a different product. This endpoint swaps the product in an order item while maintaining original quantity and pricing. It handles inventory adjustments for both old and new products, creates timeline entry for audit trail, and updates product name display automatically. ## Parameters * **order_pk**: Order primary key (integer) * **item_id**: ID of the order item to modify * **new_product_id**: ID of the new product to swap to ## Request Body Example: ```json { "item_id": 456, "new_product_id": 789 } ``` ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/orders/12345/items/swap" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"item_id": 456, "new_product_id": 789}' ``` ## Response Example: ```json { "message": "Order item product swapped successfully", "item_id": 456, "old_product_name": "Old Product Name", "new_product_name": "New Product Name - Variant" } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `item_id` | integer | yes | ID of the order item to modify | | `new_product_id` | integer | yes | ID of the new product to swap to | Example: ```json { "item_id": 456, "new_product_id": 789 } ``` **Responses** - `200` - Order item product swapped successfully - `400` - Bad request - invalid parameters - `401` - Missing API credentials - `404` - Order, item, or new product not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "item_id": 456, "message": "Order item product swapped successfully", "new_product_name": "New Product Name - Variant", "old_product_name": "Old Product Name" } ``` ## Returns Return orders with status and channel filtering. ### GET /returns **List returns** Retrieve a paginated list of returns for your organization, newest first. Returns that are not linked to an order are excluded. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of returns per page (max 100) * **status**: Comma-separated list of return statuses to filter by: * `requested` - nothing handled yet, needs action * `partly_accepted` - some lines handled, some not * `accepted` - fully handled * **handle**: Sales channel handle to filter by (e.g. "shopify", "bol") * **channel_id**: Sales channel ID to filter by **Note**: `handle` and `channel_id` must be supplied together - sending only one is rejected with a 400 rather than silently returning unfiltered results. ## Returns A JSON object containing: * List of returns, each with its order and line items * Pagination information (`current_page`, `total_pages`) * Total count Each return carries an uppercase `status` of `REQUESTED`, `PARTLY_ACCEPTED` or `RETURN_ACCEPTED` (note that the filter values above are lowercase and that `accepted` maps to `RETURN_ACCEPTED`). Per-line `items[].is_handled` shows which lines are done. `next` and `previous` are booleans, not URLs - increment `page` to page through results. `product_id` and `sku` are null when a line could not be matched to a product; fall back to `title`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `status` | query | string | no | Filter returns by status (comma-separated): requested, partly_accepted, accepted | | `handle` | query | `channel` \| `amazon` \| `bol` \| `mirakl` \| `etsy` \| `kaufland` \| `shopify` \| `woocommerce` \| `b2b-portal` | no | Sales channel handle. Must be used together with channel_id | | `channel_id` | query | integer | no | Sales channel ID. Must be used together with handle | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Returns list retrieved successfully - `400` - Invalid filter or page request - `401` - Missing or invalid API credentials - `403` - Organization tier does not include API access - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 42, "next": true, "previous": false, "current_page": 1, "total_pages": 5, "results": [ { "id": 1234, "return_id": "R2026-1003", "status": "PARTLY_ACCEPTED", "is_handled": false, "custom_return": true, "created_at": "2026-08-30T10:12:00Z", "handle": "bol", "channel_id": 12, "order": { "id": 987, "order_number": "SP-1029", "channel_order_number": "3012345678", "customer_name": "Jane Doe" }, "items_count": 2, "total_quantity": 3, "items": [ { "id": 55, "title": "Blue Mug", "rma_id": "R2026-1003-55", "quantity": 1, "reason": "Damaged", "is_handled": true, "tracking_provider": "PostNL", "tracking_code": "3SABC123", "product_id": 4410, "sku": "MUG-BLUE" } ] } ] } ``` `400` example: ```json { "error": "Invalid page request", "details": "Requested page 9999 but only 5 pages available", "total_pages": 5, "total_count": 42 } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `403` example: ```json { "detail": "Upgrade to Growth tier or higher for API access" } ``` `500` example: ```json { "error": "Internal server error" } ``` ## Purchase Orders Supplier procurement workflows. ### GET /purchase-orders **Get purchase orders list** Get list of purchase orders with filtering options. Rows come back under `results`, wrapped in the standard paginated envelope: `count`, `current_page`, `total_pages`, and `next` / `previous` (both `false` when there is no further page rather than a URL). Filter with `status` and `supplier_id`. Every row also has `delivered_date`, which stays `null` until someone marks the order delivered - it is omitted from the example below because null-valued fields are stripped when this schema is rendered. ## Delivery progress Every row carries `total_delivered`, `total_remaining` and `fully_delivered`, which is enough to render a progress bar or a "12 / 30 delivered" badge without a per-row detail fetch. Note that delivering goods does **not** advance `status` - a fully delivered purchase order stays `ORDERED` until someone explicitly marks it delivered. Use `fully_delivered` / `total_remaining` to tell whether goods have arrived, not `status`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | string | no | Filter by status | | `supplier_id` | query | integer | no | Filter by supplier ID | | `page` | query | integer | no | Page number | | `page_size` | query | integer | no | Items per page | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Purchase orders retrieved successfully - `401` - Missing or invalid API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 10, "next": false, "previous": false, "current_page": 1, "total_pages": 1, "results": [ { "id": 48, "cart_id": "000009", "supplier": { "id": 6, "name": "Example Supplier Ltd", "contact_name": "John Doe" }, "status": "ORDERED", "order_total": 0.0, "shipping_total": 0.0, "processed_by": "Sander Hegeman", "created_at": "2026-08-13T18:41:31.401525+00:00", "updated_at": "2026-08-13T18:41:31.409214+00:00", "expected_date": "2026-08-26T22:00:00+00:00", "items_count": 1, "total_units": 2, "total_delivered": 2, "total_remaining": 0, "fully_delivered": true }, { "id": 42, "cart_id": "000002", "supplier": { "id": 6, "name": "Example Supplier Ltd", "contact_name": "John Doe" }, "status": "ORDERED", "order_total": 11.32, "shipping_total": 0.0, "processed_by": "Sander Hegeman", "created_at": "2026-04-12T16:10:06.794594+00:00", "updated_at": "2026-04-12T16:10:06.804489+00:00", "expected_date": "2026-04-18T22:00:00+00:00", "items_count": 0, "total_units": 0, "total_delivered": 1, "total_remaining": 1, "fully_delivered": false } ] } ``` ### POST /purchase-orders **Create purchase order** Create a new purchase order with flexible product identification. ## Product Identification Each item in the order can be identified using ANY ONE of the following methods: * **product_id**: Database ID (legacy method, continues to work) * **barcode**: Product barcode/EAN for lookup * **sku**: Product SKU for lookup ## Validation Rules * **Exactly one identifier required**: Must provide product_id OR barcode OR sku (not multiple) * **Product must exist**: Product must be active and associated with the specified supplier * **Supplier association**: Product must be linked to the supplier for the purchase order * **MOQ constraints**: Minimum order quantities are automatically applied ## Delivery warehouse `delivery_warehouse_id` is optional. When omitted the purchase order is delivered to the organization's default warehouse, which matters for multi-warehouse organizations - use `GET /warehouses` to look up the id you want. `expected_delivery` is optional too and can safely be left out. ## Request Examples ### Mixed Identifier Types ```json { "supplier_id": 123, "items": [ { "product_id": 456, "quantity": 10, "purchase_price": 25.50, "supplier_reference": "SUP-REF-123" }, { "barcode": "123456789", "quantity": 5, "purchase_price": 12.25 }, { "sku": "WIDGET-001", "quantity": 20 } ], "delivery_warehouse_id": 3 } ``` ### Barcode Scanner Integration ```json { "supplier_id": 123, "items": [ {"barcode": "987654321", "quantity": 15}, {"barcode": "567890123", "quantity": 8} ] } ``` ## Error Handling Detailed error messages include item index for easy debugging: * `"Item 0: Must provide one of: product_id, barcode, or sku"` * `"Item 1: Provide only one identifier, not multiple"` * `"Item 2: Product not found (sku: INVALID-SKU)"` ## Benefits * **Flexibility**: Use the most convenient product identifier for your workflow * **Integration-friendly**: Perfect for barcode scanners and external systems * **Backwards compatible**: Existing product_id usage continues to work * **Clear validation**: Detailed error messages help identify issues quickly Creates purchase order with inbound tracking, validates product availability, applies MOQ constraints, and returns confirmation details. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `supplier_id` | integer | yes | Supplier ID | | `items` | array of object | yes | List of items to order with flexible product identification | | `order_note` | string | no | Optional order note | | `shipping_cost` | number | no | Shipping cost | | `expected_delivery` | string | no | Expected delivery date (ISO format) | | `delivery_warehouse_id` | integer | no | Warehouse the goods are delivered to. Defaults to the organization's default warehouse when omitted | | `processed_by` | string | no | Who processed the order | **Responses** - `200` - Creates a new purchase order with flexible product identification - `201` - Purchase order created successfully - `400` - Invalid request payload with detailed error information - `401` - Missing or invalid API credentials - `422` - Validation Error - `500` - Internal server error `201` example: ```json { "order_id": 12345, "order_number": "PO-2026-001", "supplier_id": 123, "total_amount": 315.5, "items_count": 3, "status": "PENDING", "created_at": "2026-02-19T14:30:00Z" } ``` `400` example: ```json { "detail": "Item 0: Must provide one of: product_id, barcode, or sku" } ``` ### POST /purchase-orders/recommendations **Start purchase order recommendations generation** Start generating purchase order recommendations (async task). Expected payload: ```json { "supplier_id": 123, "lead_time": 7, "durability": 14, "scope_days": 30, "include_flagged": false, "include_inbound": true } ``` Returns task_id for monitoring progress via `/recommendations/status/{task_id}`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) **Responses** - `200` - Starts async task to generate purchase order recommendations and returns task_id - `422` - Validation Error ### GET /purchase-orders/recommendations/status/{task_id} **Get recommendations task status** Get the status and result of a recommendations task. Returns: - `PROCESSING`: Task is still running - `COMPLETED`: Task finished successfully with results - `FAILED`: Task failed with error details **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `task_id` | path | string | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns the status and result of a recommendations generation task - `422` - Validation Error ### GET /purchase-orders/suppliers/list **Get suppliers list** Get list of suppliers for purchase order creation. Returns all suppliers with their contact information and product counts. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns list of suppliers for purchase order creation - `422` - Validation Error ### GET /purchase-orders/{order_id} **Get purchase order details** Get detailed information for a specific purchase order. Includes order items, supplier information, and inbound tracking details. ## Delivered quantities Each entry in `items` carries the delivery state of that line: | Field | Meaning | | --- | --- | | `delivered_quantity` | Cumulative quantity actually delivered into stock | | `remaining_quantity` | Still outstanding (ordered - delivered, floored at 0) | | `dispatched_quantity` | Claimed by an in-flight parcel but not yet applied to stock | | `invoiced_quantity` | Quantity on the supplier invoice | `inbound_tracking` is an object, not a list. It rolls the whole purchase order up into `total_ordered` / `total_delivered` / `total_remaining` / `total_parcels`, then carries two arrays: `items` (per product: `ordered`, `incoming_units`, `delivered`, `invoiced`, `remaining`) and `parcels` (every delivery registered against the order, each with `parcel_id`, `index_number`, `reference` and `status`). `totals` repeats `total_delivered`, `total_remaining` and `fully_delivered` alongside the money fields. `inbound_tracking.parcels` is where you get the `parcel_id` for `POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete`. This endpoint is the completion signal after creating a parcel: poll it rather than `parcel.status`, which stays `PROCESSING`. See `POST /purchase-orders/{order_id}/parcels/create`. A non-zero `dispatched_quantity` means a parcel is mid-flight. If it never clears, that parcel is stuck - see `POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete`. ## `status` is not a delivery signal Delivering goods does not advance `status` - a fully delivered purchase order stays `ORDERED` until someone explicitly marks it delivered in the Stockpilot UI, because marking it delivered has accounting side effects. To tell whether goods have arrived, read `totals.fully_delivered` and `totals.total_remaining`, never `status`. `delivered_date` follows `status`, not the goods: it stays `null` on a fully delivered order until someone marks it delivered. It is absent from the example below only because null-valued fields are stripped when this schema is rendered. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Purchase order retrieved successfully - `401` - Missing or invalid API credentials - `404` - Purchase order not found for this organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 48, "cart_id": "000009", "supplier": { "id": 6, "name": "Example Supplier Ltd", "contact_name": "John Doe", "order_email": "orders@example.com" }, "status": "ORDERED", "order_total": 0.0, "shipping_total": 0.0, "processed_by": "Sander Hegeman", "order_note": "", "created_at": "2026-08-13T18:41:31.401525+00:00", "updated_at": "2026-08-13T18:41:31.409214+00:00", "expected_date": "2026-08-26T22:00:00+00:00", "items": [ { "product_id": 68573, "sku": "ABC-1", "product_name": "Widget - Blue", "supplier_reference": "SUP-REF-123", "ordered_quantity": 2, "purchase_price": 0.0, "line_total": 0.0, "current_stock": 4, "backorder_amount": 0, "available_stock": 4, "recent_sales_30_days": 0, "delivered_quantity": 2, "remaining_quantity": 0, "dispatched_quantity": 0, "invoiced_quantity": 0 } ], "inbound_tracking": { "total_ordered": 2, "total_delivered": 2, "total_remaining": 0, "total_parcels": 1, "expected_delivery": "2026-08-26T22:00:00+00:00", "items": [ { "product_id": 68573, "ordered": 2, "incoming_units": 0, "delivered": 2, "invoiced": 0, "remaining": 0 } ], "parcels": [ { "parcel_id": "042252", "index_number": 1, "reference": "D-2026-000009-1", "status": "PROCESSING" } ] }, "totals": { "items_count": 1, "total_units": 2, "subtotal": 0.0, "total_with_shipping": 0.0, "total_delivered": 2, "total_remaining": 0, "fully_delivered": true } } ``` ### POST /purchase-orders/{order_id}/parcels/create **Create a parcel against a purchase order** Register a delivery against a purchase order, in full or in part. A **parcel** is one delivery against the purchase order. Send the quantities that arrived; a partial delivery is simply a parcel for less than the outstanding quantity, and a purchase order can have as many parcels as it takes to arrive in full. ## Partial parcel Send the lines that arrived. Identify each line by **exactly one** of `product_id`, `sku` or `barcode` - sending two is a `400`. ```json { "items": [ {"product_id": 456, "quantity": 4}, {"sku": "ABC-1", "quantity": 2} ], "reference": "PACKSLIP-88213" } ``` * `quantity` must be a positive integer and cannot exceed what is still outstanding on that line. * `reference` is optional and is stored on the parcel - one is generated when omitted. ## Full parcel Omit `items` entirely and everything still outstanding is taken: ```json {"reference": "PACKSLIP-88213"} ``` An empty body `{}` does the same. ## Asynchronous by default (202) Processing is asynchronous. A `202` means the parcel was queued and **the stock has not moved yet**: `applied` is `false`, `delivered_quantity` is the pre-parcel number, and the `projected_delivered_quantity` / `projected_remaining_quantity` fields are optimistic UI values rather than facts. Do **not** poll `parcel.status` - it stays `PROCESSING` and is not a completion signal. Re-fetch `GET /purchase-orders/{order_id}` instead to see confirmed quantities. ## Synchronous mode (200) Add `"async": false` and the request blocks until the stock has actually moved, returning `200` with `applied: true` and the real `delivered_quantity` / `remaining_quantity` per line (instead of the `projected_*` fields). Synchronous mode runs backorder allocation, warehouse writes and an external accounting sync inline, so it can be slow. Prefer the default async mode for large purchase orders. ## Suggested flow 1. `GET /purchase-orders/{order_id}` and pre-fill the parcel inputs with `remaining_quantity`. 2. `POST /purchase-orders/{order_id}/parcels/create` with the adjusted quantities. 3. On `202`, show the `projected_*` numbers optimistically in a "processing" state. 4. Re-fetch the purchase order detail a few seconds later for the confirmed `delivered_quantity`. 5. `fully_delivered` is `true` once nothing is outstanding. ## Errors | Code | Meaning | | --- | --- | | 400 | Bad or missing identifier, non-positive quantity, product not on this purchase order, duplicate product, over-delivery, or nothing left to receive | | 404 | Purchase order does not exist for this organization | | 409 | A parcel is already being processed on this purchase order | | 500 | Failed to queue the parcel | Error bodies are `{"error": "..."}`, and item-level messages are prefixed with the item index, e.g. `"Item 1: cannot receive 11, only 6 remaining for ABC-1"`. The `409` is worth handling explicitly: only one parcel can be in flight per purchase order at a time, including one a warehouse user started in the Stockpilot UI. Surface it as "a delivery is currently being processed for this purchase order, try again shortly" rather than a generic failure, and do not retry in a tight loop. The body tells you what is blocking you - `blocking_parcel_id` and `dispatched_lines` (the quantities that parcel has claimed against each line). Almost always the right response is to wait a few seconds and re-fetch `GET /purchase-orders/{order_id}`. If the same quantities are still dispatched well after the fact, that parcel never completed and `POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete` clears it. The `hint` field spells out that same call, with the upstream `/api` prefix. ## Delivering does not change the purchase order status A fully delivered purchase order stays `ORDERED` until someone explicitly marks it delivered or completed, because marking it delivered has accounting side effects. Drive your UI off `fully_delivered` / `total_remaining` rather than `status`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of object | no | Lines that arrived in this parcel. Omit entirely to take everything still outstanding on the purchase order | | `reference` | string | no | Reference stored on the parcel, e.g. a packing slip number. Auto-generated when omitted | | `async` | boolean | no | When true (default) the parcel is queued and the endpoint returns 202 with projected quantities. Set to false to block until stock has actually moved and receive 200 with confirmed quantities | **Responses** - `200` - Synchronous parcel (`async: false`) - stock has been moved, quantities are confirmed - `202` - Parcel queued (default). Stock has **not** moved yet - `applied` is `false` and the `projected_*` fields are optimistic - `400` - Validation error - bad or missing identifier, non-positive quantity, product not on this purchase order, duplicate product, over-delivery, or nothing left to receive - `404` - Purchase order does not exist for this organization - `409` - A parcel is already being processed on this purchase order. The body names the blocking parcel and the quantities it claimed - `422` - Validation Error - `500` - Failed to queue the parcel `200` example: ```json { "purchase_order_id": 48, "cart_id": "1042", "applied": true, "parcel": { "parcel_id": "748219", "index_number": 3, "reference": "PACKSLIP-88213", "status": "PROCESSING" }, "items": [ { "product_id": 456, "sku": "ABC-1", "quantity": 4, "ordered_quantity": 10, "delivered_quantity": 4, "remaining_quantity": 6 } ], "totals": { "total_ordered": 10, "total_delivered": 4, "total_remaining": 6, "fully_delivered": false }, "failed_items": [], "status": "COMPLETED" } ``` `202` example: ```json { "purchase_order_id": 48, "cart_id": "1042", "applied": false, "parcel": { "parcel_id": "748219", "index_number": 3, "reference": "PACKSLIP-88213", "status": "PROCESSING" }, "task_id": "3f2a9c10-8b4e-4d2f-9a77-1c0b5e2d4a31", "items": [ { "product_id": 456, "sku": "ABC-1", "quantity": 4, "ordered_quantity": 10, "delivered_quantity": 0, "projected_delivered_quantity": 4, "projected_remaining_quantity": 6 } ], "totals": { "total_ordered": 10, "total_delivered": 0, "projected_total_delivered": 4, "projected_total_remaining": 6, "projected_fully_delivered": false }, "status": "PROCESSING", "message": "Parcel queued. Poll GET /purchase-orders/48 for applied quantities." } ``` `400` example: ```json { "error": "Item 1: cannot receive 11, only 6 remaining for ABC-1" } ``` `404` example: ```json { "error": "Purchase order not found" } ``` `409` example: ```json { "error": "A parcel is already being processed for this purchase order", "blocking_parcel_id": "748219", "dispatched_lines": [ { "product_id": 456, "sku": "ABC-1", "dispatched_quantity": 4 } ], "hint": "Wait for the in-flight parcel to finish, or remove it with POST /api/purchase-orders/48/parcels/748219/delete" } ``` `500` example: ```json { "error": "Failed to queue parcel for processing: broker unavailable" } ``` ### POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete **Remove an unprocessed parcel** Remove a parcel that was never applied to stock, releasing the quantities it claimed. Creating a parcel happens in two steps: the quantities are *dispatched* against each purchase order line, then a worker applies them to stock. If that second step never runs the quantities stay dispatched, and because only one parcel may be in flight per purchase order at a time, every later `POST /purchase-orders/{order_id}/parcels/create` returns `409`. Removing the parcel releases those quantities so deliveries can continue. Dispatched quantities are visible as `dispatched_quantity` on each line of `GET /purchase-orders/{order_id}`, and in the `dispatched_lines` array of the `409` body, which also names the `blocking_parcel_id` to pass here. Failing that, every parcel on the order is listed with its `parcel_id` and `status` under `inbound_tracking.parcels`. ## This does not reverse stock Only quantities that were never applied are released. Anything a completed parcel already delivered stays delivered - `delivered_quantity` and `total_delivered` are untouched. A parcel whose `status` is `COMPLETED` is refused with a `409`. Removal is permanent: the parcel is deleted rather than marked failed, so it disappears from `total_parcels` and from the purchase order's parcel history. The delivery it represented has to be re-created with `POST /purchase-orders/{order_id}/parcels/create`. ## Releases every dispatched line on the purchase order The release is scoped to the purchase order, not to this parcel alone: every line with a non-zero `dispatched_quantity` is reset, whichever parcel claimed it. In practice only one parcel is ever in flight at a time, so this is the same set - but do not use this endpoint to prune one of several pending parcels. ## Do not reach for this on every 409 A `409` from `POST /purchase-orders/{order_id}/parcels/create` normally means a parcel really is in flight, quite possibly one a warehouse user started in the Stockpilot UI seconds ago - removing it throws away their work. Treat this as a recovery tool for a parcel that is genuinely stuck: 1. On `409`, note `blocking_parcel_id`, wait a few seconds and re-fetch `GET /purchase-orders/{order_id}`. 2. If `delivered_quantity` moved, the parcel completed - nothing to remove. 3. If the same `dispatched_quantity` values are still there well after the fact, the parcel never completed. `POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete`. 4. Re-create the delivery with `POST /purchase-orders/{order_id}/parcels/create`. ## Errors | Code | Meaning | | --- | --- | | 400 | The purchase order has no inbound tracking record | | 404 | Purchase order or parcel does not exist for this organization | | 409 | The parcel is `COMPLETED` - its quantities are in stock and cannot be released | Error bodies are `{"error": "..."}`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_id` | path | integer | yes | | | `parcel_id` | path | string | yes | | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Parcel removed and its dispatched quantities released - `400` - The purchase order has no inbound tracking record - `404` - Purchase order or parcel does not exist for this organization - `409` - The parcel has already been applied to stock and cannot be removed - `422` - Validation Error `200` example: ```json { "purchase_order_id": 48, "cart_id": "1042", "removed_parcel_id": "748219", "released": [ { "product_id": 456, "sku": "ABC-1", "released_quantity": 4 } ], "total_released": 4, "message": "Parcel removed. You can create a new parcel for this purchase order now." } ``` `400` example: ```json { "error": "Purchase order has no inbound tracking record" } ``` `404` example: ```json { "error": "Purchase order not found" } ``` `409` example: ```json { "error": "This parcel has already been completed and cannot be removed" } ``` ## Invoices Invoice retrieval and sending. ### GET /invoices/get/{order_pk} **Get invoice for an order** Retrieve an invoice PDF for a specific order. Returns a PDF file stream. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | path | integer | yes | | | `X-CLIENT-ID` | header | string | yes | Your API client ID | | `X-CLIENT-SECRET` | header | string | yes | Your API client secret | **Responses** - `200` - Invoice retrieved successfully - returns PDF file stream - `401` - Missing or invalid API credentials - `404` - Invoice not found for the specified order - `422` - Validation Error - `500` - Internal server error or upstream service error `401` example: ```json { "detail": "Missing required headers: X-CLIENT-ID, X-CLIENT-SECRET" } ``` `404` example: ```json { "success": false, "message": "Invoice not found" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### POST /invoices/send **Send invoice for an order** Send an invoice for a specific order. ## Parameters * **order_pk**: Order primary key as integer (form data) * **invoice**: Invoice PDF file (optional). If provided, Stockpilot will use this invoice, otherwise it creates a new one from Stockpilot data ## Behavior - If `invoice` file is provided: Uses the uploaded PDF as the invoice - If `invoice` file is not provided: Stockpilot automatically generates a new invoice from order data ## Returns A JSON object containing: * **success**: Boolean indicating operation success * **message**: Descriptive message about the operation result **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) **Responses** - `200` - Invoice sent successfully - `400` - Failed to send invoice - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "success": true, "message": "Invoice sent successfully" } ``` `400` example: ```json { "success": false, "message": "Failed to send invoice" } ``` ## Shipping Shipping integrations and label generation. ### GET /shipping/integrations **List available shipping integrations** Returns all configured shipping integrations (e.g. PostNL, DHL, Sendcloud, Bol.com) for the authenticated organization. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns shipping carriers configured for the organization - `422` - Validation Error ### GET /shipping/label-suggestion **Get label suggestion based on shipping rules** Returns a suggested shipping label template based on your configured shipping rules. This may resolve to: - A real label template (e.g. created in the back office) - A virtual template (e.g. Bol.com VVB label or letter label) ### Example request `/shipping/label-suggestion?order_pk=123` ### Example response ```json { "suggested": { "label_template": "DHL NL", "vvb_label_type": null, "letter_type": null } } ``` The resolution logic is fully rule-based, matching conditions like SKU, weight, country, etc. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `order_pk` | query | string | yes | Internal order ID to evaluate shipping rules against | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns the most appropriate label template for the given order based on configured rules - `422` - Validation Error ### GET /shipping/label-templates **Get available shipping label templates** Returns available shipping label templates for the authenticated organization. This includes both: - **Database-defined templates** (created via the backoffice) - **Virtual templates**, which are auto-generated for convenience: ### Virtual Templates #### Bol.com VVB labels (`carrier_identifier` = `VVB_MAILBOX` / `VVB_PARCEL`) If you have one or more BolAPIConnect channels connected, you get these per channel: - `id = vvb_mailbox_` → `Bol.com Mailbox Label` - `id = vvb_parcel_` → `Bol.com Parcel Label` Use the Bol channel ID from `/shipping/integrations` to select the right template. #### Letter labels These are always available: - `id = letter_unstamped` → `Unstamped Letter` - `id = letter_stamped` → `Stamped Letter` ### Query examples - `GET /shipping/label-templates` → all templates - `GET /shipping/label-templates?carrier=vvb_mailbox&id=42` → Bol.com mailbox template for Bol channel 42 - `GET /shipping/label-templates?carrier=letter&id=unstamped` → Unstamped letter template ### Response Format ```json [ { "id": "vvb_parcel_42", "name": "Bol.com Parcel Label", "carrier_identifier": "VVB_PARCEL" }, { "id": "letter_unstamped", "name": "Unstamped Letter", "carrier_identifier": "LETTER_UNSTAMPED" }, ... ] **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `carrier` | query | string | no | Optional carrier type, e.g. 'vvb_mailbox', 'vvb_parcel', or 'letter' | | `id` | query | string | no | Optional template ID (e.g. channel ID for vvb, or 'unstamped'/'stamped' for letter) | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Returns label templates including virtual ones like Bol.com (VVB) and Letter types - `422` - Validation Error ### POST /shipping/request-label **Request a shipping label** Initiates the creation of a shipping label for a given order. Depending on the `template_id` and `carrier_id`, the backend will determine how to generate the label: - For `vvb_` labels (Bol.com): auto-maps to Mailbox/Parcel logic - For `letter` or `_letter` templates: creates an internal letter label - For other template-based carriers: triggers a standard label flow ### Request Body - `template_id`: The template identifier (e.g. `vvb_mailbox_4`, `letter_unstamped`, `123`) - `carrier_id`: The carrier source (e.g. `bol-4`, `letter`, `sendcloud-1`) - `order_pk`: Primary key of the order ### Returns A JSON object containing: - `status`: `queued` - `entity_id`: Use in `/retrieve-label` endpoint - `order_pk`: Order identifier - `service`: Resolved carrier/service type (e.g. `bol`, `letter`, `dhl`) **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `template_id` | integer or string | yes | | | `carrier_id` | integer or string | yes | | | `order_pk` | integer | yes | | **Responses** - `200` - Queues label generation and returns task ID and metadata - `422` - Validation Error ### POST /shipping/retrieve-label **Retrieve a generated shipping label** Retrieves a previously requested shipping label using the `entity_id` returned from `/request-label`. This endpoint checks task status and returns a streaming PDF file when complete. ### Request Body - `entity_id`: The Entity ID that you retrieve from the `/request-label` endpoint - `service`: The carrier block (e.g. `bol`, `letter`, `dhl`) - `order_pk`: The internal order PK ### Returns A `200` response with a streamed PDF if ready. Adds tracking metadata to headers: - `X-Tracking-Number` - `X-Carrier` - `X-Label-Status` A `202` response is returned if the label is still processing. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `entity_id` | string | yes | | | `service` | string | yes | | | `order_pk` | integer | yes | | **Responses** - `200` - Returns a PDF with tracking and carrier metadata - `422` - Validation Error ## Customers Customer records. ### GET /customers **List customers** Retrieve a list of all customers with their complete details. ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/customers" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "success": true, "customers": [ { "id": 123, "customer_code": "CUST-001", "business_name": "Acme Corp", "invoice_email": "billing@acme.com", "orders_email": "orders@acme.com", "phone_number": "+31 20 123 4567", "website_url": "https://acme.com", "invoice_street": "123 Business St", "invoice_city": "Amsterdam", "invoice_country": "NL", "vat_number": "NL123456789B01", "payment_terms": "30" } ], "count": 1 } ``` ## Returns A JSON object containing: * **success**: Boolean indicating if the request was successful * **customers**: List of customer objects with all their details * **count**: Total number of customers Each customer object includes: * **Basic info**: id, customer_code, business_name * **Contact info**: invoice_email, orders_email, phone_number, website_url * **Invoice address**: invoice_street, invoice_house_num, invoice_suffix, invoice_zip, invoice_city, invoice_country * **Shipping address**: shipping_name, shipping_street, shipping_house_num, shipping_suffix, shipping_zip, shipping_city, shipping_country * **Financial info**: vat_number, bank_name, bank_number, payment_terms * **Notes**: special_notes **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Customers list retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "success": true, "customers": [ { "id": 123, "customer_code": "CUST-001", "business_name": "Acme Corp", "invoice_email": "billing@acme.com", "orders_email": "orders@acme.com", "phone_number": "+31 20 123 4567", "website_url": "https://acme.com", "invoice_street": "123 Business St", "invoice_house_num": "123", "invoice_suffix": "A", "invoice_zip": "1000AA", "invoice_city": "Amsterdam", "invoice_country": "NL", "shipping_name": "Acme Warehouse", "shipping_street": "456 Warehouse Rd", "shipping_house_num": "456", "shipping_suffix": "", "shipping_zip": "3000BB", "shipping_city": "Rotterdam", "shipping_country": "NL", "vat_number": "NL123456789B01", "bank_name": "ING Bank", "bank_number": "NL91ABNA0417164300", "payment_terms": "30", "special_notes": "Rush orders only" } ], "count": 1 } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ### PATCH /customers/{customer_id}/update **Update customer details** Update customer details by customer ID. ## cURL Example: ```bash curl -X PUT "https://api.stockpilot.dev/customers/123/update" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{ "business_name": "Updated Corp Name", "invoice_email": "new-billing@acme.com", "payment_status": "good", "payment_terms": 14 }' ``` ## Request Body All fields are optional - only provide the fields you want to update: ### Available Fields: * **business_name**: Business name * **invoice_email**: Invoice email address * **orders_email**: Orders email address * **phone_number**: Phone number * **payment_status**: Payment status (must be: good, overdue, blocked) * **payment_terms**: Payment terms in days * **vat_number**: VAT number * **special_notes**: Special notes about the customer ## Response Example: ```json { "success": true, "message": "Customer updated successfully", "customer": { "id": 123, "business_name": "Updated Corp Name", "invoice_email": "new-billing@acme.com", "orders_email": "new-orders@acme.com", "phone_number": "+31 20 999 8888", "payment_status": "good", "payment_terms": 14, "vat_number": "NL987654321B01", "special_notes": "Updated customer notes" } } ``` ## Returns A JSON object containing: * **success**: Boolean indicating if the request was successful * **message**: Success message * **customer**: Updated customer object with the modified fields ## Notes * Only the fields provided in the request body will be updated * Customer ID must exist in the system * Payment status validation: only accepts 'good', 'overdue', or 'blocked' * All other customer fields (addresses, bank details, etc.) remain unchanged **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `customer_id` | path | integer | yes | | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `business_name` | string | no | Business name | | `invoice_email` | string | no | Invoice email address | | `orders_email` | string | no | Orders email address | | `phone_number` | string | no | Phone number | | `payment_status` | string | no | Payment status: good, overdue, blocked | | `payment_terms` | integer | no | Payment terms in days | | `vat_number` | string | no | VAT number | | `special_notes` | string | no | Special notes about the customer | Example: ```json { "business_name": "Updated Corp Name", "invoice_email": "new-billing@acme.com", "orders_email": "new-orders@acme.com", "payment_status": "good", "payment_terms": 14, "phone_number": "+31 20 999 8888", "special_notes": "Updated customer notes", "vat_number": "NL987654321B01" } ``` **Responses** - `200` - Customer updated successfully - `400` - Invalid request data - `401` - Missing API credentials - `404` - Customer not found - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "success": true, "message": "Customer updated successfully", "customer": { "id": 123, "business_name": "Updated Corp Name", "invoice_email": "new-billing@acme.com", "orders_email": "new-orders@acme.com", "phone_number": "+31 20 999 8888", "payment_status": "good", "payment_terms": 14, "vat_number": "NL987654321B01", "special_notes": "Updated customer notes" } } ``` `400` example: ```json { "detail": "Invalid payment status. Must be: good, overdue, or blocked" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Customer with ID 123 not found" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ## Analytics Sales reporting and forecasting. ### GET /analytics/items/sales **Get item sales analytics** Get sales analytics for a specific inventory item. ## Product Identification (choose exactly one): - **id**: Product ID (primary key) - query parameter: ?id=123 - **sku**: Product SKU - query parameter: ?sku=PRODUCT-SKU - **barcode**: Product barcode/EAN - query parameter: ?barcode=123456789 ## Query Parameters: - **range**: Number of days to look back (default: 14) - **include_channels**: Include per-channel breakdown (default: false) - **metrics**: Comma-separated list of metrics to include (default: all) - Options: total_orders, total_items, revenue, forecast, pricing, daily_breakdown ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/analytics/items/sales?sku=PROD-123&range=30&include_channels=true" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "item": { "id": 456, "sku": "PROD-123", "product_name": "Sample Product", "current_stock": 50, "backorder_amount": 10 }, "period": { "days": 30, "start_date": "2023-01-01T00:00:00Z", "end_date": "2023-01-30T23:59:59Z" }, "total_orders": 25, "total_items_sold": 75, "revenue": { "total_revenue": 1875.50, "average_order_value": 75.02 }, "pricing": { "average_selling_price": 25.00, "highest_selling_price": 30.00, "lowest_selling_price": 20.00 }, "forecast": { "daily_forecast": 2.5, "weekly_forecast": 17.5, "monthly_forecast": 75.0, "method": "filtered_average" }, "daily_breakdown": [ { "date": "2023-01-01", "quantity_sold": 3, "orders": 2, "revenue": 75.00 } ], "channel_breakdown": [ { "channel": "Shopify", "total_items_sold": 45, "total_orders": 15, "total_revenue": 1125.00 } ] } ``` ## Error Responses: - **400**: Missing or multiple identifiers, invalid parameters - **404**: Item not found **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | query | integer | no | Product ID (primary key) | | `sku` | query | string | no | Product SKU | | `barcode` | query | string | no | Product barcode/EAN | | `range` | query | integer | no | Number of days to look back | | `include_channels` | query | boolean | no | Include per-channel breakdown | | `metrics` | query | string | no | Comma-separated list of metrics (total_orders, total_items, revenue, forecast, pricing, daily_breakdown) | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Sales analytics retrieved successfully - `400` - Missing or multiple identifiers, invalid parameters - `404` - Item not found - `422` - Validation Error `200` example: ```json { "item": { "id": 456, "sku": "PROD-123", "product_name": "Sample Product", "current_stock": 50, "backorder_amount": 10 }, "period": { "days": 30, "start_date": "2023-01-01T00:00:00Z", "end_date": "2023-01-30T23:59:59Z" }, "total_orders": 25, "total_items_sold": 75, "revenue": { "total_revenue": 1875.5, "average_order_value": 75.02 }, "pricing": { "average_selling_price": 25.0, "highest_selling_price": 30.0, "lowest_selling_price": 20.0 }, "forecast": { "daily_forecast": 2.5, "weekly_forecast": 17.5, "monthly_forecast": 75.0, "method": "filtered_average" }, "daily_breakdown": [ { "date": "2023-01-01", "quantity_sold": 3, "orders": 2, "revenue": 75.0 } ], "channel_breakdown": [ { "channel": "Shopify", "total_items_sold": 45, "total_orders": 15, "total_revenue": 1125.0 } ] } ``` `400` example: ```json { "detail": "Must provide one of: id, sku, or barcode" } ``` `404` example: ```json { "detail": "Item with SKU 'PROD-123' not found" } ``` ### GET /analytics/product-order-history **Get product order history** Get order history for a specific product based on ID, EAN, or SKU. ## Product Identification (choose exactly one): - **id**: Product ID (primary key) - query parameter: ?id=123 - **sku**: Product SKU - query parameter: ?sku=PRODUCT-SKU - **barcode**: Product barcode/EAN - query parameter: ?barcode=123456789 ## Query Parameters: - **start_date**: Start date filter (YYYY-MM-DD format, optional) - **end_date**: End date filter (YYYY-MM-DD format, optional) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/analytics/product-order-history?sku=HEADPHONES-PRO&start_date=2026-01-01&end_date=2026-01-31" \ -H "X-CLIENT-ID: your_client_id" \ -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Data: - **Product Information**: ID, SKU, name, and barcode - **Order Details**: Order IDs, channels, dates, quantities - **Customer Information**: Names, emails, shipping addresses - **Filtering Applied**: Date range used for the query ## Use Cases: - **Customer Support**: Find all orders containing a specific product - **Product Analysis**: Track which customers buy certain products - **Channel Performance**: See which sales channels sell specific items - **Date Range Analysis**: Filter orders within specific time periods ## Error Responses: - **400**: Missing or multiple identifiers, invalid date format - **404**: Product not found **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | query | integer | no | Product ID (primary key) | | `sku` | query | string | no | Product SKU | | `barcode` | query | string | no | Product barcode/EAN | | `start_date` | query | string | no | Start date filter (YYYY-MM-DD format) | | `end_date` | query | string | no | End date filter (YYYY-MM-DD format) | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Product order history retrieved successfully - `400` - Missing or multiple identifiers, invalid date format - `404` - Product not found - `422` - Validation Error `200` example: ```json { "product": { "id": 456, "sku": "HEADPHONES-PRO", "product_name": "Premium Wireless Headphones", "barcode": "5901234123457" }, "filters": { "start_date": "2026-01-01", "end_date": "2026-01-31" }, "total_orders": 15, "orders": [ { "id": 12345, "order_id": "ORD-2026-001", "channel": "Shopify Store", "channel_order_id": "SP-789", "created_at": "2026-01-15T10:30:00Z", "quantity": 2, "customer_name": "John Doe", "customer_email": "john.doe@example.com", "ship_street": "123 Main St", "ship_city": "Amsterdam", "ship_country": "NL" }, { "id": 12346, "order_id": "ORD-2026-002", "channel": "Bol.com", "channel_order_id": "BOL-456", "created_at": "2026-01-20T14:45:00Z", "quantity": 1, "customer_name": "Jane Smith", "customer_email": "jane.smith@example.com", "ship_street": "456 Oak Ave", "ship_city": "Rotterdam", "ship_country": "NL" } ] } ``` `400` example: ```json { "detail": "Must provide one of: id, sku, or barcode" } ``` `404` example: ```json { "detail": "Product with SKU 'HEADPHONES-PRO' not found" } ``` ### GET /analytics/sales-summary **Get sales summary** Get sales summary across all items for the organization. ## Query Parameters: - **range**: Number of days to look back (default: 14) - **top_items**: Number of top selling items to include (default: 10) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/analytics/sales-summary?range=30&top_items=5" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "period": { "days": 30, "start_date": "2023-01-01T00:00:00Z", "end_date": "2023-01-30T23:59:59Z" }, "summary": { "total_items_sold": 350, "total_orders": 85, "total_revenue": 8750.50, "unique_products_sold": 25, "average_order_value": 102.95 }, "top_selling_items": [ { "item_id": 456, "sku": "PROD-123", "product_name": "Sample Product", "barcode": "1234567890", "total_sold": 75, "total_orders": 25, "total_revenue": 1875.50 } ] } ``` ## Returns: - Overall sales statistics - Top selling items by quantity - Revenue metrics and trends - Order analytics summary **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `range` | query | integer | no | Number of days to look back | | `top_items` | query | integer | no | Number of top selling items to include | | `x-client-id` | header | string | yes | Your API client ID | | `x-client-secret` | header | string | yes | Your API client secret | **Responses** - `200` - Sales summary retrieved successfully - `401` - Missing API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "period": { "days": 30, "start_date": "2023-01-01T00:00:00Z", "end_date": "2023-01-30T23:59:59Z" }, "summary": { "total_items_sold": 350, "total_orders": 85, "total_revenue": 8750.5, "unique_products_sold": 25, "average_order_value": 102.95 }, "top_selling_items": [ { "item_id": 456, "sku": "PROD-123", "product_name": "Sample Product", "barcode": "1234567890", "total_sold": 75, "total_orders": 25, "total_revenue": 1875.5 } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream error: Connection failed" } ``` ## Sales Channels Sales channel integrations. ### GET /sales-channels **List all channels** Retrieve a paginated list of all available sales channels and integrations. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of channels per page (max 100) ## cURL Example: ```bash curl -X GET "https://api.stockpilot.dev/sales-channels?page=1&page_size=10" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" ``` ## Response Example: ```json { "channels": [ { "id": 1, "name": "My Shopify Store", "handle": "shopify", "is_active": true, "logo": "https://sp-s3-bucket.s3.amazonaws.com/static/assets/images/channel-icons/shopify-icon.png", "channel_is_forwarded": false, "is_inventory_source": true, "is_synchronized": true, "created_at": "2023-01-15T10:30:00Z" }, { "id": 2, "name": "WooCommerce Store", "handle": "woocommerce", "is_active": true, "is_inventory_source": false, "is_synchronized": true } ] } ``` ## Supported Handles: - **shopify**, **woocommerce**, **amazon**, **bol**, **etsy**, **kaufland**, **mirakl**, **b2b-portal** ## Returns A JSON object containing: * List of connected sales channels with configuration details * Channel status and synchronization information * Logo URLs and integration metadata **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - List of channels retrieved successfully - `401` - Missing API credentials - `403` - Invalid credentials - `422` - Validation Error `200` example: ```json { "channels": [ { "id": 1, "name": "My Shopify Store", "handle": "shopify", "is_active": true, "logo": "https://sp-s3-bucket.s3.amazonaws.com/static/assets/images/channel-icons/shopify-icon.png", "channel_is_forwarded": false, "is_inventory_source": true, "is_synchronized": true, "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-15T10:30:00Z" }, { "id": 2, "name": "WooCommerce Store", "handle": "woocommerce", "is_active": true, "logo": "https://sp-s3-bucket.s3.amazonaws.com/static/assets/images/channel-icons/woo-icon.png", "channel_is_forwarded": false, "is_inventory_source": false, "is_synchronized": true, "created_at": "2023-02-01T14:20:00Z", "updated_at": "2023-02-01T14:20:00Z" } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `403` example: ```json { "error": "Invalid credentials" } ``` ### POST /sales-channels/sync-listings **Sync listings for a channel** Queue a listing sync for one connected sales channel. Listings are pulled from the channel into Stockpilot in the background. The call returns as soon as the job is queued - it does not wait for the sync to finish. ## Request Body * **channel**: Channel handle, exactly as returned in the `handle` field of `GET /sales-channels` * **channel_id**: ID of the connected channel, from the `id` field of the same response * **full_sync**: Optional, defaults to `false`. Re-imports every listing rather than only what changed since the last sync. Slower and heavier on the channel's rate limits - use it when reconciling after a suspected gap. ## Request Body Example ```json { "channel": "shopify", "channel_id": 1, "full_sync": false } ``` ## cURL Example: ```bash curl -X POST "https://api.stockpilot.dev/sales-channels/sync-listings" -H "X-CLIENT-ID: your_client_id" -H "X-CLIENT-SECRET: your_client_secret" -H "Content-Type: application/json" -d '{"channel": "shopify", "channel_id": 1}' ``` ## Returns `202` with `status: "queued"` and the background `task_id` when a sync was started. Only one sync runs per channel at a time. If one is already pending or in flight the call is a no-op and returns `200` with `status` set to `already_queued` or `already_syncing`, together with the `last_synced` timestamp of the previous run - so it is safe to retry without stacking up duplicate jobs. ## Supported Handles `amazon`, `ankorstore`, `bigcommerce`, `bol`, `ccvshop`, `cdiscount`, `ebay`, `etsy`, `faire`, `floriday`, `fnacdarty`, `kaufland`, `lightspeed`, `magento`, `mijnwebwinkel`, `mirakl`, `opencart`, `orderchamp`, `otto`, `prestashop`, `shopify`, `shopware`, `squarespace`, `target`, `temu`, `tiktok`, `valkaspos`, `walmart`, `woocommerce` Channels outside this list return `400` - either they have no listing sync, or they are not a listing-bearing channel at all. Channels belonging to another organization return `404`, the same as an ID that does not exist. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `channel` | string | yes | Channel handle, as returned in the `handle` field of `GET /sales-channels`. Supported: amazon, ankorstore, bigcommerce, bol, ccvshop, cdiscount, ebay, etsy, faire, floriday, fnacdarty, kaufland, lightspeed, magento, mijnwebwinkel, mirakl, opencart, orderchamp, otto, prestashop, shopify, shopware, squarespace, target, temu, tiktok, valkaspos, walmart, woocommerce | | `channel_id` | integer | yes | ID of the connected channel, as returned in the `id` field of `GET /sales-channels` | | `full_sync` | boolean | no | Re-import every listing instead of only what changed since the last sync. Slower and heavier on the channel's rate limits - leave false unless you are reconciling after a suspected gap | Example: ```json { "channel": "shopify", "channel_id": 1, "full_sync": false } ``` **Responses** - `200` - A sync is already queued or already running - nothing was started - `202` - Sync queued successfully - `400` - Missing fields, or a channel that does not support listing sync - `401` - Missing API credentials - `403` - Invalid credentials - `404` - Channel not found, or not owned by your organization - `422` - Validation Error `200` example: ```json { "status": "already_queued", "channel": "shopify", "channel_id": 1, "last_synced": "2026-09-01T09:14:22Z" } ``` `202` example: ```json { "status": "queued", "task_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "channel": "shopify", "channel_id": 1, "full_sync": false } ``` `400` example: ```json { "error": "channel and channel_id are required" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `403` example: ```json { "error": "Invalid credentials" } ``` `404` example: ```json { "error": "Channel not found" } ``` ## Webhooks Manage outbound webhook subscriptions. All webhooks are scoped to your organization. A webhook ID belonging to another organization returns `404`, the same as one that does not exist. See **Webhook Delivery Contract** for the envelope, signature verification, payload shapes and retry behaviour. ### GET /webhooks **List webhooks** Retrieve a paginated list of webhook subscriptions for your organization. Both active and deactivated webhooks are returned - check `is_active`. A webhook that has been soft deleted, or auto-deactivated after repeated delivery failures, stays in this list with `is_active: false` and keeps its delivery history. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of webhooks per page (max 100) * **category**: Filter by event category, the namespace part of the event name * **event**: Filter by exact event type, e.g. `inventory.stock_changed` ## Returns A JSON object containing the list of webhooks and pagination information. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `category` | query | string | no | Filter by event category (the event namespace, e.g. `orders` or `inventory`) | | `event` | query | `orders.completed` \| `inventory.stock_changed` | no | Filter by exact event type | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Webhooks retrieved successfully - `401` - Missing or invalid API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 2, "current_page": 1, "total_pages": 1, "results": [ { "id": 42, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1", "is_active": true, "created_at": "2026-09-01T09:14:22Z", "updated_at": "2026-09-02T14:03:11Z" }, { "id": 43, "name": "Completed orders to fulfilment", "event": "orders.completed", "target_url": "https://ops.example.com/hooks/orders", "is_active": false, "created_at": "2026-08-14T11:02:00Z", "updated_at": "2026-09-01T06:30:41Z" } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### POST /webhooks/create **Create a webhook** Create a webhook subscription. ## Request Body * **name**: Human readable name for the webhook * **event**: Event type to subscribe to - `orders.completed` or `inventory.stock_changed` * **target_url**: HTTPS endpoint that receives deliveries. Must be publicly resolvable; private, loopback, link-local and cloud-metadata addresses are rejected here and again at delivery time. Redirects are not followed. * **secret**: Optional signing secret, used verbatim when supplied (min 16 characters). Omit it and Stockpilot generates one. * **warehouse_id**: Optional. Restrict `inventory.stock_changed` deliveries to changes in a single warehouse. Omit to receive events for all warehouses. The payload still carries every warehouse in `quantities`; this filters what triggers a delivery. ## Request Body Example ```json { "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "secret": "a-secret-of-at-least-16-chars", "warehouse_id": "WH1" } ``` ## Returns The created webhook. The `id` is what arrives in the `X-Stockpilot-Webhook` header on every delivery - store it if you route by webhook. The secret is never echoed back. See **Webhook Delivery Contract** for signature verification and the retry schedule. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Human readable name for this webhook | | `event` | `orders.completed` \| `inventory.stock_changed` | yes | Event type that triggers this webhook | | `target_url` | string | yes | HTTPS endpoint that receives the delivery. Must be publicly resolvable | | `secret` | string | no | Signing secret used for X-Stockpilot-Signature. Used verbatim when supplied (min 16 characters); omit it and Stockpilot generates one | | `warehouse_id` | string | no | Restrict inventory.stock_changed deliveries to changes in a single warehouse. Omit to receive events for all warehouses | Example: ```json { "event": "inventory.stock_changed", "name": "Stock changes to ERP", "secret": "a-secret-of-at-least-16-chars", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1" } ``` **Responses** - `201` - Webhook created successfully - `400` - Invalid payload - unreachable, non-HTTPS or disallowed target URL - `401` - Missing or invalid API credentials - `422` - Validation Error - `500` - Internal server error `201` example: ```json { "id": 42, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1", "is_active": true, "created_at": "2026-09-01T09:14:22Z", "updated_at": "2026-09-02T14:03:11Z" } ``` `400` example: ```json { "detail": "target_url must be an HTTPS address that resolves publicly" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### GET /webhooks/events **List available event types** List the event types you can subscribe to, with a description of what triggers each. Query this at runtime rather than hardcoding the list, so new event types become available to your integration without a redeploy. See **Webhook Delivery Contract** for the payload each event carries. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Event types retrieved successfully - `401` - Missing or invalid API credentials - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "results": [ { "event": "orders.completed", "category": "orders", "description": "An order reaches the completed state, by transition or by arriving already completed. Fires once per order." }, { "event": "inventory.stock_changed", "category": "inventory", "description": "A product's stock quantity changes in any warehouse." } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### GET /webhooks/{webhook_id} **Get a webhook** Retrieve a single webhook subscription. Webhooks are scoped to your organization. An ID belonging to another organization returns `404`, the same as an ID that does not exist. The signing secret is never returned. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Webhook retrieved successfully - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 42, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1", "is_active": true, "created_at": "2026-09-01T09:14:22Z", "updated_at": "2026-09-02T14:03:11Z" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### POST /webhooks/{webhook_id}/delete **Delete a webhook** Delete a webhook subscription. This is a **soft delete**: the webhook stops delivering and its `is_active` becomes `false`, but the record and its delivery history are kept so past attempts remain available for debugging. It continues to appear in `GET /webhooks`. Reverse it with `POST /webhooks/{id}/reactivate`. Returns `204` with no response body. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `204` - Webhook deactivated successfully. No response body - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### GET /webhooks/{webhook_id}/deliveries **List recent deliveries** List recent delivery attempts for a webhook, newest first - for debugging a failing integration. Each entry carries the `status`, the `response_code` your endpoint returned, and an `error` when the request never completed (timeout, DNS failure, rejected address). Retried attempts share a `delivery_id` and differ by `attempt`, so a single event that was retried appears as several rows. Delivery history is kept after a webhook is deleted or auto-deactivated. ## Parameters * **page**: Page number for pagination (starts at 1) * **page_size**: Number of attempts per page (max 100) **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `page` | query | integer | no | Page number for pagination | | `page_size` | query | integer | no | Number of items per page | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Deliveries retrieved successfully - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "count": 3, "current_page": 1, "total_pages": 1, "results": [ { "delivery_id": "9c1f0f8e5b7a4c2d8e3f1a2b3c4d5e6f", "event": "inventory.stock_changed", "attempted_at": "2026-09-02T14:03:12Z", "status": "failed", "response_code": 502, "error": "Endpoint returned 502", "attempt": 3 }, { "delivery_id": "9c1f0f8e5b7a4c2d8e3f1a2b3c4d5e6f", "event": "inventory.stock_changed", "attempted_at": "2026-09-02T14:02:41Z", "status": "failed", "error": "Read timeout after 10s", "attempt": 2 } ] } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### POST /webhooks/{webhook_id}/reactivate **Reactivate a webhook** Reactivate a webhook that was deleted or auto-deactivated. Stockpilot auto-deactivates a webhook after 5 fully-failed deliveries within 24 hours. Fix your endpoint first, confirm with `POST /webhooks/{id}/test`, then reactivate - otherwise it will simply deactivate again. Reactivating does not replay missed deliveries. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Webhook reactivated successfully - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 42, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1", "is_active": true, "created_at": "2026-09-01T09:14:22Z", "updated_at": "2026-09-02T14:03:11Z" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### POST /webhooks/{webhook_id}/test **Send a test delivery** Send a sample envelope to the webhook's `target_url` and return what your endpoint replied with. The sample carries the same headers and signature as a real delivery, so this is the fastest way to check your signature verification. The response is passed through as received - status code, body and all - rather than normalised, so you see exactly what your endpoint returned. A test delivery is recorded in `GET /webhooks/{id}/deliveries` alongside real attempts, and does not count towards the auto-deactivation threshold. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Responses** - `200` - Test delivery attempted. The body is what your endpoint returned - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "delivery_id": "9c1f0f8e5b7a4c2d8e3f1a2b3c4d5e6f", "status": "succeeded", "response_code": 200, "response_body": "{\"ok\": true}" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ### POST /webhooks/{webhook_id}/update **Update a webhook** Update a webhook subscription. Only the fields you send are changed. Omitting `secret` leaves the current secret untouched; sending one replaces it verbatim (min 16 characters). Changing `target_url` re-runs the address checks - private, loopback, link-local and cloud-metadata addresses are rejected. ## Request Body Example ```json { "name": "Stock changes to ERP (v2)", "target_url": "https://erp.example.com/hooks/stockpilot/v2" } ``` **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `webhook_id` | path | integer | yes | Webhook ID | | `x-client-id` | header | string | no | Your API client ID | | `x-client-secret` | header | string | no | Your API client secret | **Request body** (required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | Human readable name for this webhook | | `event` | `orders.completed` \| `inventory.stock_changed` | no | Event type that triggers this webhook | | `target_url` | string | no | HTTPS endpoint that receives the delivery | | `secret` | string | no | Replacement signing secret (min 16 characters). Omit to leave the current secret untouched | | `warehouse_id` | string | no | Restrict inventory.stock_changed deliveries to changes in a single warehouse | Example: ```json { "name": "Stock changes to ERP (v2)", "target_url": "https://erp.example.com/hooks/stockpilot/v2" } ``` **Responses** - `200` - Webhook updated successfully - `400` - Invalid payload - unreachable, non-HTTPS or disallowed target URL - `401` - Missing or invalid API credentials - `404` - Webhook not found, or not owned by your organization - `422` - Validation Error - `500` - Internal server error `200` example: ```json { "id": 42, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "target_url": "https://erp.example.com/hooks/stockpilot", "warehouse_id": "WH1", "is_active": true, "created_at": "2026-09-01T09:14:22Z", "updated_at": "2026-09-02T14:03:11Z" } ``` `400` example: ```json { "detail": "target_url must be an HTTPS address that resolves publicly" } ``` `401` example: ```json { "detail": "Missing API credentials" } ``` `404` example: ```json { "detail": "Webhook 42 not found" } ``` `500` example: ```json { "detail": "Upstream service error" } ``` ## Webhook Delivery Contract Stockpilot pushes events to an HTTPS endpoint you control. You register an endpoint with `POST /webhooks/create`, verify the signature on every request, and respond `2xx` quickly. Delivery is **at-least-once**, so your handler must be idempotent. Manage webhooks with the operations under **Webhooks**. This page documents what Stockpilot sends and what your endpoint has to do with it. ## Available events | Event | Fires when | | --- | --- | | `orders.completed` | An order reaches the completed state - either by transitioning to it, or by arriving already completed (e.g. a Bol.com import). Fires once per order. | | `inventory.stock_changed` | A product's stock quantity changes in any warehouse. | `GET /webhooks/events` returns the same list at runtime, so an integration can discover new event types without a redeploy. ## The envelope Every delivery has the same outer shape. Only `data` varies by event. ```json { "webhook_id": 42, "organization_id": 1874, "name": "Stock changes to ERP", "event": "inventory.stock_changed", "event_triggered_at": "2026-09-02T14:03:11+00:00", "delivery_id": "9c1f0f8e5b7a4c2d8e3f1a2b3c4d5e6f", "data": { } } ``` ## Headers | Header | Value | | --- | --- | | `Content-Type` | `application/json` | | `User-Agent` | `Stockpilot-Webhooks/1.0` | | `X-Stockpilot-Event` | `inventory.stock_changed` | | `X-Stockpilot-Delivery` | `9c1f0f8e5b7a4c2d8e3f1a2b3c4d5e6f` | | `X-Stockpilot-Webhook` | `42` | | `X-Stockpilot-Organization` | `1874` | | `X-Stockpilot-Signature` | `base64(HMAC-SHA256(raw_request_body, webhook_secret))` | `webhook_id` and `organization_id` are mirrored into headers as well as the body, so you can route a request before parsing it. ## Verifying the signature Compute the HMAC over the **raw body bytes, before any JSON parsing**. Re-serialising a parsed body changes whitespace and key order and will not match. ```python import base64, hashlib, hmac expected = base64.b64encode( hmac.new(secret.encode(), raw_body, hashlib.sha256).digest() ).decode() valid = hmac.compare_digest(expected, request.headers["X-Stockpilot-Signature"]) ``` Use `hmac.compare_digest` rather than `==` so the comparison is constant-time. ## Payload for orders.completed The same object shape that `GET /orders/get-single-order` returns. See that operation for the full field list. ## Payload for inventory.stock_changed This payload does **not** mirror a REST endpoint. It is purpose-built so that multi-warehouse stock is unambiguous. ```json { "id": 34897, "sku": "201156", "name": "Vichy Homme Structure Force 50 ml", "barcode": "3337875647212", "offered_stock": 3, "total_on_hand": 4, "incoming": 12, "backorder": 0, "quantities": [ {"warehouse": "Main", "warehouse_id": "WH1", "is_default": true, "sums_onto_offered": true, "available": 3, "reserved": 1, "on_hand": 4, "inbound": 12}, {"warehouse": "Amazon FBA", "warehouse_id": "WH2", "is_default": false, "sums_onto_offered": false, "available": 1, "reserved": 0, "on_hand": 1, "inbound": 6} ] } ``` ### Field meanings | Field | Meaning | | --- | --- | | `offered_stock` | What Stockpilot actually offers to sales channels: the sum of warehouses with `sums_onto_offered: true`, minus buffer stock. **This is the number most integrations want.** | | `total_on_hand` | On-hand across contributing warehouses, including reserved. | | `incoming` | Units on open purchase orders. Product-level, not attributed to a warehouse. | | `backorder` | Units currently on backorder. | | `quantities[].available` | On-hand in that warehouse **excluding** reserved. | | `quantities[].on_hand` | On-hand in that warehouse **including** reserved. | | `quantities[].reserved` | Units allocated to open orders. | | `quantities[].inbound` | Stock in transit into that warehouse. | | `quantities[].sums_onto_offered` | Whether this warehouse contributes to `offered_stock`. | | `quantities[].is_default` | Whether this is the default warehouse. | ### Do not sum inbound across warehouses On the default warehouse, per-warehouse `inbound` repeats the product-level `incoming`, because purchase orders land there. Summing `inbound` across warehouses and comparing the result to `incoming` double-counts. In the example above, `incoming` is `12` and `WH1.inbound` is the same `12` - not an additional 12. ### No deltas No before/after pair is sent. Deliveries are coalesced (see below), so a previous - new pair would be misleading: the "previous" value may be several writes stale. **Treat every payload as current state.** ## Warehouse scoping `warehouse_id` is optional on create and update, and controls which stock events a webhook receives: - **Omitted** - the webhook receives `inventory.stock_changed` for every warehouse. - **Set** - the webhook receives only events for changes in that warehouse. A scoped webhook still receives the **full product payload**, with every warehouse present in `quantities`. Scoping filters which changes trigger a delivery; it does not trim the body. So `offered_stock` remains the org-wide offered figure, not a per-warehouse subtotal. Use one scoped webhook per warehouse when different systems own different warehouses, and route on `X-Stockpilot-Webhook`. ## Secrets `secret` is optional on both create and update: - Supply one and it is used verbatim. Minimum 16 characters. - Omit it and Stockpilot generates one for you. ## Multi-tenant integrations If you are a platform receiving webhooks on behalf of many Stockpilot organizations: 1. Create one webhook per customer org, all with your own shared secret. 2. Store the returned `webhook_id` against your customer record. 3. On delivery, verify with that constant secret and route on `X-Stockpilot-Webhook`. Because `webhook_id` and `organization_id` are both mirrored into headers, you can identify the sender before parsing the body. **Caveat:** a shared secret is visible to every org admin who has it configured. If your customers need to be mutually distrustful, use a distinct secret per org instead. ## Consumer contract - **Respond `2xx` within 10 seconds.** Queue the payload and return immediately; do not process inline. - **A non-2xx response or a timeout is a failed attempt.** Retries run 15 times over roughly 17 hours: 10s, 30s, 1m, 2m, 5m, 10m, 20m, 30m, 1h, 1h, 2h, 2h, 3h, 3h, 3h. - **After 5 fully-failed deliveries in 24 hours the webhook is auto-deactivated.** Reactivate it with `POST /webhooks/{id}/reactivate` or from the Settings UI. - **Endpoints must be HTTPS and publicly resolvable.** Private, loopback, link-local and cloud-metadata addresses are rejected at create time and again at delivery time. - **Redirects are not followed.** - **Delivery is at-least-once.** Deduplicate on `delivery_id`. - **`inventory.stock_changed` is coalesced** over roughly 5 seconds **per product per warehouse**, so a burst of writes to one product in one warehouse arrives as a single delivery carrying the final quantity. Coalescing does **not** merge across warehouses: if the same product changes in two warehouses inside that window you receive two deliveries, each with its own `delivery_id`, and each carrying the full `quantities` array. Deduplicating on `delivery_id` will not collapse them - and should not, since both are real changes. Use `GET /webhooks/{id}/deliveries` to see recent attempts with their status, response code and error, and `POST /webhooks/{id}/test` to send a sample envelope and see exactly what your endpoint returned. ## Networking Deliveries originate from Stockpilot's **worker infrastructure**, not from the API gateway. Customers who IP-allowlist inbound traffic need the worker egress ranges - allowlisting the gateway's addresses will not work.