{
  "openapi": "3.0.3",
  "info": {
    "title": "Redwood Loader API",
    "version": "0.2",
    "description": "Loads CSV files into `warehouse.utility` in Supabase and manages the\npackages, sub-packages (feeds) and column mappings that drive a load.\n\nEverything the mapping screen does goes through these endpoints, so any\nother application can do the same.\n\n**Authentication** — every `/api` endpoint except `/api/auth/config`\nneeds a Supabase Auth JWT as a bearer token: the same token the Redwood\nweb portal sends. Obtain one from Supabase Auth\n(`POST {supabase.url}/auth/v1/token?grant_type=password` with the\npublishable key as `apikey`) and send `Authorization: Bearer <token>`.\nThe user must be an active row in `public.profiles`.\n\n**Who may do what** — decided by the portal role in `public.profiles.role`\n(`GET /api/auth/me` returns it as `access`):\n\n| portal role | access | may |\n|---|---|---|\n| `redwood_employee` | `REDWOOD` | everything, every package |\n| `company_super_user` | `COMPANY_ADMIN` | see its own company's packages, feeds, mappings and history; inspect and load files into them |\n| `regular_user` | `NONE` | nothing but `/api/auth/me` |\n\nA company admin's calls are confined to packages whose `company_id` is the\nadmin's own company: lists are filtered, and a package/feed/run of another\ncompany answers 403. Creating or changing packages, feeds and mappings, and\neverything under `/api/legacy`, `/api/migration`, `/api/watcher`,\n`/api/users` and `/api/companies`, is Redwood-only.\n\n**Errors** — always JSON: `{\"error\": \"...\", \"status\": 400}`.\n401 = no/invalid token, 403 = valid token but the role may not do this\n(or the package belongs to another company), 404 = no such\npackage/feed/run, 400 = bad input, 413 = file too large,\n422 = the load itself failed (nothing was written).\n"
  },
  "servers": [
    {
      "url": "http://localhost:8080"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Supabase Auth access token (ES256), audience `authenticated`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          }
        }
      },
      "Package": {
        "type": "object",
        "properties": {
          "package_id": {
            "type": "integer",
            "format": "int64"
          },
          "package_name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "company_id": {
            "type": "string",
            "format": "uuid"
          },
          "company_name": {
            "type": "string"
          },
          "feed_count": {
            "type": "integer"
          },
          "warehouse_rows": {
            "type": "integer"
          },
          "last_load": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "NewPackage": {
        "type": "object",
        "required": [
          "name",
          "companyId"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "companyId": {
            "type": "string",
            "format": "uuid",
            "description": "public.companies.id"
          },
          "loadType": {
            "type": "integer",
            "enum": [
              9,
              15,
              16,
              1,
              6,
              12,
              7,
              8,
              13,
              17,
              18,
              3,
              5,
              10,
              14
            ],
            "default": 9,
            "description": "type of the first feed: 9 Utility, 15 Utility Small, 16 Utility Large, 1 Shipment, 6 Container Movement, 12 Portal Order, 7 Warehouse On-Hand, 8 Warehouse Activity, 13 Portal Shipment, 17 Financial Invoice, 18 Financial Invoice Master, 3 Invoice, 5 Custom, 10 Recommendation, 14 Utility Pharma"
          },
          "dateFormats": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Java DateTimeFormatter patterns for the feed. Default dd/MM/yyyy, yyyy-MM-dd, dd-MMM-yyyy."
          }
        }
      },
      "Feed": {
        "type": "object",
        "description": "A sub-package — one load type within a package.",
        "properties": {
          "sub_package_id": {
            "type": "integer",
            "format": "int64"
          },
          "package_id": {
            "type": "integer",
            "format": "int64"
          },
          "package_name": {
            "type": "string"
          },
          "company_name": {
            "type": "string"
          },
          "load_type": {
            "type": "integer",
            "description": "9 = Utility"
          },
          "load_type_name": {
            "type": "string"
          },
          "target_table": {
            "type": "string",
            "example": "warehouse.utility"
          },
          "enabled": {
            "type": "boolean"
          },
          "rows_to_remove": {
            "type": "integer"
          },
          "columns_to_remove": {
            "type": "integer"
          },
          "min_filled_cells": {
            "type": "integer"
          },
          "date_formats": {
            "type": "string",
            "description": "comma-separated"
          },
          "header_string": {
            "type": "string",
            "nullable": true
          },
          "is_partial_load": {
            "type": "boolean"
          },
          "is_binary_skip": {
            "type": "boolean"
          },
          "is_utility_msr_skip": {
            "type": "boolean"
          },
          "mapped_columns": {
            "type": "integer"
          },
          "warehouse_rows": {
            "type": "integer"
          },
          "last_load": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "FeedSettings": {
        "type": "object",
        "description": "Fields left out (null) are unchanged.",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "rowsToRemove": {
            "type": "integer",
            "minimum": 0
          },
          "columnsToRemove": {
            "type": "integer",
            "minimum": 0
          },
          "minFilledCells": {
            "type": "integer",
            "minimum": 1
          },
          "dateFormats": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "headerString": {
            "type": "string",
            "description": "expected header; a differing file header is a warning at load"
          },
          "isPartialLoad": {
            "type": "boolean",
            "description": "blank incoming measures keep the stored value"
          },
          "isBinarySkip": {
            "type": "boolean",
            "description": "dimension1 matched case-insensitively. Part of the row key: changing it changes what counts as the same row."
          },
          "isUtilityMsrSkip": {
            "type": "boolean",
            "description": "load rows with no measure instead of rejecting them"
          },
          "isMultitab": {
            "type": "boolean",
            "description": "Excel only: read every sheet (appended) instead of the first"
          },
          "tabsToIgnore": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Excel only: sheet names to skip when isMultitab (only when tabsToLoad is empty)"
          },
          "tabsToLoad": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Excel only: the sheets to load, by name, appended; when given it decides alone, so tabs the client adds later are ignored. Empty = isMultitab / tabsToIgnore decide. An empty list clears it."
          },
          "isAlias": {
            "type": "boolean",
            "description": "Shipment: also resolve carriers through the package's carrier alias table"
          },
          "isLocationAlias": {
            "type": "boolean",
            "description": "Shipment: resolve ports through the package's location aliases"
          },
          "loadTypeRc": {
            "type": "string",
            "description": "Recommendation (10): LOAD TYPE used when the file maps none; empty clears it"
          },
          "replyMailId": {
            "type": "string",
            "description": "Comma-separated addresses the load confirmation e-mail goes to after every load of this feed; empty clears it; omit to keep"
          },
          "isEmail": {
            "type": "boolean",
            "description": "Whether the load confirmation e-mail is sent (default true); needs replyMailId"
          },
          "menuId": {
            "type": "string",
            "description": "Recommendation (10): portal menu id used when the file maps none; whole number; empty clears it"
          }
        }
      },
      "Field": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "dimension1"
          },
          "label": {
            "type": "string"
          },
          "group": {
            "type": "string",
            "enum": [
              "key",
              "measure",
              "date"
            ]
          },
          "key": {
            "type": "boolean",
            "description": "part of the row identity"
          }
        }
      },
      "Mapping": {
        "type": "object",
        "properties": {
          "header": {
            "type": "string",
            "description": "column header in the file"
          },
          "target": {
            "type": "string",
            "description": "warehouse column"
          },
          "key": {
            "type": "boolean"
          },
          "meta_id": {
            "type": "integer"
          },
          "keep_history": {
            "type": "boolean",
            "description": "record the value on every load (load.field_history",
            "status HISTORY)": null
          },
          "keep_changes": {
            "type": "boolean",
            "description": "record it when it differs from the stored value (CHANGE)",
            "or the record is new (NEW); a blank cell in a column the file carries counts": null
          }
        }
      },
      "SaveMappings": {
        "type": "object",
        "required": [
          "mappings"
        ],
        "properties": {
          "mappings": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "header",
                "target"
              ],
              "properties": {
                "header": {
                  "type": "string"
                },
                "target": {
                  "type": "string",
                  "description": "one of GET /api/utility/fields"
                },
                "keepHistory": {
                  "type": "boolean",
                  "default": false
                },
                "keepChanges": {
                  "type": "boolean",
                  "default": false
                }
              }
            }
          },
          "rowsToRemove": {
            "type": "integer",
            "description": "normally set through PUT /api/feeds/<id> instead"
          },
          "dateFormats": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "normally set through PUT /api/feeds/<id> instead"
          }
        }
      },
      "Inspect": {
        "type": "object",
        "properties": {
          "file": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "delimiter": {
            "type": "string",
            "enum": [
              "comma",
              "semicolon",
              "tab",
              "pipe"
            ]
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "header": {
                  "type": "string"
                },
                "target": {
                  "type": "string",
                  "nullable": true,
                  "description": "proposed warehouse column"
                },
                "source": {
                  "type": "string",
                  "nullable": true,
                  "enum": [
                    "saved",
                    "guessed"
                  ]
                }
              }
            }
          },
          "sample": {
            "type": "array",
            "description": "the first data rows, as cell arrays",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          "sampleRows": {
            "type": "integer"
          }
        }
      },
      "LoadResult": {
        "type": "object",
        "properties": {
          "loadHistoryId": {
            "type": "integer",
            "format": "int64"
          },
          "file": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "Success",
              "Warning",
              "Errors"
            ]
          },
          "staged": {
            "type": "integer",
            "description": "rows read from the file"
          },
          "merged": {
            "type": "integer",
            "description": "rows inserted or updated; fewer than staged means the file repeated a key"
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "line N: reason"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "mapped column(s) absent from the file; column(s) the previous load of the same file (same name pattern on the feed) had and this one lacks; header differs from header_string; repeated keys collapsed. File columns nothing maps are not a warning."
          }
        }
      },
      "Run": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "package_id": {
            "type": "integer",
            "format": "int64"
          },
          "sub_package_id": {
            "type": "integer",
            "format": "int64"
          },
          "file_name": {
            "type": "string"
          },
          "load_date": {
            "type": "string",
            "format": "date-time"
          },
          "load_completed": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "success": {
            "type": "integer"
          },
          "failure": {
            "type": "integer"
          },
          "error_type": {
            "type": "string",
            "enum": [
              "Success",
              "Warning",
              "Errors"
            ]
          },
          "error_log": {
            "type": "string",
            "nullable": true
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "mail_status": {
            "type": "string",
            "enum": [
              "NONE",
              "PENDING",
              "SENT",
              "FAILED"
            ],
            "description": "The run's confirmation e-mail (the legacy \"<Load type> Data Load Confirmation\", from dataservices@redwood.trade to every comma-separated address in the feed's reply_mail_id, success or failure, when the feed's is_email is ticked). NONE = is_email off or no address; PENDING = queued (sent within seconds, retried every LOADER_MAIL_TICK seconds; stays PENDING while LOADER_SMTP_HOST is unset); SENT; FAILED with mail_error (SMTP error, no address, or the run was older than LOADER_MAIL_MAX_AGE hours when the mailer reached it)."
          },
          "mail_error": {
            "type": "string",
            "nullable": true
          },
          "mail_to": {
            "type": "string",
            "nullable": true,
            "description": "the feed's reply_mail_id (GET /api/history/{id} only)"
          }
        }
      }
    }
  },
  "paths": {
    "/api/auth/config": {
      "get": {
        "summary": "What a client needs to sign in against Supabase Auth",
        "security": [],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authUrl": {
                      "type": "string",
                      "example": "https://<ref>.supabase.co/auth/v1"
                    },
                    "publishableKey": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/me": {
      "get": {
        "summary": "The signed-in user, from public.profiles, with `access` (REDWOOD / COMPANY_ADMIN / NONE)",
        "description": "The one call every active portal user may make, whatever their role.",
        "responses": {
          "200": {
            "description": "id, email, full_name, role, is_active, company_id, company_name, access"
          },
          "403": {
            "description": "token valid but no portal profile"
          }
        }
      }
    },
    "/api/db/check": {
      "get": {
        "summary": "Database connection diagnosis",
        "responses": {
          "200": {
            "description": "connected and ready"
          },
          "503": {
            "description": "not connected",
            "or connected with problems (body says which)": null
          }
        }
      }
    },
    "/api/companies": {
      "get": {
        "summary": "Companies (public.companies) — a plain list, or with page/size/q a page with user/package counts",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/users": {
      "get": {
        "summary": "Portal users (public.profiles) with loader activity, paged",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/loads": {
      "get": {
        "summary": "Every run across all feeds, newest first, paged",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "file or package name"
          },
          {
            "name": "packageId",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this package's runs"
          },
          {
            "name": "feedId",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this feed's runs"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/load-types": {
      "get": {
        "summary": "Load types (load.load_type)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/utility/fields": {
      "get": {
        "summary": "The warehouse columns a file column may map to — for Utility (9), Utility Small (15) or Utility Large (16)",
        "description": "Without `loadType`, the 43 columns of `warehouse.utility`. With `loadType=15`,\nUtility Small (load_type + dimension1..3 as key, measure1..5, user_defined_date_1..3);\nwith `loadType=16`, Utility Large (dimension1..12 key, dimension13..15 as plain\ndimensions, measure1..40, user_defined_date_1..15). `group` is key, dimension,\nmeasure or date.\n",
        "parameters": [
          {
            "name": "loadType",
            "in": "query",
            "schema": {
              "type": "integer",
              "enum": [
                9,
                15,
                16,
                1,
                6,
                12,
                7,
                8,
                13,
                17,
                18,
                3,
                5,
                10,
                14
              ],
              "default": 9
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Field"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/packages": {
      "get": {
        "summary": "All packages (by name) — or, with page/size/q, one page of them, most recently loaded first",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "when given, the response is {items, page, size, total, pages}"
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Package"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a package together with its UTILITY feed",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPackage"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packageId": {
                      "type": "integer"
                    },
                    "subPackageId": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "missing name/company",
            "or the name exists for that company": null
          }
        }
      }
    },
    "/api/packages/from-legacy/{legacyId}": {
      "parameters": [
        {
          "name": "legacyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          },
          "description": "legacy PACKAGE.ID"
        }
      ],
      "get": {
        "summary": "Preview a copy of a legacy package — the feeds and mappings that would be created",
        "description": "Reads the legacy PACKAGE, its SUB_PACKAGE_DETAIL rows and their META and reports, per sub-package, the target load type (9/U → Utility, 15/US → Utility Small, 16/UL → Utility Large; others are not copied), how many META rows become mappings and which are skipped (no file column name, or a column the target layout does not have). Nothing is written.\n",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "legacyPackageId": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "company": {
                      "type": "object",
                      "description": "legacyId, legacyName, supabaseId, supabaseName, resolved"
                    },
                    "feeds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "legacySubPackageId": {
                            "type": "integer"
                          },
                          "legacyLoadType": {
                            "type": "string"
                          },
                          "loadType": {
                            "type": "integer",
                            "nullable": true
                          },
                          "loadTypeName": {
                            "type": "string",
                            "nullable": true
                          },
                          "skipped": {
                            "type": "boolean"
                          },
                          "mappings": {
                            "type": "integer"
                          },
                          "keyColumns": {
                            "type": "integer"
                          },
                          "metaSkipped": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "settings": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    "feedsToCreate": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "legacy package not found"
          },
          "503": {
            "description": "legacy MySQL not configured"
          }
        }
      }
    },
    "/api/packages/from-legacy/feeds/{feedId}/sync": {
      "post": {
        "summary": "Add to a feed the mappings its legacy sub-package has that the feed lacks (by file header); nothing already mapped changes (Redwood-only)",
        "description": "For feeds copied from the legacy before a legacy setter was recognised (the utility\ndate columns, setUserDefDateN) or when the legacy META grew since the copy. The\nlegacy sub-package must be the same load type as the feed. Returns added (header →\ncolumn), skipped (with the reason) and the feed's mappings afterwards.\n",
        "parameters": [
          {
            "name": "feedId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "packageId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "legacySubPackageId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "legacy SUB_PACKAGE_DETAIL.ID"
          }
        ],
        "responses": {
          "200": {
            "description": "{feedId, legacySubPackageId, added:[…], skipped:[…], mappings:[…]}"
          },
          "400": {
            "description": "feed or legacy sub-package not found",
            "or load types differ": null
          },
          "503": {
            "description": "legacy MySQL not configured"
          }
        }
      }
    },
    "/api/packages/from-legacy": {
      "post": {
        "summary": "Create a new package from a legacy package (settings and mappings only)",
        "description": "A brand-new package with new ids, one feed per supported legacy sub-package, its engine settings copied and its META converted to mappings. No load history and no warehouse rows — use the migration for those. name/description/companyId override what the legacy package says; the company is required when the legacy company has no Supabase id.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "legacyPackageId"
                ],
                "properties": {
                  "legacyPackageId": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "companyId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packageId": {
                      "type": "integer"
                    },
                    "name": {
                      "type": "string"
                    },
                    "legacyPackageId": {
                      "type": "integer"
                    },
                    "feeds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "subPackageId": {
                            "type": "integer"
                          },
                          "legacySubPackageId": {
                            "type": "integer"
                          },
                          "loadType": {
                            "type": "integer"
                          },
                          "loadTypeName": {
                            "type": "string"
                          },
                          "mappings": {
                            "type": "integer"
                          },
                          "metaSkipped": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "feedsSkipped": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "no supported sub-package",
            "no company": null,
            "or the name exists for that company": null
          },
          "503": {
            "description": "legacy MySQL not configured"
          }
        }
      }
    },
    "/api/packages/{packageId}": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "One package",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Package"
                }
              }
            }
          },
          "404": {
            "description": "not found"
          }
        }
      },
      "put": {
        "summary": "Rename / re-describe / move a package to another company",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPackage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "saved"
          },
          "400": {
            "description": "invalid"
          }
        }
      },
      "delete": {
        "summary": "Soft-delete a package (delete_flag); its data and history are kept",
        "responses": {
          "200": {
            "description": "deleted"
          },
          "404": {
            "description": "not found"
          }
        }
      }
    },
    "/api/packages/{packageId}/feeds": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "The package's sub-packages (feeds)",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Feed"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a feed to a package — Utility (9), Utility Small (15) or Utility Large (16); one per type per package",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "loadType": {
                    "type": "integer",
                    "enum": [
                      9,
                      15,
                      16,
                      1,
                      6,
                      12,
                      7,
                      8,
                      13,
                      17,
                      18,
                      3,
                      5,
                      10,
                      14
                    ],
                    "default": 9
                  },
                  "dateFormats": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created — packageId",
            "subPackageId": null
          },
          "400": {
            "description": "the package already has a feed of that type"
          }
        }
      }
    },
    "/api/feeds": {
      "get": {
        "summary": "Every feed with its S3 prefix (load.sub_package_path)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "A feed's resolved configuration and column mappings, as the loader sees them",
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "disabled",
            "deleted or missing": null
          }
        }
      },
      "put": {
        "summary": "Change a feed's parsing and load settings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the updated feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feed"
                }
              }
            }
          },
          "400": {
            "description": "invalid"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/rules": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "The feed's rules (load.feed_rule)",
        "description": "Per-feed rules, the legacy SUB_PACKAGE_RULES plus 2.0's own. File-name rules (PARTIAL_FILES, 315, IF_STA, IF_COS, CONTRACT_STAT, PC_SHIPMENT, E-STRONG) match the file name with `contains` (several patterns separated by #_#) and switch a flag on for that load; PARTIAL_FILES makes it a partial load. ORDER_RELINK (on | shipment) and XML_ROW (element name) are read by the Portal Shipment load and the XML reader. `types` lists what the loader knows.\n",
        "responses": {
          "200": {
            "description": "{ subPackageId, rules: [{ id, rule_type, rule_value, created_at, updated_at }], types: [{ type, value, what }] }"
          },
          "404": {
            "description": "feed not found"
          }
        }
      },
      "put": {
        "summary": "Replace the feed's rules (Redwood only)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "ruleType"
                  ],
                  "properties": {
                    "ruleType": {
                      "type": "string",
                      "example": "ORDER_RELINK"
                    },
                    "ruleValue": {
                      "type": "string",
                      "example": "on"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the rules as GET returns them"
          },
          "400": {
            "description": "feed not found or a rule without a type"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/mappings": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "The feed and its saved column mappings",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "$ref": "#/components/schemas/Feed"
                    },
                    "mappings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Mapping"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Replace the feed's column mappings",
        "description": "Replaces the whole mapping, so send every header you want to keep. Key\ncolumns (load_type, dimension1..12) are marked unique automatically.\n`keepHistory` / `keepChanges` switch field-change tracking on for a column\n(the legacy META KEEP_HISTORY / KEEP_CHANGES): each load then writes\nload.field_history rows in one statement — see GET /api/feeds/{id}/changes.\nSeveral headers may map to the same target (alternative names used by\ndifferent files, e.g. \"ETA Date\" and \"Estimate Date\" → user_defined_date_1);\nat load the first one present in the file is used. Refused when no\ndimension or no measure is mapped, or the same header appears twice.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveMappings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "saved — the mapping as stored"
          },
          "400": {
            "description": "invalid mapping (message says why)"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/prefill": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "summary": "The mapping screen's pre-fill for a list of headers (no file needed)",
        "description": "For each header: the feed's saved target (`source` saved), else the load\ntype's guess from the name (`source` guessed); a target is taken once\nonly — the same rule as POST /api/files/inspect. Used by the API intake\nendpoint dialog, whose output columns are known before the first file\nexists. Also returns the feed, its saved mapping, the load type's fields\nand whether the feed chooses its own key columns, so one call draws an editor.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "headers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "ID",
                      "Name",
                      "Currency"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{feed, columns: [{index, header, target, source}], mappings, fields, feedChoosesKey}"
          },
          "404": {
            "description": "feed not found"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/changes": {
      "get": {
        "summary": "Field changes recorded by this feed's loads, newest first, paged",
        "description": "One row per record × tracked field per load: `status` NEW (no stored record yet),\nCHANGE (file value differs from the stored one — blank counts) or HISTORY\n(keep_history: every load). `old_value` is what the warehouse held before the\nload, `new_value` what the file brought; `version` counts per record × field.\nWritten before the merge in the load's own transaction, so it never drifts\nfrom the data.\n",
        "parameters": [
          {
            "name": "subPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "run",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this load_history run"
          },
          {
            "name": "field",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "warehouse column"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "NEW",
                "CHANGE",
                "HISTORY"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "record key contains"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/changes/fields": {
      "get": {
        "summary": "Tracked fields of the feed with change counts",
        "parameters": [
          {
            "name": "subPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/packages/{packageId}/changes": {
      "get": {
        "summary": "The change log of a package — what changed, in which file, what it was before and which earlier file carried the previous value",
        "description": "Rows of `load.field_history`, newest first. Each row carries the load it came from\n(`load_history_id`, `file_name`, `load_date`, `user_email`) and `prev_load_history_id`,\n`prev_file_name`, `prev_load_date`, `prev_line_no`: the earlier load that brought the\nprevious value of the same record × field (null for a record's first value). Both files\ndownload via `GET /api/loads/{historyId}/file`. `q` matches the record key, the old or the\nnew value; `from`/`to` bound the day the change was recorded.\n",
        "parameters": [
          {
            "name": "packageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "feedId",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this feed"
          },
          {
            "name": "run",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this load_history run"
          },
          {
            "name": "field",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "NEW",
                "CHANGE",
                "HISTORY"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "record key",
            "old value or new value contains": null
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/packages/{packageId}/changes/fields": {
      "get": {
        "summary": "Tracked fields of the package (or one feed) with change counts — for the Change history filter",
        "parameters": [
          {
            "name": "packageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "feedId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/load": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "summary": "Load a CSV, Excel (.xlsx/.xls) or XML file into the feed's warehouse table",
        "description": "Synchronous; the whole file is one transaction and the response arrives\nwhen it has committed or rolled back. Rows whose key (package +\nload_type + dimension1..12) already exists are updated; others are\ninserted.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "loaded (outcome may still be Warning — check rejected)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoadResult"
                }
              }
            }
          },
          "400": {
            "description": "no mapping",
            "or no LoadSpec for the load type": null
          },
          "404": {
            "description": "feed disabled",
            "deleted or missing": null
          },
          "413": {
            "description": "file larger than MAX_UPLOAD"
          },
          "422": {
            "description": "the load failed and was rolled back; error names the cause"
          }
        }
      }
    },
    "/api/feeds/{subPackageId}/history": {
      "parameters": [
        {
          "name": "subPackageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "limit",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 10,
            "maximum": 200
          },
          "description": "plain list"
        },
        {
          "name": "page",
          "in": "query",
          "schema": {
            "type": "integer"
          },
          "description": "when given, a page object instead"
        },
        {
          "name": "size",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 15,
            "maximum": 100
          }
        }
      ],
      "get": {
        "summary": "Recent runs for a feed",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Run"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/files/inspect": {
      "post": {
        "summary": "Read a file's (CSV or Excel) header and first rows and propose a mapping",
        "description": "Nothing is stored. Only the first rows are parsed.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "subPackageId": {
                    "type": "integer",
                    "description": "read the file with this feed's settings (rows to skip) and propose from its saved mapping — the usual way"
                  },
                  "rowsToRemove": {
                    "type": "integer",
                    "default": 0,
                    "description": "only without subPackageId"
                  },
                  "packageId": {
                    "type": "integer",
                    "description": "only without subPackageId — use this package's saved mapping for the proposal"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Inspect"
                }
              }
            }
          },
          "422": {
            "description": "the file could not be read"
          }
        }
      }
    },
    "/api/history": {
      "get": {
        "summary": "Recent runs across all feeds",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/history/{historyId}": {
      "parameters": [
        {
          "name": "historyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "One run with its complete error log, package / feed / user names and its field-change counts (field_changes, changes_new, changes_changed)",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Run"
                }
              }
            }
          },
          "404": {
            "description": "not found"
          }
        }
      }
    },
    "/api/legacy/status": {
      "get": {
        "summary": "Legacy MySQL connection status and table counts",
        "description": "503 with {\"error\": \"legacy MySQL is not configured\"} when LEGACY_DB_URL is unset.",
        "responses": {
          "200": {
            "description": "connected"
          },
          "503": {
            "description": "not configured or unreachable"
          }
        }
      }
    },
    "/api/legacy/packages": {
      "get": {
        "summary": "Legacy PACKAGE rows with company, counts and last load (paged)",
        "description": "Paged: {items, page, size, total, pages}. Default 15 per page, max 100.",
        "parameters": [
          {
            "name": "includeDeleted",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "package name",
            "company name or id": null
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok — legacy column names (ID, NAME, COMPANY_NAME, SUB_PACKAGES, META_ROWS, LAST_LOAD, …)"
          }
        }
      }
    },
    "/api/legacy/packages/{packageId}": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "One legacy package with counts and audit columns",
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "not found"
          }
        }
      }
    },
    "/api/legacy/packages/{packageId}/sub-packages": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "summary": "Legacy SUB_PACKAGE_DETAIL rows for a package (all 43 columns)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/legacy/packages/{packageId}/meta": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "subPackageId",
          "in": "query",
          "schema": {
            "type": "integer"
          },
          "description": "narrow to one sub-package (rows with null SUB_PACKAGE_ID are always included)"
        },
        {
          "name": "q",
          "in": "query",
          "schema": {
            "type": "string"
          },
          "description": "column name or file header"
        },
        {
          "name": "page",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 1
          }
        },
        {
          "name": "size",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 15,
            "maximum": 100
          }
        }
      ],
      "get": {
        "summary": "Legacy META rows (column mappings) for a package (paged)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/legacy/packages/{packageId}/history": {
      "parameters": [
        {
          "name": "packageId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "page",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 1
          }
        },
        {
          "name": "size",
          "in": "query",
          "schema": {
            "type": "integer",
            "default": 15,
            "maximum": 100
          }
        }
      ],
      "get": {
        "summary": "Legacy LOAD_HISTORY for a package (paged)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/legacy/users": {
      "get": {
        "summary": "Legacy USER rows (paged) — never PASSWORD, SECRET_KEY or S3_ACCESS",
        "parameters": [
          {
            "name": "includeDeleted",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/legacy/companies": {
      "get": {
        "summary": "Legacy COMPANIES with their Supabase ids (paged)",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 15,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/migration/preflight/{legacyPackageId}": {
      "get": {
        "summary": "Pre-flight for migrating one legacy package — reads MySQL and Supabase, writes nothing",
        "description": "Everything an operator must decide before migrating: the legacy package and\ncompany (with the matching `public.companies` row when COMPANIES.SUPABASE_ID\nresolves), whether the package already exists in `load.package` and how many\nfeeds/META/history/utility rows it has there, every legacy user the package\nreferences and whether they match a `public.profiles` e-mail, each feed with\nits legacy load type and the default target, META totals (rows without a\nwarehouse column are skipped), a LOAD_HISTORY summary, and per utility table\n(UTILITY, UTILITY_LARGE, UTILITY_SMALL, UTILITY_PHARMA) the row count, id\nrange and record types.\n",
        "parameters": [
          {
            "name": "legacyPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "no such legacy package"
          },
          "503": {
            "description": "legacy MySQL is not configured"
          }
        }
      }
    },
    "/api/migration/run": {
      "post": {
        "summary": "Start a migration job (dryRun=true reads everything and writes nothing)",
        "description": "Runs in the background on the server; poll `/api/migration/jobs/{id}`.\nOrder: `load.package` → feeds (`load.sub_package_detail`, one per mapped load\ntype) → `load.meta` → `load.load_history` (batched) → utility rows in COPY\nchunks, each chunk its own transaction, merged with\n`on conflict (package_id, nk_sha) do nothing`. Legacy ids are preserved with\n`overriding system value`, existing rows are left untouched, and the identity\nsequences are moved past the highest id afterwards. Utility rows whose\nLOAD_HISTORY_ID is not in `load.load_history` are attached to one synthetic\n\"legacy migration\" run created for the package.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "legacyPackageId",
                  "options"
                ],
                "properties": {
                  "legacyPackageId": {
                    "type": "integer",
                    "format": "int64"
                  },
                  "options": {
                    "type": "object",
                    "required": [
                      "defaultOwner"
                    ],
                    "properties": {
                      "defaultOwner": {
                        "type": "string",
                        "format": "uuid",
                        "description": "auth.users id used wherever a legacy user has no portal profile"
                      },
                      "companyId": {
                        "type": "string",
                        "format": "uuid",
                        "nullable": true,
                        "description": "public.companies id; required when the legacy company has no SUPABASE_ID"
                      },
                      "loadTypeMap": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "integer"
                        },
                        "description": "legacy SUB_PACKAGE_DETAIL.LOAD_TYPE → load.load_type id; unmapped feeds are skipped. Default {\"9\": 9, \"U\": 9}"
                      },
                      "includeHistory": {
                        "type": "boolean",
                        "default": true
                      },
                      "includeUtility": {
                        "type": "boolean",
                        "default": true
                      },
                      "utilityTables": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "default all four legacy utility tables"
                      },
                      "includeReference": {
                        "type": "boolean",
                        "default": true,
                        "description": "copy the shipment master data (COUNTRY, LOCATION, CMF, CARRIER, LSP, CONTAINER_TYPE + aliases, CONTAINER) into ref.*; shared, re-runs add only what is missing"
                      },
                      "includeShipment": {
                        "type": "boolean",
                        "default": true,
                        "description": "copy the package's SHIPMENT rows (ids kept) and SHIPMENT_CONTAINER links (one per shipment × container, newest legacy row) when the package has a Shipment feed"
                      },
                      "warehouseTables": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "the other warehouse tables to copy, by legacy name: CONTAINER_MOVEMENT, PORTAL_ORDER, PORTAL_ORDER_LINE, ORDER_SHIPMENT, PORTAL_ORDER_EXPED_LINE, PORTAL_ORDER_ASN, WAREHOUSE_ONHAND, WH_ONHAND_HISTORY, WAREHOUSE_ACTIVITY, FINANCIAL_INVOICE, FINANCIAL_INVOICE_MASTER. Omitted = all, [] = none. Each needs a migrated feed of its load type. Rows get new Supabase ids; a child finds its parent by natural key (order number; order + line id + item ref 1; container + key); pointers at shipments / master data (ids kept) are used as they are and cleared when missing. Re-runs add only what is missing."
                      },
                      "chunkSize": {
                        "type": "integer",
                        "default": 100000,
                        "maximum": 500000
                      },
                      "dryRun": {
                        "type": "boolean",
                        "default": false
                      }
                    }
                  }
                }
              },
              "example": {
                "legacyPackageId": 123,
                "options": {
                  "defaultOwner": "e0767445-ccc2-46fd-bd24-ddaacc081af9",
                  "loadTypeMap": {
                    "9": 9
                  },
                  "includeHistory": true,
                  "includeUtility": true,
                  "chunkSize": 100000,
                  "dryRun": true
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "the job, with id, status, phase, counts and log"
          },
          "400": {
            "description": "bad options",
            "or a live migration of that package is already running": null
          },
          "503": {
            "description": "legacy MySQL is not configured"
          }
        }
      }
    },
    "/api/migration/reference": {
      "post": {
        "summary": "Copy master data (ref.*) from legacy MySQL on its own — e.g. CONTAINER_TYPE before the first shipment load",
        "description": "Starts a background job like `/run` but without a package: COUNTRY, LOCATION, CMF,\nCARRIER, LSP, CONTAINER_TYPE, CONTAINER_TYPE_ALIAS and CONTAINER → `ref.*`, ids kept,\n`on conflict do nothing`, identity sequences bumped. `tables` picks a subset (legacy\nnames; dependency order is kept); empty = all. Poll `/api/migration/jobs/{id}`;\nthe job's `legacyPackageId` is 0.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tables": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "COUNTRY",
                        "LOCATION",
                        "CMF",
                        "CARRIER",
                        "LSP",
                        "CONTAINER_TYPE",
                        "CONTAINER_TYPE_ALIAS",
                        "CONTAINER"
                      ]
                    }
                  },
                  "dryRun": {
                    "type": "boolean",
                    "default": false
                  },
                  "chunkSize": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "the job, with id, status, phase, counts and log"
          },
          "400": {
            "description": "bad options",
            "or a live migration of that package is already running": null
          },
          "503": {
            "description": "legacy MySQL is not configured"
          }
        }
      }
    },
    "/api/migration/jobs": {
      "get": {
        "summary": "Migration jobs run by this server instance, newest first",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/migration/jobs/{id}": {
      "get": {
        "summary": "One job — status (running|done|failed|cancelled), phase, counters and the last 60 log lines",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "no such job"
          }
        }
      }
    },
    "/api/migration/jobs/{id}/cancel": {
      "post": {
        "summary": "Ask a running job to stop after the chunk it is on",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "404": {
            "description": "no such job"
          }
        }
      }
    },
    "/api/entities": {
      "get": {
        "summary": "The entities of a company (a company admin sees their own company's, whatever companyId says)",
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[{id, company_id, company_name, name, description, mode (LIVE|STAGING), refresh_minutes, refresh_after_load, status, api_enabled, row_limit, last_built, last_build_ms, last_build_rows, last_error, next_refresh, check_seconds, check_rows, check_live_allowed, members, columns, api_keys, building}]"
          }
        }
      },
      "post": {
        "summary": "Create an entity (Redwood). Measured first; a LIVE entity the check doesn't allow is saved as STAGING.",
        "description": "The body is the definition (see GET /api/entities/{id} → definition): companyId, name, description, mode, refreshMinutes (≥ 5, default 1440),\nrefreshAfterLoad, apiEnabled, rowLimit, defaultOrder [{column, dir}], members [{subPackageId, alias, joinOrder (1 = base), joinType (INNER|LEFT),\njoinsTo (joinOrder of an earlier member), rows (parent|lines), joins [{metaId, toMetaId}]}], columns [{member, metaId | expression \"child:<column>\",\nname, position, showScreen, exposeApi, format, kind}], filters [{column, operator, value}].\nThe answer carries the check (seconds, rows, estimatedRows, timedOut, liveAllowed, reason, directJoins, rowsPerJoin), the mode it was saved with,\nforcedStaging, and needsBuild (a STAGING table is built by the refresher right after).\n",
        "responses": {
          "200": {
            "description": "{id, mode, forcedStaging, needsBuild, check, buildError}"
          },
          "400": {
            "description": "what is wrong with the definition",
            "in plain words": null
          }
        }
      }
    },
    "/api/entities/status": {
      "get": {
        "summary": "The refresher — tick interval, last tick, entities building now",
        "responses": {
          "200": {
            "description": "{tickSeconds, lastTick, building: [ids]}"
          }
        }
      }
    },
    "/api/entities/subpackages": {
      "get": {
        "summary": "The subpackages of a company that can be members",
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[{id, package_id, package_name, load_type, type_name, enabled, mapped_columns}]"
          }
        }
      }
    },
    "/api/entities/subpackages/{id}/columns": {
      "get": {
        "summary": "A member's columns as the file names them — what an entity can show or join on",
        "description": "Every mapped column of the subpackage (load.meta): metaId, header (the file column name), target (the warehouse column), kind\n(text|number|date|bool), key (the member's record key: a join onto it is direct), onChild (a line column, needs rows = lines),\navailable (false when the target isn't stored, so it can't be shown). A type with a child table (Portal Order lines, Shipment\ncontainers, Invoice charge lines) also offers the child's own columns under the type's labels, with ref \"child:<column>\" instead of a metaId.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{subPackageId, packageId, packageName, loadType, typeName, table, keyColumns, child, columns[]}"
          }
        }
      }
    },
    "/api/entities/preview": {
      "post": {
        "summary": "The wizard's last step — the real query measured, nothing saved (Redwood)",
        "description": "Runs the definition's select under the live limit (loader.entity.live-max-seconds, 5 s): the count, EXPLAIN's estimate, the first 50 rows,\nthe row count after each join, which joins are indirect (not onto the other member's key), and whether LIVE is allowed.\n",
        "responses": {
          "200": {
            "description": "{seconds, rows, estimatedRows, timedOut, liveAllowed, reason, directJoins {joinOrder: bool}, indirectJoins, rowsPerJoin [{member, joinOrder, rows}], columns, preview [[…]], sql, mode}"
          }
        }
      }
    },
    "/api/entities/{id}": {
      "get": {
        "summary": "The definition, its state and the last builds",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{definition, state {company_name, status, last_built, last_build_ms, last_build_rows, last_error, next_refresh, check_*, building}, builds[]}"
          }
        }
      },
      "put": {
        "summary": "Replace the definition (Redwood); the id, API keys and build history are kept",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "as POST /api/entities"
          }
        }
      },
      "delete": {
        "summary": "Delete the entity (status DELETED) and drop its view or table; the loaded data is untouched (Redwood)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{deleted}"
          }
        }
      }
    },
    "/api/entities/{id}/status": {
      "post": {
        "summary": "Enable or disable: {active: true|false} (Redwood). A disabled entity is not read, refreshed or exposed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, status}"
          }
        }
      }
    },
    "/api/entities/{id}/api": {
      "post": {
        "summary": "Turn the external API on or off: {enabled: true|false} (Redwood). Off by default.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, apiEnabled}"
          }
        }
      }
    },
    "/api/entities/{id}/data": {
      "get": {
        "summary": "Rows of the entity (the columns with showScreen), filtered, sorted, paged; format=csv downloads",
        "description": "Request filters: one query parameter per filter, `f.<column>=<op>.<value>` — `f.Status=eq.Active`, `f.Amount=gt.100`,\n`f.ETA=between.2026-01-01,2026-02-01`, `f.Ref=in.A,B,C`, `f.Note=is_null`; a value with no operator means eq. Operators:\neq ne gt gte lt lte like in between is_null not_null; values are checked against the column's kind. `order=Col,-Other`.\nA LIVE entity reads its view (the join runs now); a STAGING one reads its table and says dataAsOf / nextRefresh.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1000,
              "maximum": 50000
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{columns [{name, kind, format}], rows [[…]], total, limit, offset, mode, dataAsOf, nextRefresh} — or text/csv"
          },
          "503": {
            "description": "a STAGING entity not built yet"
          }
        }
      }
    },
    "/api/entities/{id}/refresh": {
      "post": {
        "summary": "Build the STAGING table now (Redwood); returns at once, the refresher does the work",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, queued, note}"
          }
        }
      }
    },
    "/api/entities/{id}/builds": {
      "get": {
        "summary": "The refresh history — started, finished, rows, ms, outcome, error, trigger (save|schedule|load|manual), loads (the member loads the data came from)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[test_entity.entity_build rows]"
          }
        }
      }
    },
    "/api/entities/{id}/files": {
      "get": {
        "summary": "The files behind the entity — each member's recent loads (download one with GET /api/loads/{id}/file) and what the last build read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "loads per member"
          }
        ],
        "responses": {
          "200": {
            "description": "{members [{alias, subPackageId, joinOrder, loads [{id, file_name, load_date, load_completed, success, failure, error_type, server_name}]}], lastBuild}"
          }
        }
      }
    },
    "/api/entities/{id}/keys": {
      "get": {
        "summary": "The entity's API keys (Redwood) — never the key itself, only its first 12 characters",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[{id, name, key_prefix, scopes, ip_allow, created_date, expires_at, revoked_at, last_used, calls}]"
          }
        }
      },
      "post": {
        "summary": "Make a key (Redwood): {name, ipAllow [..], expiresAt}. The plain key is in this answer once; only its SHA-256 is stored.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, key, keyPrefix, note}"
          }
        }
      }
    },
    "/api/entities/{id}/keys/{keyId}": {
      "delete": {
        "summary": "Revoke a key (Redwood); calls with it stop at once",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{revoked}"
          }
        }
      }
    },
    "/api/entities/{id}/calls": {
      "get": {
        "summary": "The audit of external calls (Redwood) — when, key, path, query, rows, ms, status, ip",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[test_entity.entity_api_call rows]"
          }
        }
      }
    },
    "/api/public/entities/{id}": {
      "get": {
        "summary": "External — what the entity exposes (columns, kinds, formats, mode, data-as-of). Header X-Api-Key, no sign-in.",
        "description": "The key must belong to this entity, be unrevoked and unexpired, come from an allowed IP when the key has an allow-list,\nand stay under its rate (loader.entity.api-rate-per-minute, 60). The entity must be ACTIVE with its API switched on.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "X-Api-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, name, description, mode, dataAsOf, nextRefresh, columns [{name, kind, format}], filters}"
          },
          "403": {
            "description": "missing",
            "unknown": null,
            "revoked": null,
            "expired or wrong-entity key; API off": null
          },
          "503": {
            "description": "rate limit"
          }
        }
      }
    },
    "/api/public/entities/{id}/data": {
      "get": {
        "summary": "External — the rows of the exposed columns, with the same filters, order, paging and format=csv as /api/entities/{id}/data",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "X-Api-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1000,
              "maximum": 50000
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{columns, rows, total, limit, offset, mode, dataAsOf, nextRefresh} — or text/csv"
          }
        }
      }
    },
    "/api/watcher": {
      "get": {
        "summary": "Directory watcher status — directory, interval, parallelism, counters, what is loading now",
        "description": "The watcher loads files dropped into `<LOADER_WATCH_DIR>/<subPackageId>/`, one folder\nper feed named by its id. A file (CSV or Excel) is picked up once it has not changed for\n`settleSeconds`, moved to `processing/` while it loads, then to `success/` or\n`error/` (with a `.log` beside it). Up to `parallel` files load at once, never two of\nthe same feed; `inFlight` lists them as `feed/file`. `enabled` is false when\n`LOADER_WATCH_DIR` is unset. `s3` reports the S3 sync (uri, syncs, lastSync,\narchived, lastError) when `LOADER_S3_BUCKET` is set.\n",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/watcher/pending": {
      "get": {
        "summary": "Every feed folder under the watched directory and the files waiting in it",
        "responses": {
          "200": {
            "description": "[{subPackageId, known, enabled, packageName, loadTypeName, files:[{name, bytes, modified, loadable, settled}], successCount, errorCount}]"
          },
          "503": {
            "description": "watcher not configured"
          }
        }
      }
    },
    "/api/watcher/events": {
      "get": {
        "summary": "The most recent loads the watcher did, newest first",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "[{at, subPackageId, file, result, detail, historyId}]"
          }
        }
      }
    },
    "/api/watcher/log": {
      "get": {
        "summary": "The loader's recent log lines — what a load is doing right now",
        "description": "The last lines the loader logged (its own INFO and up, anything else WARN and up; `loader.log.lines`, default 2000, kept in memory and gone on restart), each with a sequence number. A screen reads with `after=0` for the newest `limit` lines, then polls with `after=<seq it got>` and shows only what is new. `dropped` says how many lines after `after` were left out by `limit`; an `after` older than `oldest` has missed lines. Every load logs here (upload, watcher, intake): header read, rows staged, change lines, merge, linked rows, commit, and per sheet of a workbook.\n",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "the `seq` of the last line already shown"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 200,
              "maximum": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{seq, oldest, dropped, lines: [{seq, at, level, logger, thread, message}]}"
          }
        }
      }
    },
    "/api/watcher/scan": {
      "post": {
        "summary": "Scan now instead of waiting for the interval — claims ready files and starts loading them, returns at once",
        "responses": {
          "200": {
            "description": "status plus dispatched: how many files this scan picked up"
          },
          "503": {
            "description": "watcher not configured"
          }
        }
      }
    },
    "/api/watcher/feeds/{subPackageId}/force": {
      "post": {
        "summary": "Load the named files of a feed folder now, outside the scan",
        "description": "Each file is claimed immediately (moved to processing/, so no scan can take it) and loaded on the watcher's forced pool — no scan, no settle wait, no watcher slot, and a paused watcher does not matter. One file per feed still holds: a forced file waits for the feed's running load to finish, and several forced files of one feed load one after the other in the order given. load_history.server_name is \"watcher-forced\". Files not in the folder, not loadable or still being written are returned in `skipped`.\n",
        "parameters": [
          {
            "name": "subPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "files"
                ],
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "file names as listed by /api/watcher/pending"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ subPackageId, queued: [names], skipped: { name: reason }, busy: file loading for this feed now or null }"
          }
        }
      }
    },
    "/api/watcher/feeds/{subPackageId}/files": {
      "post": {
        "summary": "Put files into the feed's input folder (and the bucket, with S3 in front) — the portal's upload",
        "description": "Each file lands as `<dir>/<subPackageId>/<name>` under the watch directory and, when\n`LOADER_S3_BUCKET` is set, is copied to `<prefix>/<subPackageId>/<name>` in the bucket\n(`aws s3 cp`), so it is treated like a file dropped in S3: loaded on the next scan once it\nhas settled, archived to `success/` or `error/` in S3 afterwards. Nothing is loaded by this\ncall; `force` loads it at once. A file that is not a loadable type, has the name of a file\nalready waiting, or that S3 refuses is reported in `errors` (400 when none could be taken).\n",
        "parameters": [
          {
            "name": "subPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "one or more files (CSV",
                    "Excel": null,
                    "XML": null,
                    "up to MAX_UPLOAD)": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ subPackageId, uploaded: [{subPackageId, file, bytes, directory, s3}], errors: { name: reason } }"
          },
          "400": {
            "description": "no file could be taken",
            "or the name is not a file name": null
          },
          "503": {
            "description": "watcher not configured"
          }
        }
      }
    },
    "/api/watcher/feeds/{subPackageId}/folder": {
      "post": {
        "summary": "Create the feed's input folder (watch directory and, with S3 in front, the bucket prefix)",
        "description": "Idempotent. Makes <dir>/<subPackageId>/ locally and, when LOADER_S3_BUCKET is set, the zero-byte folder marker <prefix>/<subPackageId>/ in the bucket, so files can be dropped before the first sync.",
        "parameters": [
          {
            "name": "subPackageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ directory, directoryCreated, s3: uri or null }"
          },
          "400": {
            "description": "watcher not configured",
            "or S3 refused": null
          }
        }
      }
    },
    "/api/watcher/pause": {
      "post": {
        "summary": "Stop picking up files (a load in flight finishes)",
        "responses": {
          "200": {
            "description": "status"
          }
        }
      }
    },
    "/api/watcher/resume": {
      "post": {
        "summary": "Resume picking up files",
        "responses": {
          "200": {
            "description": "status"
          }
        }
      }
    },
    "/api/packages/{packageId}/search": {
      "get": {
        "summary": "Search the text of a package's loaded files — every line containing the text, newest load first",
        "description": "Every loaded file gets a searchable gzip copy in S3\n(`search/package_id=<pkg>/month=<yyyy-MM>/<loadHistoryId>.csv.gz`); Athena scans only\nthis package's partitions for the months between `from` and `to` (default: the last\n12 months). Case-insensitive substring match on the raw line. 503 when file lookup is\noff (no `LOADER_SEARCH_BUCKET` / `LOADER_S3_BUCKET`) or Athena fails; company admins\nmay search their own packages.\n",
        "parameters": [
          {
            "name": "packageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{hits:[{loadHistoryId, subPackageId, fileName, loadedAt, outcome, lineNo, text}], query, scannedBytes, millis, truncated, indexedFiles (counted only when there are no hits, else -1)}"
          },
          "400": {
            "description": "text shorter than 2 characters"
          },
          "503": {
            "description": "file lookup off or Athena query failed"
          }
        }
      }
    },
    "/api/loads/{historyId}/file": {
      "get": {
        "summary": "Download the file a run loaded, as it was uploaded",
        "description": "Since file lookup went live every loaded file is kept in S3\n(`search-files/package_id=<pkg>/<run>/<file>`); for older runs the watcher's archive\n(`<prefix>/<feedId>/success|error/<stamp>_<file>`, nearest the load time) is used.\nCompany admins may download their own packages' files. 404 when no copy exists\n(an API upload from before file lookup).\n",
        "parameters": [
          {
            "name": "historyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file (application/octet-stream",
            "Content-Disposition attachment)": null
          },
          "404": {
            "description": "no copy of this file is kept"
          },
          "503": {
            "description": "file lookup off"
          }
        }
      }
    },
    "/api/search/status": {
      "get": {
        "summary": "File lookup status — on/off, S3 location, Athena workgroup/table, counters, backfill progress (Redwood-only)",
        "responses": {
          "200": {
            "description": "{enabled, location, athena:{region, workgroup, database, table}, indexed, uploadFailures, searches, lastError, backfill:{state, …}}"
          }
        }
      }
    },
    "/api/search/backfill": {
      "post": {
        "summary": "Index the files already archived in S3 by the watcher (success/ and error/ folders), in the background (Redwood-only)",
        "description": "Walks `s3://<bucket>/<prefix>/<feedId>/success|error/` (the watcher's S3 prefix unless\n`inputPrefix` is given), pairs each object with its\n`load.load_history` row (feed + file name, newest run) and writes the search copy.\nObjects without a history row are skipped. One backfill at a time; poll\n`/api/search/status`.\n",
        "parameters": [
          {
            "name": "inputPrefix",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "overrides the watcher's prefix (LOADER_S3_PREFIX)"
          },
          {
            "name": "packageId",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "only this package's files"
          }
        ],
        "responses": {
          "200": {
            "description": "{started, status}"
          },
          "503": {
            "description": "file lookup off"
          }
        }
      }
    },
    "/api/intake/status": {
      "get": {
        "summary": "E-mail intake scheduler status — tick interval, what is queued/running, default directory, whether LOADER_SECRET_KEY is set",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/intake/mailboxes": {
      "get": {
        "summary": "IMAP mailboxes (passwords never returned)",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "post": {
        "summary": "Add a mailbox — the password is stored AES-encrypted with LOADER_SECRET_KEY",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "host",
                  "username",
                  "password"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Sunridge orders"
                  },
                  "host": {
                    "type": "string",
                    "example": "imap.gmail.com"
                  },
                  "port": {
                    "type": "integer",
                    "default": 993
                  },
                  "useSsl": {
                    "type": "boolean",
                    "default": true
                  },
                  "username": {
                    "type": "string",
                    "example": "orders@redwood.trade"
                  },
                  "password": {
                    "type": "string",
                    "description": "Plain; Gmail: an App Password. Encrypted before it is stored."
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created"
          }
        }
      }
    },
    "/api/intake/mailboxes/{id}": {
      "put": {
        "summary": "Change a mailbox (omit password to keep the current one)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "delete": {
        "summary": "Delete a mailbox that has no rules",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "deleted"
          },
          "400": {
            "description": "still has rules"
          }
        }
      }
    },
    "/api/intake/mailboxes/{id}/test": {
      "post": {
        "summary": "Connect with the stored password and list the folders",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ok: true, folders: [...]}"
          },
          "502": {
            "description": "{ok: false, error}"
          }
        }
      }
    },
    "/api/intake/rules": {
      "get": {
        "summary": "Rules with their mailbox and processed/failed counts",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "post": {
        "summary": "Add a rule; enabled=true runs it immediately",
        "description": "A rule watches `sourceFolder` of its mailbox for e-mails whose subject contains\n`subjectContains` (case-insensitive) and, if set, whose From contains `fromContains`.\nAttachments whose extension is in `attachmentTypes` are saved to `localDir`\n(blank = `<default dir>/<ruleId>/`) as `yyyyMMdd_HHmmss_<name>` and, when `s3Uri` is\nset, copied there with `aws s3 cp`. The e-mail is then moved to `processedFolder`, or to\n`errorFolder` when it had no matching attachment or a file could not be delivered.\nMessage-IDs are recorded, so an e-mail is never handled twice.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mailboxId",
                  "name",
                  "subjectContains"
                ],
                "properties": {
                  "mailboxId": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": false
                  },
                  "sourceFolder": {
                    "type": "string",
                    "default": "INBOX"
                  },
                  "subjectContains": {
                    "type": "string"
                  },
                  "fromContains": {
                    "type": "string",
                    "nullable": true
                  },
                  "attachmentTypes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": [
                      "xlsx",
                      "csv"
                    ]
                  },
                  "localDir": {
                    "type": "string",
                    "description": "blank = default directory"
                  },
                  "s3Uri": {
                    "type": "string",
                    "nullable": true,
                    "example": "s3://bucket/load/input/sunridge/"
                  },
                  "processedFolder": {
                    "type": "string",
                    "default": "Processed"
                  },
                  "errorFolder": {
                    "type": "string",
                    "default": "Error"
                  },
                  "subPackageId": {
                    "type": "integer",
                    "nullable": true,
                    "description": "which feed the files are for (reference only)"
                  },
                  "pollSeconds": {
                    "type": "integer",
                    "default": 60,
                    "minimum": 15
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created"
          }
        }
      }
    },
    "/api/intake/rules/{id}": {
      "put": {
        "summary": "Change a rule; switching enabled from false to true runs it immediately",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "delete": {
        "summary": "Delete a rule and its history",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "deleted"
          }
        }
      }
    },
    "/api/intake/rules/{id}/enabled": {
      "post": {
        "summary": "Turn a rule on (runs at once) or off",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the rule"
          }
        }
      }
    },
    "/api/intake/rules/{id}/run": {
      "post": {
        "summary": "Check the mailbox now",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "{queued: true|false}"
          }
        }
      }
    },
    "/api/intake/runs": {
      "get": {
        "summary": "Runs (each check of a rule), newest first, paged",
        "parameters": [
          {
            "name": "ruleId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/intake/messages": {
      "get": {
        "summary": "E-mails handled (with their files), newest first, paged",
        "parameters": [
          {
            "name": "ruleId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "processed",
                "no_attachment",
                "failed"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/status": {
      "get": {
        "summary": "API intake scheduler status — tick interval, parallelism, queued/running endpoints, feed folder root, default directory, whether LOADER_SECRET_KEY is set",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/feeds": {
      "get": {
        "summary": "Feeds (sub-packages) that an output can deliver to, with package and company",
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/sources": {
      "get": {
        "summary": "API sources (credentials never returned), each with its company_id and company_name; companyId lists one company's",
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "post": {
        "summary": "Add a source — credentials are stored AES-encrypted with LOADER_SECRET_KEY. A source belongs to one company, and its endpoints deliver only to that company's subpackages.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "baseUrl",
                  "companyId"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Sloom Cin7 Core"
                  },
                  "companyId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The company the source belongs to (public.companies.id); on a change, its endpoints must not deliver to another company's subpackages"
                  },
                  "baseUrl": {
                    "type": "string",
                    "example": "https://inventory.dearsystems.com/ExternalApi/v2"
                  },
                  "authType": {
                    "type": "string",
                    "enum": [
                      "none",
                      "header",
                      "query",
                      "basic",
                      "bearer"
                    ],
                    "default": "none"
                  },
                  "credentials": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "header/query: each entry is a header or parameter name → value; basic: username + password; bearer: token. Plain here, encrypted before it is stored."
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Plain static headers sent with every call"
                  },
                  "timeoutSeconds": {
                    "type": "integer",
                    "default": 60
                  },
                  "ratePerMinute": {
                    "type": "integer",
                    "description": "Max calls a minute across every endpoint of the source; null = no throttle"
                  },
                  "retries": {
                    "type": "integer",
                    "default": 3
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created"
          }
        }
      }
    },
    "/api/api-intake/sources/{id}": {
      "put": {
        "summary": "Change a source (credentials sent are laid over the stored ones — omit a value to keep it; ratePerMinute 0 clears the throttle; companyId moves it to another company only if none of its endpoints delivers to a subpackage of the old one)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "delete": {
        "summary": "Delete a source that has no endpoints",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/sources/{id}/credentials": {
      "get": {
        "summary": "The names of the stored credentials (never their values)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{id, authType, names: [..]}"
          }
        }
      }
    },
    "/api/api-intake/sources/{id}/test": {
      "post": {
        "summary": "One GET of the given path (or the base URL) with the stored credentials — status, time, first 2000 characters; an HTML answer counts as a failure",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string",
                    "example": "/me"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ok, status, url, ms, body} or {ok: false, error}"
          }
        }
      }
    },
    "/api/api-intake/endpoints": {
      "get": {
        "summary": "Endpoints with their outputs, schedule, last/next run and run counts",
        "parameters": [
          {
            "name": "sourceId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "post": {
        "summary": "Add an endpoint with its outputs (one file each, Excel by default); an output's subpackage must belong to the source's company, else 400",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sourceId",
                  "name",
                  "path"
                ],
                "properties": {
                  "sourceId": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string",
                    "example": "Sales (list)"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": false
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "GET",
                      "POST"
                    ],
                    "default": "GET"
                  },
                  "path": {
                    "type": "string",
                    "example": "/saleList"
                  },
                  "query": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Fixed query parameters"
                  },
                  "body": {
                    "type": "string",
                    "description": "POST body"
                  },
                  "recordsPath": {
                    "type": "string",
                    "example": "SaleList",
                    "description": "Where the records are in a page; blank = the response root"
                  },
                  "pagination": {
                    "type": "object",
                    "description": "{style: none|page|offset|cursor, pageParam, offsetParam, limitParam, limit, start, cursorPath, cursorParam, maxPages}"
                  },
                  "incremental": {
                    "type": "object",
                    "description": "{param: updatedSince, format: iso|iso-local|date|epoch|epoch-ms|<pattern>, overlapMinutes, initial, untilParam}; absent = a full pull every run"
                  },
                  "detail": {
                    "type": "object",
                    "description": "{path: /sale/{ID}, recordsPath} — one more call per list record; outputs read the detail record, the list record is ^"
                  },
                  "scheduleMinutes": {
                    "type": "integer",
                    "description": "Every N minutes"
                  },
                  "scheduleTime": {
                    "type": "string",
                    "example": "02:00",
                    "description": "Once a day at this server-local time"
                  },
                  "outputs": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "description": "Present to update an existing output"
                        },
                        "name": {
                          "type": "string",
                          "example": "Sale Invoice"
                        },
                        "enabled": {
                          "type": "boolean",
                          "default": true
                        },
                        "recordsPath": {
                          "type": "string",
                          "example": "Invoices.Lines",
                          "description": "Nested array to expand into rows; blank = one row per record"
                        },
                        "columns": {
                          "type": "array",
                          "description": "May be empty while the columns are being picked (the window saves before loading the API's fields); runs skip an output without columns",
                          "items": {
                            "type": "object",
                            "required": [
                              "name",
                              "path"
                            ],
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "path": {
                                "type": "string",
                                "example": "^.OrderNumber"
                              }
                            }
                          }
                        },
                        "format": {
                          "type": "string",
                          "enum": [
                            "xlsx",
                            "csv"
                          ],
                          "default": "xlsx",
                          "description": "The file written — Excel (one sheet named after the output) or CSV"
                        },
                        "filePattern": {
                          "type": "string",
                          "default": "{endpoint}_{output}_r{run}_{stamp}",
                          "description": "The format's extension is appended"
                        },
                        "s3Uri": {
                          "type": "string",
                          "example": "s3://redwood-databucket/load/input/3170/"
                        },
                        "localDir": {
                          "type": "string"
                        },
                        "subPackageId": {
                          "type": "integer",
                          "description": "Feed whose watcher folder receives the file when no s3Uri/localDir is given"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created"
          }
        }
      }
    },
    "/api/api-intake/endpoints/{id}": {
      "get": {
        "summary": "One endpoint with its outputs",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "put": {
        "summary": "Change an endpoint (outputs are replaced by the list sent; the next run is recomputed from the schedule)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      },
      "delete": {
        "summary": "Delete an endpoint and its run history",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/endpoints/{id}/enabled": {
      "post": {
        "summary": "Turn an endpoint on or off — on runs it at once when it has a schedule",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/endpoints/{id}/run": {
      "post": {
        "summary": "Run now; with a window (since, optional until) a backfill that does not move the incremental mark",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "since": {
                    "type": "string",
                    "example": "2024-01-01"
                  },
                  "until": {
                    "type": "string",
                    "example": "2024-02-01T00:00:00Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{id, queued: true|false, note}"
          }
        }
      }
    },
    "/api/api-intake/endpoints/{id}/preview": {
      "post": {
        "summary": "Fetch the first page (and one detail call) without writing anything — calls made, fields of the record, first rows of every output",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "rows",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ok, error?, calls, records, fields, detailFields, outputs: [{id, name, columns, rows}]}"
          }
        }
      }
    },
    "/api/api-intake/endpoints/{id}/download": {
      "get": {
        "summary": "Pull one output now and return it as a file (Excel or CSV) — recorded as a run with trigger download, nothing delivered",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "outputId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "xlsx",
                "csv"
              ]
            },
            "description": "Default = the output's format"
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "2026-10-01"
            },
            "description": "Optional window for an incremental endpoint"
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Stop after this many records of the list — a sample (the mapping screen opened from API intake pulls 30)"
          }
        ],
        "responses": {
          "200": {
            "description": "The file (Content-Disposition attachment; X-Rows and X-Run-Id headers)"
          }
        }
      }
    },
    "/api/api-intake/files/{id}/download": {
      "get": {
        "summary": "A file a run wrote (api_run_file id) — the object in S3 (where it was put, or the watcher's success/error archive), or the directory it was written to; 404 when none can be found (a direct download keeps no copy)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file"
          },
          "404": {
            "description": "No copy kept"
          }
        }
      }
    },
    "/api/api-intake/runs": {
      "get": {
        "summary": "Runs (each refresh of an endpoint), newest first, paged — calls, pages, records, files written with where they went and the load each got",
        "parameters": [
          {
            "name": "endpointId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sourceId",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "running",
                "done",
                "failed"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/api/api-intake/runs/{id}/calls": {
      "get": {
        "summary": "The HTTP calls of one run (URL without credentials, status, ms, bytes, attempt)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    }
  }
}
