{"openapi":"3.1.0","info":{"title":"NetXL Customer API","version":"1.0","description":"The NetXL customer API provides programmatic access to the NetXL product catalogue,\nordering, quoting and account management.\n\n## Authentication\nEvery endpoint (except `/check-health`, `/oauth/token` and the OpenAPI documents) requires\neither a 64-character API key, supplied in the `X-Api-Key` header (preferred) or the `api-key`\nquery parameter, or a bearer token in the `Authorization` header. Requests with a missing or\ninvalid credential receive `401` with an empty JSON body.\n\nAn API key is the customer themselves, and is the right choice for a customer\u0027s own integration.\nA bearer token is an application acting for a customer who authorised it at www.netxl.com — the\nNetXL MCP server, for instance — and is obtained from `POST /oauth/token`. The two confer the\nsame access; nothing in this API can tell them apart.\n\nRepeatedly presenting invalid credentials (10 within 60 seconds) blocks the source IP address\nfor 5 minutes; blocked requests receive `403`. If the authentication backend is\nunavailable, requests receive `503`.\n\n## Dates\nAll timestamps are UK local time in the format `yyyy-MM-dd\u0027T\u0027HH:mm:ss`\n(e.g. `2026-01-15T10:30:00`). No timezone offset is included.\n\n## Content negotiation\n`GET /product` and `GET /order` can return JSON or CSV. The format is chosen from the\n`Accept` header, or the `accept` query parameter as an override. If no `Accept` header is\nsent, the response defaults to **CSV**; `Accept: text/html` is also treated as CSV.\nSend `Accept: application/json` for JSON.\n","contact":{"name":"NetXL","url":"https://www.netxl.com"}},"servers":[{"url":"https://api.netxl.com"}],"security":[{"ApiKeyHeader":[]},{"ApiKeyQuery":[]},{"BearerToken":[]}],"tags":[{"name":"Products","description":"Product catalogue with customer-specific pricing and stock"},{"name":"Orders","description":"Order creation, confirmation and retrieval"},{"name":"Quotes","description":"Quote creation"},{"name":"Customer","description":"Account details, credit and address book"},{"name":"Reference Data","description":"Categories, manufacturers and product attributes"},{"name":"Authentication","description":"Token exchange for applications acting on a customer\u0027s behalf"},{"name":"Health","description":"Service health"},{"name":"Documentation","description":"This specification"}],"paths":{"/":{"get":{"tags":["Documentation"],"operationId":"getApiIndex","summary":"API index","description":"Returns links to this specification.","security":[],"responses":{"200":{"description":"Links to the API documentation.","headers":{"Link":{"description":"RFC 8631 `service-desc` links to the OpenAPI documents.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"documentation":{"type":"object","properties":{"openapi_json":{"type":"string"},"openapi_yaml":{"type":"string"}}}}}}}}}}},"/product":{"get":{"tags":["Products"],"operationId":"listProducts","summary":"List products","description":"Returns active products with pricing and stock specific to the authenticated\ncustomer. Returns CSV by default when no `Accept` header is sent — use\n`Accept: application/json` (or `?accept\u003dapplication/json`) for JSON.\n\nJSON results are paginated: without a `limit` you get the first 100 products, and the\n`X-Total-*` and `Link` response headers say where in the catalogue you are. Pass\n`limit\u003d0` to fetch everything at once.\n\nCSV is the exception. It is the catalogue feed, and it returns the **whole catalogue**\nwhen no `limit` is sent, exactly as it did before paging existed. Send a `limit` to page\nit.\n\nPrices and stock are cached per customer for up to 5 minutes, so a stock level may\nlag a sale by that much.\n","parameters":[{"name":"manufacturer","in":"query","description":"Comma-separated list of manufacturer keys to filter by.","schema":{"type":"string"},"example":"yealink,poly"},{"name":"category","in":"query","description":"Comma-separated list of category names to filter by.","schema":{"type":"string"}},{"name":"query","in":"query","description":"Keyword filter matched against product details.","schema":{"type":"string"}},{"name":"attribute","in":"query","description":"Comma-separated list of attribute filters in the form `name\u003coperator\u003evalue`,\nwhere the operator is one of `\u003d`, `\u003c`, `\u003e`, `\u003c\u003d`, `\u003e\u003d` (e.g. `ports\u003e\u003d4`).\n","schema":{"type":"string"},"example":"ports\u003e\u003d4"},{"name":"reduced","in":"query","description":"Set to `true` to return only reduced (refurbished) products, `false` to exclude them.","schema":{"type":"string","enum":["true","false"]}},{"name":"inStock","in":"query","description":"Set to `true` to return only products that are currently in stock.","schema":{"type":"string","enum":["true"]}},{"name":"sort","in":"query","description":"Sort order for the results. Any other value returns `400`.","schema":{"type":"string","enum":["price","availability","sku","name"]}},{"name":"extra","in":"query","description":"Comma-separated list of additional fields to include, e.g. `description`,\n`related`, `similar`.\n","schema":{"type":"string"},"example":"description,related"},{"name":"accept","in":"query","description":"Overrides content negotiation, e.g. `text/csv` or `application/json`.","schema":{"type":"string","enum":["application/json","text/csv"]}},{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"One page of the matching products.","headers":{"X-Total-Count":{"$ref":"#/components/headers/XTotalCount"},"X-Page":{"$ref":"#/components/headers/XPage"},"X-Limit":{"$ref":"#/components/headers/XLimit"},"X-Total-Pages":{"$ref":"#/components/headers/XTotalPages"},"Link":{"$ref":"#/components/headers/Link"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Product"}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/product/{sku}":{"get":{"tags":["Products"],"operationId":"getProduct","summary":"Get a product","description":"Returns a single product by SKU. The `description` and `short_description` fields\nare always populated on this endpoint. Always returns JSON.\n","parameters":[{"name":"sku","in":"path","required":true,"description":"The product SKU (case-insensitive).","schema":{"type":"string"}},{"name":"extra","in":"query","description":"Comma-separated list of additional fields to include, e.g. `related`, `similar`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The product.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Product"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/order":{"get":{"tags":["Orders"],"operationId":"listOrders","summary":"List orders","description":"Returns the authenticated customer\u0027s orders (open and completed order statuses only),\nnewest first. Returns CSV by default when no `Accept` header is sent — use\n`Accept: application/json` (or `?accept\u003dapplication/json`) for JSON.\n\nResults are paginated: without a `limit` you get the 100 most recent orders, and the\n`X-Total-*` and `Link` response headers say where in the history you are. Pass `limit\u003d0`\nto fetch every order at once.\n\nThis applies to **CSV as well as JSON** — unlike `GET /product`, there is no unpaginated\nCSV feed here, so a CSV client that wants the whole history must send `limit\u003d0`.\n\n**Breaking change.** Orders were previously returned in no particular order (in practice,\noldest first) and are now explicitly newest first, which paging requires. A client that\nreads the last row as the most recent order must be updated.\n","parameters":[{"name":"accept","in":"query","description":"Overrides content negotiation, e.g. `text/csv` or `application/json`.","schema":{"type":"string","enum":["application/json","text/csv"]}},{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"One page of the customer\u0027s orders, newest first.","headers":{"X-Total-Count":{"$ref":"#/components/headers/XTotalCount"},"X-Page":{"$ref":"#/components/headers/XPage"},"X-Limit":{"$ref":"#/components/headers/XLimit"},"X-Total-Pages":{"$ref":"#/components/headers/XTotalPages"},"Link":{"$ref":"#/components/headers/Link"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}},"text/csv":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Orders"],"operationId":"createOrder","summary":"Create an order","description":"Validates and prices an order, returning the available shipping options and a\n`confirmation_code`. The order is held as *pending* until confirmed with\n`PUT /order/{uuid}` (using the `confirmation_code` as the UUID), or immediately\nconfirmed when `auto_confirm` is `true`. Pending orders expire after 24 hours.\n\nWhen `auto_confirm` is `true`, either `shipping_method` must be supplied or the\naccount must have a default shipping method, otherwise `412` is returned.\n\nSend an `Idempotency-Key` header to make retries safe — see the header description below.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"responses":{"201":{"description":"The validated order, enriched with pricing, shipping options and a confirmation code.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/order/{reference}":{"get":{"tags":["Orders"],"operationId":"getOrder","summary":"Get an order","description":"Returns a single order, including line items, shipping and tracking events.","parameters":[{"name":"reference","in":"path","required":true,"description":"The numeric order id or the order reference.","schema":{"type":"string"}}],"responses":{"200":{"description":"The order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Orders"],"operationId":"confirmOrder","summary":"Confirm a pending order","description":"Confirms a pending order created with `POST /order`. The path parameter is the\n`confirmation_code` returned when the order was created. Only the\n`shipping_method` field of the request body is used — pass the chosen option from\nthe `shipping_options` returned at creation. If omitted, the account\u0027s default\nshipping method is used where one exists (`412` otherwise).\n","parameters":[{"name":"reference","in":"path","required":true,"description":"The `confirmation_code` returned by `POST /order`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shipping_method":{"$ref":"#/components/schemas/CreateOrderShipping"}}}}}},"responses":{"200":{"description":"The confirmed order, including its `order_id` and `order_reference`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"410":{"$ref":"#/components/responses/Gone"},"412":{"$ref":"#/components/responses/PreconditionFailed"}}}},"/order/{reference}/item/{orderItemId}/configuration":{"post":{"tags":["Orders"],"operationId":"addItemConfiguration","summary":"Add provisioning configuration to an order item","description":"Attaches broadband (ADSL) provisioning configurations to a configurable order line\nitem. The number of configurations must not exceed the item\u0027s remaining\nunconfigured quantity.\n","parameters":[{"name":"reference","in":"path","required":true,"description":"The numeric order id or the order reference.","schema":{"type":"string"}},{"name":"orderItemId","in":"path","required":true,"description":"The order item id.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConfigRequest"}}}},"responses":{"200":{"description":"The updated order item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/order/{reference}/video":{"get":{"tags":["Orders"],"operationId":"getOrderVideo","summary":"Get the packing video for an order","description":"Returns links to the packing video recorded while the order was dispatched, where\none is available.\n","parameters":[{"name":"reference","in":"path","required":true,"description":"The order reference.","schema":{"type":"string"}}],"responses":{"200":{"description":"The packing video links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderVideoResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/order/{reference}/invoice":{"get":{"tags":["Orders"],"operationId":"getOrderInvoice","summary":"Get the invoice for an order","description":"Returns the invoice for the order as a PDF.\n\nAn order only has an invoice once it has been invoiced, which is reflected in its\n`status`. Asking any earlier returns a `412` — the invoice does not exist yet, rather\nthan being withheld, so it is worth asking again later.\n","parameters":[{"name":"reference","in":"path","required":true,"description":"The order reference.","schema":{"type":"string"}}],"responses":{"200":{"description":"The invoice.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"412":{"$ref":"#/components/responses/PreconditionFailed"}}}},"/quote":{"post":{"tags":["Quotes"],"operationId":"createQuote","summary":"Create a quote","description":"Validates and prices an order without placing it, creating a quote instead.\nRaises a ticket with NetXL support who will adjust pricing on the quote before getting\nback to you with the final pricing. The confirmation code can then be used to confirm the order\nat the updated quoted pricing\nThe request body is the same as `POST /order`, except `auto_confirm` must not be\nset (`400` otherwise).\n\nSend an `Idempotency-Key` header to make retries safe — see the header description below.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"responses":{"201":{"description":"The priced quote.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/customer":{"get":{"tags":["Customer"],"operationId":"getCustomer","summary":"Get account details","responses":{"200":{"description":"The authenticated customer\u0027s account details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/customer/credit":{"get":{"tags":["Customer"],"operationId":"getCustomerCredit","summary":"Get credit overview","description":"Returns the customer\u0027s credit limit, outstanding balance and outstanding credit orders.","responses":{"200":{"description":"The credit overview.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditOverview"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/customer/address":{"get":{"tags":["Customer"],"operationId":"listAddresses","summary":"List saved addresses","responses":{"200":{"description":"The customer\u0027s saved addresses.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerAddress"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Customer"],"operationId":"createAddress","summary":"Create an address","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerAddress"}}}},"responses":{"201":{"description":"The created address, including its new `id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerAddress"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/customer/address/{id}":{"delete":{"tags":["Customer"],"operationId":"deleteAddress","summary":"Delete an address","parameters":[{"name":"id","in":"path","required":true,"description":"The address id.","schema":{"type":"integer"}}],"responses":{"204":{"description":"The address was deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/category":{"get":{"tags":["Reference Data"],"operationId":"listCategories","summary":"List categories","description":"Returns the category tree, two levels deep.","responses":{"200":{"description":"The top-level categories with their children.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Category"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/manufacturer":{"get":{"tags":["Reference Data"],"operationId":"listManufacturers","summary":"List manufacturers","responses":{"200":{"description":"All manufacturers, ordered by name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Manufacturer"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/attribute":{"get":{"tags":["Reference Data"],"operationId":"listAttributes","summary":"List product attributes","description":"Returns the product attributes that can be used with the `attribute` filter on `GET /product`.","responses":{"200":{"description":"All product attributes and their possible values.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Attribute"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/oauth/token":{"post":{"tags":["Authentication"],"operationId":"exchangeToken","summary":"Exchange a token (RFC 8693)","description":"Exchanges a JWT issued by `https://www.netxl.com`, carrying a customer\u0027s consent for an\napplication to act for them, for a short-lived access token for this API.\n\nThis endpoint is for the confidential clients NetXL has registered — currently the NetXL MCP\nserver. It is not part of the customer-facing API, and a customer with an API key has no need\nof it. The client authenticates with HTTP Basic (preferred) or with `client_id` and\n`client_secret` in the body.\n\nThe `subject_token` is the JWT the authorization server issued to the client. It is audienced\nfor the client, not for this API, and is never accepted as authentication in its own right —\nonly exchanged here for a token that is. Present the result as `Authorization: Bearer`.\n","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","subject_token","subject_token_type"],"properties":{"grant_type":{"type":"string","const":"urn:ietf:params:oauth:grant-type:token-exchange"},"subject_token":{"type":"string","description":"The JWT issued by `https://www.netxl.com`."},"subject_token_type":{"type":"string","const":"urn:ietf:params:oauth:token-type:jwt"},"requested_token_type":{"type":"string","const":"urn:ietf:params:oauth:token-type:access_token","description":"Optional. If given, must be an access token."},"resource":{"type":"string","const":"https://api.netxl.com","description":"Optional. If given, must be this API."},"client_id":{"type":"string","description":"Required unless the client authenticates with HTTP Basic."},"client_secret":{"type":"string","description":"Required unless the client authenticates with HTTP Basic."}}}}}},"responses":{"200":{"description":"An access token for the customer named by the subject token.","headers":{"Cache-Control":{"description":"Always `no-store`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["access_token","issued_token_type","token_type","expires_in"],"properties":{"access_token":{"type":"string","description":"A 64-character opaque token. Send as `Authorization: Bearer`."},"issued_token_type":{"type":"string","const":"urn:ietf:params:oauth:token-type:access_token"},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer","description":"Seconds until the token expires.","examples":[900]}}}}}},"400":{"description":"The request, or the subject token, was not acceptable. `invalid_grant` means the subject\ntoken was refused — no further detail is given.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"The client did not authenticate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"403":{"description":"The source IP address is blocked (see Authentication)."},"503":{"description":"The authentication backend is unavailable."}}}},"/check-health":{"get":{"tags":["Health"],"operationId":"checkHealth","summary":"Health check","security":[],"responses":{"200":{"description":"The service is healthy.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/openapi.json":{"get":{"tags":["Documentation"],"operationId":"getOpenApiJson","summary":"This specification as JSON","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/openapi.yaml":{"get":{"tags":["Documentation"],"operationId":"getOpenApiYaml","summary":"This specification as YAML","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/yaml":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"64-character customer API key."},"ApiKeyQuery":{"type":"apiKey","in":"query","name":"api-key","description":"64-character customer API key (alternative to the `X-Api-Key` header)."},"BearerToken":{"type":"http","scheme":"bearer","description":"An access token from `POST /oauth/token`, held by an application a customer has authorised to\nact for them. It confers exactly what that customer\u0027s own API key would, and expires in\nminutes.\n\nA request that carries a bearer token is authenticated on that token alone. It does not fall\nback to the API key, so an expired token is a `401` rather than a request that quietly\nsucceeds as somebody else.\n"}},"parameters":{"Page":{"name":"page","in":"query","required":false,"description":"Which page of results to return, counting from 1. A page beyond the end of the results is\nnot an error — it returns an empty list.\n","schema":{"type":"integer","minimum":1,"default":1},"example":2},"Limit":{"name":"limit","in":"query","required":false,"description":"How many results to return per page, to a maximum of 500. Use `limit\u003d0` to return\neverything in one response, which for the full catalogue is a large document.\n\nResults are paginated by default: a request that does not ask for a `limit` gets the first\n100. The one exception is the CSV catalogue feed — `GET /product` rendered as CSV, which is\nwhat you get with no `Accept` header — where a missing `limit` still means the whole\ncatalogue, so that feeds written before paging existed are not silently truncated. Naming a\n`page` opts even that request back into paging. The exception does **not** extend to\n`GET /order`, which is paginated as CSV just as it is as JSON.\n\nWhich page you are looking at is described by the `X-Total-Count`, `X-Page`, `X-Limit` and\n`X-Total-Pages` response headers, and the `Link` header carries ready-made URLs for the\nfirst, previous, next and last pages. The body itself is always a plain array, so that the\nCSV representation stays a plain table.\n","schema":{"type":"integer","minimum":0,"maximum":500},"example":100},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"A unique, client-generated value (a UUID is ideal) identifying this request, so that an\naccidental replay — a retried timeout, a double-clicked button — does not create a second\norder or quote.\n\nThe first request using a given key is processed normally and its response is recorded.\nAny later request using the same key returns that recorded response verbatim, with an\n`Idempotent-Replay: true` header, and creates nothing.\n\nKeys are remembered permanently, so a key that created an order will replay that order\u0027s\nresponse however long ago it was used.\n\nKeys are scoped to your account and to the request itself. Re-using a key with a different\nmethod, path or body returns `400`. If an earlier request with the same key is still being\nprocessed, `409` is returned — wait and retry. A request that fails leaves the key unused,\nso you are free to retry it with the same key.\n","schema":{"type":"string","maxLength":255},"example":"7f1e0f6c-2a5e-4a9b-9c3d-1b2c3d4e5f60"}},"headers":{"XTotalCount":{"description":"How many results there are in total, across every page, after filtering.","schema":{"type":"integer"},"example":4312},"XPage":{"description":"Which page this is, counting from 1.","schema":{"type":"integer"},"example":2},"XLimit":{"description":"How many results a page holds. `0` means the results were not paginated.","schema":{"type":"integer"},"example":100},"XTotalPages":{"description":"How many pages there are at this `limit`.","schema":{"type":"integer"},"example":44},"Link":{"description":"RFC 8288 links to the `first`, `prev`, `next` and `last` pages, whichever of them exist.\nEach link carries the rest of the query across, so following `next` keeps your filters.\n","schema":{"type":"string"},"example":"\u003c/product?page\u003d3\u0026limit\u003d100\u003e; rel\u003d\"next\", \u003c/product?page\u003d44\u0026limit\u003d100\u003e; rel\u003d\"last\""},"IdempotentReplay":{"description":"Present and `true` when this response was replayed from a previous request with the same\n`Idempotency-Key`, meaning nothing was created.\n","schema":{"type":"string","enum":["true"]}}},"responses":{"IdempotencyConflict":{"description":"An earlier request with the same `Idempotency-Key` is still being processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"The request was invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"The API key is missing or invalid.","content":{"application/json":{"schema":{"type":"object"}}}},"PaymentRequired":{"description":"The account has no credit terms or insufficient available credit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The resource was not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Gone":{"description":"The pending order has expired (older than 24 hours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PreconditionFailed":{"description":"A shipping method is required and no account default exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"An unexpected error occurred.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"OAuthError":{"type":"object","description":"An OAuth 2.0 error response (RFC 6749 5.2).","required":["error"],"properties":{"error":{"type":"string","enum":["invalid_request","invalid_client","invalid_grant","invalid_target","unsupported_grant_type"]}}},"LocalDateTime":{"type":"string","description":"UK local time in the format `yyyy-MM-dd\u0027T\u0027HH:mm:ss`, with no timezone offset.","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$","examples":["2026-01-15T10:30:00"]},"Error":{"type":"object","properties":{"message":{"type":"string","description":"A description of the error."}}},"Product":{"type":"object","properties":{"sku":{"type":"string"},"ean":{"type":["string","null"]},"name":{"type":"string"},"manufacturer":{"type":"string","description":"The manufacturer key."},"weight_kg":{"type":"number"},"box_width_mm":{"type":"number"},"box_height_mm":{"type":"number"},"box_length_mm":{"type":"number"},"images":{"type":"array","items":{"$ref":"#/components/schemas/ProductImage"}},"availability":{"$ref":"#/components/schemas/ProductAvailability"},"single_unit_cost":{"type":"number"},"five_plus_unit_cost":{"type":"number"},"ten_plus_unit_cost":{"type":"number"},"consumer_unit_cost":{"type":"number","description":"The recommended retail price."},"description":{"type":"string","description":"Only included when requested via `extra\u003ddescription` (always included on `GET /product/{sku}`)."},"description_html":{"type":"string","description":"Only included when requested via `extra\u003ddescription_html`."},"short_description":{"type":"string","description":"Only included when requested via `extra\u003dshort_description` (always included on `GET /product/{sku}`)."},"store_url":{"type":"string"},"categories":{"type":"array","items":{"type":"string"},"description":"The category keys the product belongs to."},"attributes":{"type":"object","additionalProperties":{"type":"string"},"description":"Attribute name to value map."},"related_products":{"type":"array","items":{"type":"string"},"description":"SKUs of related products. Only included when requested via `extra\u003drelated`."},"similar_products":{"type":"array","items":{"type":"string"},"description":"SKUs of similar products. Only included when requested via `extra\u003dsimilar`."}}},"ProductImage":{"type":"object","properties":{"is_primary":{"type":"boolean"},"url":{"type":"string"}}},"ProductAvailability":{"type":"object","properties":{"available":{"type":"integer","description":"Units currently in stock."},"status":{"type":"string"},"incoming":{"type":"array","items":{"$ref":"#/components/schemas/ProductIncomingStock"}}}},"ProductIncomingStock":{"type":"object","properties":{"incoming":{"type":"integer","description":"Units on an incoming delivery."},"expected_on":{"$ref":"#/components/schemas/LocalDateTime"}}},"Order":{"type":"object","properties":{"id":{"type":"integer","readOnly":true},"order_reference":{"type":"string"},"customer_reference":{"type":"string"},"cost_net":{"type":"number"},"cost_tax":{"type":"number"},"cost_gross":{"type":"number"},"created_on":{"$ref":"#/components/schemas/LocalDateTime"},"status":{"type":"string","readOnly":true,"description":"Where the order has got to. New values may be added over time, so treat this as a label to show rather than a fixed set to switch on.","example":"Awaiting Despatch"},"items":{"type":"array","items":{"$ref":"#/components/schemas/OrderItem"}},"shipping":{"type":"array","items":{"$ref":"#/components/schemas/OrderShipping"}}}},"OrderItem":{"type":"object","properties":{"id":{"type":"integer"},"product":{"$ref":"#/components/schemas/Product"},"quantity":{"type":"integer"},"cost_net":{"type":"number"},"cost_tax":{"type":"number"},"related_items":{"type":"array","items":{"$ref":"#/components/schemas/OrderItem"}},"configurations":{"type":"array","items":{"$ref":"#/components/schemas/OrderItemConfiguration"}},"serial_numbers":{"type":"array","items":{"$ref":"#/components/schemas/ProductSerialNumber"}}}},"OrderItemConfiguration":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProvisioningData"}}},"ProvisioningData":{"type":"object","properties":{"product":{"type":"string"},"adsl_username":{"type":"string"},"adsl_password":{"type":"string"},"wifi_ssid":{"type":"string"},"wifi_password":{"type":"string"}}},"ProductSerialNumber":{"type":"object","properties":{"mac_address":{"type":"string"},"serial_number":{"type":"string"},"provisioning_sku":{"type":"string"}}},"OrderShipping":{"type":"object","properties":{"method_id":{"type":"integer"},"contact_email":{"type":"string"},"contact_phone":{"type":"string"},"tracking_number":{"type":"string"},"cost_net":{"type":"number"},"cost_tax":{"type":"number"},"last_tracking_status":{"type":"string"},"tracking_events":{"type":"array","items":{"$ref":"#/components/schemas/TrackingEvent"}}}},"TrackingEvent":{"type":"object","properties":{"date":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"locality":{"type":"string"}}},"CreateOrderRequest":{"type":"object","required":["customer_reference","items","billing_address","shipping_address"],"properties":{"customer_reference":{"type":"string","description":"Your own reference for the order."},"items":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/CreateOrderItem"}},"billing_address":{"$ref":"#/components/schemas/CustomerAddress"},"shipping_address":{"$ref":"#/components/schemas/CustomerAddress"},"shipping_method":{"$ref":"#/components/schemas/CreateOrderShipping"},"shipping_region":{"type":"integer","description":"The shipping region. Defaults to 1 (UK mainland)."},"external_customer_id":{"type":"string","description":"An optional identifier for your own end customer."},"order_notifications":{"$ref":"#/components/schemas/NotificationDetails"},"courier_notifications":{"$ref":"#/components/schemas/NotificationDetails"},"auto_confirm":{"type":"boolean","description":"When `true`, the order is confirmed immediately without a separate\n`PUT /order/{uuid}` call. Requires a `shipping_method` or an account default\nshipping method. Not permitted on `POST /quote`.\n"},"confirmation_code":{"type":"string","readOnly":true,"description":"Populated on creation; use as the `{uuid}` in `PUT /order/{uuid}` to confirm the order."},"order_id":{"type":"integer","readOnly":true},"order_reference":{"type":"string","readOnly":true},"shipping_options":{"type":"array","readOnly":true,"items":{"$ref":"#/components/schemas/CreateOrderShipping"},"description":"The shipping methods available for this order."},"shipping_weight":{"type":"number","readOnly":true},"cost_net":{"type":"number","readOnly":true},"cost_tax":{"type":"number","readOnly":true},"cost_gross":{"type":"number","readOnly":true}}},"CreateOrderItem":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"cost_net":{"type":"number"},"id":{"type":"integer","readOnly":true}}},"CreateOrderShipping":{"type":"object","properties":{"id":{"type":"integer","description":"The shipping method id. The only field required when selecting a method."},"provider":{"type":"string"},"description":{"type":"string"},"cost_net":{"type":"number"},"expected_dispatch_date":{"$ref":"#/components/schemas/LocalDateTime"},"expected_arrival_date":{"$ref":"#/components/schemas/LocalDateTime"}}},"NotificationDetails":{"type":"object","properties":{"email_address":{"type":"string"},"phone_number":{"type":"string"}}},"CreateConfigRequest":{"type":"object","required":["configurations"],"properties":{"configurations":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/ConfigItem"}}}},"ConfigItem":{"type":"object","required":["adsl_username","adsl_password"],"properties":{"adsl_username":{"type":"string"},"adsl_password":{"type":"string"}}},"Customer":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"accounts_email":{"type":"string"},"trade_account":{"type":"boolean"},"credit_terms":{"oneOf":[{"$ref":"#/components/schemas/CustomerCreditTerms"},{"type":"null"}]}}},"CustomerCreditTerms":{"type":"object","properties":{"credit_limit":{"type":"number"},"term_days":{"type":"integer"}}},"CreditOverview":{"type":"object","properties":{"credit_limit":{"type":"number"},"total_outstanding":{"type":"number"},"available_credit":{"type":"number"},"outstanding_orders":{"type":"array","items":{"$ref":"#/components/schemas/CreditOrderStatus"}}}},"CreditOrderStatus":{"type":"object","properties":{"total_cost":{"type":"number"},"total_paid":{"type":"number"},"total_due":{"type":"number"},"created_on":{"$ref":"#/components/schemas/LocalDateTime"},"due_on":{"$ref":"#/components/schemas/LocalDateTime"},"is_overdue":{"type":"boolean"},"customer_reference":{"type":"string"},"order_reference":{"type":"string"}}},"CustomerAddress":{"type":"object","required":["contact_name","street_one","city","zip","country_code"],"properties":{"id":{"type":"integer","readOnly":true},"contact_name":{"type":"string"},"company_name":{"type":"string"},"street_one":{"type":"string"},"street_two":{"type":"string"},"street_three":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"country_code":{"type":"string"},"contact_number":{"type":"string"},"tax_number":{"type":"string"}}},"Category":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"children":{"type":["array","null"],"items":{"$ref":"#/components/schemas/Category"},"description":"Child categories, or null when there are none. Populated at most two levels deep."}}},"Manufacturer":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"}}},"Attribute":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"possible_values":{"type":"array","items":{"$ref":"#/components/schemas/AttributeValue"}}}},"AttributeValue":{"type":"object","properties":{"value":{"type":"string"}}},"OrderVideoResponse":{"type":"object","properties":{"video_file":{"type":"string","description":"URL of the packing video file."},"streaming_playlist":{"type":"string","description":"URL of the HLS (m3u8) streaming playlist."}}}}}}