# Recogida y entrega

La recogida y la última milla con flota propia usan las mismas API. Crear un pedido de recogida o de entrega → imprimir la etiqueta local → seguir → suscribirse a las notificaciones de eventos → cancelar un pedido de prueba.

`type` selecciona la parada: `D` entrega, `P` recogida. Complete los pasos en secuencia. La cotización es opcional y no es necesaria antes de crear el pedido.

Sustituya `YOUR_HOST` y `ACCESS_TOKEN` por los valores de su entorno.

## 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"}'
```

Coloque el `access_token` devuelto en la cabecera de la petición:

```
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 este token devuelve `401`.

## 2. Cotizar (opcional)

Este paso es opcional. Solo devuelve una cotización; no se crea ningún pedido. Crear el pedido no exige una cotización previa. Establezca `type` en `D` (entrega) o `P` (recogida).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_postcode": "H9S2H9",
    "from_country": "CA",
    "to_postcode": "H4B2T5",
    "to_country": "CA",
    "packages": [{
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "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,
  "shipping_price": "6.99",
  "currency": "CAD",
  "price_details": {
    "shipping_fee": "6.99",
    "tax_details": [{ "tax_name": "HST", "tax_rate": "13.00", "tax": "0.91" }]
  }
}
```

**GraphQL** (escalar JSON — sin selection set) (`ordersRate` ([Manual GraphQL](/api/graphql/documentation#/orders/ordersRate))):

```graphql
mutation {
  ordersRate(
    type: "D"
    from_postcode: "H9S2H9"
    from_country: "CA"
    to_postcode: "H4B2T5"
    to_country: "CA"
    packages: [{ weight: 1, weight_unit: 2, length: 30, width: 20, height: 10, dimension_unit: 2 }]
  )
}
```

**Verificación:** si ejecuta esta llamada, `result` es true y `shipping_price` es un número. Repita con `type` `P` para cotizar una recogida. Un precio vacío significa que el código postal no está en una zona activa. La creación no depende de este paso.

## 3. Crear el pedido

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

La petición debe incluir `Idempotency-Key` para que un reintento no pueda crear un segundo pedido.

### Entrega (`type` `D`)

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-local-001" \
  -d '{
    "type": "D",
    "need_pick_up": 0,
    "ref": "DEV-LOCAL-001",
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "delivery_instruction": "Leave at the side door"
  }'
```

### Recogida (`type` `P`)

El mismo endpoint. La dirección es el punto de recogida.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-pickup-001" \
  -d '{
    "type": "P",
    "need_pick_up": 1,
    "ref": "DEV-PICKUP-001",
    "name": "Jane Sender",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-P-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "pickup_instruction": "Ring the side bell"
  }'
```

| Campo | Significado |
|---|---|
| `type` | `D` entrega o `P` recogida |
| `need_pick_up` | `0` — ya está en el almacén. `1` — un conductor debe recoger el paquete |
| `ref` | Referencia externa para buscar y conciliar |
| `name` / address | Entrega: destinatario. Recogida: punto de recogida |
| `auto_deduplication` | `1` rechaza un segundo paquete con la misma `ref` de paquete |

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

```json
{ "result": true, "id": 12345, "tracking_number": "SR123456789012", "orders_status_id": 2 }
```

`orders_status_id` `2` es Nuevo. Guarde `id`, `tracking_number` y `ref`.

**Verificación:** envíe el **mismo** cuerpo con el mismo `Idempotency-Key` otra vez. Debe obtener el mismo `id` y no debe crear un segundo pedido.

El pedido **no** se crea cuando:

| `code` | Qué hacer |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` tiene required / available / shortfall. Recargue y reintente. |
| `OUT_OF_DELIVERY_AREA` | La dirección está fuera de la zona de servicio y la empresa borra esos pedidos. Envíe una dirección dentro de la zona de servicio. |
| `IDEMPOTENCY_CONFLICT` | Se reutilizó la misma clave de idempotencia con un cuerpo de petición distinto. Emita una clave nueva. |

Un pedido fuera de zona que se conserva puede devolver `result: true` con `shipping_price: null` y un `warning`. Lea ese campo.

## 4. Consultar el pedido

```bash
curl "https://YOUR_HOST/api/v1/orders/list?page=1&per_page=50" \
  -H "Authorization: Bearer ACCESS_TOKEN"

curl https://YOUR_HOST/api/v1/orders/12345 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**REST:** `GET /api/v1/orders/list` — todos los pedidos de la cuenta, los más recientes primero, cada uno con sus paquetes y sus líneas de artículos. Envíe `page` y `per_page` juntos para paginar (`per_page` máximo 1000); sin ellos recibirá los 1000 pedidos más recientes y un indicador `truncated`. [Manual REST](/api/documentation#/paths/v1-orders-list/get)

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

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

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

**Verificación:** el pedido pertenece a la cuenta autenticada. `ref` coincide con el valor que creó. `tracking_number` coincide con el paso 3.

## 5. Imprimir la etiqueta local

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

```bash
curl -X POST https://YOUR_HOST/api/v1/shipping/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "SR123456789012",
    "type": "TRACKING_NUMBER",
    "base64": 1,
    "hide_sender_address": 0,
    "hide_receiver_address": 0
  }'
```

`type`: `TRACKING_NUMBER`, `ORDER_ID` o `REF`. `base64: 0` (predeterminado) envía un PDF. Con `base64: 1` todo el cuerpo de la respuesta es una cadena JSON de nivel superior que contiene el PDF en base64, no un objeto con un campo `pdf_data`. Llame a `POST /api/v2/shipping/getShippingLabel` si prefiere recibir la etiqueta dentro de un objeto JSON normal.

**GraphQL:** `shippingGetShippingLabel` ([Manual GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([Manual GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) siempre devuelve JSON (`pdf_data`).

**Verificación:** el PDF se abre. Una etiqueta de entrega muestra al destinatario; una etiqueta de recogida muestra la dirección de recogida. Un campo de dirección oculto se representa en blanco en la etiqueta.

## 6. Seguir

Endpoint público de seguimiento; no se requiere token de acceso.

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

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

La misma URL acepta su `ref` cuando se guardó como número externo.

**GraphQL** (tipado — necesita selection set):

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    returntosender
    rejectedbyrecipient
    postcode
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

Bifurque por `tracking_event_status_id`, no por `description` (esa cadena sigue `Accept-Language`):

| `tracking_event_status_id` | Lado | Significado |
|---|---|---|
| `100` | ambos | Pedido recibido |
| `300` / `301` | entrega | En instalación |
| `450` | entrega | En reparto |
| `500` | entrega | Entregado |
| `501` | entrega | Entrega fallida, hace falta un plan nuevo |
| `460` | recogida | En recogida |
| `510` | recogida | Recogido |
| `512` | recogida | Recogida fallida, intentar más tarde |
| `513` | recogida | Problema de recogida |

`data` va de más reciente a más antiguo. Trate la primera fila como actual. `deliveried: true` después de `500`.

En `500` o `510`, `proofs[]` puede llevar `type` `1` (firma) o `2` (foto), más `file_id` y `signed_url`. Una foto subida después de ese evento no está en ese payload — suscríbase a `pod.files_updated` en el paso 7.

**Verificación:** justo después de crear, el evento más reciente es `100` y `deliveried` es false. Un número desconocido es `result: false` / 404 — muestre no encontrado, no invente eventos de seguimiento.

## 7. Configurar notificaciones de eventos

Configure las URL de retrollamada que exige este flujo:

| Ajuste | Evento | Cuándo |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persistir `id` y `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Estado visible para el cliente |
| `tracking_event_webhook_url` | `tracking.event` | Línea de tiempo de recogida o entrega |
| `pod_files_webhook_url` | `pod.files_updated` | Foto/firma después de la recogida o de la entrega |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Una cancelación que envió fue rechazada |

**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",
    "order_status_change_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:** cree un pedido de prueba y vea `order.created` con el mismo `id` / `tracking_number`. El receptor debe rechazar una firma no válida con `401`. Una segunda entrega del mismo `X-Webhook-Event-Id` no debe procesarse dos veces.

## 8. Cancelar un pedido de prueba

**REST:** `POST /api/v1/orders/cancel` — [Manual REST](/api/documentation#/paths/v1-orders-cancel/post) — exactamente uno de `order_id`, `tracking_number`, `external_tracking_number`.

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tracking_number":"SR123456789012"}'
```

Ya cancelado: `200` con `already_cancelled: true`. **GraphQL:** `ordersCancel` ([Manual GraphQL](/api/graphql/documentation#/orders/ordersCancel)).

**Verificación:** el seguimiento público ya no trata el envío como activo. Si la cancelación se rechaza (`409`, por ejemplo `ORDER_ALREADY_IN_DELIVERY`), se dispara `order.cancel_failed`.

## 9. Creación por lote (opcional)

- `POST /api/v1/client/batchOrderCreate` — [Manual REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — espera a que se haya procesado cada fila.
- `POST /api/v1/client/batchOrderCreateAsync` — [Manual REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — devuelve de inmediato un identificador de trabajo; consulte `GET /api/v1/client/async/{id}` — [Manual REST](/api/documentation#/paths/v1-client-async-id/get) o reciba `order.create_async`.

Los mismos campos del paso 3, como array de pedidos. Cada fila puede ser `type` `D` o `P`. **GraphQL:** `clientBatchOrderCreate` ([Manual GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([Manual GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Verificación:** cada fila tiene su propio `result`. Use el endpoint asíncrono al enviar más de unas 100 filas.

## Lista de pruebas

Use un `ref` de prueba como `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Opcional) La cotización devuelve un precio para un código postal en zona con `type` `D`.
- [ ] (Opcional) La cotización devuelve un precio para un código postal en zona con `type` `P`.
- [ ] Crear una entrega devuelve `id` + `tracking_number`; el mismo `Idempotency-Key` no crea un segundo pedido.
- [ ] Crear una recogida devuelve `id` + `tracking_number`; `need_pick_up` es `1`.
- [ ] Lista / detalle muestra el pedido de esta cuenta.
- [ ] El PDF de la etiqueta local se abre y muestra al destinatario o la dirección de recogida.
- [ ] El seguimiento público devuelve la línea de tiempo sin token; el evento más reciente es `100`.
- [ ] Llega `order.created`; la firma v2 verifica.
- [ ] Cancelar devuelve `result: true` (o `already_cancelled: true` al reintentar).
