# NetXL API > NetXL (netxl.com) is a UK trade supplier of networking hardware. The NetXL Customer API gives > trade customers programmatic access to customer-specific real-time pricing and stock, product > search, quote requests, order creation and confirmation, and account/credit data. A machine-readable OpenAPI 3.1 specification is available at https://api.netxl.com/openapi.yaml (or /openapi.json). An MCP server for AI agents is available at https://mcp.netxl.com. Every storefront product and category page is also available as plain markdown by appending `.md` to its URL (drop the trailing slash) — see https://www.netxl.com/llms.txt for the storefront index. The complete API documentation follows. --- # Introduction ## Introduction The NetXL API is a powerful set of tools that allow you to automate key interactions with us. These include getting real-time pricing and stock availability, requesting quotes, and creating and confirming orders. The API is free and available to all trade customers, however to create an order you are required to have a credit account. Payment via PayPal and Credit/Debit card is not currently supported by the API. The documentation below will show how to get the most out of the API. It includes code samples in several popular programming languages, as well as guides on using the API with several popular GUI based tools; perfect for customers with less development experience. A machine-readable [OpenAPI 3.1 specification](https://api.netxl.com/openapi.yaml) of the full API is also available (as [JSON](https://api.netxl.com/openapi.json) too). It can be imported into tools like Postman, used to generate client code, or given to AI assistants and agents to help them work with the API. If you would rather not write any code at all, we also run an [MCP server](#mcp-server) that connects your NetXL account directly to AI assistants such as Claude and ChatGPT, letting you search the catalogue, request quotes and place orders in plain language. --- # Getting Started ## Getting Started First things first, in order to use the API, you will need to create an API key. We use this to identify the requests you make as coming from your account, which ensures we return the correct information to you, and nobody else can see your pricing and order data. To create an API key, visit the [API](https://www.netxl.com/account/api) section of the NetXL customer dashboard. Your API key is a unique 64 character string tied to your account; you should not share it with anyone you do not trust, and you can disable it at any time or create a new key from your dashboard. ## Accessing the API There are three main ways to access the API. The most common is to build an integration using a programming language such as [Python](https://www.python.org/) or [Java](https://www.oracle.com/java/technologies/). This allows you to automate calls to the API, tightly integrating into existing systems. The second is to use a graphical tool such as [Postman](https://www.postman.com/) or [Paw](https://paw.cloud/) (or even a Web Browser). This method lets you query the API for data on an ad-hoc basis, without any need for computer programming experience. The third is our [MCP server](#mcp-server), which connects your account to an AI assistant such as Claude or ChatGPT. You ask for what you want in plain language and the assistant makes the calls for you. It needs no programming experience and no API key. ## API Sandbox You can access the sandbox API by using the hostname https://sandbox.api.netxl.com Please [contact support](/contact/) if you would like an API key for the sandbox The sandbox API uses its own database and does not reflect accurate stock, pricing or product data ## Response Data Formats - JSON & CSV Most API commands return responses in JSON format. This is the best format for automated integrations, and is also human readable due to its structured layout and key/value structure. An example response for the /customer command is shown on the right. The product and order list commands (`GET /product` and `GET /order`) also support returning the response as a CSV file; this is useful for customers who wish to import the data into a third party such as Microsoft Excel or Google Sheets. Be aware that these two commands return **CSV by default** — if you send no `Accept` header (or `Accept: text/html`, as a Web Browser does) you will receive a CSV, making it easy to create links to updated pricing or order information. To receive JSON, send `Accept: application/json`, as [explained below](#change-response-type). ### EXAMPLE JSON RESPONSE ```json { "first_name": "Geoff", "last_name": "Wilson", "email_address": "geoff@example.com", "trade_account": true, "credit_terms": { "credit_limit": 5000.00, "term_days": 30 } } ``` ## Paginating Results The two list commands — `GET /product` and `GET /order` — are paginated. Rather than returning the whole catalogue or order history in a single response, they return one page at a time, which keeps responses fast and predictable. **Choosing a page** Two query string parameters control pagination, and both are optional: - `page` — which page of results to return, counting from `1`. Defaults to `1`. Asking for a page beyond the end of the results is not an error; it simply returns an empty list. - `limit` — how many results to return per page, up to a maximum of `500`. When you do not send a `limit`, you get the first `100` results. Send `limit=0` to return everything in a single response — for the full product catalogue this is a large document, so only do so when you really need the lot. **Knowing where you are** Every paginated response carries a set of headers describing the full result set, so you never have to guess whether there is more to fetch: - `X-Total-Count` — how many results there are in total, across every page, after any filters are applied. - `X-Page` — which page this response is, counting from `1`. - `X-Limit` — how many results a page holds. A value of `0` means the results were not paginated. - `X-Total-Pages` — how many pages there are at the current `limit`. - `Link` — [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288) links to the `first`, `prev`, `next` and `last` pages, whichever of them exist. Each link carries the rest of your query string across, so following `next` keeps your filters in place. The response body itself is always a plain array of results, so that the CSV representation stays a plain table. **CSV and the product catalogue feed** There is one deliberate exception. `GET /product` rendered as CSV — which is what you get when you send no `Accept` header, as described in [response data formats](#response-formats) — is the catalogue feed, and a request that does not ask for a `limit` still returns the **whole catalogue**, exactly as it did before pagination existed. This keeps feeds written against the old behaviour from being silently truncated. Naming a `page` (or a `limit`) opts even that request back into paging. This exception does **not** extend to `GET /order`: order history is paginated as CSV just as it is as JSON. A CSV client that wants the entire history in one response must send `limit=0`. Order results are returned **newest first**, which pagination requires. (Previously orders came back in no particular order, in practice oldest first — a client that read the last row as the most recent order should be updated.) ### Request: `GET /product?page=2&limit=100` **HTTP** ```http GET /product?page=2&limit=100 HTTP/1.1 X-Api-Key: Host: api.netxl.com Accept: application/json Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/product", headers={ "X-Api-Key": "", "Accept": "application/json" }, params={ "page": 2, "limit": 100 } ) ``` ### RESPONSE HEADERS ```json X-Total-Count: 4312 X-Page: 2 X-Limit: 100 X-Total-Pages: 44 Link: ; rel="first", ; rel="prev", ; rel="next", ; rel="last" ``` --- # Your First Request ## Your First Request Now that we have an API key and have decided on how to access the API (programmatically or with a tool), it's time to make our first request. The NetXL API is a [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) API. The base URL for the API is shown on the right. For our first request, we'll ask the API to return our customer information, which includes our name, email address, and details of any credit account we hold. ### NetXL API Endpoint ```json https://api.netxl.com ``` ## Authenticating your API requests For every request you need to provide the API key as a form of authentication. There are two ways to do this, and you can choose whichever you prefer: 1. Supply the key as a HTTP Header in the request 2. Use a Query String in the URL to provide it ## Provide the key as a HTTP header To provide the API key as an extra HTTP header, include the following header in your HTTP requests ``` X-Api-Key: ``` ## Provide the key using a query string Providing the API key using a query string is as simple as appending the following key/value to the end of every URL you're making requests to ``` api-key= ``` ### Query String Example ```json https://api.netxl.com/customer?api-key= ``` ## Selecting the response type The product and order list commands (`GET /product` and `GET /order`) support returning the response as a CSV file, useful for importing into third party application such as Microsoft Excel or Google Sheets. To request the response as a CSV file you should set the 'Accept' header on your request to `text/csv` as follows ``` Accept: text/csv ``` For these two commands CSV is the **default**: a request with no `Accept` header (or `Accept: text/html`) returns a CSV. To receive JSON you must set the `Accept` header to `application/json`. It's recommended to send the header explicitly on every request so you always get the format you expect. You can also override the format with the `accept` query parameter, for example `?accept=application/json`. ## Making the request Now that you have everything set up, it's time to make your first API request. We have a blog post containing examples of calling the API both using a programming language (Python) and a graphical tool (Postman). 1. Read the [API beginner's guide](https://www.netxl.com/blog/general/api-beginners-guide/) You can also check your API key is valid by clicking this [link](https://api.netxl.com/customer?api-key=) in your browser, this will show your basic account details, you should see something similar to the example on the right ### Request: `GET /customer` ### RESPONSE ```json API Response will appear here! ``` --- # Error Handling ## Error Handling If you receive an error response from the API, the first thing you should check is the HTTP response status code. Most errors are caused by invalid requests being sent to the API. In these cases we'll return a 4xx status code, and it should be possible to fix the error by making changes to the request you are sending. You should double-check the documentation for the command in question, and it can also be helpful to consult the message returned in the response body, to see why the command failed. In rare cases, the API can fail due to a server side issue. In this case we'll return a 5xx status code, and you'll likely not be able to fix the issue yourself. You can try the request again at a later date, or contact our support team for assistance. ## Possible HTTP Error Codes Below is a list of the error codes the API can return. You can read more about HTTP status codes [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) ``` 400 Bad Request ``` Indicates that the API could not complete the request, check the response message for the cause of the error ``` 401 Unauthorized ``` Your API key is missing or not valid, please check it and try again. Unlike other errors, a 401 response has an empty JSON body ``` 402 Payment Required ``` Your account has no credit terms, or there is insufficient available credit on your account for the order ``` 403 Forbidden ``` Your IP address has been temporarily blocked after repeated requests with an invalid API key (10 within 60 seconds). Check your API key, then wait 5 minutes before trying again ``` 404 Not Found ``` This indicates either the URL was not valid, or the object you requested does not exist, for example trying to query an invalid order reference ``` 409 Conflict ``` An earlier request with the same `Idempotency-Key` is still being processed (see [safe retries](#idempotency)). Wait a moment and try again ``` 410 Gone ``` The pending order you tried to confirm has expired — orders must be confirmed within 24 hours of creation ``` 412 Precondition Failed ``` A shipping method is required for the order and your account has no default shipping method, provide one in the `shipping_method` field ``` 500 Internal Server Error ``` An unexpected error occurred in the API, you should try your command again or contact support for assistance ``` 503 Service Unavailable ``` The API is temporarily unavailable, for example while the authentication backend is unreachable, try your command again in a few minutes ### Error Body Response - `message` (string) — Indicates what caused the returned error, especially useful for diagnosing 4xx errors ### Request: `POST /order` **JSON** ```json { "customer_reference": "API-ORDER-01", "items": [ { "sku": "UDM", "quantity": 1 }, { "sku": "UAP-AC-PRO", "quantity": 5 } ], "billing_address": { "id": 1 }, "shipping_address": { "id": 1 }, "auto_confirm": true, "shipping_method": { "id": 100 } } ``` ### 400 Bad Request ```json { "message": "Requested shipping method 100 is not valid for this order" } ``` --- # MCP Server ## MCP Server As well as the REST API documented here, we run a hosted **MCP server** at `https://mcp.netxl.com/mcp`. It exposes the same Customer API over the [Model Context Protocol](https://modelcontextprotocol.io/), the open standard that AI assistants use to call external tools. Connecting it to an AI client such as Claude, Claude Code or ChatGPT lets you work with your NetXL account in plain language — searching the catalogue, checking your pricing and stock, requesting quotes, and creating and confirming orders — without writing any code. It is the same account, the same pricing and the same permissions as the REST API. Anything the API can do on your behalf, the MCP server can do on your behalf, with one deliberate exception described in [Tools available over MCP](#mcp-tools). You do **not** give your API key to the AI client. Access is granted through a browser sign-in and consent screen, and can be withdrawn at any time from your account — see [Connecting an AI client](#mcp-connecting) and [Managing access](#mcp-managing-access). A NetXL trade account is required, and as with the REST API you need a credit account to place orders. ## Connecting an AI client The server is remote, so there is nothing to install and nothing to run. You give your AI client one URL — `https://mcp.netxl.com/mcp` — and it does the rest. Most clients have a menu for this. In the Claude apps, go to **Settings → Connectors → Add custom connector** and paste the URL. In Claude Code, run the command shown opposite. Clients that are configured with a JSON file instead take the `mcpServers` block opposite. ChatGPT and other MCP-capable tools accept the same URL wherever they ask for a remote MCP server. The first time the client connects, it will open a browser window at netxl.com asking you to sign in and approve access. Approve it once and the client stays connected; there is no key to copy and nothing to paste back. Behind the scenes this is a standard [OAuth 2.1](https://oauth.net/2.1/) authorisation-code flow with PKCE, and clients register themselves automatically, so no setup is needed at our end or yours. Your API key is never sent to the AI client, the model, or the company operating it: the MCP server exchanges your consent for a short-lived credential of its own, server to server. If your client reports that it could not sign in, check that it is allowed to open a browser window and that you are logged in to netxl.com in that browser. **Important:** The Codex app currently has an open issue that prevents the MCP server and Codex from completing the OAuth flow (https://github.com/openai/codex/issues/31573) ### Request: `MCP SERVER URL` ``` https://mcp.netxl.com/mcp ``` **Claude Code** ```bash claude mcp add --transport http netxl https://mcp.netxl.com/mcp ``` **JSON config** ```json { "mcpServers": { "netxl": { "type": "http", "url": "https://mcp.netxl.com/mcp" } } } ``` ## Tools available over MCP The server exposes 17 tools. You do not call these by name — you ask in plain language and the client picks the tool — but it is worth knowing what it can and cannot reach. **Product catalogue** — `search_products`, `get_product`. Search and filter the catalogue and read a single product, with your own contract pricing and live stock. **Reference data** — `list_categories`, `list_manufacturers`, `list_attributes`. The keys and values that the product filters accept, as described in [Categories & Attributes](#filter-data). **Orders and quotes** — `list_orders`, `get_order`, `get_order_video`, `create_order`, `confirm_order`, `create_quote`, `add_order_item_configuration`. **Account** — `get_customer`, `get_credit`, `list_addresses`, `create_address`, `delete_address`. Read-only tools are marked as such, so a client that asks before taking action will only prompt you on the ones that change something. ## Ordering is always two steps The REST API can price and place a billable order in a single call using [auto confirmation](#auto-confirm). That option is **deliberately not available over MCP**. `create_order` always produces a *pending* order and a `confirmation_code`, showing you the line prices, delivery options and total; `confirm_order` is a separate, explicit step that places it. This means an AI client cannot spend your money in one move, and it must show you the costs first. A pending order that you never confirm expires after 24 hours and costs nothing. If you want negotiated pricing rather than list pricing, ask for a quote — `create_quote` — and we will come back to you. ## What it cannot do Cancelling or amending an existing order is not exposed as a tool — and it is not in the REST API either. To cancel or change an order, raise a support ticket or call us, as described in [Cancel or Amend Order](#cancel-order). Prices returned over MCP are specific to your account and **exclude VAT**, exactly as they do over the REST API. Timestamps are UK local time. ## Managing access Every AI client you connect appears under **Connected AI Tools** on your [API settings page](/account/api), along with the date you approved it. To withdraw access, press **Disconnect**. The client's credential stops working within the hour, and it will have to ask for your approval again before it can do anything further. Disconnecting a tool does not touch your API key, and revoking or rolling your API key does not disconnect an AI tool — the two are independent credentials. Access is granted per client. Approving Claude does not grant anything to any other tool, and disconnecting one leaves the others working. An AI client acts as you. It sees your pricing, your credit position, your addresses and your order history, and — once you confirm an order — it can commit your account to a purchase. Only connect clients you trust, and read what `create_order` reports back before confirming it. --- # Account Details & Addresses ## Account Details & Addresses These commands allow you to query your account details, including your credit status and shipping addresses. Contact our support team if you wish to apply for a credit account. ### Command Endpoints ```json GET /customer GET /customer/credit GET /customer/address POST /customer/address DELETE /customer/address/ ``` ## Get your account details Return the basic details of your account ### Attributes - `first_name` (string) — The first name of the account holder. - `last_name` (string) — The surname of the account holder. - `email_address` (string) — Login email address of the account holder. - `accounts_email` (string, optional) — Account email address that will receive copies of invoices - `trade_account` (boolean) — Is your account flagged as a trade account. - `credit_terms` (object, optional) — Details of your credit account. - `credit_limit` (number) — Your accounts current credit limit, this is your actual limit and does not include any outstanding orders. - `term_days` (integer) — How many days after an order to settle the payment ### Request: `GET /customer` **HTTP** ```http GET /customer HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/customer", headers={ "X-Api-Key": "", }, ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/customer")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json { "first_name": "Geoff", "last_name": "Wilson", "email_address": "geoff@example.com", "trade_account": true, "credit_terms": { "credit_limit": 5000.00, "term_days": 30 } } ``` ## Get your credit details Return the details of your credit account, including your credit limit, total outstanding order value, and available credit. The response will also include details of any open orders. ### Attributes - `credit_limit` (number) — Your current account credit limit. - `total_outstanding` (number) — Total outstanding value of unpaid orders. - `available_credit` (number) — Available credit for new orders. - `outstanding_orders` (list) — List of outstanding orders, detailing each due date and amount due. - `total_cost` (number) — The total gross cost of the order. - `total_paid` (number) — Total amount already paid to the order. - `total_due` (number) — Outstanding amount still to be paid. - `created_on` (datetime) — When the order was accepted by the system, as UK local time in the format `yyyy-MM-dd'T'HH:mm:ss`. - `due_on` (datetime) — When full payment is due for the order, as UK local time in the format `yyyy-MM-dd'T'HH:mm:ss`. - `is_overdue` (boolean) — Is payment overdue for this order. - `customer_reference` (string) — The reference you provided for this order - `order_reference` (string) — Our reference for the order ### Request: `GET /customer/credit` ``` https://api.netxl.com/customer/credit ``` **HTTP** ```http GET /customer/credit HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/customer/credit", headers={ "X-Api-Key": "", }, ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/customer/credit")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json { "credit_limit": 5000.00, "total_outstanding": 851.50, "available_credit": 4148.50, "outstanding_orders": [ { "total_cost": 851.50, "total_paid": 0, "total_due": 851.50, "created_on": "2021-04-14T09:30:00", "due_on": "2021-05-14T09:30:00", "is_overdue": false, "customer_reference": "API-ORDER-01", "order_reference": "NXL-11111111" } ] } ``` ## Get Addresses Return a list of all active addresses on the account. The "id" field can be used in the /order command to avoid having to supply full address data every time. ### Attributes - `id` (integer) — ID of the address, provide this in the order command to use this address, or in the DELETE command to remove it. - `contact_name` (string) — The recipients name. - `company_name` (string, optional) — Name of the recipient company. - `street_one` (string) — The first line of the recipients address. - `street_two` (string, optional) — Second line of the recipients address. - `street_three` (string, optional) — Third line of the recipients address. - `city` (string) — Recipients city. - `state` (string, optional) — Recipients county. - `zip` (string) — Recipients postal code. - `country_code` (string) — The two letter country code of the address, should be a valid country from the ISO 3166-1 alpha-2 list - `contact_number` (string, optional) — Contact phone number for the recipient, this will be provided to the courier for delivery. - `tax_number` (string, optional) — Tax registration number associated with the address, where one has been provided. ### Request: `GET /customer/address` **HTTP** ```http GET /customer/address HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/customer/address", headers={ "X-Api-Key": "", }, ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/customer/address")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Distribution Ltd", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000", "tax_number": "GB123456789" } ] ``` ## Create delivery address This command can be used to create a new address, whose ID can then be reused when creating orders. **Important!** Please ensure the correct country code is provided; incorrect codes (such as GB for Jersey) will result in incorrect shipping estimates and may result in orders being cancelled. For UK addresses, the 'contact_number' field can be provided in either local or international format (e.g. 0330043300 or +44330043300). For all other countries the number should always be provided in international format. ### Request: `POST /customer/address` **HTTP** ```http POST /customer/address HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 Content-Length: 246 {"contact_name":"Geoff Wilson","company_name":"NetXL Limited","street_one":"Unit 4 Riverside Business Centre","street_two":"Walnut Tree Close","city":"Guildford","state":"Surrey","zip":"GU1 4UG","country_code":"GB","contact_number":"03300433000"} ``` **Python** ```python import requests response = requests.post( url="https://api.netxl.com/customer/address", headers={ "X-Api-Key": "", }, json={ "contact_name": "Geoff Wilson", "company_name": "NetXL Limited", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); Gson gson = new GsonBuilder().create(); HashMap newAddress = new HashMap<>(); newAddress.put("contact_name", "Geoff Wilson"); newAddress.put("company_name", "NetXL Limited"); newAddress.put("street_one", "Unit 4 Riverside Business Centre"); newAddress.put("street_two", "Walnut Tree Close"); newAddress.put("city", "Guildford"); newAddress.put("state", "Surrey"); newAddress.put("zip", "GU1 4UG"); newAddress.put("country_code", "GB"); newAddress.put("contact_number", "03300433000"); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/customer/address")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(newAddress))) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Limited", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" } ``` ## Delete an address Remove an address from your account using its ID, as returned by the [address list](#existing-address) command. A successful deletion returns a `204 No Content` response with an empty body. Requesting an ID that does not exist on your account returns a `404 Not Found`. ### Request: `DELETE /customer/address/` **HTTP** ```http DELETE /customer/address/123456 HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.delete( url="https://api.netxl.com/customer/address/123456", headers={ "X-Api-Key": "", }, ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/customer/address/123456")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .DELETE() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json 204 No Content ``` --- # Product Catalogue ## Product Catalogue One of the key features of the API is the ability to search and filter the product catalogue. Products can be filtered by manufacturer, category or attribute. The catalogue can additionally be searched using a free text field. Each product that is returned will include a single, 5+ and 10+ price. These prices reflect any custom pricing or discounts applied to your account. Real time stock info is returned for each product; the number available at the time of the request is returned in the product object. Additionally, trade account customers can see the expected arrival dates of new stock from manufacturers. By default only a subset of the product data is returned, however, it is possible to request the additional data is returned in the command, as documented below. Please be aware that requesting additional data may reduce the response speed of the command. Note that `GET /product` returns a **CSV by default** when no `Accept` header is sent — send `Accept: application/json` for JSON, as described in [selecting the response type](#change-response-type). `GET /product/` always returns JSON, and always includes the product's `description` and `short_description`. ### ENDPOINTS ```json GET /product GET /product/:sku ``` ## The Product Object The product object provides comprehensive details about every product available via NetXL. ### Product Object - `sku` (string) — The SKU for the product, used in the order command - `ean` (string) — The EAN of the product, this is normally a 13 digit EU EAN. Null where no EAN is recorded - `weight_kg` (number) — Weight of the product (including box) in kilograms - `box_width_mm` (number) — Width of the boxed product in millimeters - `box_height_mm` (number) — Height of the boxed product in millimeters - `box_length_mm` (number) — Length of the boxed product in millimeters - `images` (list) — A list of product image details - `is_primary` (boolean) — Whether this is the primary image for the product - `url` (string) — The url where the image is located - `availability` (object) — Availability data for the product - `available` (integer) — The current number of available units for this product - `status` (string) — A textual description of the product's stock status - `incoming` (list) — A list of incoming stock deliveries due to arrive - `incoming` (integer) — The number of units due to arrive in this delivery - `expected_on` (datetime) — When NetXL expects to receive the delivery (UK local time, `yyyy-MM-dd'T'HH:mm:ss`), this is not necessarily the date these units will be available - `single_unit_cost` (number) — The net cost for a single unit based on your account pricing - `five_plus_unit_cost` (number) — The net cost for 5 or more units based on your account pricing - `ten_plus_unit_cost` (number) — The net cost for 10 or more units based on your account pricing - `consumer_unit_cost` (number) — The recommended retail price for a single unit - `name` (string) — The name of the product - `manufacturer` (string) — The product's manufacturer - `description` (string, optional) — Detailed information about the product, this is the same as the copy on the NetXL product pages. Only included when requested via `extra=description` (always included on `GET /product/`) - `description_html` (string, optional) — The product description as HTML. Only included when requested via `extra=description_html` - `short_description` (string, optional) — A summary of the key features of the product. Only included when requested via `extra=short_description` (always included on `GET /product/`) - `store_url` (string) — A link to the product page on NetXL - `categories` (list of string) — A list of categories this product belongs to - `attributes` (key/value pairs) — A list of key/value pairs for the attributes applied to the product - `related_products` (list of string, optional) — A list of related product SKUs for this product. Only included when requested via `extra=related` - `similar_products` (list of string, optional) — A list of similar product SKUs for this product. Only included when requested via `extra=similar` ### Example Product Object ```json { "sku": "UAP-AC-LITE", "ean": "810354023521", "weight_kg": 0.870, "box_width_mm": 206, "box_height_mm": 91, "box_length_mm": 196, "images": [ { "is_primary": true, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/Unifi-UAP-AC-LITE.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-connections.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-rear.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-front.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-side.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-multiple.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-2.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-3.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-4.jpg" } ], "availability": { "available": 200 }, "single_unit_cost": 69.99, "five_plus_unit_cost": 64.99, "ten_plus_unit_cost": 59.99, "name": "Ubiquiti UAP-AC-LITE UniFi Dual-Band WiFi WLAN Access Point - PoE", "manufacturer": "ubiquiti", "store_url": "https://www.netxl.com/wifi-access-points/ubiquiti-uap-ac-lite-unifi-wifi-wireless-access-point/", "categories": [ "networking", "wifi-access-points" ], "attributes": { "range": "400" } } ``` ## Searching the product catalogue In order to search and filter the product catalogue, you should build up a query string to append to the end of the API endpoint (https://api.netxl.com/product). This query string allows you to filter the catalogue by manufacturers, categories, and attributes. ### Query String Parameters - `manufacturer` (string, optional) — Filter results to certain brands, supply a comma delimited list of manufacturer keys (grandstream,ubiquiti,2n) as returned by the [manufacturer list](#manufacturer-list) - `query` (string, optional) — Free text search of the product name/details - `category` (string, optional) — category - Filter results to specific categories, e.g. IP Hardware PBX, Voip Adapters - `inStock` (string, optional) — When "true" only return in stock products (false has no effect) - `reduced` (string, optional) — Set to "true" to return only reduced (refurbished) products, or "false" to exclude them - `sort` (string, optional) — How to sort the returned results, must be one of - price, availability, sku or name - `extra` (string, optional) — List of extra fields to return in the response, supports the following fields: description, short_description, related, similar, description_html - `attribute` (string, optional) — List of attributes to filter by (see [attribute filtering](#attribute-filtering)) - `accept` (string, optional) — Overrides content negotiation, set to application/json or text/csv to force the response format - `page` (integer, optional) — Which page of results to return, counting from 1 (see [paginating results](#pagination)) - `limit` (integer, optional) — How many results to return per page, up to 500; use 0 to return everything (see [paginating results](#pagination)) ### Request: `GET /product` **HTTP** ```http GET /product?manufacturer=ubiquiti&query=UAP-AC-PRO HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/product", headers={ "X-Api-Key": "", "Accept": "application/json" }, params={ "manufacturer":"ubiquiti", "query": "UAP-AC-PRO" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/product?manufacturer=ubiquiti&query=UAP-AC-PRO")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "sku": "UAP-AC-PRO", "ean": "810354023514", "weight_kg": 0.972, "box_width_mm": 295, "box_height_mm": 60, "box_length_mm": 250, "images": [ { "is_primary": true, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/Unifi-UAP-AC-PRO.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-pro-wifi-access-point-side.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-pro-wifi-access-point-rear.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-1.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-2.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-3.jpg" } ], "availability": { "available": 200, "incoming": [ { "incoming": 150, "expected_on": "2022-12-31T00:00:00" } ] }, "single_unit_cost": 149.99, "five_plus_unit_cost": 134.99, "ten_plus_unit_cost": 119.99, "name": "Ubiquiti Unifi AP-AC Pro - Wifi access point (With POE Injector)", "manufacturer": "ubiquiti", "store_url": "https://www.netxl.com/wifi-access-points/ubiquiti-uap-ac-pro-indoor-outdoor-unifi-poe-wireless-access-point/", "categories": [ "networking", "wifi-access-points" ], "attributes": { "range": "400" } } ] ``` ## Attribute Filtering String attributes can be filtered by equality, for example: ``` supports_poe=true ``` Number attributes can be filtered by `<` `<=` `=` `>=` `>`, for example: ``` poe_ports>=16 ``` This would query all switches with at least 16 PoE ports. A list of attributes and their types can be obtained using the [attribute](#attribute-list) list command. ### Request: `GET /product?range>=300,range<=400` **HTTP** ```http GET /product?attribute=range%3E%3D300,range%3C%3D400 HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/product", headers={ "X-Api-Key": "", "Accept": "application/json" }, params={ "attribute": "range>=300,range<=400" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/product?attribute=range%3E%3D300,range%3C%3D400")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "sku": "UAP-AC-LITE", "ean": "810354023521", "weight_kg": 0.870, "box_width_mm": 206, "box_height_mm": 91, "box_length_mm": 196, "images": [ { "is_primary": true, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/Unifi-UAP-AC-LITE.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-connections.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-rear.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-front.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-side.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/uap-ac-lite-wifi-access-point-multiple.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-2.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-3.jpg" }, { "is_primary": false, "url": "https://storage.googleapis.com/nxl-content/ubiquiti/unifi-network-controller-4.jpg" } ], "availability": { "available": 200 }, "single_unit_cost": 69.99, "five_plus_unit_cost": 64.99, "ten_plus_unit_cost": 59.99, "name": "Ubiquiti UAP-AC-LITE UniFi Dual-Band WiFi WLAN Access Point - PoE", "manufacturer": "ubiquiti", "store_url": "https://www.netxl.com/wifi-access-points/ubiquiti-uap-ac-lite-unifi-wifi-wireless-access-point/", "categories": [ "networking", "wifi-access-points" ], "attributes": { "range": "400" } } ] ``` --- # Orders and Quotes ## Orders and Quotes The API can be used to query your order history. Credit account customers can also create orders using their credit limit. `GET /order` returns the orders on your account **newest first**, and the results are [paginated](#pagination): without a `limit` you get the 100 most recent orders, and the `X-Total-*` and `Link` response headers tell you where in your history you are. Unlike the product catalogue, this applies to the CSV response too — send `limit=0` to fetch every order in one go. There are two ways to create an order. The first is a two step process, where the order must be confirmed using a secondary API call. The other is a single call, that will create and confirm the order at once. Use this method when you do not require pricing or shipping quotes. ### List Orders — Query String Parameters - `accept` (string, optional) — Overrides content negotiation, set to application/json or text/csv to force the response format - `page` (integer, optional) — Which page of orders to return, counting from 1 (see [paginating results](#pagination)) - `limit` (integer, optional) — How many orders to return per page, up to 500; use 0 to return your whole order history (see [paginating results](#pagination)) ### ENDPOINTS ```json GET /order GET /order/ GET /order//invoice GET /order//video POST /order PUT /order/ POST /order//item//configuration POST /quote ``` ## The Order Object An order is represented in two forms. The **request fields** are what you send when creating an order or quote; the **response fields** are returned when querying, creating or confirming an order. The example on the right shows an order as returned by `GET /order`. ### Request Fields - `customer_reference` (string) — Your order reference - `items` (list) — A list of items to include in this order - `sku` (string) — The SKU for this product - `quantity` (integer) — How many units of this product are required, must be greater than 0 - `cost_net` (number, optional) — The net cost of each unit for this order, this depends on your pricing and total units required - `billing_address` (object) — Billing address to use for the order, you can either provide the full address, or the ID of an [existing address](#existing-address) - `id` (integer, optional) — ID of an existing address to use for the billing address - `contact_name` (string) — Who is the invoice addressed to. - `company_name` (string, optional) — Company to address the invoice to. - `street_one` (string) — The first line of the billing address. - `street_two` (string, optional) — Second line of the billing address. - `street_three` (string, optional) — Third line of the billing address. - `city` (string) — Billing address city. - `state` (string, optional) — Billing address county. - `zip` (string) — Billing address postal code. - `country_code` (string) — The two letter country code of the address, should be a valid country from the ISO 3166-1 alpha-2 list - `contact_number` (string, optional) — Contact phone number for the billing address. - `tax_number` (string, optional) — Tax registration number associated with the address. - `shipping_address` (object) — Shipping address to use for the order, you can either provide the full address, or the ID of an [existing address](#existing-address) - `id` (integer, optional) — ID of an existing address to use for the shipping address - `contact_name` (string) — The recipients name. - `company_name` (string, optional) — Name of the recipient company. - `street_one` (string) — The first line of the recipients address. - `street_two` (string, optional) — Second line of the recipients address. - `street_three` (string, optional) — Third line of the recipients address. - `city` (string) — Recipients city. - `state` (string, optional) — Recipients county. - `zip` (string) — Recipients postal code. - `country_code` (string) — The two letter country code of the address, should be a valid country from the ISO 3166-1 alpha-2 list - `contact_number` (string, optional) — Contact phone number for the recipient, this will be provided to the courier for delivery. - `tax_number` (string, optional) — Tax registration number associated with the address. - `shipping_method` (object, optional) — The shipping method to use for this order, identified by its ID — must be one returned in the shipping_options list - `id` (integer) — The ID of the shipping method to use, the only field required when selecting a method - `shipping_region` (integer, optional) — The shipping region for the order. Defaults to 1 (UK mainland). - `external_customer_id` (string, optional) — An optional identifier for your own end customer. - `order_notifications` (object, optional) — Email address or WhatsApp number to send order updates to, if this field is omitted the account email will be used. - `email_address` (string) — Email address to send order updates to. - `phone_number` (string) — WhatsApp number to send updates to. - `courier_notifications` (object, optional) — Email address or phone number to send notifications to when supported by the courier - `email_address` (string) — Email address to send courier notifications to. - `phone_number` (string) — Phone number to send courier notifications to, should be a UK mobile number - `auto_confirm` (boolean, optional) — Should this order be [automatically confirmed](#auto-confirm) ### Response Fields - `id` (integer) — A unique identifier for this order. Used for subsequent API requests referencing this order (returned as `order_id` when creating or confirming an order) - `order_reference` (string) — The NetXL order reference, starts either NXL or DUK depending on your packing choice - `status` (string) — Where the order has got to, for example `Awaiting Despatch`. New values may be added over time, so treat this as a label to show rather than a fixed set to switch on - `confirmation_code` (string) — The code required to confirm this order, orders must be confirmed within 24 hours of creation - `created_on` (datetime) — When the order was created, as UK local time in the format `yyyy-MM-dd'T'HH:mm:ss` - `cost_net` (number) — The net cost of the items in the order (**does not** include shipping) - `cost_tax` (number) — The applicable tax for the order - `cost_gross` (number) — The gross cost of the order - `shipping_weight` (number) — The total weight of this order for shipping purposes - `shipping_options` (list) — A list of valid shipping methods for this order, along with a cost for each - `id` (integer) — The ID of the shipping method, provide this in the `shipping_method` field when confirming the order - `provider` (string) — The shipping provider - `description` (string) — Details about the shipping method - `cost_net` (number) — The net cost of the shipping method, valid only for this order - `expected_dispatch_date` (datetime) — When the order is expected to be dispatched using this method - `expected_arrival_date` (datetime) — When the order is expected to arrive using this method - `items` (list) — A list of items in this order - `id` (integer) — A unique identifier for this item. Used for subsequent API requests referencing this item - `product` (object) — The [product](#product-object) for this item, including its `sku` - `quantity` (integer) — How many units of this product are in the order - `cost_net` (number) — The net cost of each unit for this order - `cost_tax` (number) — The tax applicable to each unit for this order - `related_items` (list) — Related items included with this item (for example bundled accessories), with the same structure as an order item - `configurations` (list) — Provisioning configurations applied to this item (see [preconfiguration](#preconfiguration)), each holding its details in a `data` object - `serial_numbers` (list) — Serial numbers allocated to this item once dispatched - `mac_address` (string) — The MAC address of the unit - `serial_number` (string) — The serial number of the unit - `provisioning_sku` (string) — The SKU used for provisioning the unit - `shipping` (list) — The shipments for this order, including tracking information where available - `method_id` (integer) — The ID of the shipping method used - `contact_email` (string) — Email address given to the courier for this shipment - `contact_phone` (string) — Phone number given to the courier for this shipment - `cost_net` (number) — The net cost of this shipment - `cost_tax` (number) — The tax applicable to this shipment - `tracking_number` (string) — The courier tracking number, where available - `last_tracking_status` (string) — The most recent tracking status reported by the courier - `tracking_events` (list) — The tracking events reported by the courier - `date` (string) — When the event occurred - `type` (string) — The type of tracking event - `description` (string) — A description of the tracking event - `locality` (string) — Where the event occurred ### Example Order Object ```json [{ "id": 123, "order_reference": "NXL-11111111", "status": "Awaiting Despatch", "customer_reference": "API-ORDER-01", "items": [{ "id": 1234567, "product": { "sku": "UAP-AC-LITE", "ean": "810354023521", "weight_kg": 0.870, "box_width_mm": 206, "box_height_mm": 91, "box_length_mm": 196 }, "quantity": 3, "cost_net": 69.99, "cost_tax": 13.99, "related_items": [{ "product": { "sku": "UK-C5-WH-190", "ean": "634158934193", "weight_kg": 0.130, "box_width_mm": 180, "box_height_mm": 50, "box_length_mm": 50 }, "quantity": 3, "cost_net": 0.00, "cost_tax": 0.00, "related_items": [] }] }], "shipping": [{ "method_id": 1, "contact_email": "geoff@example.com", "contact_phone": "03300433000", "cost_net": 5.99, "cost_tax": 1.20 }], "order_notifications": { "email_address": "geoff@example.com", "phone_number": "07000000001" }, "courier_notifications": { "phone_number": "07000000002" } }] ``` ## Creating an order To create an order, you need to provide three key things to the API: 1. A reference for you to identify this order 2. A list of Item SKU's and quantities you wish to order 3. The billing and shipping addresses Additionally, if using [auto confirmation](#auto-confirm), you will be required to provide a shipping method, or set a default method in the NetXL dashboard. **Providing the item list** For each item you wish to order, you are required to provide the SKU and the quantity of units required. SKU's can be obtained using the [product catalogue search](#searching-products) or from the NetXL website. **Billing & Shipping Addresses** For the addresses, you can either provide the full address details, or use an [existing address](#existing-address) by providing its ID. If you provide both the details and an ID, then the ID will take priority and the details will be ignored. **Optional Additional Parameters** You may additionally provide the following additional parameters with your order request: 1. auto_confirm: Should this order be automatically confirmed 2. shipping_method: The shipping method to use for this order To make order creation safe to retry — so a timeout or double submission cannot create the order twice — send an `Idempotency-Key` header, as described in [safe retries](#idempotency). ### Request: `POST /order` **HTTP** ```http POST /order HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 Content-Length: 228 {"customer_reference":"API-ORDER-01","items":[{"sku":"UDM","quantity":1},{"sku":"UAP-AC-PRO","quantity":5}],"billing_address":{"id":123456},"shipping_address":{"id":123456},"auto_confirm":false,"shipping_method":{"id":1}} ``` **Python** ```python import requests response = requests.post( url="https://api.netxl.com/order", headers={ "X-Api-Key": "", }, json={ "customer_reference": "API-ORDER-01", "items": [{ "sku": "UDM", "quantity": 1 }, { "sku": "UAP-AC-PRO", "quantity": 5 }], "billing_address": { "id": 123456 }, "shipping_address": { "id": 123456 }, "auto_confirm": False, "shipping_method": { "id": 1 } } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); Gson gson = new GsonBuilder().create(); HashMap udm = new HashMap<>(); udm.put("sku", "UDM"); udm.put("quantity", 1); HashMap acPro = new HashMap<>(); acPro.put("sku", "UAP-AC-PRO"); acPro.put("quantity", 5); ArrayList> items = new ArrayList<>(List.of(udm, acPro)); HashMap address = new HashMap<>(); address.put("id", 123456); HashMap newOrder = new HashMap<>(); newOrder.put("customer_reference", "API-ORDER-01"); newOrder.put("auto_confirm", false); newOrder.put("items", items); newOrder.put("billing_address", address); newOrder.put("shipping_address", address); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/order")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(newOrder))) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### Order Created Successfully ```json { "items": [ { "sku": "UDM", "quantity": 1, "cost_net": 299.99 }, { "sku": "UAP-AC-PRO", "quantity": 5, "cost_net": 149.99 } ], "confirmation_code": "00000000-0000-0000-0000-000000000000", "customer_reference": "API-ORDER-01", "shipping_options": [ { "id": 2, "provider": "DPD - Next Working Day by 12:00", "cost_net": 13.49 }, { "id": 1, "provider": "DPD - Next Working Day", "cost_net": 6.99 }, { "id": 3, "provider": "DPD - Next Working Day by 10:30", "cost_net": 18.99 }, { "id": 4, "provider": "DPD - Saturday", "cost_net": 13.49 }, { "id": 5, "provider": "DPD - Saturday by 12:00", "cost_net": 26.49 }, { "id": 6, "provider": "DPD - Saturday by 10:30", "cost_net": 31.99 }, { "id": 8, "provider": "DPD - Sunday by 12:00", "cost_net": 26.49 }, { "id": 7, "provider": "DPD - Sunday", "cost_net": 13.49 }, { "id": 52, "provider": "Royal Mail Tracked - Next Working Day", "cost_net": 4.49 }, { "id": 53, "provider": "Royal Mail Tracked - Two Working Days", "cost_net": 3.99 } ], "shipping_method": { "id": 1 }, "shipping_weight": 6.620, "cost_net": 1049.94, "cost_tax": 209.988, "cost_gross": 1259.928, "billing_address": { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Distribution Ltd", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" }, "shipping_address": { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Distribution Ltd", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" }, "auto_confirm": false } ``` ## Safe Retries with Idempotency Keys Creating an order or a quote (`POST /order` and `POST /quote`) is not something you want to happen twice by accident. A retried timeout or a double-clicked button should not leave you with two identical orders. To make these requests safe to retry, send an `Idempotency-Key` header. The key is a unique, client-generated value that identifies the request — a UUID is ideal. Generate a fresh one for each genuinely new order or quote, and reuse the same one when you retry that request. **How it behaves** - The first request using a given key is processed normally, and its response is recorded. - Any later request using the same key returns that recorded response verbatim, along with an `Idempotent-Replay: true` header, and creates nothing. So a retry that reaches us after the original succeeded gives you back the original order rather than a duplicate. - Keys are remembered permanently, so a key that created an order will always replay that order's response, however long ago it was first used. **Rules and edge cases** - Keys are scoped to your account and to the request itself. Reusing a key with a different method, path or request body returns a `400` — a key belongs to one specific request. - If an earlier request with the same key is still being processed, you receive a `409 Conflict`. Wait a moment and retry. - A request that fails leaves the key unused, so you are free to retry it — with the same key — once you have addressed the cause. The `Idempotency-Key` header is optional. Omit it and requests behave exactly as before, with no de-duplication. ### Request: `POST /order` **HTTP** ```http POST /order HTTP/1.1 X-Api-Key: Host: api.netxl.com Idempotency-Key: 7f1e0f6c-2a5e-4a9b-9c3d-1b2c3d4e5f60 Connection: close User-Agent: My API Client 1.0 Content-Length: 228 {"customer_reference":"API-ORDER-01","items":[{"sku":"UDM","quantity":1},{"sku":"UAP-AC-PRO","quantity":5}],"billing_address":{"id":123456},"shipping_address":{"id":123456},"auto_confirm":false,"shipping_method":{"id":1}} ``` **Python** ```python import requests import uuid response = requests.post( url="https://api.netxl.com/order", headers={ "X-Api-Key": "", "Idempotency-Key": str(uuid.uuid4()) }, json={ "customer_reference": "API-ORDER-01", "items": [{ "sku": "UDM", "quantity": 1 }, { "sku": "UAP-AC-PRO", "quantity": 5 }], "billing_address": { "id": 123456 }, "shipping_address": { "id": 123456 }, "auto_confirm": False, "shipping_method": { "id": 1 } } ) ``` ## Confirming an Order Once you've created an order, you can check the pricing and shipping costs. If you're happy with these, you can use the confirmation_code provided to confirm the order. It's at this point that your order will be placed and entered into our fulfilment queue. **Confirming the order** To confirm the order, you need to provide a shipping method. If no shipping method is provided but you have a default method set on your account, we will try to use this where applicable (i.e. where the default method was returned in the shipping_options field). Otherwise you will receive an error response. Upon confirming your order, the response will include an order ID and reference from NetXL. These can be used to query the order API, or when contacting NetXL for assistance. ### Request: `PUT /order/` **HTTP** ```http PUT /order/00000000-0000-0000-0000-000000000000 HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 Content-Length: 30 {"shipping_method":{"id":1}} ``` **Python** ```python import requests response = requests.put( url="https://api.netxl.com/order/00000000-0000-0000-0000-000000000000", headers={ "X-Api-Key": "", }, json={ "shipping_method": { "id": 1 } } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); Gson gson = new GsonBuilder().create(); HashMap shippingMethod = new HashMap<>(); shippingMethod.put("id", 1); HashMap confirmOrder = new HashMap<>(); confirmOrder.put("shipping_method", shippingMethod); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/order/00000000-0000-0000-0000-000000000000")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .PUT(HttpRequest.BodyPublishers.ofString(gson.toJson(confirmOrder))) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json { "customer_reference": "API-ORDER-01", "order_id": 111111, "order_reference": "DUK-11111111", "items": [{ "sku": "UDM", "quantity": 1, "cost_net": 299.99 }, { "sku": "UAP-AC-PRO", "quantity": 5, "cost_net": 149.99 } ], "shipping_method": { "id": 3, "provider": "DPD - Next Working Day by 10:30", "cost_net": 18.99 }, "shipping_weight": 6.620, "cost_net": 1049.94, "cost_tax": 209.988, "cost_gross": 1259.928, "billing_address": { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Distribution Ltd", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" }, "shipping_address": { "id": 123456, "contact_name": "Geoff Wilson", "company_name": "NetXL Distribution Ltd", "street_one": "Unit 4 Riverside Business Centre", "street_two": "Walnut Tree Close", "city": "Guildford", "state": "Surrey", "zip": "GU1 4UG", "country_code": "GB", "contact_number": "03300433000" }, "auto_confirm": false } ``` ## Understanding Auto Confirmation When creating an order, it's possible to skip the confirmation stage by setting `auto_confirm` to true in the original request. In order to use auto-confirmation, you must send a shipping_method parameter in your request, or have a default shipping method set in your NetXL dashboard. **Invalid Shipping Method** If the shipping method you provided is not valid for the order, you will receive an error response. You can check the valid shipping methods by disabling the auto-confirmation flag. **Pricing** Auto confirmed orders will use the current live price for each item. This may be more or less than the previous price paid for an item. NetXL will not provide refunds for differences in price where an item has increased in price. You may however still cancel an order prior to shipping. ## Cancel or Amend Order To cancel or make changes to an order, please raise a [support ticket](mailto:help@netxl.com) or call [0330 043 3000](tel:03300433000) for assistance. ## Requesting a Quote To request a quote for an order, send your POST request to /quote instead of /order. This will raise a ticket with NetXL customer support, who will create a quote for the requested items. You will be notified via email once the quote has been created. As with order creation, you can send an `Idempotency-Key` header to make the request safe to retry — see [safe retries](#idempotency). ### Request: `POST /quote` **HTTP** ```http POST /quote HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 Content-Length: 228 {"customer_reference":"API-ORDER-01","items":[{"sku":"UDM","quantity":1},{"sku":"UAP-AC-PRO","quantity":5}],"billing_address":{"id":123456},"shipping_address":{"id":123456},"shipping_method":{"id":1}} ``` **Python** ```python import requests response = requests.post( url="https://api.netxl.com/quote", headers={ "X-Api-Key": "", }, json={ "customer_reference": "API-ORDER-01", "items": [{ "sku": "UDM", "quantity": 1 }, { "sku": "UAP-AC-PRO", "quantity": 5 }], "billing_address": { "id": 123456 }, "shipping_address": { "id": 123456 }, "shipping_method": { "id": 1 } } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); Gson gson = new GsonBuilder().create(); HashMap udm = new HashMap<>(); udm.put("sku", "UDM"); udm.put("quantity", 1); HashMap acPro = new HashMap<>(); acPro.put("sku", "UAP-AC-PRO"); acPro.put("quantity", 5); ArrayList> items = new ArrayList<>(List.of(udm, acPro)); HashMap address = new HashMap<>(); address.put("id", 123456); HashMap newOrder = new HashMap<>(); newOrder.put("customer_reference", "API-ORDER-01"); newOrder.put("items", items); newOrder.put("billing_address", address); newOrder.put("shipping_address", address); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/quote")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(newOrder))) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ## Preconfiguration Once an order has been created, you can then submit configuration details for any order items that support preconfiguration. The number of configurations submitted must not exceed the item's remaining unconfigured quantity. In the response, each configuration on the order item holds its details in a `data` object, which includes the provisioning `product` along with the submitted values (and `wifi_ssid`/`wifi_password` where the product supports wireless preconfiguration). ### Parameters - `configurations` (list) — A list of configuration details - `adsl_username` (string) — The ADSL username to apply to the unit - `adsl_password` (string) — The ADSL password to apply to the unit ### Request: `POST /order//item//configuration` **HTTP** ```http POST /order/123/item/1234567/configuration HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 Content-Length: 154 {"configurations":[{"adsl_username":"admin1@example.com","adsl_password":"password1"},{"adsl_username":"admin2@example.com","adsl_password":"password2"}]} ``` **Python** ```python import requests response = requests.post( url="https://api.netxl.com/order/123/item/1234567/configuration", headers={ "X-Api-Key": "" }, json={ "configurations": [ { "adsl_username": "admin1@example.com", "adsl_password": "password1" }, { "adsl_username": "admin2@example.com", "adsl_password": "password2" } ] } ) ``` ### Configuration Submitted Successfully ```json { "id": 1234567, "product": { "sku": "TD-W9970-CONFIG", ... }, "quantity": 2, "cost_net": 49.99, "cost_tax": 10.00, ..., "configurations": [ { "data": { "product": "TD-W9970-CONFIG", "adsl_username": "admin1@example.com", "adsl_password": "password1" } }, { "data": { "product": "TD-W9970-CONFIG", "adsl_username": "admin2@example.com", "adsl_password": "password2" } } ] } ``` ## Order Packing Video Orders are recorded as they are packed. Once an order has been dispatched, you can retrieve links to its packing video using the order reference. If no video is available for the order (for example because it has not yet been packed), the command returns a `404 Not Found`. ### Attributes - `video_file` (string) — URL of the packing video file - `streaming_playlist` (string) — URL of the HLS (m3u8) streaming playlist for the video ### Request: `GET /order//video` **HTTP** ```http GET /order/NXL-11111111/video HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/order/NXL-11111111/video", headers={ "X-Api-Key": "", }, ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/order/NXL-11111111/video")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json { "video_file": "https://api.netxl.com/order/NXL-11111111/video/packing.mp4", "streaming_playlist": "https://api.netxl.com/order/NXL-11111111/video/playlist.m3u8" } ``` ## Order Invoice You can download the invoice for an order as a PDF using its order reference. The response is the raw PDF document (`application/pdf`), so save it to a file rather than trying to parse it. An order only has an invoice once it has been invoiced, which is reflected in its [`status`](#order-object). Asking any earlier returns a `412 Precondition Failed` — the invoice does not exist yet, rather than being withheld, so it is worth asking again later. An unknown order reference returns a `404 Not Found`. ### Request: `GET /order//invoice` **HTTP** ```http GET /order/NXL-11111111/invoice HTTP/1.1 X-Api-Key: Host: api.netxl.com Accept: application/pdf Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/order/NXL-11111111/invoice", headers={ "X-Api-Key": "", }, ) if response.status_code == 200: with open("NXL-11111111.pdf", "wb") as invoice: invoice.write(response.content) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/order/NXL-11111111/invoice")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofFile(Path.of("NXL-11111111.pdf"))); ``` --- # Categories & Attributes ## Categories & Attributes These commands can be used to obtain a list of categories and attributes that can then be used to filter the product catalogue. ### Command Endpoints ```json GET /category GET /attribute GET /manufacturer ``` ## Categories Returns a list of categories and their names. These can be used to filter the product catalogue using the `category` query string parameter. ### The Category Object - `key` (string) — The identifier used by the api for this category - `name` (string) — The human friendly name for this category - `children` (list) — Subcategories of this category - `key` (string) — The identifier used by the api for this category - `name` (string) — The human friendly name for this category ### Request: `GET /category` **HTTP** ```http GET /category HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/category", headers={ "X-Api-Key": "", "Accept": "application/json" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/category")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "key": "networking", "name": "Networking", "children": [ { "key": "poe-switches", "name": "PoE Switches" }, { "key": "routers", "name": "Routers" }, { "key": "wifi-access-points", "name": "WiFi Access Points" }, { "key": "dsl-modems", "name": "ADSL/VDSL Modems" }, { "key": "point-to-point-wireless", "name": "WiFi Point to Point" }, { "key": "3g-4g-routers", "name": "3G & 4G Routers" }, { "key": "switches", "name": "Switches" }, { "key": "powerline-adapters", "name": "Powerline Adapters" } ] } ] ``` ## Attributes Returns a list of attributes and their possible values. These can be used to filter the product catalogue using the `attribute` query string parameter. ### The Attribute Object - `key` (string) — The identifier used by the api for this attribute - `name` (string) — The human friendly name for this attribute - `type` (string) — What type of attribute this is, either Boolean (boolean), String (string) or Number (int), affects which filters can be used in the product catalogue search - `possible_values` (list) — list of all possible values for this attribute, can be used to help filter the product catalogue - `value` (string) — One of the possible values for this attribute ### Request: `GET /attribute` **HTTP** ```http GET /attribute HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/attribute", headers={ "X-Api-Key": "", "Accept": "application/json" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/attribute")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "key": "sip-accounts", "name": "SIP Accounts", "type": "int", "possible_values": [ { "value": "4" }, { "value": "3" }, { "value": "1" }, { "value": "16" }, { "value": "2" }, { "value": "0" }, { "value": "20" }, { "value": "12" }, { "value": "6" }, { "value": "10" }, { "value": "5" }, { "value": "8" } ] } ] ``` ## Manufacturers Returns a list of manufacturers and their names. These can be used to filter the product catalogue using the `manufacturer` query string parameter. ### The Manufacturer Object - `key` (string) — The identifier used by the api for this manufacturer - `name` (string) — The human friendly name for this manufacturer ### Request: `GET /manufacturer` **HTTP** ```http GET /manufacturer HTTP/1.1 X-Api-Key: Host: api.netxl.com Connection: close User-Agent: My API Client 1.0 ``` **Python** ```python import requests response = requests.get( url="https://api.netxl.com/manufacturer", headers={ "X-Api-Key": "", "Accept": "application/json" } ) ``` **Java 11+** ```java HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.netxl.com/manufacturer")) .timeout(Duration.ofSeconds(10)) .header("X-Api-Key", "") .GET() .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### RESPONSE ```json [ { "key": "draytek", "name": "DrayTek" }, { "key": "grundig", "name": "Grundig" }, { "key": "grandstream", "name": "Grandstream" }, { "key": "ubiquiti", "name": "Ubiquiti" } ] ```