{
  "openapi": "3.1.0",
  "info": {
    "title": "ChoiceLink Order Intake API",
    "version": "1.0.0",
    "summary": "Submit orders into ChoiceLink for review and sending to First Choice Metals.",
    "description": "A server-to-server JSON API for partners to submit orders into ChoiceLink. A submitted order is **staged** as a normal order with status `Pending` in the shop bound to your API key. A person at that shop then reviews it and clicks **Send to FCM** inside ChoiceLink to finalize it. **This API never sends to FCM on its own.**\n\nKey facts:\n- One API key is bound to exactly one shop. Orders you submit land in that shop.\n- You reference products by ChoiceLink **item codes** and **color codes** (pull them from `/api/v1/catalog`).\n- All endpoints live under `/api/v1`. Authenticate with `Authorization: Bearer <key>` (an `x-api-key: <key>` header also works).",
    "contact": { "name": "ChoiceLink", "url": "https://v2.choice-link.net/api-docs" }
  },
  "servers": [
    { "url": "https://v2.choice-link.net", "description": "Production" }
  ],
  "tags": [
    { "name": "Setup", "description": "Verify your key and discover the catalog." },
    { "name": "Orders", "description": "Submit and track orders." }
  ],
  "security": [
    { "bearerAuth": [] },
    { "apiKeyAuth": [] }
  ],
  "paths": {
    "/api/v1/preflight": {
      "get": {
        "tags": ["Setup"],
        "summary": "Check key validity and shop readiness",
        "operationId": "preflight",
        "description": "Confirms your key works and reports whether the bound shop is configured well enough to (a) stage orders and (b) have those orders sent to FCM by a human. Any valid key works (no specific scope required).",
        "x-required-scope": null,
        "responses": {
          "200": {
            "description": "Key is valid; readiness reported.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PreflightResponse" },
                "example": {
                  "ok": true,
                  "keyName": "Acme ERP",
                  "scopes": ["orders:write", "orders:read", "catalog:read"],
                  "expiresAt": null,
                  "shop": { "id": "665f0a1b2c3d4e5f60718293", "name": "North Shop" },
                  "company": "First Choice Metals",
                  "canStageOrders": true,
                  "canBeSentToFcm": true,
                  "missing": [],
                  "warnings": []
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/catalog": {
      "get": {
        "tags": ["Setup"],
        "summary": "List the item codes and color codes you may order",
        "operationId": "getCatalog",
        "description": "The authoritative, machine-readable list of item codes and color codes the key's company offers. Pull this once to build the crosswalk from your own SKUs to our codes, and re-sync when it changes. Requires the `catalog:read` scope.",
        "x-required-scope": "catalog:read",
        "responses": {
          "200": {
            "description": "The catalog for the key's company.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CatalogResponse" },
                "example": {
                  "company": "First Choice Metals",
                  "shop": "North Shop",
                  "updatedAt": "2026-06-03T15:00:00.000Z",
                  "itemCodes": [
                    { "code": "RC9-10", "description": "RIDGE CAP (29ga) 10'6\"" },
                    { "code": "ST9-LF", "description": "SPECIAL TRIM (29ga) Custom-Length" }
                  ],
                  "colors": [
                    { "code": "BR", "name": "Brown" },
                    { "code": "WH", "name": "White" }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/ShopNotLinked" }
        }
      }
    },
    "/api/v1/orders": {
      "post": {
        "tags": ["Orders"],
        "summary": "Submit (stage) an order",
        "operationId": "createOrder",
        "description": "Stages an order as `Pending` in the key's shop. Each line references one of our item codes and color codes, plus a quantity. Length (`feet`/`inches`) is taken from the catalog for fixed-length items and from your input for custom-length items. Special-trim items (codes containing `ST9-` or `ST6-`) additionally require a `description`. Validation is strict and up front: unknown/cross-account references, special-trim lines missing a description, or lines that can't be matched to an FCM product are rejected with `422` and no order is created. Requires the `orders:write` scope. Honors an optional `Idempotency-Key` header.",
        "x-required-scope": "orders:write",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OrderRequest" },
              "examples": {
                "flat": {
                  "summary": "Flat lines (the standard form)",
                  "value": {
                    "externalRef": "PO-10432",
                    "lines": [
                      { "itemCode": "RC9-10", "color": "BR", "quantity": 12 },
                      { "itemCode": "AG9", "color": "BR", "quantity": 8, "feet": 12, "inches": 0 }
                    ]
                  }
                },
                "specialTrim": {
                  "summary": "Special-trim line (ST9-/ST6- requires a description)",
                  "value": {
                    "externalRef": "PO-10433",
                    "lines": [
                      { "itemCode": "ST9-LF", "color": "BR", "quantity": 4, "feet": 10, "inches": 6, "description": "L-trim 3.5x1.5 bent 90" }
                    ]
                  }
                },
                "grouped": {
                  "summary": "Labeled groups (multi-building)",
                  "value": {
                    "externalRef": "PO-10432",
                    "groups": [
                      { "reference": "Building A", "lines": [{ "itemCode": "RC9-10", "color": "BR", "quantity": 12 }] },
                      { "reference": "Building B", "lines": [{ "itemCode": "GUTTER", "color": "WH", "quantity": 8 }] }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order staged as Pending.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OrderCreatedResponse" },
                "example": {
                  "id": "665f0a1b2c3d4e5f60718293",
                  "orderNumber": "PO-10432",
                  "status": "Pending",
                  "sentToFCM": false,
                  "externalRef": "PO-10432",
                  "lines": [
                    { "itemCode": "RC9-10", "color": "BR", "resolvedProductId": "RC9BR10" },
                    { "itemCode": "ST9-LF", "color": "BR", "resolvedProductId": "ST9BRLF", "description": "L-trim 3.5x1.5 bent 90" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadJson" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": {
            "description": "Shop not linked to a company, or the `Idempotency-Key` was reused with a different body.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Idempotency-Key was already used with a different request body." }
              }
            }
          },
          "422": {
            "description": "Validation failed; no order created. Either `details` (per-line reference problems) or `lines` (per-line FCM-match problems) is present.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ValidationError" },
                "examples": {
                  "references": {
                    "summary": "Unknown / cross-account refs, or a special-trim line missing its description",
                    "value": {
                      "error": "One or more lines could not be resolved against your catalog.",
                      "details": [
                        { "line": "lines[1]", "itemCode": "WIDGET", "problem": "unknown item code" },
                        { "line": "lines[2]", "itemCode": "ST9-LF", "problem": "special-trim items require a `description`" }
                      ]
                    }
                  },
                  "fcmMatch": {
                    "summary": "Resolves, but can't be matched to an FCM product",
                    "value": {
                      "error": "One or more lines cannot be matched to an FCM product.",
                      "lines": [
                        { "label": "Ridge Cap", "itemCode": "RIDGE-", "ok": false, "problem": "item code \"RIDGE-\" requires a color code" }
                      ]
                    }
                  },
                  "empty": {
                    "summary": "No lines provided",
                    "value": { "error": "At least one order line is required (provide `lines` or `groups`)." }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The order could not be staged due to a server error.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Failed to stage the order." }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/{id}": {
      "get": {
        "tags": ["Orders"],
        "summary": "Get an order's status",
        "operationId": "getOrder",
        "description": "Returns the current status of one order, scoped to the key's shop. Lets you poll an order through Pending → sent → Confirmed/Shipped. Requires the `orders:read` scope.",
        "x-required-scope": "orders:read",
        "parameters": [{ "$ref": "#/components/parameters/OrderId" }],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OrderStatusResponse" },
                "example": {
                  "id": "665f0a1b2c3d4e5f60718293",
                  "orderNumber": "PO-10432",
                  "fcmOrderNumber": null,
                  "status": "Pending",
                  "sentToFCM": false,
                  "source": "api",
                  "externalRef": "PO-10432",
                  "createdAt": "2026-06-03T15:01:00.000Z",
                  "sentAt": null,
                  "groups": [
                    {
                      "reference": "PO-10432",
                      "lines": [
                        { "itemCode": "RC9-10", "color": "BR", "quantity": 12, "feet": 10, "inches": 6 },
                        { "itemCode": "ST9-LF", "color": "BR", "quantity": 4, "feet": 10, "inches": 6, "description": "L-trim 3.5x1.5 bent 90" }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "No order with that id in your shop (also returned for malformed ids).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Order not found." }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send your key as `Authorization: Bearer ck_live_…`."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Alternative to Bearer: send your key as `x-api-key: ck_live_…`."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string" },
        "description": "A unique id for this order. Replaying the same key with the same body returns the original response (no duplicate). Replaying it with a different body returns 409."
      },
      "OrderId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "pattern": "^[0-9a-fA-F]{24}$" },
        "description": "The order id returned by POST /api/v1/orders."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, invalid, revoked, or expired key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Invalid API key." } } }
      },
      "Forbidden": {
        "description": "Key is valid but lacks the required scope, or is not bound to a shop.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "API key is missing the required scope 'orders:write'." } } }
      },
      "ShopNotLinked": {
        "description": "The shop bound to the key is not linked to a company.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "The shop bound to this key is not linked to a company." } } }
      },
      "BadJson": {
        "description": "The request body was not valid JSON.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Request body must be valid JSON." } } }
      }
    },
    "schemas": {
      "OrderLine": {
        "type": "object",
        "required": ["itemCode", "color", "quantity"],
        "properties": {
          "itemCode": { "type": "string", "description": "An item code from /api/v1/catalog.", "example": "RC9-10" },
          "color": { "type": "string", "description": "A color code from /api/v1/catalog. Required on every line (POLY codes ignore the value but one must be present).", "example": "BR" },
          "quantity": { "type": "integer", "minimum": 1, "description": "Whole number of pieces.", "example": 12 },
          "feet": { "type": "number", "default": 0, "description": "Length in feet (decimals allowed). For fixed-length items the catalog length is used and any value you send is normalized to it; for custom-/linear-length items, send the length you want. Defaults to 0.", "example": 10 },
          "inches": { "type": "number", "default": 0, "description": "Length in inches (decimals allowed). See `feet`. Defaults to 0.", "example": 6 },
          "description": { "type": "string", "description": "Required for special-trim items (codes containing `ST9-` or `ST6-`); ignored for all other items. Sent to FCM as the item's trim description, formatted as \"<description> - <COLOR>\". Omitting it on a special-trim line returns 422.", "example": "L-trim 3.5x1.5 bent 90" }
        }
      },
      "OrderGroup": {
        "type": "object",
        "required": ["lines"],
        "properties": {
          "reference": { "type": "string", "description": "A label for this group (e.g. a building name); shown alongside the lines in ChoiceLink and FCM.", "example": "Building A" },
          "lines": { "type": "array", "items": { "$ref": "#/components/schemas/OrderLine" } }
        }
      },
      "OrderRequest": {
        "type": "object",
        "description": "Provide either `lines` (flat, the standard form) or `groups`/`customPackages` (labeled groups). If `lines` is present it takes precedence; otherwise `groups`, then `customPackages` (an alias).",
        "properties": {
          "externalRef": { "type": "string", "nullable": true, "description": "Your PO / reference. Becomes the order number and the FCM customer PO.", "example": "PO-10432" },
          "lines": { "type": "array", "items": { "$ref": "#/components/schemas/OrderLine" } },
          "groups": { "type": "array", "items": { "$ref": "#/components/schemas/OrderGroup" } },
          "customPackages": { "type": "array", "items": { "$ref": "#/components/schemas/OrderGroup" }, "description": "Alias for `groups`." }
        }
      },
      "OrderCreatedResponse": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "The order id; use it with GET /api/v1/orders/{id}." },
          "orderNumber": { "type": "string", "description": "Equals externalRef when provided, otherwise an API-generated number." },
          "status": { "type": "string", "enum": ["Pending"], "example": "Pending" },
          "sentToFCM": { "type": "boolean", "example": false },
          "externalRef": { "type": "string", "nullable": true },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemCode": { "type": "string" },
                "color": { "type": "string" },
                "resolvedProductId": { "type": "string", "description": "The FCM product id (strProductID2) this line will send as." },
                "description": { "type": "string", "description": "Echoed back for special-trim lines: the trim description we captured. Absent for non-special-trim items." }
              }
            }
          },
          "idempotentReplay": { "type": "boolean", "description": "Present and true only when this response is a replay of a prior identical request." }
        }
      },
      "OrderStatusResponse": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "orderNumber": { "type": "string" },
          "fcmOrderNumber": { "type": "string", "nullable": true, "description": "FCM's order number once sent." },
          "status": { "$ref": "#/components/schemas/OrderStatus" },
          "sentToFCM": { "type": "boolean" },
          "source": { "type": "string", "nullable": true, "example": "api" },
          "externalRef": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "sentAt": { "type": "string", "format": "date-time", "nullable": true },
          "groups": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "reference": { "type": "string" },
                "lines": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "itemCode": { "type": "string", "nullable": true },
                      "color": { "type": "string", "nullable": true },
                      "quantity": { "type": "integer" },
                      "feet": { "type": "number", "nullable": true },
                      "inches": { "type": "number", "nullable": true },
                      "description": { "type": "string", "description": "Present only for special-trim lines: the trim description captured at submission." }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "OrderStatus": {
        "type": "string",
        "enum": ["Pending", "Confirmed", "Shipped", "Received"],
        "description": "Pending = staged, awaiting a human to Send to FCM. The later states track the order after sending."
      },
      "PreflightResponse": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "description": "True when canStageOrders AND canBeSentToFcm are both true." },
          "keyName": { "type": "string", "nullable": true },
          "scopes": { "type": "array", "items": { "type": "string" } },
          "expiresAt": { "type": "string", "format": "date-time", "nullable": true },
          "shop": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } },
          "company": { "type": "string", "nullable": true },
          "canStageOrders": { "type": "boolean", "description": "Whether POST /orders can stage an order (shop is linked to a company)." },
          "canBeSentToFcm": { "type": "boolean", "description": "Whether a staged order can be sent to FCM (company API key + shop customer id set)." },
          "missing": { "type": "array", "items": { "type": "string" }, "description": "Blocking configuration gaps." },
          "warnings": { "type": "array", "items": { "type": "string" }, "description": "Non-blocking gaps (e.g. optional warehouse ids)." }
        }
      },
      "CatalogResponse": {
        "type": "object",
        "properties": {
          "company": { "type": "string", "nullable": true },
          "shop": { "type": "string" },
          "updatedAt": { "type": "string", "format": "date-time" },
          "itemCodes": {
            "type": "array",
            "items": { "type": "object", "properties": { "code": { "type": "string" }, "description": { "type": "string", "nullable": true } } }
          },
          "colors": {
            "type": "array",
            "items": { "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" } } }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      },
      "ValidationError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "details": {
            "type": "array",
            "description": "Per-line reference problems (unknown/cross-account item code or color, bad quantity).",
            "items": {
              "type": "object",
              "properties": {
                "line": { "type": "string", "description": "Locator, e.g. \"lines[1]\" or \"groups[0].lines[2]\"." },
                "itemCode": { "type": "string" },
                "color": { "type": "string" },
                "problem": { "type": "string" }
              }
            }
          },
          "lines": {
            "type": "array",
            "description": "Per-line FCM-match results (present for the FCM-match 422).",
            "items": {
              "type": "object",
              "properties": {
                "label": { "type": "string" },
                "itemCode": { "type": "string" },
                "resolvedProductId": { "type": "string" },
                "ok": { "type": "boolean" },
                "problem": { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}
