# Etiquetas de transportadora

O serviço de etiquetas compra etiquetas de envio às transportadoras ligadas a uma conta (por exemplo UPS e Canada Post) e conserva cada etiqueta como um pedido. Uma integração lista os métodos de envio da conta, cota um volume, cria o pedido de etiqueta, compra a etiqueta no serviço de transportadora escolhido, imprime o PDF, rastreia o volume e cancela as etiquetas que não são usadas. Destina-se a lojas online, sistemas de armazém e sistemas de gestão de pedidos que expedem volumes através de transportadoras e não através dos seus próprios motoristas.

## 1. O que pode construir

Os exemplos deste guia acompanham uma única empresa: **Northbound Outfitters**, uma loja online de equipamento de exterior que expede a partir do seu armazém em 1200 Eglinton Ave E, Toronto. A sua conta tem um método Canada Post e um método UPS. Um pedido típico é uma caixa de tenda de 4,2 kg, com 60 × 30 × 25 cm, com destino a Calgary; os pedidos para os Estados Unidos seguem pela UPS.

- **Escolha da transportadora no checkout.** A loja cota o carrinho do cliente na Canada Post, apresenta os serviços com o preço e os dias de trânsito e expede com o serviço que o cliente pagou.
- **Impressão de etiquetas com um clique no armazém.** O posto de embalagem cria o pedido de etiqueta quando uma caixa é embalada, compra a etiqueta no serviço escolhido e imprime o PDF da transportadora numa impressora térmica.
- **Envios transfronteiriços com dados aduaneiros.** Os pedidos para os Estados Unidos incluem linhas de artigos (descrição, quantidade, valor, código SH) para que a etiqueta UPS seja emitida com os seus dados comerciais.
- **Atualizações automáticas de estado para o cliente.** A loja guarda o número de rastreio da transportadora, apresenta a linha temporal do rastreio público na página do pedido e atualiza o pedido quando um webhook `tracking.event` indica que o volume foi entregue.

## 2. O que este guia abrange

Este guia abrange o serviço de etiquetas v1 (`/api/v1/labelservice/...`): um método de envio (uma conta de transportadora) por chamada. Use-o quando a integração já sabe com que método de envio expede, ou quando mantém uma integração existente com o serviço de etiquetas.

Para novas integrações, o Uniorder (`/api/v1/uniorder/...`) é o ponto de entrada único recomendado. Os guias do Uniorder, «Uniorder: uma API para cada envio» e «Cotação e pedido num só fluxo», cotam de uma só vez todos os serviços de transportadora da conta (juntamente com a entrega feita pela própria empresa, quando se aplica) e compram a etiqueta no serviço escolhido através do envio do respetivo `rate_id`. As mesmas chamadas imprimem, rastreiam e cancelam depois todos os pedidos.

Outros guias abrangem as restantes famílias de envios:

- Entrega pelos motoristas da própria empresa: «Recolha e entrega (frota própria)».
- Uma conta de cliente que expede através dos serviços oferecidos pela sua empresa: «Serviços de envio».
- Mercadorias guardadas num armazém e expedidas a pedido: «Armazenamento e saída».

## 3. Antes de começar

- **Conta.** Use uma conta de empresa (cliente), um funcionário dessa conta ou uma conta de cliente de uma empresa. Uma conta de empresa vê os seus próprios métodos de envio. Uma conta de cliente vê apenas os métodos que a sua empresa lhe atribuiu, e cada etiqueta que compra é debitada no seu saldo; quando a empresa ativou Auto Pause Label Service para esse cliente, uma etiqueta é recusada enquanto o saldo mais o crédito não a cobrirem.
- **Permissão de API.** A conta tem de ter o acesso à API ativado. Sem ele, todas as chamadas do serviço de etiquetas devolvem `401` com `Unauthorized`.
- **Métodos de envio.** Pelo menos um método de envio tem de estar ativo na conta (para clientes: atribuído ao cliente). Os identificadores de método diferem por conta e não devem ser fixados no código; leia-os no passo 5.
- **Dados de teste.** Use um método de envio de teste ou um sandbox da transportadora, quando estiver configurado (as tarifas trazem então `test_mode: true`), e um destino que controla. Cancele todas as etiquetas de teste compradas num método real.
- **Tratamento do token.** Inicie sessão a partir do seu servidor e mantenha aí o token. Não coloque o token nem a palavra-passe num navegador ou numa aplicação móvel.
- **Marcadores.** Substitua `YOUR_HOST` pelo anfitrião da sua plataforma e `ACCESS_TOKEN` pelo token do passo 4. Substitua os valores de `shipping_method` pelos ids da sua conta.

## 4. Autenticação

Todas as chamadas do serviço de etiquetas exigem um token bearer. A integração inicia sessão uma vez, guarda `access_token` e `expires_at` no servidor e inicia sessão outra vez antes de o token expirar.

**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":"shipping@northbound-outfitters.ca","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2027-09-28 10:15:00"
}
```

- `access_token`: envie-o em cada chamada posterior no cabeçalho abaixo.
- `expires_at`: inicie sessão outra vez antes desta hora.

```
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. Listar métodos de envio

A lista de métodos indica à integração com que contas de transportadora pode expedir e que opções cada uma aceita. Guarde o `id` de cada método que utiliza; é o `shipping_method` de todas as chamadas posteriores.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

```json
[
  {
    "id": 59,
    "name": "Canada Post",
    "unique_identifier": "CPC-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    },
    "package_type": {
      "parcel": {
        "name": "Parcel",
        "options": { "weight_options": true, "dimension_options": true }
      }
    },
    "from_contry_limit": ["CA"],
    "isUploadMethod": false
  },
  {
    "id": 61,
    "name": "UPS",
    "unique_identifier": "UPS-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    }
  }
]
```

Cada linha tem:

| Campo | Uso |
|---|---|
| `id` | `shipping_method` em cada chamada posterior |
| `name` | Nome de apresentação |
| `unique_identifier` | Código estável |
| `options.signature_option` | Assinatura disponível |
| `options.insurance_option` | Seguro disponível |
| `options.multi_package` | Mais de uma peça |
| `package_type` | Códigos `package_type` aceites e se cada um exige peso e dimensões |
| `from_contry_limit` | Países em que o endereço do remetente se pode encontrar |
| `services` | Transportadoras e serviços associados ao método; os códigos podem restringir uma cotação com `carriers` / `services` |

Envie `"id": 59` para ler apenas um método, ou `"detail": false` para receber apenas `id`, `name` e `unique_identifier`.

**GraphQL:** `labelserviceGetShippingMethodList` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList)) (escalar JSON).

**Verificação:** a lista não está vazia. Escolheu um `id` e sabe se esse método permite assinatura, seguro e vários volumes. Uma lista vazia significa que nenhum método está ativo na conta.

## 6. Cotar

Uma cotação pede preços à transportadora sem criar nada: o pedido temporário usado para o pedido HTTP é apagado e nada é cobrado. A Northbound Outfitters chama-a no checkout para apresentar os serviços da Canada Post para o carrinho. O corpo tem a mesma forma que no passo 7. `shipping_method` é obrigatório.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "transit_days": 3,
      "test_mode": false
    },
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "transit_days": 2,
      "test_mode": false
    }
  ],
  "best_rate": {
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86,
    "transit_days": 3
  }
}
```

- `rates[]`: uma entrada por serviço de transportadora, com `price`, `currency`, `transit_days` e `price_detail` (tarifa base, sobretaxa de combustível, impostos). Apresente-as ao cliente.
- `best_rate` / `shipping_price`: a primeira tarifa devolvida pelo método.
- Uma cotação não traz `rate_id` nem `id` de pedido. As etiquetas são compradas a partir das tarifas do pedido criado no passo 7.

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

Para mais de uma peça, envie `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (id da agenda de endereços) ou `shipping_from_code` pode substituir o bloco `sender_*`. `carriers` e `services` restringem a cotação aos códigos indicados.

**GraphQL:** `labelserviceRate` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceRate)).

**Verificação:** `result` é true e tem um preço (e dias de trânsito, quando a transportadora os envia). Se não houver tarifa, corrija destino / volume / método **antes** de criar.

## 7. Criar o pedido de etiqueta

Esta chamada cria o pedido de etiqueta e pede à transportadora as tarifas desse envio. Devolve o `id` do pedido e um `rate_id` por serviço. Neste momento, a etiqueta ainda não foi comprada e nada é cobrado; o passo 8 compra-a. A Northbound Outfitters chama-a quando a caixa é embalada e guarda o `id` associado ao seu pedido `NB-10482`.

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

O mesmo corpo do passo 6. Envie `Idempotency-Key`: uma repetição com a mesma chave e o mesmo corpo devolve a primeira resposta em vez de criar um segundo pedido.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-label" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "id": 128455,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
      "carrier_name": "canadapost",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "currency": "CAD",
      "transit_days": 3
    },
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c11",
      "carrier_name": "canadapost",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "currency": "CAD",
      "transit_days": 2
    }
  ],
  "best_rate": {
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86
  }
}
```

| Campo | Uso |
|---|---|
| `id` | Id de pedido Superroute — compra, descarga e cancelamento |
| `rates[].rate_id` | O serviço a comprar no passo 8; válido apenas para este pedido |
| `rates[].price` | Preço desse serviço |
| `shipping_price` | Preço de `best_rate` |

Um envio para os Estados Unidos segue pelo método UPS com as linhas de artigos exigidas pela alfândega. Este exemplo usa a forma `packages`, que inclui os artigos por caixa:

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10497-label" \
  -d '{
    "shipping_method": 61,
    "name": "Daniel Price",
    "telephone": "2065550117",
    "email": "daniel.price@example.com",
    "address_1": "500 Mercer St",
    "city": "Seattle",
    "province": "WA",
    "postcode": "98109",
    "country": "US",
    "package_type": "parcel",
    "paid_by": 1,
    "ref": "NB-10497",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA",
    "packages": [
      {
        "ref": "NB-10497-1",
        "weight": 2.6,
        "length": 45,
        "width": 30,
        "height": 20,
        "weight_unit": 2,
        "dimension_unit": 2,
        "items": [
          {
            "name": "Down sleeping bag",
            "description": "Down-filled sleeping bag, -7 C rating",
            "quantity": 1,
            "unit_price": 289.00,
            "currency": "CAD",
            "weight": 1.6,
            "hscode": "9404400000",
            "sku": "NB-SB-7C",
            "unit": "PCS"
          },
          {
            "name": "Camp stove",
            "description": "Canister camp stove",
            "quantity": 1,
            "unit_price": 79.00,
            "currency": "CAD",
            "weight": 1.0,
            "hscode": "7321111000",
            "sku": "NB-ST-01",
            "unit": "PCS"
          }
        ]
      }
    ]
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)). O corpo REST vai em `input`:

```graphql
mutation {
  labelserviceSubmitOrder(input: {
    shipping_method: 59
    name: "Emily Tremblay"
    telephone: "4035550182"
    address_1: "1415 17 Ave SW"
    city: "Calgary"
    province: "AB"
    postcode: "T2T0C8"
    country: "CA"
    weight: 4.2
    length: 60
    width: 30
    height: 25
    dimension_unit: 2
    weight_unit: 2
    package_type: "parcel"
    ref: "NB-10482"
    sender_name: "Northbound Outfitters"
    sender_telephone: "4165550140"
    sender_address_1: "1200 Eglinton Ave E"
    sender_city: "Toronto"
    sender_province: "ON"
    sender_postcode: "M3C1H9"
    sender_country: "CA"
  })
}
```

**Verificação:** a resposta tem um `id` e pelo menos um `rates[].rate_id`. Guarde ambos. A mesma `Idempotency-Key` com o mesmo corpo devolve o mesmo `id` e não cria um segundo pedido.

## 8. Comprar a etiqueta e consultar o detalhe do envio

Esta chamada compra a etiqueta no serviço escolhido, cobra-a e devolve os números de rastreio da transportadora. Quando a etiqueta já foi comprada, apenas lê o detalhe, pelo que uma chamada repetida nunca compra duas vezes. A Northbound Outfitters envia o `rate_id` do serviço que o cliente pagou.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingDetail \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  }'
```

```json
{
  "id": 128455,
  "shippingPrice": "24.86",
  "mainTrackingNumber": "7023210039414604",
  "trackingNumber": "7023210039414604",
  "needSubmitShippingInformation": false,
  "rate": {
    "carrier_name": "canadapost",
    "price": 24.86,
    "price_detail": [
      { "name": "Base charge", "amount": 18.40 },
      { "name": "Fuel surcharge", "amount": 3.60 },
      { "name": "GST", "amount": 1.10 }
    ],
    "tax_items": ["HST", "GST", "PST", "QST"]
  },
  "labelStatus": "ready",
  "shippingLabel": "JVBERi0xLjQKMS... (base64 encoded)"
}
```

| Campo | Uso |
|---|---|
| `mainTrackingNumber` | Número de rastreio da transportadora do primeiro volume; entregue-o ao cliente |
| `trackingNumber` | Números de rastreio da transportadora de todos os volumes, separados por vírgulas |
| `shippingPrice` | Valor cobrado |
| `labelStatus` | `ready`: `shippingLabel` contém o PDF. `pending`: comprada e cobrada, a transportadora ainda não gerou o ficheiro; chame outra vez mais tarde. `failed`: a obtenção em segundo plano desistiu; uma nova chamada reinicia-a |
| `needSubmitShippingInformation` | `true` quando este método exige o envio das informações de expedição (passo 13) |

`type` pode ser `ORDER_ID` (predefinição), `TRACKING_NUMBER` (o número de volume Superroute) ou `THIRD_PARTY_TRACKING_NUMBER` (o número da transportadora). Envie `rate_id` para que a etiqueta seja comprada no serviço que escolheu; sem ele, o método compra à sua tarifa predefinida.

**GraphQL:** `labelserviceGetShippingDetail` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingDetail))

```graphql
mutation {
  labelserviceGetShippingDetail(
    id: "128455"
    type: "ORDER_ID"
    rate_id: "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  )
}
```

**Verificação:** `mainTrackingNumber` não está vazio e `labelStatus` é `ready` (ou `pending`, passando a `ready` numa chamada posterior). Uma segunda chamada devolve o mesmo número de rastreio e o mesmo `shippingPrice`.

## 9. Descarregar o PDF

O armazém imprime a etiqueta da transportadora a partir desta chamada. Se a etiqueta ainda não tiver sido comprada, a primeira chamada compra-a à tarifa predefinida, como no passo 8; chame primeiro o passo 8 para fixar o serviço.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

```json
"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+..."
```

- Com `base64: 1`, o corpo é o PDF como uma única cadeia base64; descodifique-a e envie-a para a impressora.
- Com `base64: 0`, a resposta é o próprio ficheiro PDF (`application/pdf`).

`type` pode ser `ORDER_ID` (predefinição), `TRACKING_NUMBER` ou `THIRD_PARTY_TRACKING_NUMBER` (o número da transportadora). Esta é a **etiqueta oficial da transportadora**. O número de peças fica fixo pela reserva.

**GraphQL:** `labelserviceGetShippingLabel` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)). O GraphQL devolve sempre a cadeia base64.

**Verificação:** o PDF abre e mostra o código de barras / número de rastreio da transportadora do passo 8. Imprima uma cópia de teste e deite-a fora — não entregue uma etiqueta de teste a uma transportadora.

## 10. Rastrear

A loja apresenta o progresso do volume na página do pedido do cliente. O endpoint de rastreio público não exige token e aceita o número da transportadora do passo 8.

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

```bash
curl https://YOUR_HOST/api/v1/tracking/7023210039414604
```

```json
{
  "result": true,
  "is_third_party_tracking": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 430,
      "otep_status": "in_transit",
      "description": "Item in transit",
      "location_city": "Mississauga",
      "updated_at_localized": "2026-09-29 18:42"
    }
  ]
}
```

**GraphQL:** `trackingPublic` ([Manual GraphQL](/api/graphql/documentation#/tracking/trackingPublic))

```graphql
query {
  trackingPublic(trackingNumber: "7023210039414604") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      otep_status
      description
      updated_at_localized
    }
    third_party_info { tracking_number status carrier_tracking_link }
    proofs { file_id type full_url signed_url }
  }
}
```

- `is_third_party_tracking` é true quando os eventos vêm da transportadora.
- `data`: o evento mais recente primeiro. Ramifique por `tracking_event_status_id` / `otep_status`, não por `description`. Os eventos iniciais podem ainda ser «informação submetida» até a transportadora digitalizar o volume.
- `deliveried` é true e `500` é entregue; `proofs[]` pode então incluir assinatura (`type` `1`) ou foto (`type` `2`).

**Verificação:** a consulta devolve o envio que acabou de criar. Um número desconhecido ou cancelado devolve `404` com `result: false`.

## 11. Configurar notificações de eventos

Os webhooks substituem as consultas periódicas: o servidor da loja recebe cada digitalização da transportadora e atualiza o pedido sem chamar o passo 10 de forma programada.

| Definição | Evento | Quando |
|---|---|---|
| `tracking_event_webhook_url` | `tracking.event` | Digitalizações da transportadora, em entrega, entregue |
| `order_status_change_webhook_url` | `order.status_change` | Estado no seu sistema |
| `order_create_webhook_url` | `order.created` | Foi criado um pedido de etiqueta (passo 7); enviado para pedidos de etiqueta apenas quando `order_created_webhook_all_types` é `1` |

**REST:** `PUT /api/v1/webhook-settings` — [Manual REST](/api/documentation#/paths/v1-webhook-settings/put)

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "tracking_event_webhook_url",
    "order_create_webhook_url",
    "order_created_webhook_all_types",
    "webhook_sign_secret",
    "webhook_verify_ssl"
  ],
  "recipient_type": "business",
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_verify_ssl": 1
  }
}
```

- Apenas as chaves que envia são alteradas; uma chave desconhecida é recusada com `400`.
- `changed_keys` lista o que foi guardado. O segredo é sempre devolvido mascarado.

**GraphQL:** `webhookSettingsUpdate` ([Manual GraphQL](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Cada `tracking.event` traz `order_id`, `tracking_event_status_id`, `tracking_event_key`, `tracking_number` e `external_tracking_number`; associe-o ao seu pedido através de `order_id` (o `id` do passo 7).

Verifique **v2** sobre o corpo em bruto: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contra `X-Webhook-Signature-V2`. Deduplique em `X-Webhook-Event-Id`. Responda **2xx em menos de 3 segundos**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

`order_cancel_failed_webhook_url` (`order.cancel_failed`) não é enviado para os cancelamentos de etiquetas do passo 12; um cancelamento de etiqueta recusado é indicado na resposta dessa chamada.

**Verificação:** um `submitOrder` de teste produz `order.created` com o `id` do pedido, e a primeira digitalização da transportadora produz `tracking.event`. Uma assinatura inválida deve ser recusada pelo recetor com `401`.

## 12. Cancelar

Uma etiqueta que não vai ser expedida é cancelada para que a transportadora não a fature; o valor cobrado é reembolsado na conta. O cancelamento só é possível enquanto a transportadora ainda o permitir (normalmente antes da recolha).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-cancel" \
  -d '{"id": 128455}'
```

```json
{
  "result": true,
  "message": "Shipping Label cancelled successfully"
}
```

- Envie exatamente um de `id` (o id do pedido) ou `tracking_number` (o número de rastreio Superroute ou o da transportadora). Enviar os dois devolve `400`.
- `result: true`: a transportadora aceitou o cancelamento e o valor da etiqueta foi reembolsado.

**GraphQL:** `labelserviceCancelShippingLabel` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceCancelShippingLabel)).

Uma transportadora que já tem o volume recusa: a resposta é `400` com `result: false` e a mensagem da transportadora. Um pedido cuja etiqueta nunca foi comprada não pode ser cancelado através desta chamada.

**Verificação:** a resposta é `result: true`, e o rastreio público desse número devolve `404`. Uma repetição com a mesma `Idempotency-Key` devolve a resposta guardada; um novo pedido de cancelamento para o mesmo pedido devolve `400` `This order already cancelled`.

## 13. Enviar as informações de expedição e fechar o dia (só se este método o exigir)

Algumas transportadoras precisam que os envios do dia sejam transmitidos (um manifesto) antes da recolha. O passo 8 indica-o por pedido em `needSubmitShippingInformation`. Reúna esses ids de pedido ao longo do dia e envie-os depois da última etiqueta.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitShippingInformation \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [128455, 128461, 128470]}'
```

```json
{
  "result": true,
  "message": "Processed 3 orders. Success: 3, Failed: 0",
  "data": {
    "total_processed": 3,
    "success_count": 3,
    "failure_count": 0,
    "details": [
      { "order_id": 128455, "result": true, "message": "Successful" },
      { "order_id": 128461, "result": true, "message": "Successful" },
      { "order_id": 128470, "result": true, "message": "Successful" }
    ]
  }
}
```

- `details[]`: uma linha por pedido; volte a enviar os pedidos com `result: false` depois de corrigir a causa indicada em `message`.
- `404` `No eligible orders found for shipping information submission`: nenhum dos ids tem uma etiqueta comprada que ainda precise de ser enviada.

**GraphQL:** `labelserviceSubmitShippingInformation` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceSubmitShippingInformation)).

Em seguida, feche o dia. A chamada não tem corpo e abrange todos os pedidos de etiqueta de quem a faz.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "success": 0,
  "failed": 0,
  "success_ids": [],
  "failed_ids": []
}
```

- `400` com `There are orders need to submit shipping information`: algumas etiquetas compradas ainda precisam de ser enviadas; envie-as com `submitShippingInformation` e chame outra vez.

**GraphQL:** `labelserviceEndofday` ([Manual GraphQL](/api/graphql/documentation#/labelservice/labelserviceEndofday)).

Salte este passo quando nenhum pedido do dia tiver indicado `needSubmitShippingInformation: true`.

**Verificação:** `submitShippingInformation` indica `failure_count: 0`, e `endofday` responde `200`. Execute-o primeiro num método de teste.

## 14. Tratamento de erros

Os erros do serviço de etiquetas trazem uma `message`; um `code` só está presente quando a tabela o indica.

| Situação | Estado HTTP | Código | O que a integração faz |
|---|---|---|---|
| Token em falta ou expirado, ou acesso à API não ativado | `401` | — (`Unauthorized`) | Inicie sessão outra vez; se persistir, peça à empresa que ative o acesso à API |
| `shipping_method` em falta ou não disponível para quem faz a chamada | `400` | — | Volte a carregar a lista de métodos (passo 5) e use um `id` dessa lista |
| `package_type` não oferecido pelo método | `400` | — | Use uma chave de `package_type` do passo 5 |
| Endereço ou volume inválido, ou a transportadora não devolve nenhuma tarifa | `400` | — (mensagem da transportadora) | Mostre a mensagem, corrija os dados e cote outra vez |
| `auto_deduplication` é `1` e a `ref` já existe | `400` | — (`exist_order_ids`) | Use o pedido existente de `exist_order_ids` em vez de criar um novo |
| A mesma `Idempotency-Key` com um corpo diferente | `409` | `IDEMPOTENCY_CONFLICT` | Use uma chave nova para um pedido HTTP diferente |
| A mesma `Idempotency-Key` enquanto o primeiro pedido HTTP ainda está em execução | `409` | `IDEMPOTENCY_IN_PROGRESS` | Aguarde `Retry-After` segundos e tente de novo com a mesma chave e o mesmo corpo |
| O saldo mais o crédito do cliente não cobrem a etiqueta | `400` | `INSUFFICIENT_BALANCE` | Carregue saldo com base no detalhe `insufficient_balance` (`shortfall`, `add_funds_url`) e chame o passo 8 outra vez |
| Etiqueta comprada, ficheiro da transportadora ainda não disponível | `400` na primeira compra, `200` depois | `shipment_label_not_ready` | Aguarde enquanto `labelStatus` for `pending`; chame o passo 8 outra vez quando for `failed` |
| Id de pedido ou número que não pertence a quem faz a chamada | `401` | — (`Not Auth`) | Verifique o id e o `type`; use a conta que criou o pedido |
| Cancelamento recusado pela transportadora, ou o pedido já está cancelado | `400` | — | Trate a etiqueta como expedida (ou já cancelada); não repita |
| Número de rastreio desconhecido ou cancelado | `404` | — | Deixe de apresentar a linha temporal desse número |
| `endofday` com envios ainda não transmitidos | `400` | — | Execute `submitShippingInformation` para esses pedidos e chame outra vez |

## Lista de testes

Use um destino que controla e um método que possa ser cancelado:

- [ ] A lista de métodos não está vazia; capturou um `id`.
- [ ] A tarifa devolve um preço para esse método e destino.
- [ ] Submit devolve um `id` de pedido e `rates[].rate_id`; a mesma `Idempotency-Key` não cria um segundo pedido.
- [ ] `getShippingDetail` com o `rate_id` escolhido devolve `mainTrackingNumber`; uma segunda chamada não volta a cobrar.
- [ ] O PDF da etiqueta abre e mostra o número de rastreio da transportadora.
- [ ] O rastreio público encontra o envio por esse número.
- [ ] Chega `tracking.event` (e `order.created`, quando ativado); a assinatura v2 é verificada.
- [ ] Um envio transfronteiriço de teste com `items` é aceite pela transportadora.
- [ ] Cancelar funciona, **ou** confirmou que este método não pode ser cancelado depois da reserva.
- [ ] Se o método exige o fecho do dia, uma execução de teste termina sem erro.
