# Cotação e pedido num só fluxo

Este guia percorre a API Uniorder (`/api/v1/uniorder/...`) pedido HTTP a pedido HTTP, pela ordem em que uma integração é construída: autenticar, cotar, criar com o `rate_id` escolhido, imprimir a etiqueta, consultar, rastrear e cancelar o pedido, e processar envios em lote. Uma cotação lista todas as formas pelas quais a conta pode enviar um volume: a entrega feita pela própria empresa e, a pedido, todos os serviços de etiqueta das transportadoras. Criar o pedido com um `rate_id` cria o pedido para esse serviço: um pedido de entrega, ou um pedido de etiqueta com a etiqueta comprada no serviço da transportadora cotado. Destina-se a programadores de lojas online, sistemas de gestão de pedidos e ERP que expedem através de uma conta de empresa.

## 1. O que pode construir

Os exemplos seguintes acompanham uma única empresa: **Fleurs du Plateau**, uma florista em 4500 Rue Saint-Denis, Montreal (H2J 2L3), que vende ramos de flores online. Um volume típico é uma caixa de 1,2 kg com 40 × 25 × 25 cm, destinada a Jane Recipient em 6841 Rue Saint-Denis, Montreal (H2S 2S3), com o pedido web `WEB-10045`.

- **Um checkout que apresenta todas as opções de envio.** A loja cota o volume uma vez e apresenta a entrega local no próprio dia ao lado de todos os serviços de etiqueta de transportadora da conta, cada um com o seu preço, e depois cria o pedido com a opção que o cliente escolheu.
- **Impressão automática de etiquetas.** Quando o pedido é criado, a loja descarrega o PDF da etiqueta e envia-o para a impressora do posto de embalagem, quer o volume seja entregue pela empresa quer por uma transportadora.
- **Uma página de pedido com rastreio em tempo real.** A página do pedido do cliente mostra o estado e a linha temporal de eventos do envio, com o comprovativo de entrega depois de o ramo ser entregue.
- **Um lote noturno a partir do ERP.** Os pedidos por grosso do dia são cotados e criados num único job em fila de até 500 linhas, e cada resultado é associado à sua linha de pedido através de `reference`.

## 2. O que este guia abrange

Este é o guia passo a passo da API Uniorder. A visão geral do que o Uniorder oferece, e porquê, encontra-se em **Uniorder: uma API para cada envio**; este guia apresenta os pedidos HTTP, as respostas e as verificações de cada chamada.

O Uniorder é o ponto de entrada único recomendado para novas integrações que expedem volumes por entrega local ou por etiqueta de transportadora: substitui as chamadas separadas à API de entrega local e à API de etiquetas de transportadora por uma única forma de pedido. Os endpoints anteriores descritos em **Recolha e entrega (frota própria)** e **Etiquetas de transportadora** continuam disponíveis e inalterados. O Uniorder não se aplica aos serviços de envio reservados por uma conta de cliente nem aos pedidos de armazenamento e saída; para esses, use **Serviços de envio** e **Armazenamento e saída**.

## 3. Antes de começar

- **Conta.** Use uma conta de empresa (cliente), ou uma conta de funcionário da empresa, com permissão de API. Uma conta de cliente da empresa também pode chamar o Uniorder e é sempre cotada e faturada em seu próprio nome. A criação de um pedido de entrega exige a permissão de fazer pedidos.
- **Clientes.** Uma conta de empresa ou de funcionário pode cotar e fazer pedidos para um dos seus clientes com `customer_id` ou `customer_code` na cotação; o `rate_id` passa então a identificar esse cliente, e o preço segue o plano do cliente.
- **Serviços de etiquetas.** Para receber tarifas `label_service`, a conta (ou o cliente indicado) precisa de pelo menos uma conta de transportadora de etiquetas configurada.
- **Dados de teste.** Use um endereço dentro da área de entrega da empresa para as tarifas `self_delivery`, e referências de teste como `WEB-10045` que possam ser canceladas depois.
- **Tokens.** Peça o token de acesso a partir do seu servidor e mantenha-o aí. Nunca o envie para um navegador ou uma aplicação móvel.
- **Marcadores.** Substitua `YOUR_HOST` pelo anfitrião da API do seu ambiente e `ACCESS_TOKEN` pelo token do passo 4.

## 4. Autenticação

Todas as chamadas do Uniorder são feitas em nome de uma conta. Inicie sessão uma vez a partir do seu servidor, guarde o token devolvido e envie-o em cada pedido HTTP.

**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`: coloque-o no cabeçalho de cada pedido HTTP posterior:

```
Authorization: Bearer ACCESS_TOKEN
```

O GraphQL usa o mesmo cabeçalho em `POST /api/graphql`.

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

**Verificação:** o login devolve `access_token`. Os pedidos posteriores sem este token devolvem `401`.

## 5. Cotar todos os serviços

A cotação lista todas as formas pelas quais o volume pode ser expedido, com um preço e um `rate_id` para cada uma. O checkout apresenta as tarifas como opções; nada é criado nem reservado.

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

O remetente e o destinatário são endereços completos; apenas `from_address_2` e `to_address_2` são opcionais. Cada volume precisa de `weight`, `length`, `width` e `height`. Defina `quote_labels` como `true` para acrescentar os serviços de etiqueta das transportadoras; os nomes e os telefones das duas pontas passam então a ser obrigatórios. Uma conta de empresa ou de funcionário pode cotar para um dos seus clientes com `customer_id` ou `customer_code`. Uma janela de entrega (`time_window_start`, `time_window_end`, formato `YYYY-MM-DD HH:MM:SS`) é tida em conta quando o preço depende dela.

```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 feita pela empresa. No máximo uma por cotação.
- `type` `label_service`: uma por serviço de cada conta de etiquetas. Apresente `service_name`, `shipping_price` e `transit_days` ao cliente.
- `errors` lista o que não pôde ser cotado, com o respetivo `type`. Um endereço fora da área de entrega é um erro do tipo `self_delivery` com o código `OUT_OF_DELIVERY_AREA`; apresente apenas os serviços de etiqueta.
- `rate_id` é válido durante 30 minutos e apenas para a conta que pediu a cotação. Guarde-o com a sessão de checkout.
- `result` é `true` quando foi encontrada pelo menos uma tarifa.

**GraphQL:** `uniorderRate` ([Manual GraphQL](/api/graphql/documentation#/orders/uniorderRate)). A resposta é um escalar JSON, pelo que a operação não tem 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
  )
}
```

Variáveis:

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

**Verificação:** `rates` contém uma tarifa `self_delivery` para um endereço na zona e, com `quote_labels`, uma tarifa `label_service` por serviço de transportadora. Nada é criado.

## 6. Criar o pedido com a tarifa escolhida

Quando o cliente paga, a loja cria o pedido com o `rate_id` da opção escolhida e o mesmo envio. O `rate_id` determina o serviço; nenhum outro elemento do pedido HTTP o seleciona.

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

Envie um cabeçalho `Idempotency-Key`, único por pedido, em cada criação. Uma repetição com a mesma chave e o mesmo corpo devolve a primeira resposta com `replayed` `true` e não cria um 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
    }]
  }'
```

Uma tarifa `self_delivery` cria um pedido de entrega. Com `type` `D`, o destinatário é a paragem; defina `need_pick_up` como `1` para que o volume seja recolhido no remetente. Com `type` `P`, o remetente é a paragem.

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

Uma tarifa `label_service` cria um pedido de etiqueta e compra a etiqueta no serviço da transportadora cotado. `type` tem de ser `D`, e `from_name`, `from_telephone`, `to_name` e `to_telephone` são obrigatórios. Se o cliente tivesse escolhido UPS STANDARD, a resposta seria:

```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`: guarde-o com o pedido web; todas as chamadas posteriores o utilizam.
- `tracking_numbers`: os números de rastreio próprios do envio, um por volume.
- `shipping_price`: o preço cobrado. O preço do pedido é calculado quando é criado; `quoted_price` é o preço da cotação. Os dois podem ser diferentes.
- `label.main_tracking_number` e `label.shipping_label` (apenas pedido de etiqueta): o número de rastreio da transportadora e o PDF da etiqueta em base64.
- `result` `false` com o código `LABEL_PURCHASE_FAILED` (apenas pedido de etiqueta): o pedido existe, mas não tem etiqueta. Guarde o `id` e continue no passo 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
  )
}
```

Variáveis:

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

**Verificação:** `result` é `true` e `id` está preenchido. Um `rate_id` expirado ou que pertence a outra conta devolve `400` com o código `RATE_ID_INVALID`, e nada é criado.

## 7. Imprimir a etiqueta

O posto de embalagem imprime a etiqueta assim que o pedido existe. A mesma chamada devolve a etiqueta própria da empresa para um pedido de entrega e a etiqueta de transportadora comprada para um 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`: o PDF da etiqueta em base64. Descodifique-o e envie o ficheiro para a impressora.
- `hide_sender_address`, `hide_receiver_address` (`1` para ocultar): aplicam-se à etiqueta própria da empresa de um pedido de entrega.
- `label_status` (pedido de etiqueta): `ready` quando o ficheiro é devolvido. Quando a transportadora ainda não gerou o ficheiro, a resposta é `200` com `result` `false` e `label_status` `pending`; peça a etiqueta outra vez mais tarde.
- Esta chamada nunca compra uma etiqueta: uma etiqueta que não foi comprada devolve `409` com o código `LABEL_PURCHASE_FAILED`. Compre-a com o passo 11.

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

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

**Verificação:** `result` é `true` e o `pdf_data` descodificado abre como um PDF que mostra o número de rastreio do pedido.

## 8. Consultar o pedido

A loja consulta o pedido para mostrar o seu estado, endereços e volumes na página do pedido ou num ecrã de apoio ao 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` ou `label_service`; os restantes campos seguem a mesma forma em ambos.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` ou `cancelled` para um pedido de entrega, e `label_pending`, `label_purchased` ou `cancelled` para um pedido de etiqueta.
- `label` (apenas pedido de etiqueta): a transportadora, o serviço, `carrier_tracking_numbers` e `label_status` (`not_purchased`, `pending`, `ready` ou `failed`).

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

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

**Verificação:** o pedido devolve o seu `status` e `packages`, e `ref` coincide com o pedido web.

## 9. Rastrear o pedido

A página do pedido mostra a linha temporal do envio. Leia-a quando o cliente abre a página, ou mantenha-a atualizada a partir dos 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`: a linha temporal, do mais recente para o mais antigo, cada evento com `code`, `description`, `location` e hora.
- `proofs`: ficheiros de comprovativo de entrega. Apresente-os quando `status` for `delivered`.
- `carrier` (apenas pedido de etiqueta): o nome da transportadora, o número de rastreio e a ligação de rastreio (`tracking_url`).

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

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

**Verificação:** a chamada de rastreio devolve `result` `true`, o `status` do pedido e os seus `events`.

## 10. Cancelar o pedido

Quando o cliente cancela o pedido web, a loja cancela o envio com a mesma chamada para um pedido de entrega e para um pedido de etiqueta. Uma etiqueta é primeiro anulada junto da sua transportadora.

**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` quando o pedido já tinha sido cancelado antes desta chamada. Trate-o como êxito.
- Quando o pedido não é cancelado, a resposta é `409` e o pedido fica inalterado: `ORDER_STATUS_NOT_CANCELLABLE` (demasiado tarde para cancelar), `ORDER_CANCEL_REFUSED` (não pode ser cancelado agora) ou `LABEL_CANCEL_FAILED` (a transportadora não anulou a etiqueta). Mantenha o pedido web aberto e trate o envio manualmente.

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

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

**Verificação:** `result` é `true`. Cancelar novamente o mesmo pedido devolve `already_cancelled` `true`.

## 11. Comprar uma etiqueta mais tarde (só depois de LABEL_PURCHASE_FAILED)

Este passo aplica-se apenas a um pedido de etiqueta cuja criação respondeu `LABEL_PURCHASE_FAILED`. A resposta foi `200` com `result` `false`, o código `LABEL_PURCHASE_FAILED` e o `id` do pedido: o pedido é conservado sem etiqueta. Não envie o pedido outra vez; compre a etiqueta para esse pedido.

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

A criação que não conseguiu comprar a etiqueta respondeu:

```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 a etiqueta para o 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 '{}'
```

A etiqueta é comprada no serviço escolhido quando o pedido foi criado. Para comprar noutro serviço da mesma conta, envie no corpo um novo `rate_id` `label_service` do passo 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Uma etiqueta já comprada é devolvida e não é comprada novamente.

```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`: o PDF da etiqueta em base64; imprima-o como no passo 7.
- `result` `false` novamente com `LABEL_PURCHASE_FAILED`: a transportadora continua a recusar. Tente mais tarde ou compre noutro serviço com um novo `rate_id`.

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

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

**Verificação:** `result` é `true` e `label.shipping_label` contém o PDF, ou `label.label_status` é `pending` enquanto a transportadora gera o ficheiro.

## 12. Lotes

Os lotes cotam ou criam muitos envios numa única chamada, por exemplo os pedidos por grosso do ERP. Cada linha passa pela chamada individual e devolve o que essa chamada devolveria; uma linha que falha não interrompe as outras linhas.

**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)

Até 20 linhas por chamada, respondidas na mesma resposta: `shipments` para o lote de cotação, `orders` para o lote de criação. Cada linha tem os mesmos campos da chamada individual, mais um `reference` opcional que é devolvido com o seu 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`: um por linha, com o `index` da linha, o seu `reference`, e o `status` e o `body` que a chamada individual devolveria. Associe cada resultado à sua linha de pedido através de `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)

Até 500 linhas, colocadas em fila como um único job. A chamada devolve um `job_id`; leia o job até `status` ser `done` e depois leia `results`. O mesmo lote enviado outra vez enquanto o primeiro ainda está em fila devolve o primeiro job com `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`, ou `failed` com uma `message` quando o job não pôde ser processado.
- Um job é executado uma vez e não é repetido. Um `rate_id` que expira antes de a sua linha ser processada devolve `RATE_ID_INVALID` para essa linha; envie o job de criação pouco depois de o job de cotação terminar.

**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)
}
```

**Verificação:** um lote devolve um resultado por linha; um job assíncrono chega a `status` `done`.

## 13. Tratamento de erros

| Situação | Estado HTTP | Código | O que a integração faz |
|---|---|---|---|
| Um campo obrigatório está em falta ou mal formado | 400 | `VALIDATION_FAILED` | Corrija o campo indicado em `message` e envie o pedido HTTP outra vez. |
| O destinatário está fora da área de entrega (cotação) | 200 | `OUT_OF_DELIVERY_AREA` em `errors` | Ofereça apenas as tarifas `label_service`. |
| O `rate_id` expirou, está mal formado ou pertence a outra conta | 400 | `RATE_ID_INVALID` | Peça uma nova cotação e crie com o respetivo `rate_id`. Nada foi criado. |
| O pedido de etiqueta foi criado, mas a sua etiqueta não foi comprada | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Guarde o `id`; compre a etiqueta com `POST /api/v1/uniorder/{orderId}/label`. Nunca crie o pedido outra vez. |
| A etiqueta é pedida antes de ter sido comprada | 409 | `LABEL_PURCHASE_FAILED` | Compre a etiqueta com `POST /api/v1/uniorder/{orderId}/label`. |
| O pedido está demasiado avançado para ser cancelado | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Deixe o pedido como está; trate a devolução separadamente. |
| O pedido não pode ser cancelado agora | 409 | `ORDER_CANCEL_REFUSED` | Deixe o pedido como está; tente mais tarde ou contacte a empresa. |
| A transportadora não anulou a etiqueta | 409 | `LABEL_CANCEL_FAILED` | O pedido fica inalterado; tente o cancelamento outra vez mais tarde. |
| O pedido ou o job não existe ou pertence a outra conta | 404 | `ORDER_NOT_FOUND` | Verifique o `id` guardado com o pedido web. |
| Uma `Idempotency-Key` é reutilizada com um corpo diferente | 409 | `IDEMPOTENCY_CONFLICT` | Use uma chave nova para um pedido HTTP diferente. |
| O token está em falta ou expirou, ou a conta não pode fazer pedidos | 401 | — | Inicie sessão outra vez; verifique as permissões da conta. |

## Lista de testes

Use um `ref` de teste como `WEB-10045`:

- [ ] A cotação devolve uma tarifa `self_delivery` para um endereço na zona.
- [ ] Com `quote_labels`, a cotação devolve tarifas `label_service`, cada uma com um `rate_id`.
- [ ] Criar o pedido com um `rate_id` `self_delivery` devolve `id` e `tracking_numbers`.
- [ ] Criar o pedido com um `rate_id` `label_service` devolve a etiqueta do serviço cotado.
- [ ] A mesma `Idempotency-Key` não cria um segundo pedido.
- [ ] Um `rate_id` com mais de 30 minutos devolve `RATE_ID_INVALID`.
- [ ] A etiqueta de cada pedido é descodificada num PDF imprimível.
- [ ] O pedido, a sua etiqueta e o seu rastreio podem ser consultados com o `id` devolvido na criação.
- [ ] Cancelar um pedido de teste devolve `result: true`; cancelá-lo novamente devolve `already_cancelled: true`.
- [ ] Depois de `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` compra a etiqueta para o mesmo pedido.
- [ ] Um lote de duas linhas devolve dois resultados com o respetivo `reference`.
