# Cotización y pedido en un solo flujo

Esta guía recorre la API de Uniorder (`/api/v1/uniorder/...`) petición por petición, en el orden en que se construye una integración: autenticarse, cotizar, crear con el `rate_id` elegido, imprimir la etiqueta, consultar, rastrear y cancelar el pedido, y procesar envíos por lotes. Una cotización enumera todas las formas en que la cuenta puede enviar un paquete: la entrega por la propia empresa y, si se solicita, todos los servicios de etiqueta de los transportistas. Hacer el pedido con un `rate_id` crea el pedido para ese servicio: un pedido de entrega, o un pedido de etiqueta con la etiqueta comprada en el servicio de transportista cotizado. Está dirigida a desarrolladores de tiendas en línea, sistemas de gestión de pedidos y ERP que envían a través de una cuenta de empresa.

## 1. Qué puede construir

Los ejemplos siguientes se refieren a una sola empresa: **Fleurs du Plateau**, una floristería situada en 4500 Rue Saint-Denis, Montreal (H2J 2L3), que vende ramos en línea. Un paquete típico es una caja de 1,2 kg de 40 × 25 × 25 cm, destinada a Jane Recipient en 6841 Rue Saint-Denis, Montreal (H2S 2S3), con el pedido web `WEB-10045`.

- **Un proceso de pago que ofrece todas las opciones de envío.** La tienda cotiza el paquete una sola vez y muestra la entrega local en el mismo día junto a todos los servicios de etiqueta de transportista de la cuenta, cada uno con su precio, y después crea el pedido con la opción que eligió el cliente.
- **Impresión automática de etiquetas.** Cuando se crea el pedido, la tienda descarga el PDF de la etiqueta y lo envía a la impresora del puesto de embalaje, tanto si el paquete lo entrega la empresa como si lo entrega un transportista.
- **Una página de pedido con seguimiento en tiempo real.** La página del pedido del cliente muestra el estado y el historial de eventos del envío, con la prueba de entrega una vez entregado el ramo.
- **Un lote nocturno desde el ERP.** Los pedidos mayoristas del día se cotizan y se crean en un único trabajo en cola de hasta 500 filas, y cada resultado se asocia a su línea de pedido mediante `reference`.

## 2. Qué cubre esta guía

Esta es la guía paso a paso de la API de Uniorder. La descripción general de lo que ofrece Uniorder, y por qué, se encuentra en **Uniorder: una API para cada envío**; esta guía proporciona las peticiones, las respuestas y las comprobaciones de cada llamada.

Uniorder es el punto de entrada único recomendado para las nuevas integraciones que envían paquetes mediante entrega local o mediante etiqueta de transportista: sustituye las llamadas separadas a la API de entrega local y a la API de etiquetas de transportista por una sola estructura de petición. Los endpoints anteriores descritos en **Recogida y entrega (flota propia)** y **Etiquetas de transportista** siguen disponibles y sin cambios. Uniorder no se aplica a los servicios de envío reservados por una cuenta de cliente final ni a los pedidos de almacenaje y salida; para ellos, utilice **Servicios de envío** y **Almacenaje y salida**.

## 3. Antes de empezar

- **Cuenta.** Utilice una cuenta de empresa (cliente), o una cuenta de empleado de la empresa, con permiso de API. Una cuenta de cliente final de la empresa también puede llamar a Uniorder y siempre se cotiza y se factura a su propio nombre. Crear un pedido de entrega requiere el permiso para realizar pedidos.
- **Clientes finales.** Una cuenta de cliente o de empleado puede cotizar y hacer pedidos para uno de sus clientes finales con `customer_id` o `customer_code` en la cotización; el `rate_id` incluye entonces a ese cliente final, y el precio sigue el plan de ese cliente final.
- **Servicios de etiqueta.** Para recibir tarifas `label_service`, la cuenta (o el cliente final indicado) necesita al menos una cuenta de transportista de etiquetas configurada.
- **Datos de prueba.** Utilice una dirección dentro de la zona de entrega de la empresa para las tarifas `self_delivery`, y referencias de prueba como `WEB-10045` que puedan cancelarse después.
- **Tokens.** Solicite el token de acceso desde su servidor y consérvelo allí. No lo envíe nunca a un navegador ni a una aplicación móvil.
- **Marcadores de posición.** Sustituya `YOUR_HOST` por el host de la API de su entorno y `ACCESS_TOKEN` por el token obtenido en el paso 4.

## 4. Autenticarse

Cada llamada a Uniorder se realiza en nombre de una cuenta. Inicie sesión una vez desde su servidor, guarde el token devuelto y envíelo en cada petición.

**REST:** `POST /api/v1/user/login` — [Manual REST](/api/documentation#/paths/v1-user-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`: colóquelo en la cabecera de cada petición posterior:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL usa la misma cabecera en `POST /api/graphql`.

**GraphQL:** `userLogin` ([Manual GraphQL](/api/graphql/documentation#/user/userLogin))

**Verificación:** el login devuelve `access_token`. Las peticiones posteriores sin este token devuelven `401`.

## 5. Cotizar todos los servicios

La cotización enumera todas las formas en que se puede enviar el paquete, con un precio y un `rate_id` para cada una. El proceso de pago muestra las tarifas como opciones; no se crea ni se reserva nada.

**REST:** `POST /api/v1/uniorder/rate` — [Manual REST](/api/documentation#/paths/v1-uniorder-rate/post)

El remitente y el destinatario son direcciones completas; solo `from_address_2` y `to_address_2` son opcionales. Cada paquete necesita `weight`, `length`, `width` y `height`. Establezca `quote_labels` en `true` para añadir los servicios de etiqueta de los transportistas; en ese caso, los nombres y los teléfonos de ambos extremos son obligatorios. Una cuenta de cliente o de empleado puede cotizar para uno de sus clientes finales con `customer_id` o `customer_code`. Una franja de entrega (`time_window_start`, `time_window_end`, formato `YYYY-MM-DD HH:MM:SS`) se tiene en cuenta cuando el precio depende de ella.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: entrega por la empresa. Como máximo una por cotización.
- `type` `label_service`: una por cada servicio de cada cuenta de etiquetas. Muestre al cliente `service_name`, `shipping_price` y `transit_days`.
- `errors` enumera lo que no se pudo cotizar, con su `type`. Una dirección fuera de la zona de entrega es un error de tipo `self_delivery` con el código `OUT_OF_DELIVERY_AREA`; muestre solo los servicios de etiqueta.
- `rate_id` es válido durante 30 minutos y solo para la cuenta que solicitó la cotización. Consérvelo con la sesión de pago.
- `result` es `true` cuando se ha encontrado al menos una tarifa.

**GraphQL:** `uniorderRate` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderRate)). La respuesta es un escalar JSON, por lo que la operación no tiene selection set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Variables:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verificación:** `rates` contiene una tarifa `self_delivery` para una dirección dentro de la zona y, con `quote_labels`, una tarifa `label_service` por cada servicio de transportista. No se crea nada.

## 6. Crear el pedido con la tarifa elegida

Cuando el cliente paga, la tienda crea el pedido con el `rate_id` de la opción elegida y el mismo envío. El `rate_id` determina el servicio; ningún otro dato de la petición lo selecciona.

**REST:** `POST /api/v1/uniorder` — [Manual REST](/api/documentation#/paths/v1-uniorder/post)

Envíe una cabecera `Idempotency-Key`, única por pedido, en cada creación. Un reintento con la misma clave y el mismo cuerpo devuelve la primera respuesta con `replayed` `true` y no crea un segundo pedido.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

Una tarifa `self_delivery` crea un pedido de entrega. Para `type` `D` el destinatario es la parada; establezca `need_pick_up` en `1` para que el paquete se recoja en el remitente. Para `type` `P` el remitente es la parada.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

Una tarifa `label_service` crea un pedido de etiqueta y compra la etiqueta en el servicio de transportista cotizado. `type` debe ser `D`, y `from_name`, `from_telephone`, `to_name` y `to_telephone` son obligatorios. Si el cliente hubiera elegido UPS STANDARD, la respuesta sería:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: guárdelo con el pedido web; todas las llamadas posteriores lo utilizan.
- `tracking_numbers`: los números de seguimiento propios del envío, uno por paquete.
- `shipping_price`: el precio cobrado. El pedido se tarifica al crearse; `quoted_price` es el precio de la cotización. Ambos pueden diferir.
- `label.main_tracking_number` y `label.shipping_label` (solo pedido de etiqueta): el número de seguimiento del transportista y el PDF de la etiqueta en base64.
- `result` `false` con el código `LABEL_PURCHASE_FAILED` (solo pedido de etiqueta): el pedido existe pero no tiene etiqueta. Conserve el `id` y continúe con el paso 11.

**GraphQL:** `uniorderCreate` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderCreate))

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Variables:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verificación:** `result` es `true` e `id` está definido. Un `rate_id` caducado o que pertenece a otra cuenta devuelve `400` con el código `RATE_ID_INVALID`, y no se crea nada.

## 7. Imprimir la etiqueta

El puesto de embalaje imprime la etiqueta en cuanto existe el pedido. La misma llamada devuelve la etiqueta propia de la empresa para un pedido de entrega y la etiqueta del transportista comprada para un pedido de etiqueta.

**REST:** `GET /api/v1/uniorder/{orderId}/label` — [Manual REST](/api/documentation#/paths/v1-uniorder-orderId--label/get)

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: el PDF de la etiqueta en base64. Descodifíquelo y envíe el archivo a la impresora.
- `hide_sender_address`, `hide_receiver_address` (`1` para ocultar): se aplican a la etiqueta propia de la empresa de un pedido de entrega.
- `label_status` (pedido de etiqueta): `ready` cuando se devuelve el archivo. Cuando el transportista todavía no ha generado el archivo, la respuesta es `200` con `result` `false` y `label_status` `pending`; solicite la etiqueta de nuevo más tarde.
- Esta llamada nunca compra una etiqueta: una etiqueta que no se ha comprado devuelve `409` con el código `LABEL_PURCHASE_FAILED`. Cómprela con el paso 11.

**GraphQL:** `uniorderLabel` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderLabel))

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Verificación:** `result` es `true` y el `pdf_data` descodificado se abre como un PDF que muestra el número de seguimiento del pedido.

## 8. Consultar el pedido

La tienda consulta el pedido para mostrar su estado, sus direcciones y sus paquetes en la página del pedido o en una pantalla de atención al cliente.

**REST:** `GET /api/v1/uniorder/{orderId}` — [Manual REST](/api/documentation#/paths/v1-uniorder-orderId/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` o `label_service`; los demás campos tienen la misma estructura en ambos casos.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` o `cancelled` para un pedido de entrega, y `label_pending`, `label_purchased` o `cancelled` para un pedido de etiqueta.
- `label` (solo pedido de etiqueta): el transportista, el servicio, `carrier_tracking_numbers` y `label_status` (`not_purchased`, `pending`, `ready` o `failed`).

**GraphQL:** `uniorder` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorder))

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Verificación:** el pedido devuelve su `status` y sus `packages`, y `ref` coincide con el pedido web.

## 9. Rastrear el pedido

La página del pedido muestra el historial del envío. Consúltelo cuando el cliente abra la página, o manténgalo actualizado mediante webhooks.

**REST:** `GET /api/v1/uniorder/{orderId}/tracking` — [Manual REST](/api/documentation#/paths/v1-uniorder-orderId--tracking/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: el historial, del más reciente al más antiguo, cada uno con `code`, `description`, `location` y hora.
- `proofs`: archivos de prueba de entrega. Muéstrelos cuando `status` sea `delivered`.
- `carrier` (solo pedido de etiqueta): el nombre del transportista, el número de seguimiento y el enlace de seguimiento (`tracking_url`).

**GraphQL:** `uniorderTracking` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderTracking))

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Verificación:** la llamada de seguimiento devuelve `result` `true`, el `status` del pedido y sus `events`.

## 10. Cancelar el pedido

Cuando el cliente cancela el pedido web, la tienda cancela el envío con la misma llamada para un pedido de entrega y para un pedido de etiqueta. Una etiqueta se anula primero con su transportista.

**REST:** `POST /api/v1/uniorder/{orderId}/cancel` — [Manual REST](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true` cuando el pedido se canceló antes de esta llamada. Trátelo como un resultado correcto.
- Cuando el pedido no se cancela, la respuesta es `409` y el pedido no cambia: `ORDER_STATUS_NOT_CANCELLABLE` (demasiado tarde para cancelar), `ORDER_CANCEL_REFUSED` (no se puede cancelar ahora) o `LABEL_CANCEL_FAILED` (el transportista no anuló la etiqueta). Mantenga abierto el pedido web y gestione el envío manualmente.

**GraphQL:** `uniorderCancel` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderCancel))

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Verificación:** `result` es `true`. Cancelar de nuevo el mismo pedido devuelve `already_cancelled` `true`.

## 11. Comprar una etiqueta más tarde (solo después de LABEL_PURCHASE_FAILED)

Este paso se aplica solo a un pedido de etiqueta cuya creación respondió `LABEL_PURCHASE_FAILED`. La respuesta fue `200` con `result` `false`, el código `LABEL_PURCHASE_FAILED` y el `id` del pedido: el pedido se conserva sin etiqueta. No envíe el pedido de nuevo; compre la etiqueta para ese pedido.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [Manual REST](/api/documentation#/paths/v1-uniorder-orderId--label/post)

La creación que no pudo comprar la etiqueta respondió:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Compre la etiqueta para el pedido `123458`:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

La etiqueta se compra en el servicio elegido al crear el pedido. Para comprarla en otro servicio de la misma cuenta, envíe en el cuerpo un nuevo `rate_id` `label_service` del paso 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Una etiqueta ya comprada se devuelve y no se vuelve a comprar.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: el PDF de la etiqueta en base64; imprímalo como en el paso 7.
- `result` `false` de nuevo con `LABEL_PURCHASE_FAILED`: el transportista sigue rechazando la compra. Vuelva a intentarlo más tarde o compre en otro servicio con un nuevo `rate_id`.

**GraphQL:** `uniorderPurchaseLabel` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderPurchaseLabel))

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Verificación:** `result` es `true` y `label.shipping_label` contiene el PDF, o `label.label_status` es `pending` mientras el transportista genera el archivo.

## 12. Lotes

Los lotes cotizan o crean muchos envíos en una sola llamada, por ejemplo los pedidos mayoristas del ERP. Cada fila pasa por la llamada individual y devuelve lo que devolvería esa llamada; una fila que falla no detiene las demás filas.

**REST:** `POST /api/v1/uniorder/rate/batch` — [Manual REST](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [Manual REST](/api/documentation#/paths/v1-uniorder-batch/post)

Hasta 20 filas por llamada, respondidas en la misma respuesta: `shipments` para el lote de cotización, `orders` para el lote de creación. Cada fila tiene los mismos campos que la llamada individual, más un `reference` opcional que se devuelve con su resultado.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: uno por fila, con el `index` de la fila, su `reference`, y el `status` y el `body` que devolvería la llamada individual. Asocie cada resultado a su línea de pedido mediante `reference`.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [Manual REST](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [Manual REST](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [Manual REST](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Hasta 500 filas, puestas en cola como un único trabajo. La llamada devuelve un `job_id`; consulte el trabajo hasta que `status` sea `done` y luego lea `results`. El mismo lote enviado de nuevo mientras el primero sigue en cola devuelve el primer trabajo con `duplicate` `true`.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done`, o `failed` con un `message` cuando no se pudo procesar el trabajo.
- Un trabajo se ejecuta una vez y no se reintenta. Un `rate_id` que caduca antes de que se procese su fila devuelve `RATE_ID_INVALID` para esa fila; envíe el trabajo de creación poco después de que termine el trabajo de cotización.

**GraphQL:** `uniorderRateBatch` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Verificación:** un lote devuelve un resultado por fila; un trabajo asíncrono llega a `status` `done`.

## 13. Gestión de errores

| Situación | Estado HTTP | Código | Qué hace la integración |
|---|---|---|---|
| Falta un campo obligatorio o tiene un formato incorrecto | 400 | `VALIDATION_FAILED` | Corrija el campo indicado en `message` y envíe la petición de nuevo. |
| El destinatario está fuera de la zona de entrega (cotización) | 200 | `OUT_OF_DELIVERY_AREA` en `errors` | Ofrezca solo las tarifas `label_service`. |
| El `rate_id` ha caducado, tiene un formato incorrecto o pertenece a otra cuenta | 400 | `RATE_ID_INVALID` | Solicite una nueva cotización y cree el pedido con su `rate_id`. No se creó nada. |
| El pedido de etiqueta se creó pero su etiqueta no se compró | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Conserve el `id`; compre la etiqueta con `POST /api/v1/uniorder/{orderId}/label`. No cree nunca el pedido de nuevo. |
| Se solicita la etiqueta antes de que se haya comprado | 409 | `LABEL_PURCHASE_FAILED` | Compre la etiqueta con `POST /api/v1/uniorder/{orderId}/label`. |
| El pedido está demasiado avanzado para cancelarse | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Deje el pedido como está; gestione la devolución por separado. |
| El pedido no se puede cancelar ahora | 409 | `ORDER_CANCEL_REFUSED` | Deje el pedido como está; vuelva a intentarlo más tarde o contacte con la empresa. |
| El transportista no anuló la etiqueta | 409 | `LABEL_CANCEL_FAILED` | El pedido no cambia; vuelva a intentar la cancelación más tarde. |
| El pedido o el trabajo no existe o pertenece a otra cuenta | 404 | `ORDER_NOT_FOUND` | Compruebe el `id` guardado con el pedido web. |
| Se reutiliza un `Idempotency-Key` con un cuerpo diferente | 409 | `IDEMPOTENCY_CONFLICT` | Utilice una clave nueva para una petición diferente. |
| El token falta o ha caducado, o la cuenta no puede realizar pedidos | 401 | — | Inicie sesión de nuevo; compruebe los permisos de la cuenta. |

## Lista de pruebas

Use un `ref` de prueba como `WEB-10045`:

- [ ] La cotización devuelve una tarifa `self_delivery` para una dirección dentro de la zona.
- [ ] Con `quote_labels`, la cotización devuelve tarifas `label_service`, cada una con un `rate_id`.
- [ ] Hacer el pedido con un `rate_id` `self_delivery` devuelve `id` y `tracking_numbers`.
- [ ] Hacer el pedido con un `rate_id` `label_service` devuelve la etiqueta del servicio cotizado.
- [ ] El mismo `Idempotency-Key` no crea un segundo pedido.
- [ ] Un `rate_id` de más de 30 minutos devuelve `RATE_ID_INVALID`.
- [ ] La etiqueta de cada pedido se descodifica como un PDF imprimible.
- [ ] El pedido, su etiqueta y su seguimiento se pueden consultar con el `id` de la creación.
- [ ] Cancelar un pedido de prueba devuelve `result: true`; cancelarlo de nuevo devuelve `already_cancelled: true`.
- [ ] Después de `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` compra la etiqueta para el mismo pedido.
- [ ] Un lote de dos filas devuelve dos resultados con su `reference`.
