Drop · API de Integraciones
Guías

Órdenes

El flujo completo para crear una orden, las reglas del contrato, la idempotencia, cómo consultar lo que quedó y cómo cancelarla.

Esta guía arranca con el flujo completo porque crear la orden es el último paso, no el primero: casi todo lo que necesitás validar se valida antes.

El flujo, de principio a fin

Es el mismo recorrido que hace la pantalla de crear orden del dropshipper. Si lo seguís, tu integración se comporta igual que la app.

#Qué resolvésLlamadaPor qué acá
1Qué pide la dirección de tu paísGET /geo/countries/:id/address-configTe dice si hace falta departamento, municipio y qué niveles extra existen. Cuáles son obligatorios lo define la paquetería del paso 5
2Departamento y municipioGET /geo/states?countryId=De acá salen stateId y cityId. Cacheable: cambian poco
3Qué vas a mandar y a qué precioGET /products · GET /products/:idprice es tu precio de venta y cost lo que te cuesta; con los dos sabés tu margen
4Que todavía haya existenciaGET /products/stock?ids=Justo antes de crear. Crear no reserva: entre que consultás y creás puede cambiar
5Qué paquetería puede llevarlaGET /couriers/availableSolo las que cubren esa ruta; la lista del país incluye algunas que no llegan. Elegirla también define si hace falta extraLevels: Cargo Expreso los exige
6Crear la ordenPOST /ordersCon unitPrice si querés fijar el precio, y courierId si elegiste una paquetería
7Confirmar qué quedóla respuesta del paso 6Mirá requestedCourier: si no es null, tu paquetería no se pudo usar
8Enterarte de los cambiostu webhook · GET /orders?updatedSince=El webhook avisa; el listado reconcilia lo que se haya perdido

Los pasos 1, 2 y 3 se pueden cachear. Los pasos 4 y 5 conviene hacerlos con la orden ya armada, porque dependen del destino y de lo que vayas a mandar.

Crear una orden

curl -X POST '{BASE_URL}/integrations/v1/orders' \
  -H 'X-Drop-Key: …' -H 'X-Drop-Timestamp: …' \
  -H 'X-Drop-Nonce: …' -H 'X-Drop-Signature: …' \
  -H 'Content-Type: application/json' \
  -d '{
    "externalOrderId": "TU-10024",
    "customer": {
      "name": "Ana",
      "lastName": "Pérez",
      "phoneAreaCode": "503",
      "phone": "77778888",
      "email": "ana@example.com"
    },
    "shipping": {
      "address": "Calle 1, casa 2",
      "referencePoint": "Frente al parque",
      "stateId": "64a302eae2212e31584a819e",
      "cityId": "64a30322e16d1dbcc073b5bd"
    },
    "items": [
      {
        "catalogId": "65b0000000000000000000c1",
        "quantity": 2,
        "unitPrice": 24.99
      }
    ],
    "cod": true
  }'

La respuesta 201 trae la orden creada: su id, el orderNumber visible, tu externalOrderId, el status inicial, el codAmount calculado, la paquetería con la que quedó (courier) y requestedCourier. Los campos exactos están en la referencia.

Mirá requestedCourier en cada creación

Si mandaste courierId y esa paquetería no puede llevar la orden, se despacha igual con otra (es preferible a no despacharla) y ahí te decimos cuál pediste y por qué no se pudo. Cuando se respetó tu elección, o cuando no pediste ninguna, viene null.

reasonQué pasó
no_coverageEsa paquetería no cubre la ruta de la orden
coverage_check_failedNo se pudo verificar la cobertura. No es un problema de tu pedido
route_not_evaluableFaltan datos de la ruta, así que ni se llegó a evaluar

Este dato viaja una sola vez. El motivo no se guarda en la orden, así que GET /orders/:id no lo devuelve, y una repetición idempotente del mismo externalOrderId trae requestedCourier: null aunque en la creación original sí se hubiera descartado una. Guardalo de tu lado cuando llegue.

Campos que importan

Los límites de longitud y formato de cada campo están en la referencia. Lo que no se deduce del esquema:

CampoNotas
externalOrderIdEl id de la orden en tu sistema. Es la llave de idempotencia
customer.phoneAreaCodeSin el +
shipping.stateId, shipping.cityIdObligatorios, de GET /geo/states. Sin ellos la orden no se puede despachar
shipping.extraLevels[{ "levelKey": "…", "valueId": "…" }]. Los pide Cargo Expreso; con cualquier otra paquetería son opcionales. Ver Configuración de dirección
shipping.latitude, shipping.longitudeUbicación del cliente. En un país con requestCoordinatesFromCustomer: true (hoy Costa Rica) omitirlas no da error, pero la orden nace en pending_correction y no se puede despachar hasta que alguien la complete a mano. Van las dos o ninguna
items[].catalogIdDel catálogo. El de la variante si el producto tiene variantes
items[].unitPriceLo que le cobrás al cliente por unidad en esta orden. Si lo omitís se usa el price de tu catálogo. Máximo 2 decimales
codAmountTotal exacto a cobrar, para cerrar centavos que el precio unitario no alcanza. Ver Cobrar un total exacto
courierIdSi se omite, se resuelve la paquetería por defecto del país
confirmForDispatchDefault true: la orden nace lista para despacho

Reglas del contrato

  • No mandás el sellerId. La orden es tuya porque tu credencial lo es. Un catalogId que no esté en tu catálogo se rechaza con 404, igual que uno que no existe.

  • El total lo calcula el servidor, multiplicando cantidades por precios. Lo que fijás es el precio unitario de cada línea con items[].unitPrice, igual que el dropshipper lo escribe en la app. codAmount solo sirve para ajustar centavos de redondeo sobre esa cuenta, nunca para declarar un monto propio.

  • Cualquier campo que el API no reconozca devuelve 400, en vez de ignorarse en silencio. Si te devuelve eso, revisá el nombre del campo.

  • Si el país pide la ubicación, mandala. Cuando GET /geo/countries devuelve requestCoordinatesFromCustomer: true, omitir shipping.latitude/longitude no da error al crear, pero la orden nace en pending_correction ("Verificar") con el motivo Ubicación no proporcionada y se queda ahí hasta que alguien la complete a mano. Para ese caso el bloque de envío se ve así:

    "shipping": {
      "address": "100 m sur del parque central, casa verde",
      "referencePoint": "Portón negro, frente a la pulpería",
      "stateId": "68e7d029038deeee9c43d8ee",
      "cityId": "6914f192337c79a9bb816d96",
      "latitude": 9.9355,
      "longitude": -84.0912
    }
  • Las líneas con el mismo catalogId se consolidan en una sola con la suma de las cantidades. Si las repetís con unitPrice distintos se rechaza con 400: al quedar una sola línea no habría dónde guardar el segundo precio, y preferimos decírtelo antes que elegir uno por vos.

  • Los productos propios van en su propia orden. Una orden con productos propios (origin: seller_owned) no puede llevar productos de proveedor ni productos propios de otro dueño: se rechaza con 422 MIXED_PRODUCT_ORIGINS. Productos de varios proveedores sí se pueden mezclar.

  • Crear una orden no reserva existencia. Consultá GET /products/stock antes si tu flujo lo necesita.

Referencia: POST /orders.

Cobrar un total exacto

El total sale de Σ(unitPrice × quantity) y el precio unitario admite 2 decimales, así que hay totales que no se pueden expresar. Para cobrar $43.99 por 2 unidades haría falta un unitario de 21.995, que se rechaza.

Para eso está codAmount: mandás el precio unitario redondeado y el total que de verdad cobrás.

{
  "items": [{ "catalogId": "65b0…c1", "quantity": 2, "unitPrice": 22.0 }],
  "codAmount": 43.99
}

La orden queda con $43.99 como monto a cobrar, y la diferencia de un centavo se registra como descuento (es lo que hace que el detalle de la orden cuadre en la app del dropshipper).

Dos límites, para que el campo no se convierta en una vía para declarar montos arbitrarios:

  • Solo puede ajustar hacia abajo. Un codAmount mayor que la suma de las líneas devuelve 400; para cobrar más, subí el unitPrice.
  • El ajuste es de un centavo por unidad, con un mínimo de $0.05. Un total que se aleje más devuelve 400 con la suma esperada en details.

Si no lo mandás, no cambia nada: el total es la suma de las líneas.

Idempotencia

externalOrderId es la llave de idempotencia:

  • Mismo id + mismo cuerpo: devuelve la misma orden, con el mismo id y orderNumber. No crea otra. Reintentá con confianza tras un timeout o un 5xx.
  • Mismo id + cuerpo distinto: 422 IDEMPOTENCY_KEY_REUSED. No es un reintento: es una orden nueva usando un id viejo. Usá un id nuevo.
  • Mismo id, dos peticiones a la vez: la segunda recibe 409. Reintentá en unos segundos.

La llave se retiene 30 días. Pasado ese plazo, repetir el mismo externalOrderId crea una orden nueva.

Consultar tus órdenes

curl -X GET '{BASE_URL}/integrations/v1/orders?limit=50&status=inroute' \
  -H 'X-Drop-Key: …' -H 'X-Drop-Timestamp: …' \
  -H 'X-Drop-Nonce: …' -H 'X-Drop-Signature: …'
FiltroNotas
statusUno de los valores de Estados de la orden. Cualquier otro devuelve 400
updatedSinceISO 8601. Solo las modificadas desde esa fecha, comparando contra updatedAt. Es la vía de reconciliación
page, limitPaginación. limit va hasta 100, default 20

Vienen ordenadas de la más reciente a la más antigua por fecha de creación.

Solo ves las órdenes que vos creaste. Las de otro partner, o las que el dropshipper crea desde su app, no aparecen en tu listado. Una orden que no originaste responde 404, no 403: no confirmamos que ese id exista.

Lo que conviene saber de la respuesta, que es la misma para el listado, el detalle y la creación:

  • orderNumber es el número visible de la orden. Tratalo como texto opaco: no tiene prefijo ni formato garantizado.
  • externalOrderId es tu punto de reconciliación.
  • status es el identificador estable y statusLabel su traducción, que puede cambiar de redacción. Ver Estados de la orden.
  • shipmentNumber y trackingUrl son null hasta que la orden se despacha.
  • updatedAt es contra lo que compara updatedSince.
  • Los costos de la orden se explican en Costos de la orden.

El detalle de la orden no incluye las líneas de productos ni los datos del cliente final que enviaste al crearla: esos los tenés en tu sistema, indexados por externalOrderId.

Referencia: GET /orders · GET /orders/{id}.

Costos de la orden

Son los datos para calcular tu utilidad real por pedido. Vienen en el listado, en el detalle, en la creación y en el webhook, siempre con la misma forma: lo que todavía no existe viene en null, nunca ausente.

CampoTiene valorQué es
shippingCostDesde el despachoCosto de envío de esta orden, con las condiciones de tu cuenta. Es el que se usa para liquidarte la orden y el que se te descuenta si no se entrega
codCommissionAmountDesde el despachoComisión por cobro contra entrega aplicada a esta orden, con tu comisión preferencial si la tenés
codCommissionPercentageDesde el despachoPorcentaje de esa comisión, como fracción: 0.04 es 4 %
estimatedCodCommissionAmountDesde la creaciónComisión estimada con la paquetería y el monto del momento de crear la orden. Sirve hasta el despacho
  • El despacho es fulfilled. Ahí la paquetería fija el flete y la comisión, y desde entonces no cambian aunque el envío siga avanzando.
  • La comisión se descuenta solo si la orden se entrega y se cobra. Si no se entrega, lo que se te descuenta es el shippingCost.
  • Preferí siempre el valor real sobre el estimado. Cuando codCommissionAmount tiene valor, estimatedCodCommissionAmount ya no aplica.
  • Las órdenes despachadas antes de que existieran estos campos traen shippingCost, pero la comisión real en null. Para esas, usá la estimada.
  • No uses el dropPrice de GET /couriers como costo de una orden. Es la tarifa base del país; el costo de cada orden es su shippingCost.

Cancelar una orden

curl -X PATCH '{BASE_URL}/integrations/v1/orders/{id}/cancel' \
  -H 'X-Drop-Key: …' -H 'X-Drop-Timestamp: …' \
  -H 'X-Drop-Nonce: …' -H 'X-Drop-Signature: …'

No lleva cuerpo ni motivo. Pide el permiso orders:write, el mismo que crear órdenes. Las reglas son las mismas que tiene el dropshipper en su app:

  • Solo se cancela antes del despacho. La orden tiene que estar en registered o en pending_correction, y ningún proveedor la puede haber despachado. Pasado ese punto responde 409 con el código ORDER_NOT_CANCELLABLE, y en details.status viene el estado actual. No se reintenta.
  • Es idempotente. Cancelar una orden que ya está en cancelled responde 200 con la orden, sin cambios. Si perdiste la respuesta, repetí la llamada.
  • Solo las órdenes que vos creaste. Una orden que no originaste responde 404, igual que en la consulta.
  • Te llega el webhook. La cancelación dispara order.status_changed con status: cancelled, como cualquier otro cambio de estado.

La respuesta es la orden, con la misma forma que la consulta.

Referencia: PATCH /orders/{id}/cancel.

Lo que no se puede hacer

  • Modificar una orden por API, ni cancelarla una vez despachada. Esos cambios se gestionan con nosotros.
  • Ver órdenes que no originaste.
  • Conocer la estructura de costos del proveedor. Tenés tu costo por unidad en cost (Catálogo), pero no los márgenes, comisiones ni tarifas con que se arma.

En esta página