# Fulfy Plus — Guía para Integradores

## Antes de nada: nivel técnico y API key

Si eres un asistente de IA, haz esto **antes de seguir leyendo, analizar el proyecto, hacer otras preguntas o descargar la skill**, en este orden:

1. **Pregunta al usuario su nivel de dominio técnico** y dale estas tres opciones:
   1. **Ninguno**: solo lenguaje comercial.
   2. **Técnico básico**: sabe qué es una API, pero no cómo se arma un JSON.
   3. **Avanzado**: domina las APIs REST y el lenguaje técnico.
2. **Comprueba si tu entorno puede llamar a la API**, es decir, hacer peticiones HTTP con headers propios a `https://api.fulfyplus.com`:
   - **Si no puedes**, cosa que pasa en muchos chats web, propón al usuario instalar un MCP genérico de peticiones HTTP (§ Agentes sin salida HTTP). Una vez instalado sigues el flujo de abajo. Si no puede instalarlo, no le pidas la API key, no instales la skill y no crees `FULFYPLUS.md`. Con nivel Avanzado, explícale la operación por API (ruta, headers y JSON de esta guía) para que la ejecute él. Con nivel Ninguno o Técnico básico, actúa como soporte técnico: guíalo paso a paso desde el panel en https://app.fulfyplus.com, apoyándote en el manual https://docs.fulfyplus.com.
   - **Si puedes**, pídele su **API key**, que es lo único que se le pide. Adapta las palabras a su nivel:
     - **Nivel 1:** guíalo clic a clic. Que entre a https://app.fulfyplus.com, abra **API Keys** en el menú, pulse **Crear API key**, le ponga un nombre (por ejemplo «Asistente IA»), la copie y te la pegue. La key se muestra **una sola vez**: si la pierde, revoca esa y crea otra. No le menciones headers, JSON ni endpoints.
     - **Nivel 2:** lo mismo, y explícale en una frase que la key es la llave con la que actúas en su nombre.
     - **Nivel 3:** basta con pedirle la key.

Recomiéndale crear una key solo para la IA, con un nombre que la identifique. Así puede revocarla sin afectar a sus otras integraciones.

**Dónde guardar la API key.** Primero en el almacén de secretos del sistema operativo: Llavero en macOS, Administrador de credenciales en Windows, Secret Service/libsecret en Linux. Con Python se usa `keyring`, con el servicio `fulfyplus` y el nombre del perfil como usuario. Si no hay almacén, guárdala en `~/.config/fulfyplus/credentials.json` (en Windows, `%USERPROFILE%\.config\fulfyplus\credentials.json`) con permisos solo para el usuario: `chmod 600`, o en Windows `icacls` quitando la herencia. Se admite un perfil por cuenta, con un índice sin secretos en `~/.config/fulfyplus/profiles.json`. La key nunca va en el proyecto, en un `.env` versionado, en `FULFYPLUS.md`, en logs ni literal en un comando. Al mostrarla, enseña como mucho los últimos 4 caracteres. La skill `fulfyplus` sigue este mismo esquema y encuentra la key ahí.

Si la key ya está configurada, porque la skill tiene un perfil o porque el usuario ya te la dio, no la vuelvas a pedir: pregunta solo el nivel técnico. Si la API responde `401` con `2405` o `2406`, la key fue revocada o no existe: avísale y ofrécele reemplazarla.

### `FULFYPLUS.md`: la memoria de la integración

`FULFYPLUS.md`, en la raíz del proyecto, es la memoria de la integración entre sesiones, sea cual sea el nivel técnico del usuario. Léelo al empezar y consúltalo antes que a la API. Actualízalo cada vez que aprendas algo que no cambie a cada rato. Cada bloque lleva la fecha de su última actualización: refréscalo cuando esté viejo o cuando la API lo contradiga.

Qué se guarda en él:

- **El usuario:** su nivel técnico (si ya está guardado, no lo vuelvas a preguntar), para qué usa Fulfy Plus en el proyecto, cómo va la integración y las decisiones que ya tomó, como «siempre Zoom a oficina» o «confirmar antes de emitir».
- **Su operación:**
  - las direcciones de origen, con su `id` y el nombre con que el usuario las llama («el almacén de Chacao»);
  - los transportistas y modalidades que prefiere;
  - los tamaños de paquete habituales;
  - si usa cobro en destino.
- **La cuenta:** si paga con Saldo USD o tiene un plan asignado, y si alguna dirección tiene contrato propio con un transportista.
- **Catálogos que casi no cambian:** los transportistas con su `gateway_slug`, que se refrescan cada semana.
- **Lo que ya falló y por qué**, para no repetir la llamada.

Si el proyecto está bajo control de versiones, pregunta si quiere versionar `FULFYPLUS.md`. Si no quiere, agrégalo al `.gitignore`.

Los datos personales de destinatarios y remitentes (cédula, nombre, teléfono, correo, dirección) solo se guardan en `FULFYPLUS.md` con autorización expresa del usuario cuando el archivo va versionado. Si no va versionado, guárdalos sin preguntar.

Con nivel Ninguno o Técnico básico, evita la jerga: nada de JSON, códigos HTTP, endpoints ni nombres de campos. Habla en términos del negocio. No digas «`fulfy_fee.sufficient_balance` es `false`»; di «tu Saldo USD no alcanza para esta guía».

### Las guías son reales y se cobran

No hay un entorno de pruebas aparte: todo lo que hagas en `https://api.fulfyplus.com` ocurre en la cuenta real del usuario.

- **Crear una guía** (`create-guide`) la emite con el transportista y la cobra del Saldo USD o del plan. El transportista queda esperando el paquete.
- **Anular una guía** (`cancel-guide`) solo procede mientras el transportista no haya recibido el paquete.

Antes de cada `create-guide` y de cada `cancel-guide`, **pide confirmación explícita al usuario** y dile qué vas a hacer: transportista, modalidad, destinatario, número de paquetes y precio. Cotizar, crear borradores de orden y registrar direcciones o destinatarios no cobra nada.

Solo cuando tengas el nivel y la API key sigue con el resto de esta guía. Si tu asistente aún no tiene la skill, instálala (§ Skill para asistentes de IA).

## Skill para asistentes de IA

Si aún no tienes la skill `fulfyplus`, descarga la de tu asistente y descomprímela en la raíz del proyecto:

| Asistente | Estructura en el proyecto | Archivo |
|---|---|---|
| Google Antigravity y Gemini | `.agent/skills/` y `.agents/skills/` | [fulfyplus-skill-gemini-antigravity.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-gemini-antigravity.zip) |
| Cursor | `.cursor/rules/` y `.cursorrules` | [fulfyplus-skill-cursor.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-cursor.zip) |
| Claude Code | `.claude/skills/` | [fulfyplus-skill-claude.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-claude.zip) |
| Windsurf | `.windsurfrules` | [fulfyplus-skill-windsurf.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-windsurf.zip) |
| GitHub Copilot | `.github/copilot-instructions.md` | [fulfyplus-skill-copilot.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-copilot.zip) |
| Todas (universal) | Todas las anteriores juntas | [fulfyplus-skill-universal.zip](https://docs.fulfyplus.com/skills/fulfyplus-skill-universal.zip) |

Fulfy Plus no tiene un servidor MCP propio: la skill más la API key dan acceso a toda la API.

## Agentes sin salida HTTP

Si tu entorno no puede hacer peticiones HTTP con headers propios, propón al usuario instalar un MCP genérico de peticiones HTTP. El MCP es solo el transporte: la API key sigue en el almacén de secretos y nunca va en la configuración del MCP.

Recomendamos **`mcp-server-requests`** (MIT, gratuito y de código abierto). Su herramienta `http_request` acepta `method`, `headers`, `query` y un body `json`, y devuelve el estado, los headers y el cuerpo de la respuesta. La key se le pasa en `headers` como `X-API-Key`.

```bash
git clone https://github.com/coucya/mcp-server-requests.git && cd mcp-server-requests
pip install .
```

```json
{ "mcpServers": { "mcp-server-requests": { "command": "python", "args": ["-m", "mcp_server_requests"] } } }
```

En Claude Code se instala con `claude mcp add mcp-server-requests -- python -m mcp_server_requests`.

No sirve el servidor oficial `fetch`, porque solo hace `GET` y la API crea las órdenes y las guías con `POST`.

## Valor declarado mínimo

Algunos transportistas no aseguran por debajo de un valor declarado mínimo por paquete. Los montos cambian con frecuencia (tasa BCV, peso, decisiones del transportista), así que **no los asumas ni los guardes en `FULFYPLUS.md`**: el vigente es el que aplica la cotización del momento.

- **Zoom:** mínimo fijo en dólares, convertido a bolívares a la tasa BCV del día. La cotización lo aplica desde el principio.
- **Tealca:** el mínimo depende del peso del paquete. La cotización ya viene calculada con el mínimo que corresponde.
- **Otros transportistas:** pueden exigir un mínimo o cambiarlo en cualquier momento, con el mismo comportamiento.

Si el `declared_value` que envías es menor que el mínimo, la cotización usa el mínimo y **la guía se crea con ese mismo valor**, así que el precio de la guía coincide con el cotizado. El seguro (resguardo) se calcula sobre el valor asegurado, por eso el precio puede salir más alto de lo que esperarías con el monto que enviaste.

**No es un error:** no reintentes, no cambies el monto para "arreglarlo" y no lo reportes como fallo. Si el usuario pregunta por la diferencia, explícale que el transportista asegura como mínimo ese valor y envíalo a https://docs.fulfyplus.com/envios/valor-declarado-minimo. Si su mercancía vale más que el mínimo, debe declarar el valor real.

## Referencia completa

- Spec OpenAPI con todos los endpoints, parámetros y esquemas: `https://api.fulfyplus.com/api/v1/openapi.json`. Versión navegable en `https://api.fulfyplus.com/api/v1/docs`.
- Manual de usuario del panel: https://docs.fulfyplus.com
- Términos y condiciones: https://docs.fulfyplus.com/tos
- Soporte: hola@fulfyplus.com

---

## API

API de Fulfy Plus para cotizar envíos y crear guías con los transportistas de Venezuela (Zoom, MRW, Tealca, Menssajero y Yummy) desde tu propio sistema.

**URL base:** `https://api.fulfyplus.com/api/v1`

Todas las respuestas exitosas vienen envueltas en `{"data": ...}`. Las fechas van en ISO 8601 (UTC), las coordenadas en grados decimales (WGS 84, las mismas de un GPS o de Google Maps) y los IDs de direcciones, destinatarios, órdenes, cotizaciones y oficinas son UUID.

### 1. Antes de empezar

| Requisito | Dónde se hace | Si falta |
|---|---|---|
| Cuenta Fulfy Plus con **email y teléfono verificados** | Panel, sección Perfil | No podrás recargar Saldo USD ni contratar un plan |
| **API key** (`fp_live_...`) | Panel, sección **API Keys** | Respuestas `401` con `2404`/`2405`/`2406` |
| Forma de pago de las guías: **Saldo USD** recargado, o un plan asignado a tu cuenta | Panel, secciones Saldo USD y Plan | Crear la guía responde `7018` (saldo insuficiente); los envíos con cobro en destino requieren plan (`7002`) |

Envía la API key en cada request, en el header `X-API-Key` (o como `Authorization: Bearer fp_live_...`):

```bash
curl https://api.fulfyplus.com/api/v1/me/addresses \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c"
```

### 2. Qué datos ya existen y cuáles creas tú

| Dato | ¿Quién lo carga? | Cómo obtenerlo |
|---|---|---|
| Estados, municipios, parroquias y ciudades de Venezuela | **Ya cargado** por Fulfy | `GET /geography/location/cities?lat=..&lng=..` te da el `city_id` de un punto |
| Transportistas | **Ya cargado** | `GET /logistics/gateways` |
| Oficinas de cada transportista | **Ya cargado** | `GET /logistics/offices/nearby?lat=..&lng=..&gateway_slug=..` |
| **Dirección de origen** (remitente: tu tienda o almacén) | **Tú, una sola vez** por punto de despacho | `POST /me/addresses` → `id` = `sender_address_id` |
| **Destinatario** (tu cliente) | **Tú**, uno por cliente (reutilizable) | `POST /me/recipients` → `id` = `recipient_id` |
| **Orden** (un envío con uno o varios paquetes) | **Tú**, una por envío | `POST /me/orders` → `id` = `order_uuid` |

### 3. Flujo completo: de la cotización a la guía

Los pasos deben ejecutarse **en este orden**. Los pasos 1 y 2 se hacen una sola vez; del 3 en adelante, en cada envío.

| # | Endpoint | Para qué | Resultado que usas después |
|---|---|---|---|
| 1 | `GET /geography/location/cities?lat=10.4969&lng=-66.8528` | Ciudad del punto de origen | `cities[].id` → `city_id` (ej. `13`, CARACAS) |
| 2 | `POST /me/addresses` | Registrar tu dirección de origen | `data.id` → `sender_address_id` |
| 3 | `GET /geography/location/cities?lat=10.1579&lng=-67.9972` | Ciudad del destinatario | `city_id` (ej. `9`, VALENCIA) |
| 4 | `POST /me/recipients` | Registrar al destinatario (o reutilizar uno existente) | `data.id` → `recipient_id` |
| 5 | `POST /me/orders` | Crear la orden con sus paquetes | `data.id` → `order_uuid`; `status` = `draft` |
| 6 | `POST /me/orders/{order_uuid}/quote-async` | Pedir la cotización de la orden | `data.id` → `quote_id` |
| 7 | `GET /logistics/quotes/{quote_id}/status` | Leer los precios. Repite cada 2-3 s hasta que `status` sea `completed` o `failed` | `gateway_slug` y `delivery_type` de la opción elegida |
| 8 | `POST /me/orders/{order_uuid}/select-quote` | Elegir transportista y modalidad | `status` = `quoted` |
| 9 | `POST /me/orders/{order_uuid}/select-office` | **Solo** para modalidades `office` / `office_cod`: oficina donde el destinatario retira | Opcional (ver abajo) |
| 10 | `POST /me/orders/{order_uuid}/create-guide` | Emitir la guía con el transportista | `status` = `confirmed`; `tracking_numbers[].guide_number` |
| 11 | `GET /me/orders/{order_uuid}/guide-pdf/{guide_number}` | Descargar la etiqueta (Tealca también en EPL: `guide-epl`) | Archivo para imprimir |
| 12 | `GET /me/orders/{order_uuid}` | Seguimiento: estado con el transportista | `carrier_status`, `carrier_events` |

### Pasos 1 y 2 — Dirección de origen

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/addresses \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Almacén Chacao",
    "address_type": "almacen",
    "doc_type": "V",
    "doc_number": "18456789",
    "full_name": "María González",
    "email": "despachos@tiendaavila.com",
    "phone_code": "0414",
    "phone_number": "2345678",
    "latitude": 10.4969,
    "longitude": -66.8528,
    "city_id": 13,
    "gps_address": "Av. Francisco de Miranda, Chacao, Caracas, Miranda",
    "address_detail": "Edificio Parque Cristal, Torre Este, piso 3, oficina 3-2",
    "is_default": true
  }'
```

El estado, municipio y parroquia se calculan solos a partir de `latitude`/`longitude`. Para `city_id` usa una de las ciudades que devuelve `GET /geography/location/cities` para esas mismas coordenadas: es la ciudad que verá el transportista en la guía.

### Pasos 3 y 4 — Destinatario

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/recipients \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c" \
  -H "Content-Type: application/json" \
  -d '{
    "doc_type": "V",
    "doc_number": "20123456",
    "full_name": "Carlos Pérez",
    "email": "carlos.perez@gmail.com",
    "phone_code": "0424",
    "phone_number": "7654321",
    "latitude": 10.1579,
    "longitude": -67.9972,
    "city_id": 9,
    "gps_address": "Av. Bolívar Norte, Valencia, Carabobo",
    "address_detail": "Calle 137, Residencias Los Mangos, apto 4B, frente a la panadería"
  }'
```

Antes de crear un destinatario nuevo puedes buscarlo con `GET /me/recipients?search=Carlos`.

### Paso 5 — Orden

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/orders \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c" \
  -H "Content-Type: application/json" \
  -d '{
    "sender_address_id": "8f14e45f-ceea-4f6a-9c1b-2a5d3e7b9c01",
    "recipient_id": "3c9a7d21-5b4e-4f80-a6c2-9e1f0b7d4a55",
    "packages": [
      {
        "package_type": "box",
        "height_cm": 15,
        "width_cm": 20,
        "length_cm": 30,
        "weight_kg": 2.5,
        "content": "Zapatos deportivos talla 42",
        "declared_value": 9500
      }
    ]
  }'
```

- `declared_value` es el valor de la mercancía **en bolívares** (se usa para el seguro).
- Cada paquete genera **su propia guía**. Una orden con 3 paquetes produce 3 números de guía.
- `recipient_id` es obligatorio en la práctica: sin destinatario la orden no se puede cotizar (`4016`).

### Pasos 6 y 7 — Cotizar y leer los precios

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/orders/b7e2c4a1-9d3f-4e6b-8a15-0c2f7d9e3b48/quote-async \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c"
```

Responde `202` con `{"data": {"id": "e5d1f0c3-7a2b-4c9e-b6d4-3f8a1e2c5b77", "status": "pending"}}`. Con ese `id` consulta el estado:

```bash
curl https://api.fulfyplus.com/api/v1/logistics/quotes/e5d1f0c3-7a2b-4c9e-b6d4-3f8a1e2c5b77/status \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c"
```

La cotización consulta a todos los transportistas en paralelo; cada uno responde a su ritmo. `status` vale `pending` (aún no hay opciones), `partial` (ya hay opciones y otros transportistas siguen calculando), `completed` (todos terminaron y hay al menos una opción) o `failed` (todos terminaron y ninguno puede hacer el envío). Puedes elegir una opción apenas aparezca, sin esperar a `completed`. En cada elemento de `resolved[]`:

- `status: "resolved"` → trae `quotes[]`, una por modalidad de entrega disponible.
- `status: "unavailable"` / `"error"` → ese transportista no puede hacer el envío; el motivo está en `error_message`.
- `total` es el precio de referencia del transportista al público; es `null` cuando el transportista no publica tarifa al público (Menssajero, Yummy, Tealca).
- **`applied_price.amount`** es el precio del envío y `applied_price.contract` dice con qué contrato sale la guía.
- **`fulfy_fee`** es lo único que Fulfy te cobra por la guía: `amount` es el monto y `kind` de dónde sale.

**Cómo leer los dos montos.** Nunca los sumes. Quién paga `applied_price.amount` depende del caso:

| Caso | `applied_price.amount` | `fulfy_fee.amount` | Quién paga el envío |
|---|---|---|---|
| Contrato Fulfy, sin cobro en destino | Tarifa Fulfy más seguro | El mismo monto, menos el descuento si lo hay. Es un solo cobro | Tú, a Fulfy |
| Cobro en destino (`office_cod`, `door_to_door_cod`) | Precio público del transportista | Cargo de Fulfy por emitir la guía, distinto del precio del envío | El destinatario, al transportista |
| Contrato propio (`contract: "own"`) | Precio de tu contrato; revisa `unit`, puede venir en USD o VES | Cargo de Fulfy por emitir la guía, distinto del precio del envío | Tú, al transportista, según tu contrato |

Valores de `fulfy_fee.kind`:

| `kind` | Qué significa | `amount` |
|---|---|---|
| `balance_debit` | Se descuenta de tu Saldo USD. `sufficient_balance` compara tu saldo con este `amount`, no con `applied_price.amount` | Monto a descontar |
| `weekly_receipt` | Se suma a tu recibo semanal | Monto a sumar |
| `included_in_plan` | La guía está incluida en tu plan | `null` |
| `additional_guide` | Superaste las guías incluidas y se factura como guía adicional | Precio de la guía adicional |
| `none` | Fulfy no cobra por esta guía | `null` |

Ejemplo con cobro en destino: el destinatario paga 8.69 USD al transportista y a ti se te descuentan 0.15 USD de tu Saldo USD:

```json
{
  "delivery_type": "office_cod",
  "applied_price": {"amount": 8.69, "unit": "USD", "contract": "fulfy"},
  "fulfy_fee": {"kind": "balance_debit", "amount": 0.15, "unit": "USD", "sufficient_balance": true}
}
```

Ejemplo con contrato propio: el envío va por tu contrato con el transportista y Fulfy solo cobra su cargo:

```json
{
  "delivery_type": "office",
  "applied_price": {"amount": 6480.5, "unit": "VES", "contract": "own"},
  "fulfy_fee": {"kind": "balance_debit", "amount": 0.15, "unit": "USD", "sufficient_balance": true}
}
```

### Paso 8 — Elegir la opción

Copia **exactamente** el `gateway_slug` del transportista y el `delivery_type` de la opción elegida:

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/orders/b7e2c4a1-9d3f-4e6b-8a15-0c2f7d9e3b48/select-quote \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c" \
  -H "Content-Type: application/json" \
  -d '{"gateway_slug": "zoom-3a75c95d-498f-4139-8f1c-1a318e7e91ee", "delivery_type": "office"}'
```

El precio nunca lo envías tú: se toma de la cotización calculada.

### Paso 9 — Oficina de retiro (solo office y office_cod)

Busca las oficinas del transportista elegido cerca del **destinatario** y envía el `id` de la que prefiera:

```bash
curl "https://api.fulfyplus.com/api/v1/logistics/offices/nearby?lat=10.1579&lng=-67.9972&gateway_slug=zoom&limit=3"

curl -X POST https://api.fulfyplus.com/api/v1/me/orders/b7e2c4a1-9d3f-4e6b-8a15-0c2f7d9e3b48/select-office \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c" \
  -H "Content-Type: application/json" \
  -d '{"office_id": "7d750ab6-65a8-4024-b53b-5d7ed1ed81cd"}'
```

Si te saltas este paso, la guía sale hacia la oficina del transportista más cercana al destinatario (la misma que se usó para cotizar, `destination_office_id` en la cotización). La oficina debe ser del mismo transportista que elegiste en el paso 8. Para entregas a domicilio no se usa.

### Paso 10 — Crear la guía

```bash
curl -X POST https://api.fulfyplus.com/api/v1/me/orders/b7e2c4a1-9d3f-4e6b-8a15-0c2f7d9e3b48/create-guide \
  -H "X-API-Key: fp_live_9f3c2a7b1d4e6f8a0b2c4d6e8f0a1b3c"
```

La respuesta es la orden con `status: "confirmed"` y un elemento en `tracking_numbers[]` por paquete, con su `guide_number`. En ese momento se cobra la guía.

- Si algunos paquetes salieron y otros no, la respuesta trae los que fallaron en `guide_errors[]`: solo se cobra lo emitido y puedes **volver a llamar a este mismo endpoint** para emitir los que faltan.
- Si recibes `409` con `7087`, la emisión de esa orden ya está en curso: espera unos segundos y consulta `GET /me/orders/{order_uuid}` en lugar de repetir la llamada.

### Paso 11 — Etiqueta para imprimir

| Transportista | Formato | Endpoint |
|---|---|---|
| Todos | PDF (`has_pdf: true`) | `GET /me/orders/{order_uuid}/guide-pdf/{guide_number}` |
| Tealca | Además EPL para impresora térmica Zebra (`has_epl: true`) | `GET /me/orders/{order_uuid}/guide-epl/{guide_number}` |

### 4. Modalidades de entrega (delivery_type)

| `delivery_type` | Significado | Transportistas | ¿Usa oficina de destino? |
|---|---|---|---|
| `office` | El destinatario retira en una oficina | Zoom, MRW, Tealca | Sí (paso 9) |
| `office_cod` | Retiro en oficina, **cobro en destino** por el destinatario | Zoom, Tealca | Sí (paso 9) |
| `door_to_door` | Entrega en la dirección del destinatario | Zoom, MRW, Tealca | No |
| `door_to_door_cod` | Entrega a domicilio con **cobro en destino** | Zoom, Tealca | No |
| `last_mile` | Entrega urbana inmediata en moto, a cargo del transportista | Menssajero, Yummy | No |
| `last_mile_xl` | Entrega urbana del transportista para paquetes grandes | Yummy | No |
| `last_mile_xxl` | Entrega urbana del transportista para paquetes muy grandes | Yummy | No |

La cotización solo devuelve las modalidades que el transportista ofrece para esa ruta y que tu cuenta puede usar.

### 5. Estados de la orden y de la guía

`status` (la orden):

| Valor | Cuándo | Qué puedes hacer |
|---|---|---|
| `draft` | Recién creada, o después de volver a cotizar | Editar (`PUT`), cotizar, eliminar |
| `quoted` | Ya elegiste una opción (paso 8) | Elegir oficina, crear guía, volver a cotizar, eliminar |
| `confirmed` | La guía fue emitida | Descargar etiqueta, seguir el envío, anular guía |
| `cancelled` | Orden eliminada o guía anulada | Nada |

`carrier_status` (la guía con el transportista, se actualiza sola): `created` (emitida, aún no la recibe), `dispatched` (el transportista ya recibió el paquete), `in_transit`, `delivered`, `returned`, `canceled`, `unknown`. El historial completo está en `carrier_events[]`.

### 6. Reglas importantes

- **Volver a cotizar** (paso 6 otra vez) borra la opción y la oficina elegidas y regresa la orden a `draft`; repite los pasos 7 a 9. Después de emitida la guía ya no se puede cotizar (`4017`).
- `PUT /me/orders/{order_uuid}` reemplaza remitente, destinatario y paquetes, y solo funciona en `draft` (`4014`).
- `DELETE /me/orders/{order_uuid}` solo funciona antes de emitir la guía. Con la guía ya emitida usa `POST /me/orders/{order_uuid}/cancel-guide`, que anula con el transportista mientras no haya recibido el paquete (`can_cancel_guide: true`) y devuelve el cobro.
- Dimensiones: si omites `height_cm`, `width_cm` o `length_cm` (o envías `0`), se usa 5 cm para esa medida.
- Los slugs de los transportistas pueden incluir un sufijo (ej. `zoom-3a75c95d-...`): tómalos siempre de `GET /logistics/gateways` o de la cotización, no los escribas a mano. Para filtrar oficinas basta el prefijo (`gateway_slug=zoom`).
- Documentos (`doc_type`): `V` venezolano, `E` extranjero residente, `J` empresa (RIF), `G` gobierno, `P` pasaporte. Teléfonos: `phone_code` es la operadora o área (`0412`, `0414`, `0416`, `0424`, `0426`, `0212`) y `phone_number` el resto del número, de 7 dígitos.
- Listados (`GET /me/orders`, `GET /me/recipients`): paginación por cursor. Para la página siguiente envía `after=<next_cursor>`; para la anterior `before=<prev_cursor>`. `has_more` indica si hay más.

### 6.1 Cotizar sin crear orden, para tiendas en línea

En el checkout de una tienda todavía no hay pedido pagado, así que no conviene crear órdenes ni destinatarios. El flujo es:

1. **Ciudad del comprador:** `GET /geography/states` y `GET /geography/states/{state_id}/cities` para que el comprador elija su ciudad (`city_id`).
2. **Cotizar:** `POST /me/quotes` con `sender_address_id`, el punto de entrega (`destination.latitude`, `destination.longitude`, `destination.city_id` opcional) y los bultos (`packages[]`, con `declared_value` en bolívares; `0` usa el mínimo de cada transportista). Responde `202` con el `id`.
3. **Precios:** `GET /logistics/quotes/{id}/status?only_priced=true` cada 1-2 segundos hasta `completed` o `failed`. Cada opción trae `applied_price` en USD, igual que al cotizar una orden.
4. **Moneda:** si la tienda cobra en bolívares o en euros, `GET /me/exchange-rate` da las tasas BCV del día del dólar y del euro.
5. **Pedido pagado:** crea el destinatario y la orden con `external_reference` igual al número de pedido de la tienda; si repites la creación con la misma referencia recibes la misma orden (`200`), así un reintento nunca duplica el envío. Recupera la orden con `GET /me/orders?external_reference=...`. Luego sigue el flujo normal desde el paso 6.

### 6.2 Envíos desde un almacén Fulfy

Si tienes espacio contratado en un almacén Fulfy (m² o m³), tu mercancía puede salir directamente desde allí:

1. **Contrato:** `GET /me/warehouse/contracts` da tus contratos con el espacio contratado, ocupado y libre, y su estado. Con un recibo del almacén vencido (`past_due`) no se reciben entradas ni se despachan guías; los retiros en persona sí se permiten. `storage_billing` dice cómo se cobra el almacenaje: en `space` pagas cada mes el espacio contratado; en `idle` el espacio contratado es solo tu tope y se cobra únicamente lo que ocupa cada producto **sin rotación**, es decir, sin guías creadas en los últimos `free_months` meses y recibido hace al menos `free_months` meses. En `GET /me/warehouse/products`, `idle_billing_from` dice desde cuándo entra en el recibo cada producto si no le creas otra guía.
2. **Productos:** `POST /me/warehouse/products` con el `contract_id` y las medidas y el peso de **una unidad**. `GET /me/warehouse/products` es tu inventario (unidades disponibles y reservadas). Puedes corregir medidas y peso hasta que el almacén recibe el producto por primera vez; desde ahí quedan fijas (`measures_verified_at`).
3. **Entrada:** `POST /me/warehouse/inbounds` avisa qué vas a llevar. Al recibirla, el almacén confirma las cantidades reales y, la primera vez que llega cada producto, mide y pesa una unidad y corrige lo que hayas cargado mal: si la mercancía cabe en tu espacio se suma al inventario; si no, la orden entera se rechaza con el motivo y las cifras en `rejection_detail` y tu inventario no cambia.
4. **Envío:** `POST /me/warehouse/cart/preview` arma los paquetes (por defecto uno solo; puedes repartir las unidades en varios) y devuelve sus medidas, peso y el costo de embalaje por paquete. `POST /me/warehouse/cart/checkout` crea la orden con origen en el almacén; desde ahí sigue el flujo normal desde el paso 6 (cotizar, elegir, crear la guía). Los paquetes de esa orden no se editan con `PUT /me/orders/{order_uuid}`.
5. **Inventario y guía:** al crear la guía las unidades quedan reservadas; salen del inventario cuando el transportista recibe el paquete y vuelven a disponibles si la guía se anula. `GET /me/warehouse/movements` muestra cada cambio y `GET /me/warehouse/orders/{order_id}/packing-list` qué va en cada paquete.
6. **Retiro en persona** (para emergencias o mercancía que no rota): `POST /me/warehouse/pickups` con quién retira (nombre y documento); el almacén entrega con una foto de quien retira (`GET /me/warehouse/media/{photo_id}`).


### 7. Errores

Todos los errores responden JSON con la forma:

```json
{"detail": "Orden con ID b7e2c4a1-9d3f-4e6b-8a15-0c2f7d9e3b48 no encontrado [Error Nro. 4001]", "code": "NOT_FOUND", "error_number": 4001}
```

`detail` ya trae un mensaje listo para mostrar con el sufijo `[Error Nro. N]`; usa `error_number` para decidir qué hacer en tu código e inclúyelo si necesitas soporte. Los más comunes del flujo:

| `error_number` | HTTP | Causa | Qué hacer |
|---|---|---|---|
| `9001` | 422 | Datos con formato inválido; `detail` indica el campo | Corrige el campo indicado |
| `2405` / `2406` | 401 | API key inexistente o revocada | Crea otra en el panel |
| `2101` / `2001` | 404 | La dirección o el destinatario no existen en tu cuenta | Revisa `sender_address_id` / `recipient_id` |
| `2102` / `2002` | 400 | Las coordenadas están fuera de Venezuela | Envía coordenadas válidas |
| `4016` | 400 | La orden no tiene destinatario | Asígnale `recipient_id` con `PUT /me/orders/{order_uuid}` |
| `4002` | 400 | `gateway_slug`/`delivery_type` no están en la cotización | Copia los valores de la cotización |
| `4019` | 400 | Intentas elegir oficina o crear la guía sin haber elegido opción | Ejecuta el paso 8 |
| `4004` | 400 | La oficina elegida es de otro transportista | Busca oficinas con el `gateway_slug` elegido |
| `4017` | 400 | La orden ya tiene sus guías | Consulta la orden |
| `7018` | 409 | Saldo USD insuficiente | Recarga en el panel |
| `7002` | 403 | Modalidad de cobro en destino sin un plan que la permita | Elige otra modalidad o solicita el plan |
| `7087` | 409 | La guía de esa orden se está emitiendo ahora | Espera y consulta la orden |
| `4021` / `4022` / `4024` | 409 | La guía no se puede anular: el transportista ya la recibió, ya estaba anulada o el transportista no admite anulaciones | Revisa `carrier_status` |
| `4023` / `4025` / `4027` / `4028` | 500 | El transportista no confirmó la anulación (o solo anuló parte de las guías) | Consulta la orden y reintenta más tarde |

La API aplica límites de uso; si los excedes, recibirás un error con su propio código en lugar de la respuesta esperada.
