{
  "openapi": "3.1.0",
  "info": {
    "title": "Exotronic Customer API",
    "version": "1.1.0",
    "description": "Server-to-server JSON API for product catalogue data and customer order creation. This API accepts no query parameters."
  },
  "servers": [
    {
      "url": "https://api.exotronic.com.au"
    }
  ],
  "tags": [
    {
      "name": "Customer Sync",
      "description": "Read-only customer catalogue sync endpoints."
    },
    {
      "name": "Customer Orders",
      "description": "Customer order creation endpoints."
    }
  ],
  "paths": {
    "/products": {
      "get": {
        "tags": [
          "Customer Sync"
        ],
        "operationId": "getProducts",
        "summary": "Get available products with stock, pricing, and product attributes",
        "description": "Returns product records available to the authenticated customer with a non-empty SKU. Bundle weights and shipping dimensions are estimates.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Product records for the authenticated customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductsResponse"
                },
                "examples": {
                  "success": {
                    "value": [
                      {
                        "sku": "ABC-123",
                        "productTitle": "Example Product",
                        "productType": "Solar Panels",
                        "isBundle": false,
                        "barcode": "1234567890123",
                        "brand": "Example Brand",
                        "quantity": 12,
                        "price": "90.91",
                        "rrp": "105.00",
                        "breakpoints": [
                          {
                            "min_qty": 10,
                            "price": "55.00"
                          },
                          {
                            "min_qty": 20,
                            "price": "54.00"
                          }
                        ],
                        "weight": 1.25,
                        "shipping": {
                          "length": 0.5,
                          "width": 0.3,
                          "height": 0.2
                        },
                        "updatedAt": "2026-05-07T00:00:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key does not include /products access, or the source IP is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficientScope": {
                    "summary": "Key lacks /products access",
                    "value": {
                      "error": {
                        "code": "insufficient_scope",
                        "message": "This API key does not include access to /products."
                      }
                    }
                  },
                  "ipNotAllowed": {
                    "summary": "Source IP not allowed",
                    "value": {
                      "error": {
                        "code": "ip_not_allowed",
                        "message": "This API key cannot be used from the current IP address."
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Use GET only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request body is too large. Do not send a body to read endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Temporary service failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerApiKey": [],
            "customerEmail": []
          }
        ]
      }
    },
    "/order": {
      "post": {
        "tags": [
          "Customer Orders"
        ],
        "operationId": "createOrder",
        "summary": "Create a customer order from SKUs",
        "description": "Creates an order for the authenticated company. Account credit issues are returned as warnings and do not block creation. Duplicate purchase order numbers and invalid Australian shipping addresses are rejected. Set draftOnly to true to create a draft order and return costing without completing the order.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              },
              "examples": {
                "success": {
                  "value": {
                    "po": "PO-1001",
                    "address": {
                      "firstName": "Alex",
                      "lastName": "Buyer",
                      "company": "Acme RV Services",
                      "address1": "1 Example Street",
                      "address2": "Unit 4",
                      "city": "Melbourne",
                      "state": "VIC",
                      "postcode": "3000",
                      "phone": "+61300000000"
                    },
                    "lines": [
                      {
                        "sku": "ABC-123",
                        "quantity": 2
                      }
                    ],
                    "orderType": "dropship",
                    "courierType": "express",
                    "draftOnly": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order or draft order created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key does not include /order access, or the source IP is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficientScope": {
                    "summary": "Key lacks /order access",
                    "value": {
                      "error": {
                        "code": "insufficient_scope",
                        "message": "This API key cannot create orders."
                      }
                    }
                  },
                  "ipNotAllowed": {
                    "summary": "Source IP not allowed",
                    "value": {
                      "error": {
                        "code": "ip_not_allowed",
                        "message": "This API key cannot be used from the current IP address."
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A completed order already exists for the supplied PO number, or an order is already being created with that PO.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Order could not be created with the supplied account, SKU, or shipping address data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Temporary service failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerApiKey": [],
            "customerEmail": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer"
      },
      "customerEmail": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Customer-Email"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "description": "Optional structured validation details, including missing fields, invalid fields, malformed structure, and shipping address validation status.",
                "properties": {
                  "missingFields": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "invalidFields": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "field",
                        "reason"
                      ],
                      "properties": {
                        "field": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "structureIssues": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "field",
                        "expected",
                        "received"
                      ],
                      "properties": {
                        "field": {
                          "type": "string"
                        },
                        "expected": {
                          "type": "string"
                        },
                        "received": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "additionalProperties": true
              }
            }
          }
        }
      },
      "ProductItem": {
        "type": "object",
        "required": [
          "sku",
          "productTitle",
          "productType",
          "isBundle",
          "barcode",
          "brand",
          "quantity",
          "price",
          "rrp",
          "breakpoints",
          "weight",
          "shipping",
          "updatedAt"
        ],
        "properties": {
          "sku": {
            "type": "string"
          },
          "productTitle": {
            "type": "string"
          },
          "productType": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product type. Null when unavailable."
          },
          "isBundle": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "True when the product is a bundle. Null when unavailable."
          },
          "barcode": {
            "type": [
              "string",
              "null"
            ]
          },
          "brand": {
            "type": [
              "string",
              "null"
            ]
          },
          "quantity": {
            "type": "number"
          },
          "price": {
            "type": "string",
            "description": "The authenticated customer's unit price excluding GST."
          },
          "rrp": {
            "type": [
              "string",
              "null"
            ],
            "description": "GST-inclusive recommended retail price. Null when unavailable."
          },
          "breakpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "min_qty",
                "price"
              ],
              "properties": {
                "min_qty": {
                  "type": "number"
                },
                "price": {
                  "type": "string"
                }
              }
            }
          },
          "weight": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in kilograms. Null when unavailable."
          },
          "shipping": {
            "type": "object",
            "description": "Shipping dimensions in metres. Individual dimensions are null when unavailable.",
            "required": [
              "length",
              "width",
              "height"
            ],
            "properties": {
              "length": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "width": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "height": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time the product record was last updated. Null when unavailable."
          }
        }
      },
      "ProductsResponse": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/ProductItem"
        }
      },
      "OrderAddress": {
        "type": "object",
        "required": [
          "address1",
          "city",
          "postcode"
        ],
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "company": {
            "type": "string",
            "description": "Optional shipping address company name."
          },
          "address1": {
            "type": "string"
          },
          "address2": {
            "type": "string",
            "description": "Optional second address line."
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "Australian state or territory. Both 'VIC' and 'Victoria' are accepted. When supplied, it must match the postcode region."
          },
          "postcode": {
            "type": "string",
            "description": "Four-digit Australian postcode. 0000 is rejected and, when state is supplied, the postcode must match the state region."
          },
          "phone": {
            "type": "string"
          }
        }
      },
      "OrderLine": {
        "type": "object",
        "required": [
          "sku",
          "quantity"
        ],
        "properties": {
          "sku": {
            "type": "string"
          },
          "quantity": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "po",
          "address",
          "lines",
          "orderType",
          "courierType"
        ],
        "properties": {
          "po": {
            "type": "string",
            "description": "Customer purchase order number. This is treated as an idempotency key for the authenticated company account."
          },
          "address": {
            "$ref": "#/components/schemas/OrderAddress"
          },
          "lines": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/OrderLine"
            }
          },
          "orderType": {
            "type": "string",
            "enum": [
              "dropship",
              "regular"
            ]
          },
          "courierType": {
            "type": "string",
            "enum": [
              "regular",
              "express"
            ],
            "description": "Requested shipping service. If the selected service is unavailable, another suitable service may be used."
          },
          "authorityToLeave": {
            "type": "boolean",
            "description": "Optional. Defaults to true when omitted.",
            "default": true
          },
          "deliveryInstructions": {
            "type": "string",
            "description": "Optional delivery instructions for the order."
          },
          "requiredShippingDate": {
            "type": "string",
            "format": "date",
            "description": "Optional requested shipping date. Must be an exact YYYY-MM-DD date."
          },
          "draftOnly": {
            "type": "boolean",
            "description": "Optional. Defaults to false. When true, creates and returns a draft order with the full costing response instead of completing the order. Draft-only requests may reuse the same PO until a completed order exists for that PO.",
            "default": false
          }
        }
      },
      "OrderResponse": {
        "type": "object",
        "required": [
          "po",
          "name",
          "address",
          "skus",
          "orderType",
          "courierType",
          "deliveryMethod",
          "authorityToLeave",
          "draftOnly",
          "currencyCode",
          "costs",
          "account"
        ],
        "properties": {
          "po": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "The created order name, or the created draft order name when draftOnly is true."
          },
          "address": {
            "$ref": "#/components/schemas/OrderAddress"
          },
          "skus": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderLine"
            }
          },
          "orderType": {
            "type": "string",
            "enum": [
              "dropship",
              "regular"
            ]
          },
          "courierType": {
            "type": "string",
            "enum": [
              "regular",
              "express"
            ]
          },
          "deliveryMethod": {
            "type": "string",
            "enum": [
              "ship",
              "pickup"
            ],
            "description": "Delivery method selected for the created order."
          },
          "authorityToLeave": {
            "type": "boolean"
          },
          "draftOnly": {
            "type": "boolean"
          },
          "currencyCode": {
            "type": "string"
          },
          "costs": {
            "type": "object",
            "properties": {
              "lineItems": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "sku": {
                      "type": "string"
                    },
                    "productTitle": {
                      "type": "string"
                    },
                    "quantity": {
                      "type": "integer"
                    },
                    "unitCostExGst": {
                      "type": "string"
                    },
                    "lineItemCostExGst": {
                      "type": "string"
                    },
                    "lineItemCostInclGst": {
                      "type": "string"
                    }
                  }
                }
              },
              "shippingCostExGst": {
                "type": "string"
              },
              "shippingCostInclGst": {
                "type": "string"
              },
              "totalExGst": {
                "type": "string"
              },
              "totalInclGst": {
                "type": "string"
              },
              "gst": {
                "type": "string"
              }
            }
          },
          "account": {
            "type": "object",
            "properties": {
              "companyName": {
                "type": "string"
              },
              "creditLimit": {
                "type": "string"
              },
              "outstandingBeforeOrder": {
                "type": "string"
              },
              "overdueInvoices": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "orderName": {
                      "type": "string"
                    },
                    "dueDate": {
                      "type": "string"
                    },
                    "outstandingAmount": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "description": "Account or inventory conditions that require attention but did not prevent order creation.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "details": {
                  "type": "object"
                }
              }
            }
          }
        },
        "example": {
          "po": "PO-1001",
          "name": "#S278743",
          "address": {
            "firstName": "Alex",
            "lastName": "Buyer",
            "company": "Acme RV Services",
            "address1": "1 Example Street",
            "address2": "Unit 4",
            "city": "Melbourne",
            "state": "Victoria",
            "postcode": "3000",
            "phone": "0400000000"
          },
          "skus": [
            {
              "sku": "ABC-123",
              "quantity": 2
            }
          ],
          "orderType": "dropship",
          "courierType": "express",
          "deliveryMethod": "ship",
          "authorityToLeave": true,
          "draftOnly": false,
          "currencyCode": "AUD",
          "costs": {
            "lineItems": [
              {
                "sku": "ABC-123",
                "productTitle": "Example Product",
                "quantity": 2,
                "unitCostExGst": "2.16",
                "lineItemCostExGst": "4.32",
                "lineItemCostInclGst": "4.75"
              }
            ],
            "shippingCostExGst": "12.95",
            "shippingCostInclGst": "14.25",
            "totalExGst": "17.27",
            "totalInclGst": "19.00",
            "gst": "1.73"
          },
          "account": {
            "companyName": "Example Company",
            "creditLimit": "500.00",
            "outstandingBeforeOrder": "0.00",
            "overdueInvoices": []
          }
        }
      }
    }
  }
}