{
  "openapi": "3.0.3",
  "info": {
    "title": "DC Envíos · API para clientes",
    "version": "1.0.1",
    "description": "Contrato público de integración nacional. Los ejemplos son ficticios y no representan tarifas garantizadas. Las operaciones de producción pueden generar cargos. La llave no admite envíos internacionales ni webhooks para clientes. Los ajustes autorizados por administración se aplican al saldo disponible; el cliente consulta movimientos y saldo, sin ejecutar otro cobro. La conciliación de ajustes es opcional.",
    "contact": {
      "name": "Soporte DC Envíos",
      "email": "contacto@dcenvios.lat"
    }
  },
  "servers": [
    {
      "url": "https://api.dcenvios.lat",
      "description": "Producción"
    }
  ],
  "security": [
    {
      "ApiKey": []
    },
    {
      "BearerKey": []
    }
  ],
  "paths": {
    "/api/v1/balance": {
      "get": {
        "operationId": "balance",
        "summary": "Consultar saldo",
        "description": "Saldo disponible de la cuenta vinculada a tu llave, en MXN. Si aún no existe un saldo, puede devolver 0 como número. Es el saldo que ya considera los cargos aplicados por administración. No vuelvas a descontar los movimientos de cargo_adicional. Esta lectura y /movimientos son solicitudes independientes, no una instantánea transaccional conjunta.",
        "tags": [
          "Envíos"
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saldo": {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "type": "string",
                          "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
                        }
                      ],
                      "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
                    }
                  }
                },
                "example": {
                  "saldo": "1500.00"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cp/{cp}": {
      "get": {
        "operationId": "postal",
        "summary": "Consultar código postal",
        "description": "Obtén estado, municipio y colonias antes de cotizar. Conserva el nombre de la colonia seleccionada para generar la guía.",
        "tags": [
          "Catálogos"
        ],
        "parameters": [
          {
            "name": "cp",
            "in": "path",
            "required": true,
            "description": "CP mexicano de 5 dígitos.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Postal"
                },
                "example": {
                  "cp": "64000",
                  "estado": "Nuevo León",
                  "municipio": "Monterrey",
                  "ciudad": "Monterrey",
                  "colonias": [
                    "Centro"
                  ],
                  "fuente": "catalogo"
                }
              }
            }
          },
          "400": {
            "description": "Datos incompletos, clave SAT inválida o cotización vencida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Recurso no encontrado en el alcance de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Servicio temporalmente no disponible o no habilitado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sat/buscar": {
      "get": {
        "operationId": "sat",
        "summary": "Buscar producto SAT",
        "description": "Busca por nombre o clave de 8 dígitos. Devuelve hasta 20 coincidencias. Una búsqueda de menos de 2 caracteres devuelve una lista vacía. Elige el producto correcto; no adivines la clave ni uses 01010101.",
        "tags": [
          "Catálogos"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Texto del producto o clave SAT. Ejemplo: pantalones.",
            "schema": {
              "type": "string",
              "maxLength": 240
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SatSearch"
                },
                "example": {
                  "resultados": [
                    {
                      "clave": "53101500",
                      "nombre": "Pantalones y pantalones cortos",
                      "fuente": "carta_porte_31",
                      "material_peligroso": "0"
                    }
                  ],
                  "seleccion_automatica": null,
                  "requiere_precision": true
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quote": {
      "post": {
        "operationId": "quote",
        "summary": "Cotizar un envío nacional",
        "description": "Devuelve las opciones disponibles para tu cuenta, ordenadas por precio ascendente. Elige una tarifa y conserva su token; vence a los 30 minutos. Repite la cotización si cambian CP, colonia, peso, dimensiones, seguro o embalaje.",
        "tags": [
          "Envíos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              },
              "example": {
                "cp_origen": "64000",
                "cp_destino": "06000",
                "colonia_origen": "Centro",
                "colonia_destino": "Centro",
                "peso": 1,
                "largo": 20,
                "ancho": 15,
                "alto": 10,
                "satCategory": "53101500",
                "tipo_embalaje": "box",
                "asegurar": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                },
                "example": {
                  "cotizaciones": [
                    {
                      "paqueteria": "dhl",
                      "servicio": "Terrestre",
                      "dias_estimados": 3,
                      "precio": 180,
                      "zona_extendida": false,
                      "token": "TOKEN_DE_LA_OPCION_ELEGIDA",
                      "peso_real": 1,
                      "peso_volumetrico": 0.6,
                      "peso_cobrable": 1,
                      "divisor_volumetrico": 5000,
                      "costo_envio": 180,
                      "costo_proteccion": 0,
                      "cargo_manejo": 0,
                      "cargos_adicionales": [],
                      "asegurado": false,
                      "valor_declarado": 0,
                      "tipo_embalaje": "box"
                    }
                  ],
                  "cacheado": false,
                  "peso": {
                    "peso_real": 1,
                    "peso_volumetrico": 0.6,
                    "peso_cobrable": 1,
                    "divisor_volumetrico": 5000
                  },
                  "origen": {
                    "cp": "64000",
                    "ciudad": "Monterrey, Nuevo León",
                    "colonia": "Centro"
                  },
                  "destino": {
                    "cp": "06000",
                    "ciudad": "Cuauhtémoc, Ciudad de México",
                    "colonia": "Centro"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Datos incompletos, clave SAT inválida o cotización vencida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Cuenta sin activar o acceso no permitido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo completar la operación. No repitas automáticamente una compra.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Servicio temporalmente no disponible o no habilitado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/label": {
      "post": {
        "operationId": "label",
        "summary": "Generar una guía",
        "description": "Esta operación compra una guía y descuenta el saldo. Usa el token elegido, direcciones completas y una clave SAT vigente. Seguro, valor declarado y embalaje se toman de la cotización. Conserva id, numero_guia, estatus y cualquier advertencia. Una misma cotización tiene control de duplicados; consulta la sección de reintentos.",
        "tags": [
          "Envíos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LabelRequest"
              },
              "example": {
                "token_cotizacion": "TOKEN_DE_LA_OPCION_ELEGIDA",
                "remitente": {
                  "nombre": "Tienda Ejemplo",
                  "empresa": "Tienda Ejemplo",
                  "telefono": "8112345678",
                  "email": "remitente@example.com",
                  "cp": "64000",
                  "estado": "Nuevo León",
                  "ciudad": "Monterrey",
                  "colonia": "Centro",
                  "calle": "Morelos",
                  "numero": "100",
                  "referencias": "Local a pie de calle"
                },
                "destinatario": {
                  "nombre": "Cliente Ejemplo",
                  "telefono": "5512345678",
                  "email": "destinatario@example.com",
                  "cp": "06000",
                  "estado": "Ciudad de México",
                  "ciudad": "Cuauhtémoc",
                  "colonia": "Centro",
                  "calle": "Madero",
                  "numero": "5"
                },
                "paquete": {
                  "peso": 1,
                  "largo": 20,
                  "ancho": 15,
                  "alto": 10,
                  "contenido": "Pantalones de algodón",
                  "clave_sat": "53101500"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelResponse"
                },
                "example": {
                  "id": 1234,
                  "numero_guia": "GUIA_DE_EJEMPLO",
                  "tracking_number": "GUIA_DE_EJEMPLO",
                  "tracking_url": null,
                  "precio_venta": 180,
                  "estatus": "generada",
                  "protegido": false,
                  "proteccion_solicitada": false,
                  "valor_declarado": 0,
                  "desglose": {
                    "envio": 180,
                    "proteccion": 0,
                    "total": 180
                  }
                }
              }
            }
          },
          "202": {
            "description": "Solicitud aceptada pero por revisar. No generes otra guía.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelResponse"
                },
                "example": {
                  "id": 1234,
                  "estatus": "por_revisar",
                  "advertencia": "La solicitud sigue en revisión. Consulta tus guías antes de volver a intentar."
                }
              }
            }
          },
          "400": {
            "description": "Datos incompletos, clave SAT inválida o cotización vencida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Saldo insuficiente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Cuenta sin activar o acceso no permitido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Solicitud en proceso, duplicada o incompatible con el estado actual.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Datos rechazados al generar; revisa el campo indicado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo completar la operación. No repitas automáticamente una compra.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/labels": {
      "get": {
        "operationId": "labels",
        "summary": "Listar y buscar guías",
        "description": "Lista solo las guías de tu cuenta, de más reciente a más antigua. q busca por número, CP, estado, paquetería o nombre del remitente/destinatario. Una página superior a la última se ajusta a la última disponible. Para recuperar una operación pendiente, busca su id en la lista.",
        "tags": [
          "Envíos"
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Página solicitada.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Registros por página. El servidor ajusta valores al rango 10–100.",
            "schema": {
              "type": "integer",
              "minimum": 10,
              "maximum": 100,
              "default": 100
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Búsqueda opcional.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelsResponse"
                },
                "example": {
                  "guias": [
                    {
                      "id": 1234,
                      "numero_guia": "GUIA_DE_EJEMPLO",
                      "estatus": "generada",
                      "paqueteria": "dhl",
                      "precio_venta": "180.00",
                      "cp_origen": "64000",
                      "cp_destino": "06000",
                      "peso": "1",
                      "created_at": "2026-09-26T12:00:00Z",
                      "cancelacion_pendiente": false,
                      "tiene_movimiento_fisico": false
                    }
                  ],
                  "pagination": {
                    "page": 1,
                    "limit": 20,
                    "total": 1,
                    "pages": 1,
                    "q": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/label-pdf/{id}": {
      "get": {
        "operationId": "pdf",
        "summary": "Obtener la etiqueta",
        "description": "Devuelve JSON con base64, no un archivo PDF directo. El valor puede ser base64 puro o una data URL. Decodifícalo y detecta el formato: normalmente PDF, pero puede ser imagen. base64:null significa que la etiqueta aún no está disponible; consulta más tarde sin crear otra guía.",
        "tags": [
          "Envíos"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID devuelto por /label o /labels.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "base64": {
                      "type": "string",
                      "nullable": true,
                      "description": "Contenido codificado de la etiqueta, o null."
                    }
                  },
                  "required": [
                    "base64"
                  ]
                },
                "example": {
                  "base64": "JVBERi0xLjcK..."
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Recurso no encontrado en el alcance de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tracking/{guia}": {
      "get": {
        "operationId": "tracking",
        "summary": "Rastrear una guía",
        "description": "Consulta el estado normalizado y sus eventos. Usa preferentemente las guías de tu cuenta. No hay una promesa de actualización en tiempo real ni de una frecuencia fija; recomendamos consultar cada 15–30 minutos, reducir la frecuencia después de la entrega y respetar el límite global.",
        "tags": [
          "Envíos"
        ],
        "parameters": [
          {
            "name": "guia",
            "in": "path",
            "required": true,
            "description": "Número de rastreo.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracking"
                },
                "example": {
                  "estatus": "en_transito",
                  "eventos": [
                    {
                      "desc": "En tránsito hacia el destino",
                      "fecha": "2026-09-26T12:00:00Z",
                      "lugar": "Monterrey"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo completar la operación. No repitas automáticamente una compra.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cancellations": {
      "post": {
        "operationId": "cancellations",
        "summary": "Solicitar cancelación",
        "description": "Registra una solicitud; HTTP 201 no significa que la guía esté cancelada ni que el saldo haya sido devuelto. Se revisa antes de resolver. Si ya fue recolectada o registra movimiento físico, se rechaza. Consulta cancelacion_pendiente y estatus en /labels, y el saldo tras la resolución.",
        "tags": [
          "Envíos"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancellationRequest"
              },
              "example": {
                "numero_guia": "GUIA_DE_EJEMPLO",
                "guide_id": 1234,
                "motivo": "El pedido fue cancelado"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La guía ya estaba cancelada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cancellation"
                },
                "example": {
                  "ok": true,
                  "cancelada": true,
                  "idempotente": true,
                  "mensaje": "La guía ya estaba cancelada."
                }
              }
            }
          },
          "201": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cancellation"
                },
                "example": {
                  "ok": true,
                  "automatica": false,
                  "cancelada": false,
                  "mensaje": "Solicitud de cancelación enviada. Te notificaremos al resolverla."
                }
              }
            }
          },
          "400": {
            "description": "Datos incompletos, clave SAT inválida o cotización vencida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Recurso no encontrado en el alcance de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Solicitud en proceso, duplicada o incompatible con el estado actual.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/recolecciones": {
      "post": {
        "operationId": "pickup_create",
        "summary": "Solicitar recolección",
        "description": "Envía números de guías existentes y una dirección de origen común. fecha y horario se interpretan como hora local de recolección. La disponibilidad depende de la paquetería. automatica:false queda pendiente de gestión; automatica:true incluye la confirmación disponible. Puede ajustarse el horario. Evita repetir la solicitud si la respuesta se pierde: consulta primero el listado.",
        "tags": [
          "Recolecciones"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PickupRequest"
              },
              "example": {
                "fecha": "2026-10-01",
                "hora_ini": "09:00",
                "hora_fin": "18:00",
                "direccion": {
                  "nombre": "Tienda Ejemplo",
                  "empresa": "Tienda Ejemplo",
                  "telefono": "8112345678",
                  "email": "remitente@example.com",
                  "cp": "64000",
                  "estado": "Nuevo León",
                  "ciudad": "Monterrey",
                  "colonia": "Centro",
                  "calle": "Morelos",
                  "numero": "100",
                  "referencias": "Local a pie de calle"
                },
                "guias": [
                  "GUIA_DE_EJEMPLO"
                ],
                "num_paquetes": 1,
                "comentarios": "Solicitar acceso en recepción"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pickup"
                },
                "example": {
                  "ok": true,
                  "id": 55,
                  "automatica": false,
                  "estado": "pendiente"
                }
              }
            }
          },
          "400": {
            "description": "Datos incompletos, clave SAT inválida o cotización vencida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Cuenta sin activar o acceso no permitido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Solicitud en proceso, duplicada o incompatible con el estado actual.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "No se pudo completar la operación. No repitas automáticamente una compra.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "pickup_list",
        "summary": "Consultar recolecciones",
        "description": "Devuelve las 100 solicitudes más recientes de tu cuenta. Esta operación no implementa paginación. El indicador recolectado se calcula con los avances de rastreo de las guías; una solicitud registrada no equivale a un paquete recolectado.",
        "tags": [
          "Recolecciones"
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recolecciones": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Pickup"
                      }
                    }
                  }
                },
                "example": {
                  "recolecciones": [
                    {
                      "id": 55,
                      "fecha": "2026-10-01",
                      "hora_ini": "09:00",
                      "hora_fin": "18:00",
                      "estado": "pendiente",
                      "folio": null,
                      "guias": [
                        "GUIA_DE_EJEMPLO"
                      ],
                      "num_paquetes": 1,
                      "recolectado": false,
                      "recolectado_at": null,
                      "paqueteria": "dhl"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/movimientos": {
      "get": {
        "operationId": "movements",
        "summary": "Consultar movimientos y cargos",
        "description": "Devuelve los 500 movimientos más recientes de tu cuenta, ordenados por fecha descendente. No implementa filtros ni paginación. Incluye cargos efectivamente aplicados y reembolsos. Identifica cada movimiento por id y utiliza saldo_anterior/saldo_nuevo para determinar su efecto; no interpretes el signo de monto de forma aislada. Un ajuste aún pendiente puede no aparecer aquí. Los cargos se descuentan del saldo disponible cuando administración los autoriza; no se cobra mediante esta consulta ni se emite un webhook al cliente. Si no alcanza el saldo, puede quedar una parte pendiente. No supongas que una recarga cobra automáticamente lo pendiente. Guarda cada id con unicidad por cuenta; no restes de nuevo el cargo al saldo devuelto por /balance. Con más de 500 movimientos entre consultas, esta ruta no garantiza recuperar todos los registros.",
        "tags": [
          "Envíos"
        ],
        "responses": {
          "200": {
            "description": "Resultado de la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "movimientos": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Movement"
                      }
                    }
                  }
                },
                "example": {
                  "movimientos": [
                    {
                      "id": 901,
                      "tipo": "cargo_adicional",
                      "monto": "50.00",
                      "referencia": "Cargo adicional por sobrepeso o ajuste de medidas - guía GUIA_DE_EJEMPLO",
                      "notas": null,
                      "saldo_anterior": "1500.00",
                      "saldo_nuevo": "1450.00",
                      "origen": "ajuste_paqueteria",
                      "created_at": "2026-09-26T12:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/labels/reporte.csv": {
      "get": {
        "operationId": "report",
        "summary": "Descargar reporte de guías y ajustes",
        "description": "Exporta las guías de tu cuenta en CSV UTF-8 con BOM, encabezados, comas y campos entre comillas. No implementa filtros ni paginación. Incluye Total original, Ajustes posteriores, Conceptos de ajustes, Ajustes cobrados, Ajustes pendientes y Total final. Los ajustes corresponden a los registrados en la plataforma; Total final suma el original y los ajustes reportados, aunque una parte siga pendiente. Las fechas se muestran en America/Mexico_City.",
        "tags": [
          "Envíos"
        ],
        "responses": {
          "200": {
            "description": "CSV del reporte completo. Ejemplo abreviado con columnas seleccionadas.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "\"ID\",\"Numero de guia\",\"Total original\",\"Ajustes posteriores\",\"Ajustes cobrados\",\"Ajustes pendientes\",\"Total final\"\r\n\"1234\",\"GUIA_DE_EJEMPLO\",\"180\",\"50\",\"30\",\"20\",\"230\""
              }
            }
          },
          "401": {
            "description": "Llave ausente, inválida o revocada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Cuenta sin activar o acceso no permitido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de solicitudes alcanzado. Respeta Retry-After si está presente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "BearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Envía la misma llave dc_live_... como Bearer."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Descripción del problema."
          },
          "code": {
            "type": "string",
            "description": "Código estable cuando está presente."
          },
          "codigo": {
            "type": "string",
            "description": "Algunas operaciones usan codigo en lugar de code."
          },
          "retry_safe": {
            "type": "boolean",
            "description": "true indica que puede corregirse la entrada o solicitar una nueva cotización siguiendo el código devuelto."
          }
        }
      },
      "Address": {
        "type": "object",
        "properties": {
          "nombre": {
            "type": "string",
            "description": "Nombre completo del contacto. Usa datos reales, sin abreviaturas ambiguas."
          },
          "empresa": {
            "type": "string",
            "description": "Empresa; opcional."
          },
          "telefono": {
            "type": "string",
            "description": "Teléfono nacional de 10 dígitos. Recomendación de formato para evitar rechazos.",
            "pattern": "^[0-9]{10}$"
          },
          "email": {
            "type": "string",
            "description": "Correo del contacto; recomendado.",
            "format": "email"
          },
          "cp": {
            "type": "string",
            "description": "Código postal mexicano; conserva ceros iniciales.",
            "pattern": "^[0-9]{5}$"
          },
          "estado": {
            "type": "string",
            "description": "Estado según el catálogo de CP."
          },
          "ciudad": {
            "type": "string",
            "description": "Ciudad o municipio según el catálogo de CP."
          },
          "colonia": {
            "type": "string",
            "description": "Colonia exacta; debe coincidir con la cotización."
          },
          "calle": {
            "type": "string",
            "description": "Calle."
          },
          "numero": {
            "type": "string",
            "description": "Número exterior/interior como texto."
          },
          "referencias": {
            "type": "string",
            "description": "Indicaciones para localizar el domicilio; opcional."
          }
        },
        "required": [
          "nombre",
          "telefono",
          "cp",
          "estado",
          "ciudad",
          "colonia",
          "calle",
          "numero"
        ]
      },
      "Package": {
        "type": "object",
        "properties": {
          "peso": {
            "type": "number",
            "description": "Peso real en kg; conserva el valor usado al cotizar.",
            "exclusiveMinimum": true,
            "minimum": 0
          },
          "largo": {
            "type": "number",
            "description": "Largo en cm.",
            "exclusiveMinimum": true,
            "minimum": 0
          },
          "ancho": {
            "type": "number",
            "description": "Ancho en cm.",
            "exclusiveMinimum": true,
            "minimum": 0
          },
          "alto": {
            "type": "number",
            "description": "Alto en cm.",
            "exclusiveMinimum": true,
            "minimum": 0
          },
          "contenido": {
            "type": "string",
            "description": "Descripción precisa del producto. No uses únicamente una categoría genérica.",
            "minLength": 1
          },
          "clave_sat": {
            "type": "string",
            "description": "Clave vigente de 8 dígitos obtenida en /sat/buscar. Es obligatoria salvo que envíes carta_porte.clave_prod_serv.",
            "pattern": "^[0-9]{8}$"
          }
        },
        "required": [
          "peso",
          "largo",
          "ancho",
          "alto",
          "contenido"
        ]
      },
      "QuoteRequest": {
        "type": "object",
        "properties": {
          "cp_origen": {
            "type": "string",
            "description": "CP de origen en México.",
            "pattern": "^[0-9]{5}$"
          },
          "cp_destino": {
            "type": "string",
            "description": "CP de destino en México.",
            "pattern": "^[0-9]{5}$"
          },
          "colonia_origen": {
            "type": "string",
            "description": "Colonia de origen; recomendada para obtener una tarifa utilizable."
          },
          "colonia_destino": {
            "type": "string",
            "description": "Colonia de destino; recomendada para obtener una tarifa utilizable."
          },
          "peso": {
            "type": "number",
            "description": "Peso real en kg.",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "largo": {
            "type": "number",
            "description": "Largo real en cm. Recomendado para cotizar correctamente.",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "ancho": {
            "type": "number",
            "description": "Ancho real en cm.",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "alto": {
            "type": "number",
            "description": "Alto real en cm.",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "asegurar": {
            "type": "boolean",
            "description": "Solicitar protección. Confirma asegurado en la tarifa devuelta.",
            "default": false
          },
          "valor_declarado": {
            "type": "number",
            "description": "Valor en MXN; envíalo cuando asegurar=true.",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "satCategory": {
            "type": "string",
            "description": "Clave SAT del contenido para cotizar. Se envía como paquete.clave_sat al generar.",
            "pattern": "^[0-9]{8}$"
          },
          "tipo_embalaje": {
            "type": "string",
            "description": "Según disponibilidad de tu cuenta y servicio.",
            "enum": [
              "box",
              "envelope",
              "pallet"
            ],
            "default": "box"
          }
        },
        "required": [
          "cp_origen",
          "cp_destino",
          "peso"
        ]
      },
      "QuoteOption": {
        "type": "object",
        "properties": {
          "paqueteria": {
            "type": "string",
            "description": "Paquetería que transportará el envío."
          },
          "servicio": {
            "type": "string",
            "description": "Categoría del servicio."
          },
          "dias_estimados": {
            "oneOf": [
              {
                "type": "number",
                "nullable": true
              },
              {
                "type": "string"
              }
            ],
            "description": "Estimación, no una garantía de entrega."
          },
          "precio": {
            "type": "number",
            "description": "Precio final de venta para tu cuenta, en MXN."
          },
          "token": {
            "type": "string",
            "description": "Token de cotización válido 30 minutos. Trátalo como opaco."
          },
          "zona_extendida": {
            "type": "boolean",
            "description": "Indica cobertura en zona extendida."
          },
          "peso_real": {
            "type": "number",
            "description": "Peso real en kg."
          },
          "peso_volumetrico": {
            "type": "number",
            "description": "Peso volumétrico en kg."
          },
          "peso_cobrable": {
            "type": "number",
            "description": "Peso usado para cotizar, redondeado al siguiente entero."
          },
          "divisor_volumetrico": {
            "type": "number",
            "description": "Divisor aplicado: 5000."
          },
          "costo_envio": {
            "type": "number",
            "description": "Nombre heredado: importe de venta al cliente por el flete."
          },
          "costo_proteccion": {
            "type": "number",
            "description": "Nombre heredado: importe de venta al cliente por la protección."
          },
          "cargo_manejo": {
            "type": "number",
            "description": "Importe de cargos adicionales al cliente."
          },
          "cargos_adicionales": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "nombre": {
                  "type": "string",
                  "description": "Concepto."
                },
                "precio": {
                  "type": "number",
                  "description": "Importe al cliente."
                },
                "motivo": {
                  "type": "string",
                  "description": "Motivo."
                }
              }
            }
          },
          "asegurado": {
            "type": "boolean",
            "description": "La tarifa incluye protección confirmada en la cotización."
          },
          "valor_declarado": {
            "type": "number",
            "description": "Valor declarado en MXN."
          },
          "tipo_embalaje": {
            "type": "string",
            "description": "Tipo de embalaje cotizado."
          },
          "ciudad_origen": {
            "type": "string",
            "description": "Ciudad de origen."
          },
          "ciudad_destino": {
            "type": "string",
            "description": "Ciudad de destino."
          },
          "servicio_directo": {
            "type": "boolean",
            "description": "Característica del servicio."
          },
          "beneficio": {
            "type": "string",
            "description": "Descripción adicional del servicio.",
            "nullable": true
          },
          "packaging_type": {
            "type": "string",
            "description": "Clasificación de embalaje si está disponible.",
            "nullable": true
          }
        },
        "required": [
          "paqueteria",
          "servicio",
          "precio",
          "token"
        ]
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "cotizaciones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuoteOption"
            }
          },
          "cacheado": {
            "type": "boolean",
            "description": "Indica si se reutilizó una consulta reciente de tarifas."
          },
          "peso": {
            "type": "object",
            "properties": {
              "peso_real": {
                "type": "number",
                "description": "kg"
              },
              "peso_volumetrico": {
                "type": "number",
                "description": "kg"
              },
              "peso_cobrable": {
                "type": "number",
                "description": "kg"
              },
              "divisor_volumetrico": {
                "type": "number",
                "description": "Divisor"
              }
            }
          },
          "origen": {
            "type": "object",
            "properties": {
              "cp": {
                "type": "string",
                "description": "CP"
              },
              "ciudad": {
                "type": "string",
                "description": "Ciudad"
              },
              "colonia": {
                "type": "string",
                "description": "Colonia"
              }
            }
          },
          "destino": {
            "type": "object",
            "properties": {
              "cp": {
                "type": "string",
                "description": "CP"
              },
              "ciudad": {
                "type": "string",
                "description": "Ciudad"
              },
              "colonia": {
                "type": "string",
                "description": "Colonia"
              }
            }
          }
        },
        "required": [
          "cotizaciones"
        ]
      },
      "LabelRequest": {
        "type": "object",
        "properties": {
          "token_cotizacion": {
            "type": "string",
            "description": "Token de la opción elegida en /quote."
          },
          "remitente": {
            "$ref": "#/components/schemas/Address"
          },
          "destinatario": {
            "$ref": "#/components/schemas/Address"
          },
          "paquete": {
            "$ref": "#/components/schemas/Package"
          },
          "carta_porte": {
            "type": "object",
            "properties": {
              "clave_prod_serv": {
                "type": "string",
                "description": "Alternativa a paquete.clave_sat; si se envían ambas, esta clave tiene prioridad.",
                "pattern": "^[0-9]{8}$"
              },
              "descripcion": {
                "type": "string",
                "description": "Alternativa a paquete.contenido."
              }
            }
          },
          "comprador_email": {
            "type": "string",
            "description": "Correo alternativo para notificaciones si no se proporcionó destinatario.email. Sujeto a la configuración de notificaciones de la cuenta.",
            "format": "email"
          }
        },
        "required": [
          "token_cotizacion",
          "remitente",
          "destinatario",
          "paquete"
        ]
      },
      "LabelResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "description": "Identificador de la guía en DC Envíos. Guarda este valor."
          },
          "numero_guia": {
            "type": "string",
            "description": "Número de rastreo. Puede no estar disponible aún.",
            "nullable": true
          },
          "tracking_number": {
            "type": "string",
            "description": "Alias del número de rastreo.",
            "nullable": true
          },
          "tracking_url": {
            "type": "string",
            "description": "Enlace oficial de rastreo cuando está disponible.",
            "nullable": true
          },
          "precio_venta": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
              }
            ],
            "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
          },
          "estatus": {
            "type": "string",
            "description": "generada, etiqueta_pendiente o por_revisar, según el resultado."
          },
          "protegido": {
            "type": "boolean",
            "description": "Protección confirmada al generar."
          },
          "proteccion_solicitada": {
            "type": "boolean",
            "description": "Se solicitó protección."
          },
          "valor_declarado": {
            "type": "number",
            "description": "MXN."
          },
          "idempotente": {
            "type": "boolean",
            "description": "Se devolvió una guía ya creada para la misma cotización."
          },
          "pendiente": {
            "type": "boolean",
            "description": "La solicitud sigue en revisión."
          },
          "advertencia": {
            "type": "string",
            "description": "Instrucción que debe mostrarse o registrarse."
          },
          "desglose": {
            "type": "object",
            "properties": {
              "envio": {
                "type": "number",
                "description": "Precio del flete."
              },
              "proteccion": {
                "type": "number",
                "description": "Precio de protección."
              },
              "total": {
                "type": "number",
                "description": "Total cobrado. Puede incluir otros cargos; usa siempre el total devuelto."
              }
            }
          }
        }
      },
      "Guide": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "numero_guia": {
            "type": "string",
            "nullable": true
          },
          "estatus": {
            "type": "string",
            "description": "Estado de generación/cancelación."
          },
          "paqueteria": {
            "type": "string",
            "description": "Paquetería final."
          },
          "precio_venta": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
              }
            ],
            "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
          },
          "cp_origen": {
            "type": "string",
            "description": "CP de origen."
          },
          "cp_destino": {
            "type": "string",
            "description": "CP de destino."
          },
          "peso": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              }
            ],
            "description": "Peso en kilogramos."
          },
          "created_at": {
            "type": "string",
            "description": "Fecha de creación.",
            "format": "date-time"
          },
          "ultimo_estatus": {
            "type": "string",
            "description": "Último estado de rastreo.",
            "nullable": true
          },
          "ultimo_evento_at": {
            "type": "string",
            "nullable": true
          },
          "cancelacion_pendiente": {
            "type": "boolean",
            "description": "Existe una solicitud de cancelación pendiente."
          },
          "tiene_movimiento_fisico": {
            "type": "boolean",
            "description": "El envío ya tiene movimiento."
          },
          "remitente": {
            "$ref": "#/components/schemas/Address"
          },
          "destinatario": {
            "$ref": "#/components/schemas/Address"
          },
          "paquete": {
            "$ref": "#/components/schemas/Package"
          }
        }
      },
      "LabelsResponse": {
        "type": "object",
        "properties": {
          "guias": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Guide"
            }
          },
          "pagination": {
            "type": "object",
            "properties": {
              "page": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              },
              "pages": {
                "type": "integer"
              },
              "q": {
                "type": "string",
                "nullable": true
              }
            }
          }
        },
        "required": [
          "guias",
          "pagination"
        ]
      },
      "Tracking": {
        "type": "object",
        "properties": {
          "estatus": {
            "type": "string",
            "description": "Estado normalizado del envío.",
            "enum": [
              "generada",
              "recolectada",
              "en_transito",
              "en_reparto",
              "entregada",
              "incidencia"
            ]
          },
          "eventos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "desc": {
                  "type": "string",
                  "description": "Descripción del evento en español."
                },
                "fecha": {
                  "type": "string",
                  "description": "Fecha del evento; puede venir sin zona horaria. No supongas UTC si falta el offset.",
                  "nullable": true
                },
                "lugar": {
                  "type": "string",
                  "description": "Ubicación cuando está disponible.",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "CancellationRequest": {
        "type": "object",
        "properties": {
          "numero_guia": {
            "type": "string",
            "description": "Número de la guía."
          },
          "guide_id": {
            "type": "integer",
            "description": "ID de DC Envíos. Recomendado; obligatorio si hay varias guías con el mismo número."
          },
          "motivo": {
            "type": "string",
            "description": "Motivo de la cancelación; opcional."
          }
        },
        "required": [
          "numero_guia"
        ]
      },
      "Cancellation": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Solicitud registrada o resuelta previamente."
          },
          "automatica": {
            "type": "boolean",
            "description": "false indica revisión pendiente."
          },
          "cancelada": {
            "type": "boolean",
            "description": "No confundir una solicitud aceptada con una guía ya cancelada."
          },
          "idempotente": {
            "type": "boolean",
            "description": "La guía ya estaba cancelada."
          },
          "mensaje": {
            "type": "string",
            "description": "Resultado de la solicitud."
          }
        }
      },
      "Postal": {
        "type": "object",
        "properties": {
          "cp": {
            "type": "string",
            "description": "Código postal."
          },
          "estado": {
            "type": "string",
            "description": "Estado."
          },
          "municipio": {
            "type": "string",
            "description": "Municipio."
          },
          "ciudad": {
            "type": "string",
            "nullable": true
          },
          "colonias": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fuente": {
            "type": "string",
            "description": "Catálogo postal."
          }
        }
      },
      "SatItem": {
        "type": "object",
        "properties": {
          "clave": {
            "type": "string",
            "description": "Clave SAT vigente.",
            "pattern": "^[0-9]{8}$"
          },
          "nombre": {
            "type": "string",
            "description": "Descripción del catálogo."
          },
          "fuente": {
            "type": "string",
            "description": "Catálogo Carta Porte."
          },
          "material_peligroso": {
            "description": "Indicador original del catálogo; su presencia no habilita el transporte de mercancía peligrosa.",
            "nullable": true
          }
        }
      },
      "SatSearch": {
        "type": "object",
        "properties": {
          "resultados": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SatItem"
            }
          },
          "seleccion_automatica": {
            "type": "object",
            "nullable": true,
            "description": "Producto seleccionado cuando existe una coincidencia exacta; mismos campos que SatItem."
          },
          "requiere_precision": {
            "type": "boolean",
            "description": "Puede estar ausente para búsquedas vacías o por código. Pide una selección explícita cuando no haya coincidencia única."
          }
        }
      },
      "PickupRequest": {
        "type": "object",
        "properties": {
          "fecha": {
            "type": "string",
            "description": "Fecha solicitada.",
            "format": "date"
          },
          "direccion": {
            "$ref": "#/components/schemas/Address"
          },
          "guias": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "Número de guía, no ID."
            }
          },
          "hora_ini": {
            "type": "string",
            "description": "Hora inicial local.",
            "pattern": "^[0-2][0-9]:[0-5][0-9]$"
          },
          "hora_fin": {
            "type": "string",
            "description": "Hora final local.",
            "pattern": "^[0-2][0-9]:[0-5][0-9]$"
          },
          "hora": {
            "type": "string",
            "description": "Descripción opcional de horario."
          },
          "num_paquetes": {
            "type": "integer",
            "minimum": 1,
            "description": "Cantidad de paquetes; por defecto usa el número de guías."
          },
          "comentarios": {
            "type": "string",
            "description": "Indicaciones; opcional."
          }
        },
        "required": [
          "fecha",
          "direccion",
          "guias"
        ]
      },
      "Pickup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "ok": {
            "type": "boolean",
            "description": "Solicitud registrada."
          },
          "automatica": {
            "type": "boolean",
            "description": "Indica confirmación automática o gestión posterior."
          },
          "estado": {
            "type": "string",
            "description": "Estado de la solicitud."
          },
          "folio": {
            "type": "string",
            "nullable": true
          },
          "horario_ajustado": {
            "type": "boolean",
            "description": "La ventana disponible ajustó el horario solicitado."
          },
          "fecha": {
            "type": "string",
            "description": "Fecha local solicitada."
          },
          "hora_ini": {
            "type": "string",
            "nullable": true
          },
          "hora_fin": {
            "type": "string",
            "nullable": true
          },
          "direccion": {
            "$ref": "#/components/schemas/Address"
          },
          "guias": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "num_paquetes": {
            "type": "integer"
          },
          "comentarios": {
            "type": "string",
            "nullable": true
          },
          "recolectado": {
            "type": "boolean",
            "description": "Las guías asociadas ya registran recolección o un avance posterior."
          },
          "recolectado_at": {
            "type": "string",
            "nullable": true
          },
          "paqueteria": {
            "type": "string",
            "nullable": true
          },
          "error_mensaje": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "Movement": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "tipo": {
            "type": "string",
            "description": "Tipo de movimiento contable."
          },
          "monto": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
              }
            ],
            "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
          },
          "referencia": {
            "type": "string",
            "description": "Referencia visible al cliente. Puede incluir el número de guía."
          },
          "notas": {
            "type": "string",
            "nullable": true
          },
          "saldo_anterior": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
              }
            ],
            "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
          },
          "saldo_nuevo": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?[0-9]+(\\.[0-9]+)?$"
              }
            ],
            "description": "Importe en pesos mexicanos. Puede llegar como número o cadena decimal; normaliza el tipo en tu integración."
          },
          "origen": {
            "type": "string",
            "description": "Origen contable del movimiento."
          },
          "created_at": {
            "type": "string",
            "description": "Fecha del movimiento.",
            "format": "date-time"
          }
        }
      }
    }
  }
}
