# Etiquetas de transportista

Comprar un envío de transportista: listar métodos → cotizar → crear la etiqueta → descargar el PDF → seguir → recibir eventos → cancelar (o cerrar el día).

Esta guía documenta únicamente las operaciones siguientes. Complételas en secuencia.

Sustituya `YOUR_HOST`, `ACCESS_TOKEN` y `shipping_method` por los suyos. Los id de método cambian por cuenta — nunca los fije en código.

## 1. Acceso

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

```
Authorization: Bearer ACCESS_TOKEN
```

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

[Manual REST](/api/documentation#/paths/v1-user-login/post) · [Manual GraphQL](/api/graphql/documentation#/user/userLogin)

**Verificación:** el login devuelve `access_token`. Una llamada posterior sin él devuelve `401`.

## 2. Listar métodos de envío

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

Cada fila tiene:

| Campo | Uso |
|---|---|
| `id` | `shipping_method` en cada llamada posterior |
| `name` | Nombre para mostrar |
| `unique_identifier` | Código estable |
| `options.signature_option` | Firma disponible |
| `options.insurance_option` | Seguro disponible |
| `options.multi_package` | Más de una pieza |

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

**Verificación:** la lista no está vacía. Eligió un `id` y sabe si ese método admite firma, seguro y varios paquetes. Una lista vacía significa que no hay método habilitado en la cuenta.

## 3. Cotizar

Simulación. Se pide un precio al transportista; no se reserva nada. El cuerpo tiene la misma forma que al crear. `shipping_method` es obligatorio.

**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": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

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

Para más de una pieza, envíe `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (id de la libreta) o `shipping_from_code` pueden sustituir el bloque `sender_*`.

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

**Verificación:** `result` es true y tiene un precio (y días de tránsito, si el transportista los envía). Si no hay tarifa, corrija destino / paquete / método **antes** de crear.

## 4. Crear la etiqueta

Esto reserva el envío con el transportista.

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

El mismo cuerpo que el paso 3. Envíe `Idempotency-Key`.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-label-001" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

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

```json
{
  "result": true,
  "id": 12345,
  "shipping_price": "12.50",
  "tracking_numbers": ["1Z999AA10123456784"],
  "external_id": "EXT_12345"
}
```

| Campo | Uso |
|---|---|
| `id` | Id de pedido Superroute — descarga y cancelación |
| `tracking_numbers` | Facilítelos al destinatario; el seguimiento público los acepta |
| `external_id` | Id de envío del transportista |
| `shipping_price` | Importe cobrado |

**Verificación:** `tracking_numbers` no está vacío. Guarde `id` y los números. El mismo `Idempotency-Key` no debe comprar una segunda etiqueta.

## 5. Descargar el PDF

**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": "12345",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

`type` puede ser `ORDER_ID`, `TRACKING_NUMBER` o `REF`. `base64: 0` envía un PDF. Esta es la **etiqueta oficial del transportista**. El número de piezas lo fija la reserva.

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

**Verificación:** el PDF se abre y muestra el código de barras / número de seguimiento del transportista del paso 4. Imprima una copia de prueba y tírela — no entregue una etiqueta de prueba a un transportista.

## 6. Seguir

Sin token. Use un número de `tracking_numbers`:

```bash
curl https://YOUR_HOST/api/v1/tracking/1Z999AA10123456784
```

[Manual REST](/api/documentation#/operations/getPublicTracking) · [Manual GraphQL](/api/graphql/documentation#/tracking/trackingPublic)

**GraphQL:**

```graphql
query {
  trackingPublic(trackingNumber: "1Z999AA10123456784") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

`is_third_party_tracking` es true cuando los eventos vienen del transportista. Bifurque por `tracking_event_status_id` / `tracking_event_key`, no por `description`. El evento más reciente va primero en `data`. Los eventos tempranos pueden seguir siendo «información enviada» hasta que el transportista escanee el paquete. `500` es entregado; `proofs[]` entonces puede incluir firma (`type` `1`) o foto (`type` `2`).

**Verificación:** la consulta devuelve el envío que acaba de crear. Un número desconocido es `result: false` / 404.

## 7. Configurar notificaciones de eventos

| Ajuste | Evento | Cuándo |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persistir `id` y números de seguimiento del transportista |
| `tracking_event_webhook_url` | `tracking.event` | Escaneos del transportista, en reparto, entregado |
| `order_status_change_webhook_url` | `order.status_change` | Estado en su sistema |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Cancelación rechazada porque el transportista ya tiene el paquete |

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

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

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

Verifique **v2** sobre el cuerpo crudo: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contra `X-Webhook-Signature-V2`. Deduplique con `X-Webhook-Event-Id`. Responda **2xx en 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;
}
```

**Verificación:** un `submitOrder` de prueba produce `order.created` con esos `tracking_numbers`. Un secreto incorrecto es `401` de su receptor.

## 8. Cancelar

Solo mientras el transportista aún lo permita (normalmente antes de la recogida).

**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" \
  -d '{"id":"12345","type":"ORDER_ID"}'
```

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

Un transportista que ya tiene el paquete rechazará — eso es `order.cancel_failed`.

**Verificación:** una segunda cancelación es segura. El seguimiento ya no trata el envío como activo.

## 9. Cierre de día (solo si este método lo exige)

Algunos transportistas necesitan un manifiesto diario.

**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" \
  -H "Content-Type: application/json" \
  -d '{"shipping_method": 59}'
```

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

Llámalo una vez después de la última etiqueta del día de envío. Omita este paso cuando el método del paso 2 no lo exija.

**Verificación:** el cuerpo de éxito (o la confirmación del transportista) lista las etiquetas de hoy. Ejecútelo primero en un método de prueba.

## Lista de pruebas

Use un destino que controle y un método que se pueda cancelar:

- [ ] La lista de métodos no está vacía; capturó un `id`.
- [ ] La tarifa devuelve un precio para ese método y destino.
- [ ] Submit devuelve `tracking_numbers`; el mismo `Idempotency-Key` no compra una segunda etiqueta.
- [ ] El PDF de la etiqueta se abre y muestra el número de seguimiento del transportista.
- [ ] El seguimiento público encuentra el envío por ese número.
- [ ] Llega `order.created`; la firma v2 verifica.
- [ ] Cancelar funciona, **o** confirmó que este método no se puede cancelar después de reservar.
- [ ] Si el método necesita cierre de día, una ejecución de prueba termina sin error.
