---
title: "Crear un envío (genera la guía y cobra créditos)"
method: POST
path: "/shipments"
tags: ["shipments"]
---

# Crear un envío (genera la guía y cobra créditos)

`POST /shipments`

## Request

- Base URL: `https://api.enviadores.com.mx/api/v1`
- URL: `https://api.enviadores.com.mx/api/v1/shipments`
- Auth: HTTP bearer

Crea el envío contra un `rate_id` vigente (≤ 30 min) cotizado con la MISMA llave (y mismo PDV, si aplica). Solo domésticos MX (`customs`/`ocurre`, `country` ≠ MX o campos desconocidos → 422). **Bultos:** los MISMOS que se cotizaron — `package` (uno) o `packages` (2–10 en un solo envío; ambos → 422); un conjunto distinto al cotizado responde 409 `RATE_PACKAGE_MISMATCH`. **CP:** origen/destino inline deben existir en SEPOMEX (422 nombrando `from.postal_code`/`to.postal_code`, antes de retener saldo). **Seguro vs. valor declarado (no son lo mismo):** `insurance.insured_value` es la póliza de plataforma que YA se cotizó — debe coincidir con la cotización (422 `INSURANCE_MISMATCH` si difiere o falta; el total firmado nunca se recalcula al crear); `declared_value` es el valor de la mercancía que se declara a la paquetería (MXN, informativo; algunos servicios lo exigen — `DECLARED_VALUE_REQUIRED`). Remitente/destinatario van inline (la plataforma crea o reutiliza los registros del directorio con dedupe exacto normalizado) o por referencia (`from_id`/`to_id` del directorio — la fila guardada se usa tal cual). El cargo sigue el ciclo autorizar→capturar/anular — un envío rechazado nunca deja fondos retenidos. **Usa `Idempotency-Key`** en todo intento.

## Headers

- `Idempotency-Key` string
- `X-PDV-ID` string

## Request body

- ShipmentCreateRequest: Remitente: exactamente UNO de `from` (inline) o `from_id` (directorio). Destinatario: exactamente UNO de `to` o `to_id`. Ambos miembros de un par → 422; id no visible → 404 ciego. `to_id` requiere `from_id` y el destinatario debe pertenecer a ese remitente (un destinatario de otro remitente responde 404). Un lado referenciado usa la fila guardada tal cual (sin create-or-reuse). Bultos: exactamente UNO de `package` o `packages` — los MISMOS que se cotizaron.
  - `rate_id` string, required: El `id` de una tarifa de POST /rates cotizada con esta misma llave (y mismo X-PDV-ID, si se usó), con menos de 30 minutos. Trátalo como OPACO: cópialo tal cual; su formato no es parte del contrato (difiere entre el carril de prueba y el live) y no debe interpretarse ni validarse.
  - `from` ShipmentParty: Los datos se registran en el directorio del usuario de la llave con dedupe exacto (normalizado por espacios/mayúsculas), así que repetir el mismo remitente/destinatario no crea filas nuevas. Los campos exceden-longitud se recortan al límite de columna.
    - `name` string, required
    - `apellido_paterno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `apellido_materno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `company` string: Razón social (solo remitente; ignorado en destinatario).
    - `phone` string, required
    - `email` string, email
    - `street` string, required
    - `number` string: Número exterior.
    - `colonia` string, required
    - `city` string, required
    - `state` string, required
    - `postal_code` string, required
    - `country` 'MX': Opcional. Solo `MX` (sin distinción de mayúsculas/minúsculas; vacío = MX) — cualquier otro valor responde 422 (v1 es doméstico MX).
    - `rfc` string: Solo remitente; usado por algunas paqueterías.
  - `from_id` string: Id de un remitente del directorio. Mutuamente excluyente con `from`.
  - `to` ShipmentParty: Los datos se registran en el directorio del usuario de la llave con dedupe exacto (normalizado por espacios/mayúsculas), así que repetir el mismo remitente/destinatario no crea filas nuevas. Los campos exceden-longitud se recortan al límite de columna.
    - `name` string, required
    - `apellido_paterno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `apellido_materno` string: DEPRECATED (2026-09-21): el nombre completo va en `name`. Aún se acepta y se integra a `name` al guardar.
    - `company` string: Razón social (solo remitente; ignorado en destinatario).
    - `phone` string, required
    - `email` string, email
    - `street` string, required
    - `number` string: Número exterior.
    - `colonia` string, required
    - `city` string, required
    - `state` string, required
    - `postal_code` string, required
    - `country` 'MX': Opcional. Solo `MX` (sin distinción de mayúsculas/minúsculas; vacío = MX) — cualquier otro valor responde 422 (v1 es doméstico MX).
    - `rfc` string: Solo remitente; usado por algunas paqueterías.
  - `to_id` string: Id de un destinatario del directorio (debe pertenecer al `from_id` enviado). Mutuamente excluyente con `to`; requiere `from_id`.
  - `package` object
    - `weight_kg` number, required
    - `length_cm` number
    - `width_cm` number
    - `height_cm` number
    - `type` 'paquete': Opcional. Solo `paquete` en v1 (sin distinción de mayúsculas; vacío/null = paquete). Cualquier otro valor — p. ej. `sobre` — responde 422 tanto en /rates como en /shipments (envelope aún no soportado).
  - `packages` object[]: VARIOS bultos que viajan como UN solo envío (un cargo, una cotización). Mutuamente excluyente con `package`. La lista cotizada y la reservada deben ser IDÉNTICAS (mismo orden, mismas medidas) o `POST /shipments` responde 409 `RATE_PACKAGE_MISMATCH`. Solo se cotizan/reservan servicios que soportan multi-bulto.
    - `weight_kg` number, required: Peso real del bulto (kg).
    - `length_cm` number
    - `width_cm` number
    - `height_cm` number
  - `insurance` object: La cobertura que YA se cotizó (`insured_value` buys coverage; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it). Omitir `insurance` acepta la cobertura cotizada; mandar un `insured_value` DISTINTO → 422 `INSURANCE_MISMATCH` (el total firmado nunca se recalcula al crear). Una tarifa cuya paquetería no ofrece cobertura (`insurance_not_supported: true`) → 422 `INSURANCE_NOT_AVAILABLE`.
    - `insured_value` number, required: Valor asegurado (MXN) — el mismo de la cotización.
  - `declared_value` number: Valor de la mercancía DECLARADO A LA PAQUETERÍA (MXN). NO es seguro, pero la paquetería puede cobrarlo: debe ser EXACTAMENTE el valor con el que se cotizó (el asegurado) — un valor que la cotización no llevaba responde 422 `DECLARED_VALUE_MISMATCH`, salvo en servicios que lo tratan como informativo (la recuperación de `DECLARED_VALUE_REQUIRED`). Si se omite se declara el valor cotizado; si la cotización no llevaba ninguno, no se inventa. *declared_value must equal the value the rate was quoted with; a carrier may price it.*
  - `contenido` string: Descripción del contenido (aparece en la guía cuando la paquetería lo soporta).
  - `reference` string: RESERVADO — aceptado pero no persistido en v1.
  - `metadata` object: RESERVADO en el carril live (aceptado, no persistido). En el carril SANDBOX, `metadata.test_scenario` fuerza una falla para rehearsal (ver `x-test-mode`).
    - `test_scenario` 'insufficient_funds' | 'rate_expired' | 'vendor_error': Solo llaves `ek_test_`, y solo sobre ofertas SIMULADAS del catálogo: sobre una tarifa del carril FedEx de prueba (que va al entorno real de FedEx) responde `422 TEST_SCENARIO_NOT_AVAILABLE`. `insufficient_funds` → 402 CREDIT_ERROR (details {required, available, shortfall}) sin importar el saldo; `rate_expired` → 409 RATE_EXPIRED; `vendor_error` → 422 VENDOR_ERROR (retryable=false).

## Response `200`

Envío creado (o repetido idempotentemente — header `Idempotent-Replay: true`; o deduplicado contra una guía existente — `duplicate: true`, en cuyo caso `carrier`/`service` pueden ser null).

- object
  - `success` unknown, required
  - `data` object, required
    - `shipment` object, required
      - `id` string, required
      - `guia` string, nullable: Null cuando la paquetería publica la guía con retraso (`tracking_pending: true`); consúltala después vía GET /shipments/{id}.
      - `carrier` string, nullable: Null posible en respuestas `duplicate: true`.
      - `service` string, nullable
      - `status` string, required: Estatus canónico (created | collected | in_transit | out_for_delivery | ready_for_pickup | delivered | exception | returned | cancelled | unknown).
      - `total` number, nullable: Monto cobrado (MXN).
      - `currency` 'MXN', required
      - `label_url` string, nullable, required: Ruta de NUESTRO endpoint autenticado de etiquetas (/api/v1/labels/{id}) — nunca la URL cruda del proveedor.
      - `tracking_pending` boolean, required
      - `created_at` string, nullable: ISO-8601 UTC (Z).
    - `duplicate` boolean: Presente (true) cuando la paquetería devolvió una guía ya registrada y la plataforma deduplicó sin doble cargo.
    - `recovered` boolean: Presente (true) cuando la etiqueta se recuperó tras un error transitorio del proveedor.
  - `requestId` string, required: Correlación de logs; también en el header `X-Request-Id`.

## Other responses

- `400`: `INVALID_JSON` · `VALIDATION_FAILED` (rechazo del núcleo, details.validationErrors) · `INVALID_VENDOR` · `OCURRE_CARRIER_MISMATCH` · `IDEMPOTENCY_KEY_INVALID` (header presente pero vacío, >128 chars o con caracteres no imprimibles; omitirlo es válido, mandarlo mal no).
- `401`: `UNAUTHORIZED` — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos).
- `402`: `CREDIT_ERROR` — créditos insuficientes. `details`: `{required, available, shortfall}`. Recarga y reintenta con el MISMO `Idempotency-Key`.
- `403`: `INSUFFICIENT_SCOPE` — a la llave le falta el scope requerido · `PDV_NOT_ALLOWED` — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde `429 RATE_LIMITED` (el 429 tiene precedencia sobre este 403).
- `404`: `NOT_FOUND` — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño).
- `409`: `RATE_NOT_FOUND` (cotiza de nuevo) · `RATE_EXPIRED` (>30 min) · `RATE_TAMPERED` · `RATE_PDV_MISMATCH` (se cotizó bajo otro PDV) · `RATE_PACKAGE_MISMATCH` (el `package`/`packages` enviado difiere del cotizado — medidas, peso o valor declarado — reenvía el MISMO conjunto que cotizaste, o cotiza de nuevo) · `GUIA_COLLISION` · `IDEMPOTENCY_KEY_REUSED` (misma llave en otro endpoint — desde 2026-08 el carril web cuenta como otro endpoint) · `IDEMPOTENCY_KEY_PAYLOAD_MISMATCH` (misma llave, cuerpo o `X-PDV-ID` distintos: manda una llave nueva) · `IDEMPOTENCY_KEY_EXPIRED` (la llave ya se completó hace más de 15 min: consulta `GET /shipments` o manda una llave nueva) · `RATE_ADDRESS_MISMATCH` (la cotización corresponde a OTRA ruta: los CP de origen/destino con los que se está creando el envío no son los que se cotizaron — cotiza de nuevo con las direcciones definitivas; desde 2026-08 esto rechaza en lugar de solo registrarse) · `REQUOTE_REQUIRED` (el precio del envío no coincide con el de la cotización; no se espera en este carril — el total se toma de la MISMA fila firmada que el ejecutor vuelve a verificar — y se documenta por completitud: cotiza de nuevo).
- `422`: `VALIDATION_ERROR` (contrato público, details.fields — incluye CP inexistente en SEPOMEX) · `INSURANCE_NOT_AVAILABLE` (se mandó `insurance` sobre una tarifa cuya paquetería no ofrece cobertura — venía con `insurance_not_supported: true`; cotiza de nuevo y elige una con `insurance_included`, o crea sin `insurance`) · `INSURANCE_MISMATCH` (`insurance.insured_value` explícito distinto de la cobertura cotizada — `details {quoted_insured_value, requested_insured_value}`; omitirlo acepta la cobertura cotizada) · `DECLARED_VALUE_MISMATCH` (`declared_value` distinto del que se cotizó, o presente cuando la cotización no llevaba ninguno y la paquetería lo cobra — `details {quoted_declared_value, requested_declared_value}`; cotiza de nuevo) · `DECLARED_VALUE_REQUIRED` (el servicio exige `declared_value`; no se cobró) · `PRICING_CONFIG_ERROR` (margen mal configurado; contacta soporte) · `MISSING_PHONE` (remitente y/o destinatario sin teléfono válido de 10 dígitos; `details.missing` lista cuál).
- `429`: `RISK_LIMIT` (tope de guías/gasto diario de la cuenta, o tope de gasto de la LLAVE en ventana móvil de 24 h — este último con `details {daily_cap_mxn, spent_24h_mxn, attempted_mxn}`) · `RATE_LIMITED` (límite por llave; ver `Retry-After`).
- `500`: `SHIPMENT_ERROR` (falla pre-commit; el cargo se revirtió — `details.refunded`) · `ADDRESS_ERROR` (no se pudo registrar remitente/destinatario; reintenta) · `CREDIT_ERROR` (falla interna de créditos) · **`POST_COMMIT_ERROR` — EL ENVÍO ES REAL y el cargo NO se revierte (`details.committed: true`). NO reintentes con llave nueva: consulta `GET /shipments`.**
- `502`: `VENDOR_ERROR` — la paquetería rechazó/falló ANTES del commit; el cargo se revirtió (`details.refunded`). `details.vendorError: true`; el status HTTP refleja el de la paquetería (4xx/5xx). Reintenta solo si `retryable` aplica (falla de red). **Toda falla de paquetería trae `details.nextStep.action`** — `fix_data` (corrige los campos de `details.nextStep.fields`: `{party, field, label, problem, min?, max?}`; código `SHIPMENT_DATA_INVALID`, 422), `choose_other_option` (otra opción de la misma cotización; `SERVICE_UNAVAILABLE` / `CARRIER_ACCOUNT_BALANCE`), `check_shipments` (la guía podría existir: consulta `GET /shipments` antes de reintentar; `LABEL_MAY_EXIST`), `retry` (reintenta una vez con el MISMO `Idempotency-Key`), `add_declared_value`, `adjust_package`.

---

[API](https://skmtc.dev/enviadores/apis/enviadores-public-api.md) · [All operations](https://skmtc.dev/enviadores/apis/enviadores-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/enviadores/apis/enviadores-public-api/revisions/3633f08c889c?raw)
