Ó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és | Llamada | Por qué acá |
|---|---|---|---|
| 1 | Qué pide la dirección de tu país | GET /geo/countries/:id/address-config | Te dice si hace falta departamento, municipio y qué niveles extra existen. Cuáles son obligatorios lo define la paquetería del paso 5 |
| 2 | Departamento y municipio | GET /geo/states?countryId= | De acá salen stateId y cityId. Cacheable: cambian poco |
| 3 | Qué vas a mandar y a qué precio | GET /products · GET /products/:id | price es tu precio de venta y cost lo que te cuesta; con los dos sabés tu margen |
| 4 | Que todavía haya existencia | GET /products/stock?ids= | Justo antes de crear. Crear no reserva: entre que consultás y creás puede cambiar |
| 5 | Qué paquetería puede llevarla | GET /couriers/available | Solo 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 |
| 6 | Crear la orden | POST /orders | Con unitPrice si querés fijar el precio, y courierId si elegiste una paquetería |
| 7 | Confirmar qué quedó | la respuesta del paso 6 | Mirá requestedCourier: si no es null, tu paquetería no se pudo usar |
| 8 | Enterarte de los cambios | tu 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.
reason | Qué pasó |
|---|---|
no_coverage | Esa paquetería no cubre la ruta de la orden |
coverage_check_failed | No se pudo verificar la cobertura. No es un problema de tu pedido |
route_not_evaluable | Faltan 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:
| Campo | Notas |
|---|---|
externalOrderId | El id de la orden en tu sistema. Es la llave de idempotencia |
customer.phoneAreaCode | Sin el + |
shipping.stateId, shipping.cityId | Obligatorios, 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.longitude | Ubicació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[].catalogId | Del catálogo. El de la variante si el producto tiene variantes |
items[].unitPrice | Lo 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 |
codAmount | Total exacto a cobrar, para cerrar centavos que el precio unitario no alcanza. Ver Cobrar un total exacto |
courierId | Si se omite, se resuelve la paquetería por defecto del país |
confirmForDispatch | Default true: la orden nace lista para despacho |
Reglas del contrato
-
No mandás el
sellerId. La orden es tuya porque tu credencial lo es. UncatalogIdque no esté en tu catálogo se rechaza con404, 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.codAmountsolo 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/countriesdevuelverequestCoordinatesFromCustomer: true, omitirshipping.latitude/longitudeno da error al crear, pero la orden nace enpending_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
catalogIdse consolidan en una sola con la suma de las cantidades. Si las repetís conunitPricedistintos se rechaza con400: 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 con422 MIXED_PRODUCT_ORIGINS. Productos de varios proveedores sí se pueden mezclar. -
Crear una orden no reserva existencia. Consultá
GET /products/stockantes 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
codAmountmayor que la suma de las líneas devuelve400; para cobrar más, subí elunitPrice. - El ajuste es de un centavo por unidad, con un mínimo de $0.05. Un total que
se aleje más devuelve
400con la suma esperada endetails.
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
idyorderNumber. 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: …'| Filtro | Notas |
|---|---|
status | Uno de los valores de Estados de la orden. Cualquier otro devuelve 400 |
updatedSince | ISO 8601. Solo las modificadas desde esa fecha, comparando contra updatedAt. Es la vía de reconciliación |
page, limit | Paginació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:
orderNumberes el número visible de la orden. Tratalo como texto opaco: no tiene prefijo ni formato garantizado.externalOrderIdes tu punto de reconciliación.statuses el identificador estable ystatusLabelsu traducción, que puede cambiar de redacción. Ver Estados de la orden.shipmentNumberytrackingUrlsonnullhasta que la orden se despacha.updatedAtes contra lo que comparaupdatedSince.- 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.
| Campo | Tiene valor | Qué es |
|---|---|---|
shippingCost | Desde el despacho | Costo 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 |
codCommissionAmount | Desde el despacho | Comisión por cobro contra entrega aplicada a esta orden, con tu comisión preferencial si la tenés |
codCommissionPercentage | Desde el despacho | Porcentaje de esa comisión, como fracción: 0.04 es 4 % |
estimatedCodCommissionAmount | Desde la creación | Comisió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
codCommissionAmounttiene valor,estimatedCodCommissionAmountya no aplica. - Las órdenes despachadas antes de que existieran estos campos traen
shippingCost, pero la comisión real ennull. Para esas, usá la estimada. - No uses el
dropPricedeGET /courierscomo costo de una orden. Es la tarifa base del país; el costo de cada orden es sushippingCost.
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
registeredo enpending_correction, y ningún proveedor la puede haber despachado. Pasado ese punto responde409con el códigoORDER_NOT_CANCELLABLE, y endetails.statusviene el estado actual. No se reintenta. - Es idempotente. Cancelar una orden que ya está en
cancelledresponde200con 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_changedconstatus: 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.