{
  "openapi": "3.0.3",
  "info": {
    "title": "YouSend Partner API",
    "version": "1.0.0",
    "description": "Программное оформление отправлений YouSend.\n\nСпека написана ПО ФАКТУ КОДА и обновляется руками при правках FormRequest'ов и ресурсов (анти-дрейф стережёт tests/Feature/Api/OpenApiSpecTest.php: каждый путь спеки обязан существовать в роутере и наоборот).\n\nАутентификация: ключ из кабинета (раздел «API» → /api-keys), заголовок `Authorization: Bearer <ключ>`. Срок ключа — 365 дней, одновременно активных не больше 5. Пароля у API нет.\n\nТиповой сценарий: `calculate` (получить список сервисов с ценами) → `order-store` (создать отправление) → `order-confirm` (создать лейбл у перевозчика) → скачать PDF по ссылке из `labels`.\n\nВажное:\n* Все запросы, изменяющие данные, рекомендуется посылать с заголовком `Idempotency-Key` — повтор с тем же ключом вернёт уже созданное отправление вместо второго.\n* `order-confirm` может выполняться до ~60 секунд (обращение к перевозчику). Ставьте read timeout не меньше 180 секунд. Ретрай обязан считать успехом и код 409 (запрос уже выполняется), и повторный 200 с тем же трек-номером.\n* Текстовые поля принимают только ASCII: диакритика (ā, ü, š) перевозчиками не принимается.\n* Лимит — 60 запросов в минуту на ключ.\n* CORS выключен: обращаться к API из браузера нельзя, только с сервера.\n* Формат ошибок исторически различается между ручками — см. схемы ApiError, ValidationError, MessageOnly.",
    "contact": {
      "name": "YouSend",
      "url": "https://yousend.lv/ru/api-integracija",
      "email": "info@yousend.lv"
    }
  },
  "servers": [
    {
      "url": "https://yousend.lv",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "Расчёт", "description": "Цены и доступные сервисы доставки" },
    { "name": "Отправления", "description": "Создание, подтверждение, чтение" },
    { "name": "Этикетки", "description": "PDF-этикетки перевозчика" }
  ],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/api/production/v1/services/calculate": {
      "get": {
        "tags": ["Расчёт"],
        "summary": "Цены и доступные сервисы доставки",
        "description": "Публичная ручка: работает и без ключа. С ключом цены считаются с учётом персональных условий клиента.\n\nОдин недоступный перевозчик не роняет запрос — сервис просто не появится в `data`. Пустой `data` при валидном запросе — легальный успешный ответ.",
        "operationId": "calculate",
        "security": [],
        "parameters": [
          {
            "name": "country_code",
            "in": "query",
            "required": true,
            "description": "Двухбуквенный код страны получателя (ISO 3166-1 alpha-2), регистр не важен.",
            "schema": { "type": "string", "example": "AT" }
          },
          {
            "name": "zip_code",
            "in": "query",
            "required": true,
            "description": "Почтовый индекс получателя. Формат проверяется по стране; при несоответствии придёт 422 с перечнем допустимых форматов.",
            "schema": { "type": "string", "maxLength": 15, "example": "1010" }
          },
          {
            "name": "package_type",
            "in": "query",
            "required": true,
            "description": "1 — посылка (обязательны массивы width/length/height/weight), 2 — конверт (обязателен только weight, до 5 кг).",
            "schema": { "type": "integer", "enum": [1, 2], "example": 1 }
          },
          {
            "name": "send_adr",
            "in": "query",
            "required": true,
            "description": "Служебный флаг, оставшийся от выпиленной опции ADR. Передавайте 0.",
            "schema": { "type": "boolean", "example": 0 }
          },
          {
            "name": "weight",
            "in": "query",
            "required": true,
            "description": "Вес каждого места в килограммах. Массив: weight[]=0.5&weight[]=1.2. Для посылки 0.05–5000, для конверта до 5.",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "number", "minimum": 0.05, "maximum": 5000 } }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Длина каждого места в сантиметрах (обязательно при package_type=1). Размеры массивов width/length/height/weight должны совпадать.",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "number", "minimum": 1, "maximum": 5000 } }
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Ширина каждого места в сантиметрах (обязательно при package_type=1).",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "number", "minimum": 1, "maximum": 5000 } }
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Высота каждого места в сантиметрах (обязательно при package_type=1).",
            "explode": true,
            "schema": { "type": "array", "items": { "type": "number", "minimum": 1, "maximum": 5000 } }
          },
          {
            "name": "service_code",
            "in": "query",
            "required": false,
            "description": "Ограничить расчёт одним сервисом. Без него вернутся все доступные.",
            "schema": { "$ref": "#/components/schemas/ServiceCode" }
          },
          {
            "name": "parcel_price",
            "in": "query",
            "required": false,
            "description": "Стоимость содержимого. Влияет на расчёт страховки.",
            "schema": { "type": "number", "example": 100 }
          }
        ],
        "responses": {
          "200": {
            "description": "Список доступных сервисов с ценами",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "success" },
                    "message": { "type": "string", "example": "OK" },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/RateOption" }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Ошибка валидации, неверный индекс или несогласованные габариты",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/production/v1/services/order-store": {
      "post": {
        "tags": ["Отправления"],
        "summary": "Создать отправление",
        "description": "Создаёт отправление (ещё без лейбла) и возвращает его `shipment_id`. Лейбл создаётся отдельным вызовом `order-confirm`.\n\nИдемпотентность: пришлите заголовок `Idempotency-Key` (или поле `idempotency_key`). Повтор с тем же ключом вернёт то же `shipment_id` и `duplicate: true`, не создавая второе отправление. Без ключа каждый вызов создаёт новое отправление.\n\nПоля `weight`, `length`, `width`, `height` формально не описаны правилами валидации этой ручки, но фактически обязательны — они уходят в расчёт цены.",
        "operationId": "orderStore",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OrderStoreRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Отправление создано (или найдено по ключу идемпотентности)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "success" },
                    "message": { "type": "string", "example": "Shipment has been successfully created" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "shipment_id": { "type": "integer", "example": 73775 },
                        "duplicate": {
                          "type": "boolean",
                          "description": "Есть и равно true только в ответе на повтор с уже использованным Idempotency-Key.",
                          "example": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/AccountBlocked" },
          "422": {
            "description": "Ошибка валидации, недоступный сервис, ошибка перевозчика или негодный Idempotency-Key",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/production/v1/services/order-confirm": {
      "post": {
        "tags": ["Отправления"],
        "summary": "Подтвердить отправление и создать лейбл",
        "description": "Передаёт отправление перевозчику: создаёт отправку на его стороне, получает трек-номер и PDF-этикетку.\n\n⚠ Выполняется до ~60 секунд (у UPS два обращения подряд). Read timeout — не меньше 180 секунд.\n\nИдемпотентно по своей природе: повторный вызов для уже подтверждённого отправления возвращает существующий трек и НЕ обращается к перевозчику снова. Пока запрос выполняется, параллельный вызов получит 409 — это не ошибка, а «уже создаётся».\n\nДоступно клиентам с правом самостоятельного создания лейблов и постоплатой. Предоплатным лейбл создаёт менеджер после поступления оплаты.",
        "operationId": "orderConfirm",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["id"],
                "properties": {
                  "id": {
                    "type": "integer",
                    "description": "shipment_id из ответа order-store.",
                    "example": 73775
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Отправление подтверждено, лейбл готов",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "success" },
                    "message": { "type": "string", "example": "Shipment and label have been successfully created" },
                    "tracking_number": {
                      "type": "string",
                      "description": "JSON-СТРОКА (не массив) с трек-номерами: её нужно распарсить ещё раз.",
                      "example": "[{\"track\":\"1Z999AA10123456784\"}]"
                    },
                    "labels": {
                      "type": "array",
                      "description": "Подписанные временные ссылки на PDF-этикетки (срок жизни 72 часа).",
                      "items": { "type": "string", "format": "uri" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/AccountBlocked" },
          "409": {
            "description": "Подтверждение этого отправления уже выполняется другим запросом. Повторять не нужно — дождитесь и запросите состояние.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ConfirmError" },
                    {
                      "type": "object",
                      "properties": {
                        "code": { "type": "string", "enum": ["confirm_in_progress"] }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Отказ по правилам или ошибка перевозчика. Причину читать по машиночитаемому полю `code`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConfirmError" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/production/v1/services/shipments": {
      "get": {
        "tags": ["Отправления"],
        "summary": "Список id своих отправлений за период",
        "description": "Возвращает только идентификаторы. Детали — отдельным запросом `shipment/{id}`.\n\n⚠ Имя параметра `date_form` — историческая опечатка боевого контракта (не `date_from`). Переименование сломало бы существующие интеграции.\n\n⚠ Граница периода — начало дня `date_to`, поэтому отправления последнего дня диапазона в выборку не попадают: указывайте `date_to` на день больше нужного.",
        "operationId": "getShipments",
        "parameters": [
          {
            "name": "date_form",
            "in": "query",
            "required": true,
            "description": "Начало периода, YYYY-MM-DD. Именно date_form — см. описание ручки.",
            "schema": { "type": "string", "format": "date", "example": "2026-07-01" }
          },
          {
            "name": "date_to",
            "in": "query",
            "required": true,
            "description": "Конец периода, YYYY-MM-DD (не включая сам день).",
            "schema": { "type": "string", "format": "date", "example": "2026-08-01" }
          }
        ],
        "responses": {
          "200": {
            "description": "Идентификаторы отправлений",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "success" },
                    "message": { "type": "string", "example": "OK" },
                    "shipment_ids": {
                      "type": "array",
                      "items": { "type": "integer" },
                      "example": [73775, 73780]
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/AccountBlocked" },
          "422": {
            "description": "Ошибка валидации дат. Формат ответа у этой ручки отличается: без поля type.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/production/v1/services/shipment/{shipment}": {
      "get": {
        "tags": ["Отправления"],
        "summary": "Детали отправления",
        "operationId": "getShipment",
        "parameters": [{ "$ref": "#/components/parameters/ShipmentId" }],
        "responses": {
          "200": {
            "description": "Данные отправления",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "shipment": { "$ref": "#/components/schemas/Shipment" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/AccountBlocked" },
          "404": {
            "description": "Отправления с таким id не существует",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "error" },
                    "message": { "type": "string", "example": "Resource not found" }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Отправление принадлежит другому клиенту",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageOnly" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/production/v1/services/shipment/{shipment}/labels": {
      "get": {
        "tags": ["Этикетки"],
        "summary": "Ссылки на PDF-этикетки отправления",
        "description": "Возвращает подписанные временные ссылки (срок жизни 72 часа). Ссылки скачиваются БЕЗ заголовка Authorization — их можно передать во внешнюю систему.\n\nПока отправление не подтверждено, этикеток нет: придёт 422 с `code: label_not_ready`.",
        "operationId": "getShipmentLabels",
        "parameters": [{ "$ref": "#/components/parameters/ShipmentId" }],
        "responses": {
          "200": {
            "description": "Список этикеток",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "success" },
                    "message": { "type": "string", "example": "OK" },
                    "labels": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Label" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/AccountBlocked" },
          "422": {
            "description": "Этикетки ещё не созданы (`label_not_ready`) либо отправление чужое (`shipment_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": { "type": "string", "example": "error" },
                    "message": { "type": "string" },
                    "description": { "type": "string" },
                    "code": { "type": "string", "enum": ["label_not_ready", "shipment_not_found"] }
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/labels/{uuid}": {
      "get": {
        "tags": ["Этикетки"],
        "summary": "Скачать PDF-этикетку по подписанной ссылке",
        "description": "Ссылку целиком (вместе с параметрами `expires` и `signature`) выдают `order-confirm` и `shipment/{id}/labels`. Собирать её самостоятельно нельзя — подпись проверяется.\n\nАвторизации нет по замыслу: ссылку скачивает внешняя система. Доступ ограничен подписью, сроком жизни и неугадываемым uuid.",
        "operationId": "downloadLabel",
        "security": [],
        "parameters": [
          {
            "name": "uuid",
            "in": "path",
            "required": true,
            "description": "Идентификатор вложения-этикетки.",
            "schema": { "type": "string", "example": "1cch37uvdncuuvmfdenm1wplp" }
          },
          {
            "name": "expires",
            "in": "query",
            "required": true,
            "description": "Часть подписи. Приходит в готовой ссылке.",
            "schema": { "type": "integer" }
          },
          {
            "name": "signature",
            "in": "query",
            "required": true,
            "description": "Часть подписи. Приходит в готовой ссылке.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "PDF-файл этикетки",
            "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } }
          },
          "403": { "description": "Подпись неверна или ссылка истекла" },
          "404": {
            "description": "Вложение не найдено или это не этикетка",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LabelError" } } }
          },
          "410": {
            "description": "Файл этикетки больше недоступен",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LabelError" } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Ключ из кабинета: раздел «API» → /api-keys. Показывается один раз при создании."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Ваш уникальный ключ запроса (1–64 символа: буквы, цифры, точка, двоеточие, подчёркивание, дефис). Повтор с тем же ключом вернёт уже созданное отправление. Рекомендуется брать id заказа в вашей системе.",
        "schema": { "type": "string", "maxLength": 64, "pattern": "^[A-Za-z0-9._:\\-]{1,64}$", "example": "order-10231" }
      },
      "ShipmentId": {
        "name": "shipment",
        "in": "path",
        "required": true,
        "description": "shipment_id из ответа order-store.",
        "schema": { "type": "integer", "example": 73775 }
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "Ключ не передан, недействителен или истёк",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": { "message": { "type": "string", "example": "Unauthenticated." } }
            }
          }
        }
      },
      "AccountBlocked": {
        "description": "Учётная запись заблокирована",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "example": "error" },
                "message": { "type": "string", "example": "This account is blocked. Please contact your YouSend manager." },
                "code": { "type": "string", "enum": ["account_blocked"] }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Превышен лимит 60 запросов в минуту. Заголовки X-RateLimit-Limit / X-RateLimit-Remaining / Retry-After подскажут, когда повторить."
      },
      "ServerError": {
        "description": "Внутренняя ошибка",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageOnly" } } }
      }
    },
    "schemas": {
      "ServiceCode": {
        "type": "string",
        "description": "Код сервиса доставки.",
        "enum": [
          "ups_standard",
          "ups_express_saver",
          "ups_express",
          "ups_express_envelope",
          "fedex_international_priority",
          "fedex_international_economy",
          "fedex_international_connect_plus",
          "dpd_classic",
          "dpd_cod",
          "omniva_courier",
          "latvijas_pasts",
          "ems_express"
        ],
        "example": "ups_standard"
      },
      "RateOption": {
        "type": "object",
        "description": "Одна строка расчёта: сервис и его цена. Состав полей для клиентского ключа.",
        "properties": {
          "package_type": { "type": "integer", "example": 1 },
          "service_code": { "$ref": "#/components/schemas/ServiceCode" },
          "service_name": { "type": "string", "example": "UPS Standard" },
          "service_type": { "type": "integer", "description": "Внутренняя категория сервиса." },
          "final_price_without_tax": { "type": "number", "example": 24.5 },
          "final_price_with_tax": { "type": "number", "example": 29.65 },
          "is_eu": { "type": "integer", "enum": [0, 1] },
          "zip_code": { "type": "string", "example": "1010" },
          "country_code": { "type": "string", "example": "AT" },
          "dimensions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "weight": { "type": "string", "example": "0.5" },
                "length": { "type": "string", "example": "10" },
                "height": { "type": "string", "example": "10" },
                "width": { "type": "string", "example": "10" }
              }
            }
          },
          "delivery_days": { "type": "string", "nullable": true, "description": "Оценка срока доставки." },
          "delivery_date": { "type": "string", "nullable": true, "description": "Дата доставки. Заполняется только у UPS." },
          "delivery_business_days": { "type": "integer", "nullable": true, "description": "Рабочих дней в пути. Только у UPS." },
          "guaranteed": { "type": "boolean", "description": "Гарантированный срок. Только у UPS, иначе false." }
        }
      },
      "OrderStoreRequest": {
        "type": "object",
        "required": [
          "country_code",
          "zip_code",
          "service_code",
          "send_adr",
          "recipient_name",
          "recipient_phone",
          "recipient_address_1",
          "recipient_city",
          "parcel_price",
          "parcel_currency",
          "parcel_description",
          "parcel_pickup_address",
          "parcel_pickup_date",
          "reason_for_export",
          "weight"
        ],
        "properties": {
          "country_code": { "type": "string", "minLength": 2, "maxLength": 2, "example": "AT" },
          "zip_code": { "type": "string", "maxLength": 15, "example": "1010" },
          "service_code": { "$ref": "#/components/schemas/ServiceCode" },
          "send_adr": { "type": "boolean", "description": "Остаток выпиленной опции ADR. Передавайте 0.", "example": 0 },
          "package_type": { "type": "integer", "enum": [1, 2], "description": "1 — посылка, 2 — конверт.", "example": 1 },
          "weight": {
            "type": "array",
            "items": { "type": "number" },
            "description": "Вес каждого места в кг. Формально не в правилах валидации этой ручки, но фактически обязателен.",
            "example": [0.5]
          },
          "length": { "type": "array", "items": { "type": "number" }, "example": [10] },
          "width": { "type": "array", "items": { "type": "number" }, "example": [10] },
          "height": { "type": "array", "items": { "type": "number" }, "example": [10] },
          "recipient_name": { "type": "string", "maxLength": 35, "description": "Только ASCII.", "example": "John Doe" },
          "recipient_phone": { "type": "string", "maxLength": 20, "example": "+43 1 000 0000" },
          "recipient_email": { "type": "string", "format": "email", "maxLength": 100, "description": "Обязателен для omniva_courier, dpd_classic, dpd_cod." },
          "recipient_company_name": { "type": "string", "maxLength": 35, "nullable": true },
          "recipient_address_1": { "type": "string", "maxLength": 35, "example": "Stephansplatz 1" },
          "recipient_address_2": { "type": "string", "maxLength": 35, "nullable": true },
          "recipient_address_3": { "type": "string", "maxLength": 35, "nullable": true },
          "recipient_city": { "type": "string", "maxLength": 35, "example": "Wien" },
          "recipient_state_or_province": { "type": "string", "maxLength": 4, "nullable": true, "description": "Обязателен для США, Канады, Австралии и т.п." },
          "recipient_house_number": { "type": "string", "maxLength": 25, "description": "Обязателен для ems_express." },
          "parcel_price": { "type": "number", "description": "Стоимость содержимого.", "example": 100 },
          "parcel_currency": {
            "type": "string",
            "enum": ["0", "1", "2"],
            "description": "0 — EUR, 1 — USD, 2 — PLN. Любое другое значение трактуется как EUR.",
            "example": "0"
          },
          "parcel_description": {
            "type": "string",
            "description": "Описание содержимого, только ASCII. Максимальная длина зависит от сервиса: 30 (UPS, DPD), 50 (Latvijas Pasts, EMS, FedEx), 100 (Omniva).",
            "example": "Books"
          },
          "parcel_sender": { "type": "string", "maxLength": 35, "nullable": true, "description": "Отправитель на этикетке. По умолчанию — название вашей компании." },
          "parcel_pickup_address": { "type": "string", "maxLength": 100, "example": "Lacplesa 87, Riga" },
          "parcel_pickup_date": { "type": "string", "format": "date", "example": "2026-08-03" },
          "reason_for_export": { "type": "string", "enum": ["sold", "gift", "other"], "example": "sold" },
          "insured": { "type": "boolean", "description": "Платная страховка: 2% от стоимости, минимум 15 EUR." },
          "callback_success_url": {
            "type": "string",
            "nullable": true,
            "description": "Вебхук после подтверждения: YouSend выполнит GET на этот адрес с параметрами shipment_id и tracking_numbers (треки склеены символом |) и ожидает JSON {\"message\":\"OK\"} со кодом 200."
          },
          "idempotency_key": {
            "type": "string",
            "maxLength": 64,
            "pattern": "^[A-Za-z0-9._:\\-]{1,64}$",
            "description": "То же, что заголовок Idempotency-Key (заголовок приоритетнее)."
          }
        }
      },
      "Shipment": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "example": 73775 },
          "tracking_numbers": { "type": "array", "items": { "type": "string" }, "example": ["1Z999AA10123456784"] },
          "labels": {
            "type": "array",
            "description": "Подписанные временные ссылки на этикетки. Пустой массив, пока отправление не подтверждено.",
            "items": { "$ref": "#/components/schemas/Label" }
          },
          "reference_number": { "type": "string", "nullable": true },
          "service": {
            "type": "object",
            "properties": {
              "name": { "type": "string", "example": "UPS Standard" },
              "code": { "$ref": "#/components/schemas/ServiceCode" }
            }
          },
          "parcel": {
            "type": "object",
            "properties": {
              "sender": { "type": "string" },
              "price": { "type": "string" },
              "currency": { "type": "string", "enum": ["EUR", "USD", "PLN"], "description": "Текстовый код, хотя на входе передаётся число." },
              "description": { "type": "string" },
              "pickup_address": { "type": "string" },
              "pickup_date": { "type": "string", "format": "date" },
              "dimensions": { "type": "array", "items": { "type": "object" } }
            }
          },
          "recipient": {
            "type": "object",
            "properties": {
              "country": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "code": { "type": "string", "example": "AT" }
                }
              },
              "city": { "type": "string" },
              "zip_code": { "type": "string" },
              "state_or_province": { "type": "string" },
              "address_1": { "type": "string" },
              "address_2": { "type": "string" },
              "address_3": { "type": "string" },
              "company_name": { "type": "string", "nullable": true },
              "name": { "type": "string" },
              "email": { "type": "string", "nullable": true },
              "phone": { "type": "string", "nullable": true }
            }
          },
          "service_price": {
            "type": "object",
            "properties": {
              "without_tax": { "type": "string" },
              "with_tax": { "type": "string" }
            }
          }
        }
      },
      "Label": {
        "type": "object",
        "properties": {
          "filename": { "type": "string", "example": "1Z999AA10123456784.pdf" },
          "size": { "type": "integer", "description": "Размер файла в байтах." },
          "created_at": { "type": "string", "example": "2026-07-30 12:00:00" },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Подписанная временная ссылка на PDF (72 часа). Скачивается без Authorization."
          }
        }
      },
      "ApiError": {
        "type": "object",
        "description": "Формат ошибок ручек calculate и order-store.",
        "properties": {
          "type": { "type": "string", "example": "error" },
          "message": { "type": "string", "example": "Please check the highlighted fields and try again." },
          "errors": {
            "description": "Обычно объект «поле → список сообщений», но в нескольких случаях значение поля или сам errors приходит строкой.",
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    { "type": "array", "items": { "type": "string" } },
                    { "type": "string" }
                  ]
                }
              },
              { "type": "string" }
            ]
          }
        }
      },
      "ConfirmError": {
        "type": "object",
        "description": "Формат отказа ручки order-confirm. Причину читать по полю code — оно не локализуется.",
        "properties": {
          "type": { "type": "string", "example": "error" },
          "message": { "type": "string", "example": "Label generation is unavailable on prepayment" },
          "description": { "type": "string" },
          "code": {
            "type": "string",
            "enum": [
              "label_permission_denied",
              "prepayment_required",
              "shipment_not_found",
              "shipment_cancelled",
              "confirm_in_progress"
            ],
            "description": "label_permission_denied — у учётной записи нет права самостоятельного лейбла; prepayment_required — предоплатному клиенту лейбл создаёт менеджер после оплаты; shipment_not_found — отправления нет или оно чужое; shipment_cancelled — отправление отменено; confirm_in_progress — подтверждение уже выполняется. Ошибки перевозчика приходят без code, с текстом перевозчика в корне ответа."
          }
        }
      },
      "LabelError": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "example": "error" },
          "message": { "type": "string" },
          "code": { "type": "string", "enum": ["label_unavailable"] }
        }
      },
      "ValidationError": {
        "type": "object",
        "description": "Формат ошибок валидации ручки shipments (без поля type).",
        "properties": {
          "message": { "type": "string", "example": "The given data was invalid." },
          "errors": {
            "type": "object",
            "additionalProperties": { "type": "array", "items": { "type": "string" } }
          }
        }
      },
      "MessageOnly": {
        "type": "object",
        "properties": {
          "message": { "type": "string" }
        }
      }
    }
  }
}
