# Uber Direct — Delivery Status Webhook

> Referencia oficial: https://developer.uber.com/docs/deliveries/daas/references/api/webhooks/delivery-status-webhook

---

## Descripción

Se recibe un webhook de tipo `event.delivery_status` cada vez que cambia el `status` o el campo `courier_imminent` de un delivery.

**Endpoint configurado en Kuup:**
```
POST /webhook/uber
Authorization: Bearer {token_sanctum}
```

---

## Ciclo de estados

| # | `status` | `courier_imminent` | Descripción | Disparador |
|---|---|---|---|---|
| 0 | `pending` | — | Delivery aceptado, sin motorista asignado aún | API, cuando Uber busca motorista activamente |
| 1 | `pickup` | `false` | Motorista asignado, moviéndose hacia el local | App del motorista (manual) al aceptar la oferta |
| 2 | `pickup` | `true` | Motorista a 1 minuto del local | App del motorista (automático) por GPS |
| 3 | `pickup_complete` | — | Motorista completó el retiro en el local | App del motorista (manual) al deslizar completar retiro |
| 4 | `dropoff` | `false` | Motorista moviéndose hacia el cliente | App del motorista (manual) tras completar retiro |
| 5 | `dropoff` | `true` | Motorista a 1 minuto del cliente | App del motorista (automático) por GPS |
| 6 | `delivered` | — | Delivery completado | App del motorista (manual) al deslizar completar entrega |
| — | `canceled` | — | Delivery cancelado | Endpoint CancelDelivery, Direct Dashboard o razón interna |
| — | `returned` | — | Cancelado y `undeliverable_action = return` | Se crea un nuevo delivery para devolver los items; ID empieza con `ret_` |
| — | `shopping_completed` | — | Motorista completó compras (solo Courier Pick & Pack) | App del motorista (manual) |

> **Nota:** `canceled` y `returned` no son obligatorios para todos los deliveries.

### Flujo con devolución (returned)

```
original (del_...):  pending → pickup → pickup_complete → dropoff → canceled
retorno  (ret_...):                                                 → pickup → pickup_complete → dropoff → delivered (= returned en original)
```

---

## Mapeo a `delivery_requests`

| Status recibido | Campo actualizado | Fuente del timestamp |
|---|---|---|
| `pickup` (primera vez) | `pickup_started_at` | `data.updated` |
| `pickup_complete` | `arrived_at_store_at` | `data.pickup.status_timestamp` o `data.updated` |
| `dropoff` (primera vez) | `dropoff_started_at` | `data.updated` |
| `delivered` | `delivered_at` | `data.updated` |
| `canceled` / `returned` | `canceled_at`, `cancellation_reason` | `data.updated` |
| cualquier estado | `last_status`, `webhook_count`, `last_webhook_at` | siempre |
| cualquier estado | `pickup_eta` | `data.pickup_eta` si viene |

> Los timestamps **solo se guardan la primera vez**. Uber puede enviar múltiples webhooks con el mismo estado (ej. varios `pickup` cuando `courier_imminent` cambia).

---

## Estructura del payload

```json
{
    "account_id": "",
    "batch_id": "bat_voEiX66nUf-D4XzKRlpJLQ",
    "created": "2023-08-01T06:28:22.695Z",
    "customer_id": "fb109f30-d2f0-5447-a0fa-884a44394axx",
    "delivery_id": "del_QbLowiwHQM-b4e8YmOZNOw",
    "developer_id": "",
    "id": "evt_Bouz7BhPTYGDz9FFQNgODw",
    "kind": "event.delivery_status",
    "live_mode": true,
    "route_id": "rte_I8-ffNPuT82EP1yaBmG1jg",
    "short_batch_id": "RlpJLQ",
    "batch_id": "bat_voEiX66nUf-D4XzKRlpJLQ",
    "status": "delivered",
    "dropoff_sequence_number": 1,
    "total_dropoff_count": 1,
    "data": {
        "id": "del_QbLowiwHQM-b4e8YmOZNOw",
        "uuid": "41B2E8C22C0740CF9BE1EF1898E64D3B",
        "status": "delivered",
        "kind": "delivery",
        "live_mode": true,
        "complete": true,
        "created": "2023-08-01T06:26:13.896Z",
        "updated": "2023-08-01T06:28:22.611Z",
        "currency": "usd",
        "fee": 9200,
        "quote_id": "dqt_QbLowiwHQM-b4e8YmOZNOw",
        "route_id": "rte_I8-ffNPuT82EP1yaBmG1jg",
        "batch_id": "bat_voEiX66nUf-D4XzKRlpJLQ",
        "short_batch_id": "RlpJLQ",
        "tracking_url": "https://www.ubereats.com/...",
        "external_id": "",
        "deliverable_action": "deliverable_action_meet_at_door",
        "courier_imminent": false,
        "pickup_ready": "2023-08-01T06:26:14Z",
        "pickup_deadline": "2023-08-01T06:46:14Z",
        "pickup_eta": "2023-08-01T06:27:04.748Z",
        "pickup_action": "default",
        "dropoff_ready": "2023-08-01T06:26:14Z",
        "dropoff_deadline": "2023-08-01T07:21:24Z",
        "dropoff_eta": "2023-08-01T06:28:22.564Z",
        "dropoff_sequence_number": 1,
        "undeliverable_action": "",
        "undeliverable_reason": "",
        "manifest": {
            "description": "1 X Small Box",
            "total_value": 0
        },
        "manifest_items": [
            {
                "name": "Small Box",
                "quantity": 1,
                "price": 0,
                "size": "large",
                "dimensions": { "depth": 40, "height": 40, "length": 40 },
                "must_be_upright": false
            }
        ],
        "courier": {
            "name": "Sam",
            "rating": "5.00",
            "vehicle_type": "car",
            "vehicle_make": "",
            "vehicle_model": "",
            "vehicle_color": "",
            "vehicle_license_plate": "**E54Q",
            "phone_number": "+15555555557",
            "img_href": "https://...",
            "location": { "lat": 0, "lng": 0 },
            "location_description": null,
            "unmarked_location_description": "...",
            "public_phone_info": {
                "formatted_phone_number": "+15555555557,48023618",
                "phone_number": "+15555555557",
                "pin_code": "48023618"
            }
        },
        "pickup": {
            "address": "175 Greenwich St, New York, NY 10007",
            "detailed_address": {
                "street_address_1": "175 Greenwich St",
                "street_address_2": "",
                "city": "New York",
                "state": "NY",
                "zip_code": "10007-2438",
                "country": "US"
            },
            "location": { "lat": 40.71093, "lng": -74.0119 },
            "name": "Coffee Shop",
            "notes": "",
            "phone_number": "+15555555555",
            "status": "completed",
            "status_timestamp": "2023-08-01T06:27:04.748Z",
            "external_store_id": "227",
            "verification": {},
            "verification_requirements": {}
        },
        "dropoff": {
            "address": "231 Hudson St, New York, NY 10013",
            "detailed_address": {
                "street_address_1": "231 Hudson St",
                "street_address_2": "",
                "city": "New York",
                "state": "NY",
                "zip_code": "10013",
                "country": "US"
            },
            "location": { "lat": 40.724533, "lng": -74.00839 },
            "name": "DROPOFF T.",
            "notes": "",
            "phone_number": "+15555555556",
            "status": "completed",
            "status_timestamp": "2023-08-01T06:28:22.564Z",
            "verification": {
                "picture": { "image_url": "https://..." },
                "pin_code": { "entered": "9999" },
                "signature": { "image_url": "...", "signer_name": "Sam", "signer_relationship": "self" },
                "barcodes": [{ "type": "CODE39", "value": "123", "scan_result": { "outcome": "SUCCESS", "timestamp": "..." } }]
            },
            "verification_requirements": {
                "picture": true,
                "pincode": { "enabled": true, "value": "9999" },
                "signature": true,
                "signatureRequirement": { "collect_signer_name": true, "collect_signer_relationship": true, "enabled": true },
                "barcodes": [{ "type": "CODE39", "value": "123" }]
            }
        }
    }
}
```

---

## Fields Definition (nivel raíz)

| Campo | Descripción |
|---|---|
| `account_id` | ID único (prefijo `acc_`) de la cuenta del developer a la que pertenece este delivery. |
| `created` | Timestamp de cuando se generó el evento. |
| `customer_id` | ID único (prefijo `cus_`) del customer al que pertenece este delivery. |
| `delivery_id` | ID único del delivery al que aplica el evento. |
| `developer_id` | ID único (prefijo `dev_`) del developer al que corresponde el `customer_id`. |
| `id` | ID único de esta instancia del evento (prefijo `evt_`). |
| `kind` | Tipo de evento en detalle (valor fijo: `event.delivery_status`). |
| `data` | Detalles del webhook. Ver **Data Object Definition**. |
| `live_mode` | Flag que indica si el evento aplica a un delivery en vivo vs. de prueba. |
| `status` | Estado del delivery al que refiere el evento. |
| `batch_id` | Cuando un delivery es parte de un lote, ID único (prefijo `bat_`) del lote. Puede usarse para identificar deliveries agrupados con el mismo motorista. **IMPORTANTE:** si el motorista cancela en camino al retiro, se asigna otro motorista y el `batch_id` cambia. |
| `route_id` | ID único (prefijo `rte_`) de la ruta que está tomando el motorista. |
| `short_batch_id` | Últimos 6 dígitos del `batch_id`. Indica qué paquetes deben agruparse en el retiro. |
| `dropoff_sequence_number` | En lotes grandes: número de secuencia original del dropoff en el batch. En otros casos: índice actual del dropoff (stops restantes). |

---

## Data Object Definition

El objeto `data` contiene información detallada y actualizada del delivery en cada webhook.

| Campo | Descripción |
|---|---|
| `id` | ID único del delivery al que aplica el evento. |
| `status` | Estado del delivery al que refiere el evento. |
| `created` | Fecha/hora en que se creó el delivery. |
| `updated` | Fecha/hora de la última actualización del delivery. |
| `pickup_eta` | Tiempo estimado en que el motorista llegará al punto de retiro. |
| `pickup_ready` | Cuando el delivery está listo para ser retirado. Inicio de la ventana de retiro. |
| `pickup_deadline` | Límite antes del cual debe completarse el retiro. Fin de la ventana de retiro. |
| `dropoff_eta` | Tiempo estimado en que el motorista llegará al punto de entrega. |
| `dropoff_ready` | Cuando el delivery está listo para ser entregado. Inicio de la ventana de entrega. |
| `dropoff_deadline` | Límite antes del cual debe completarse la entrega. Fin de la ventana de entrega. |
| `quote_id` | Identificador del quote usado al crear el delivery (prefijo `dqt_`). |
| `fee` | Monto en centavos (¹/₁₀₀ de unidad monetaria) que se cobrará por el delivery. Ej: $10.99 → `1099`. |
| `currency` | Código de moneda ISO de tres letras, en minúsculas (ej. `clp`, `usd`). |
| `deliverable_action` | Acción que debe realizar el motorista en la entrega. Ver Get Delivery response para más detalle. |
| `tip` | Monto en centavos (¹/₁₀₀ de unidad monetaria) que se pagará al motorista como propina. |
| `manifest.reference` | Referencia que identifica el manifiesto. |
| `manifest.description` | **[DEPRECADO]** Descripción detallada de lo que el motorista entregará. Preferir `manifest_items[].name`. |
| `manifest.total_value` | Valor en centavos (¹/₁₀₀) de los items del delivery. Ej: $10.99 → `1099`. |
| `manifest_items[].name` | Descripción del item. |
| `manifest_items[].quantity` | Cantidad de items. |
| `manifest_items[].size` | Tamaño aproximado: `small`, `medium`, `large`, `xlarge`. Por defecto `small`. |
| `manifest_items[].dimensions.length` | Largo en centímetros. |
| `manifest_items[].dimensions.height` | Alto en centímetros. |
| `manifest_items[].dimensions.depth` | Profundidad en centímetros. |
| `manifest_items[].price` | Precio en centavos (¹/₁₀₀). Ej: $10.99 → `1099`. |
| `manifest_items[].weight` | Peso en gramos. **Requerido** cuando se usan `dimensions`. |
| `pickup` | Detalles del punto de retiro. Ver **Delivery Info Definition**. |
| `dropoff` | Detalles del punto de entrega. Ver **Delivery Info Definition**. |
| `return` | Detalles del delivery de devolución. Ver **Delivery Info Definition**. |
| `courier.name` | Nombre e inicial del apellido del motorista. |
| `courier.rating` | **[DEPRECADO]** Calificación del motorista en escala 1.0 a 5.0. |
| `courier.vehicle_type` | Tipo de vehículo: `bicycle`, `car`, `van`, `truck`, `scooter`, `motorcycle`, `walker`. |
| `courier.phone_number` | Teléfono enmascarado del motorista. Solo puede recibir llamadas/SMS del teléfono del dropoff. |
| `courier.location.lat` | Latitud de la ubicación actual del motorista. |
| `courier.location.lng` | Longitud de la ubicación actual del motorista. |
| `courier.unmarked_location_description` | Descripción en texto libre de la ubicación del motorista cuando está en un lugar sin marcar. Solo presente si el motorista lo indicó. |
| `courier.img_href` | URL de la foto de perfil del motorista. |
| `courier.public_phone_info.formatted_phone_number` | Teléfono formateado del motorista. |
| `courier.public_phone_info.phone_number` | Teléfono anonimizado del motorista. |
| `courier.public_phone_info.pin_code` | Pin para el teléfono anonimizado del motorista. |
| `courier.vehicle_license_plate` | Últimos 4 dígitos de la patente del vehículo del motorista. |
| `live_mode` | Flag que indica si el delivery es real (`true`) o de prueba (`false`). |
| `related_deliveries[].id` | ID único del delivery relacionado. |
| `related_deliveries[].relationship` | Naturaleza del delivery relacionado: `"original"` para el viaje de ida, `"returned"` para el viaje de devolución. |
| `tracking_url` | URL para rastrear al motorista durante el delivery. |
| `courier_imminent` | `true` cuando el motorista está a 1 minuto del punto de retiro o entrega. |
| `undeliverable_reason` | Si el delivery no pudo entregarse, razón por la que fue no entregable. |
| `undeliverable_action` | Si el delivery no pudo entregarse, acción tomada por el motorista. Ver Get Delivery response para más detalle. |
| `complete` | Flag que indica si el delivery está en curso (`false`) o finalizado (`true`). |
| `kind` | Tipo de objeto (valor fijo: `delivery`). |
| `uuid` | ID alternativo del delivery. UUID v4 sin guiones, case-insensitive. Ej: `41B2E8C22C0740CF9BE1EF1898E64D3B`. Usar `id` para identificación. |
| `batch_id` | ID del lote si el delivery está agrupado (prefijo `bat_`). Cambia si el motorista cancela en trayecto. |
| `route_id` | ID único (prefijo `rte_`) de la ruta del motorista. |
| `cancelation_reason.primary_reason` | Siempre `#Uber`. |
| `cancelation_reason.secondary_reason` | `CUSTOMER_CANCEL`, `COURIER_CANCEL`, `MERCHANT_CANCEL` o `UBER_CANCEL`. |
| `pickup_action` | Acción que debe realizarse en el retiro. |

> **Nota:** `courier.location` y datos del vehículo **no se incluyen** en el webhook de `delivered`.

---

## Delivery Info Definition

Esta definición aplica a los objetos `pickup`, `dropoff` y `return` dentro del objeto `data`.

| Campo | Descripción |
|---|---|
| `name` | Nombre de la persona en el waypoint. |
| `phone_number` | Teléfono del waypoint. |
| `address` | Dirección del waypoint. |
| `detailed_address.street_address_1` | Primera línea de la calle del waypoint. |
| `detailed_address.street_address_2` | Segunda línea opcional (depto, piso, etc.). |
| `detailed_address.city` | Ciudad del waypoint. |
| `detailed_address.state` | Estado/región del waypoint. |
| `detailed_address.zip_code` | Código postal del waypoint. |
| `detailed_address.country` | País del waypoint (código ISO, ej. `CL`, `US`). |
| `location.lat` | Latitud del waypoint. |
| `location.lng` | Longitud del waypoint. |
| `notes` | Instrucciones adicionales en el waypoint (ej. "Dpto, llamar al llegar"). |
| `status` | Estado del waypoint: `pending` o `completed`. |
| `status_timestamp` | Timestamp exacto en que el motorista completó la acción. Solo presente cuando `status = completed`. |
| `external_store_id` | Solo en `pickup`: ID del local en el sistema del merchant. |
| `courier_notes` | Cuando se requiere foto como prueba de entrega, notas del motorista indicando dónde dejó los items. |
| `verification.signature.image_url` | URL de la imagen de la firma realizada en el waypoint. |
| `verification.signature.signer_name` | Nombre de quien firmó el paquete. |
| `verification.signature.signer_relationship` | Relación de quien firmó con el destinatario. |
| `verification.barcodes[].value` | Valor codificado en el barcode. |
| `verification.barcodes[].type` | Tipo de barcode. Valores válidos: `CODE39`, `CODE39_FULL_ASCII`, `CODE128`, `QR`. |
| `verification.barcodes[].scan_result.outcome` | Resultado del escaneo: éxito o fallo. |
| `verification.barcodes[].scan_result.timestamp` | Timestamp de cuando se generó el evento de escaneo. |
| `verification.picture.image_url` | URL de la foto tomada en el waypoint. |
| `verification.identification.min_age_verified` | Indica si la identificación fue verificada/escaneada exitosamente (verificación de edad mínima). |
| `verification.pin_code.entered` | Valores ingresados durante la verificación por PIN. |
| `verification.completion_location.lat` | Latitud donde se completó el trabajo. |
| `verification.completion_location.lng` | Longitud donde se completó el trabajo. |
| `verification_requirements.signature` | Flag que indica si se requiere firma en el waypoint. |
| `verification_requirements.signatureRequirement.collect_signer_name` | Flag que indica si se requiere el nombre del firmante. |
| `verification_requirements.signatureRequirement.collect_signer_relationship` | Flag que indica si se requiere la relación entre firmante y destinatario. |
| `verification_requirements.signatureRequirement.enabled` | Flag que indica si la firma está habilitada en el waypoint. |
| `verification_requirements.barcodes[].type` | Tipo de barcode requerido. |
| `verification_requirements.barcodes[].value` | Valor del barcode requerido. |
| `verification_requirements.picture` | Flag que indica si se requiere foto en el waypoint. |
| `verification_requirements.pincode.enabled` | Flag que indica si se requiere PIN en el waypoint. |
| `verification_requirements.pincode.value` | Valor del PIN configurado para verificación. |

---

## Notas importantes

- Uber envía **múltiples webhooks con el mismo `status`** cuando `courier_imminent` cambia. Validar siempre antes de sobreescribir timestamps.
- Si un delivery es **devuelto** (`returned`), Uber crea automáticamente un nuevo delivery con ID `ret_...` y lo incluye en el campo `return` del objeto de respuesta. El delivery original pasa de `canceled` a `returned`.
- El webhook de `delivered` **no incluye** lat/lng del motorista ni datos del vehículo.
- `pickup.status_timestamp` indica el momento exacto en que el motorista completó el retiro — es el dato más preciso para `arrived_at_store_at`.
