Drop · API de Integraciones

Webhooks

Drop hace un POST a tu URL cuando cambia el estado de una orden tuya. Estados, firma, reintentos y reconciliación.

En vez de consultar en bucle, nos das una URL y Drop te avisa cuando algo cambia. La registrás en la tarjeta «Notificaciones» de Configuraciones → Integraciones, y ahí mismo aparece el signing secret, distinto del secreto de tu credencial, con el que verificás cada entrega.

Qué vas a recibir

EventoCuándo se dispara
order.status_changedCambió el estado de una orden tuya

Es un único evento a propósito: cubre tanto los cambios que se originan en Drop como los que reporta la paquetería, con el vocabulario de estados de abajo. No hay un evento aparte por guía.

Solo recibís eventos de las órdenes que vos originaste. Las que el dropshipper crea por su cuenta, o las de otro origen, no te generan notificaciones.

Los eventos describen estado, no transición: no traen el estado anterior. Aplicar el mismo evento dos veces te deja igual que aplicarlo una.

Estados de la orden

Esta es la tabla que necesitás para interpretar un webhook. Los mismos valores aparecen en el campo status de las respuestas de GET /orders y en su filtro ?status=, así que sirve para las dos cosas.

Cada orden trae dos campos: status, el identificador estable contra el que tenés que programar, y statusLabel, su traducción al español lista para mostrar. statusLabel puede cambiar de redacción sin aviso; status no.

El ciclo de vida tiene dos tramos. Primero la orden se gestiona dentro de Drop: se valida, se prepara y sale de bodega. A partir de ahí la mueve la paquetería y los cambios los reporta ella. Ambos tramos usan el mismo vocabulario: no tenés que distinguir de dónde viene el cambio.

Tramo 1: la orden dentro de Drop

statusstatusLabelQué significa
registeredOrden registradaEstado inicial normal. La orden quedó completa y en la cola del proveedor, que es quien la despacha. No requiere nada de tu parte
pending_correctionVerificarLa orden está detenida y no avanza sola. Ocurre al crearla si faltan datos de entrega (departamento o municipio, o las coordenadas en un país que las exige) o si la paquetería asignada no tiene cobertura para esa ruta. También es donde cae una orden que el proveedor devolvió con un motivo de rechazo. Alguien tiene que corregirla
rejectedOrden rechazadaFinal. La orden estuvo en pending_correction durante 72 horas sin corregirse y se descartó automáticamente. Ya no se va a despachar
cancelledOrden canceladaFinal. Se canceló antes de despacharse, desde la app del dropshipper o desde tu integración con PATCH /orders/{id}/cancel. Solo es posible mientras la orden no haya salido
fulfilledOrden despachadaLa orden se despachó y se generó su guía. Es la frontera entre los dos tramos: acá aparecen shipmentNumber y trackingUrl, y de ahora en adelante los cambios los reporta la paquetería

Tramo 2: el envío en manos de la paquetería

statusstatusLabelQué significa
createdEnvío creadoLa guía existe en el sistema de la paquetería, que todavía no la tomó
registered_in_courierRegistrado en courierLa paquetería registró el envío en su operación
recollectedRecolectadoEl paquete ya está físicamente en poder de la paquetería
inrouteEn ruta a destinoEn ruta hacia la dirección de entrega. Es donde un envío pasa la mayor parte del tiempo
deliveredEntregadoEntregado al cliente final. Si la orden era contra entrega, es cuando se cobró. Único desenlace exitoso
not_deliveredNo entregadoSe intentó entregar y no se pudo. No es final: suele haber otro intento y el envío vuelve a inroute. Si no prospera, termina en returned_to_warehouse
not_recollectedNo recolectadoLa paquetería no logró recolectar el paquete en el origen. El envío todavía no arrancó
in_lockerEn lockerEl paquete está en un locker esperando que el cliente lo retire. Cuenta como tránsito
in_agencyEn agenciaEl paquete está en una agencia esperando que el cliente lo retire. Cuenta como tránsito
issueProblemas en gestiónIncidencia levantada por la paquetería que no encaja en los estados anteriores. Requiere seguimiento: llega con su motivo. Ver Motivos de una novedad
cancelled_in_boxfulEnvío canceladoEl envío se anuló en la operación logística
returned_to_warehouseDevuelto a warehouseFinal en la práctica. El paquete volvió a bodega sin entregarse. Si era contra entrega, no se cobró

Cómo tratarlos en tu código

Si necesitás agrupar, esta es la clasificación que usamos internamente:

GrupoEstados
Sin movimientoregistered, created, registered_in_courier, fulfilled
En tránsitorecollected, inroute, in_locker, in_agency
Requiere atenciónpending_correction, issue, not_delivered, not_recollected
Cerrado sin entregacancelled, rejected, cancelled_in_boxful, returned_to_warehouse
Entregadodelivered
  • El avance no es lineal. Un envío puede ir inroute → not_delivered → inroute → delivered. No asumas una secuencia fija ni que un estado no se repite.
  • Solo delivered cierra bien el ciclo. returned_to_warehouse, cancelled_in_boxful, cancelled y rejected lo cierran sin entrega; todo lo demás va a seguir cambiando.
  • No infieras el cobro del estado del envío, más allá de que delivered significa que se cobró y returned_to_warehouse que no. La liquidación es un proceso aparte.

Tolerá valores que no conozcas. Si la paquetería agrega un estado nuevo, lo vas a recibir tal como nos llega en lugar de perderlo; tu cliente no debería romperse por eso.

Forma de la petición

Drop hace un POST a tu URL con Content-Type: application/json:

POST /tu-endpoint HTTP/1.1
Content-Type: application/json
X-Drop-Event-Id: evt_9f1c2a04-91be-4d33-9d20-2b6e0c7a51ff
X-Drop-Event-Type: order.status_changed
X-Drop-Signature: t=1786462395,v1=4f3c…

{
  "id": "evt_9f1c2a04-91be-4d33-9d20-2b6e0c7a51ff",
  "type": "order.status_changed",
  "createdAt": "2026-08-11T18:22:04.118Z",
  "data": {
    "orderId": "65b000000000000000000301",
    "orderNumber": "1786733712601",
    "sellerId": "65b0000000000000000000f1",
    "status": "inroute",
    "statusLabel": "En ruta a destino",
    "shipmentNumber": "BX-99120",
    "trackingUrl": "https://…/BX-99120",
    "shippingCost": 3.5,
    "codCommissionAmount": 2,
    "codCommissionPercentage": 0.04,
    "estimatedCodCommissionAmount": 2,
    "reasons": [],
    "occurredAt": "2026-08-11T18:22:04.118Z"
  }
}
CampoNotas
idIdentificador del evento. Repite el header X-Drop-Event-Id
typeTipo de evento. Hoy siempre order.status_changed
createdAtCuándo se generó el evento. No cambia entre reintentos
data.orderIdOrden afectada. Es el id que va en GET /orders/:id
data.orderNumberNúmero visible de la orden
data.sellerIdTu identificador de dropshipper
data.statusEstado nuevo, en el vocabulario de arriba: un cambio de la paquetería llega como "inroute", no como un código numérico
data.statusLabelTraducción al español del estado
data.shipmentNumberNúmero de guía. null mientras la orden no se haya despachado
data.trackingUrlEnlace de seguimiento. null mientras la orden no se haya despachado
data.shippingCostCosto de envío de la orden. null mientras la orden no se haya despachado
data.codCommissionAmountComisión por cobro contra entrega aplicada al despachar. null mientras la orden no se haya despachado
data.codCommissionPercentagePorcentaje de esa comisión, como fracción (0.04 = 4 %). null mientras la orden no se haya despachado
data.estimatedCodCommissionAmountComisión estimada al crear la orden
data.reasonsPor qué la orden quedó en pending_correction o en issue. Vacío en cualquier otro estado. Ver Motivos de "Verificar" y Motivos de una novedad
data.occurredAtCuándo ocurrió el cambio. Usalo para descartar eventos atrasados, no el orden de llegada
testSolo aparece, con valor true, en el evento de prueba. Un evento real no trae la clave. Ver más abajo

Las cabeceras que acompañan el POST:

HeaderNotas
X-Drop-Event-IdLlave de deduplicación. Estable entre reintentos
X-Drop-Event-TypeEl mismo valor de type, para enrutar sin parsear el cuerpo
X-Drop-Signaturet=<timestamp>,v1=<hmac>. Ver Verificá la firma

El payload no lleva datos del cliente final: ni nombre, ni teléfono, ni correo, ni dirección. Si los necesitás, pedilos con GET /orders/:id usando tu credencial: así queda registrado quién accedió a esos datos y cuándo.

Motivos de "Verificar"

Cuando status es pending_correction, reasons te dice qué hay que corregir. Te llega tanto si la orden nace en ese estado al crearla como si pasa a él después, porque el proveedor o la bodega la devolvió.

"reasons": [
  {
    "code": "NO_COVERAGE",
    "label": "Sin cobertura",
    "detail": "El courier Forza no tiene cobertura para la ruta de la orden"
  }
]
CampoNotas
codeIdentificador estable. Programá contra este, no contra label
labelTraducción al español; puede cambiar de redacción
detailExplicación que dejó quien la mandó a verificar. null si no dejó ninguna

Puede venir más de un motivo: al crear la orden, por ejemplo, puede faltar la ubicación y además no haber cobertura. Los códigos posibles:

codeCuándo
NO_COVERAGELa paquetería no cubre la ruta de la orden
MISSING_COORDINATESEl país exige la ubicación del cliente y la orden no la trae
MISSING_STATE_OR_CITYFalta el departamento o el municipio
INCOMPLETE_ADDRESSEl proveedor la devolvió: dirección incompleta
CUSTOMER_REQUESTEl proveedor la devolvió: lo pidió el cliente
OUT_OF_STOCKEl proveedor la devolvió: no hay stock

Tratá un code desconocido como un motivo más, no como un error: la lista puede crecer sin cambiar de versión. En ese caso mostrá label.

Motivos de una novedad

Cuando status es issue, reasons te dice qué reportó la paquetería, con la misma forma que los motivos de "Verificar". detail trae el comentario del repartidor, que es lo que conviene contarle al cliente final.

"reasons": [
  {
    "code": "COURIER_MANAGEMENT",
    "label": "Gestión del courier",
    "detail": "No se encuentra ella en el lugar"
  }
]
CampoNotas
codeTipo de incidencia. Identificador estable: programá contra este
labelTraducción al español; puede cambiar de redacción
detailComentario de la paquetería, en texto libre. null si no dejó ninguno

Los códigos que vas a ver con más frecuencia:

codeCuándo
COURIER_MANAGEMENTLa paquetería necesita una gestión: el detalle dice cuál
FAILED_DELIVERYNo se pudo entregar
DELIVERY_DELAYLa entrega se retrasó
SHIPMENT_NO_MOVEMENTLa guía no registra movimiento
PACKAGE_DAMAGEEl paquete llegó dañado
PACKAGE_LOSTEl paquete se extravió
COURIER_INCIDENTLa incidencia no tiene tipo; label trae el estado del courier

Igual que en "Verificar", tratá un code desconocido como un motivo más y mostrá label.

Dos cosas a tener en cuenta:

  • Llega con unos segundos de atraso. La paquetería registra el detalle después del cambio de estado, así que el evento issue sale entre 30 segundos y un minuto y medio después. Si mientras tanto la orden cambió de estado, ese evento te llega antes: ordená por occurredAt.
  • Siempre trae al menos un motivo. Si la paquetería no registra el detalle en ese tiempo, el evento issue no se envía. Vas a recibir el siguiente cambio de estado de la orden, y siempre podés consultarla con GET /orders/:id.

El evento de prueba

En la tarjeta «Notificaciones», una vez guardada tu URL, aparece el botón «Enviar evento de prueba»: sirve para comprobar que tu endpoint responde antes de que pase nada real. Llega por el mismo camino que uno de verdad, misma firma, mismas cabeceras y el mismo sobre, así que podés validarlo con tu código de producción. Lo que lo distingue es la clave test:

{
  "id": "evt_test_9f1c2a04-91be-4d33-9d20-2b6e0c7a51ff",
  "type": "order.status_changed",
  "createdAt": "2026-08-19T18:22:04.118Z",
  "test": true,
  "data": {
    "orderId": "000000000000000000000000",
    "orderNumber": "TEST-0001",
    "status": "registered",
    "shipmentNumber": null,
    "trackingUrl": null,
    "shippingCost": null,
    "codCommissionAmount": null,
    "codCommissionPercentage": null,
    "estimatedCodCommissionAmount": null,
    "reasons": []
  }
}

Descartalo apenas lo identifiques

Antes de tocar tu base de datos:

if (event.test) return res.sendStatus(200);

La orden 000000000000000000000000 no existe: es un relleno para que el sobre tenga la misma forma que uno real. Si tu código no filtra por test y va a buscarla, no la va a encontrar, y según cómo esté escrito puede terminar creando un registro de una orden que nunca existió.

Verificá la firma antes de procesar

Sin esto, cualquiera que conozca tu URL puede inventarte órdenes.

X-Drop-Signature tiene la forma t=<timestamp>,v1=<hmac>, donde el HMAC es:

HMAC_SHA256( tu_signing_secret, "<timestamp>.<cuerpo crudo>" )

En hex minúscula. El cuerpo es el que llegó, byte a byte: no lo re-serialices antes de verificar.

verify-webhook.js
const crypto = require('crypto');

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')));

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (age > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  // Comparación en tiempo constante.
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

El timestamp va dentro del HMAC para que una entrega capturada no se pueda reenviar indefinidamente: rechazá las que estén fuera de tu ventana.

Entrega y reintentos

La entrega es at-least-once: podés recibir el mismo evento más de una vez. X-Drop-Event-Id es tu llave de deduplicación: es estable entre reintentos, guardalo y descartá los repetidos.

Contamos como éxito cualquier respuesta 2xx. Todo lo demás, incluidos 3xx, timeouts y errores de conexión, es un fallo y se reintenta.

Respondé rápido: el timeout es de 10 segundos. Si tu procesamiento es lento, encolá el evento y respondé 200 de inmediato.

IntentoEspera desde el anterior
2.º1 minuto
3.º5 minutos
4.º15 minutos
5.º1 hora
6.º6 horas
7.º en adelante24 horas

Son 6 intentos por defecto. Agotados, la entrega queda marcada como fallida y no se vuelve a intentar.

Tu suscripción puede desactivarse sola tras 20 fallas consecutivas, para no insistir contra un endpoint muerto. Reactivarla es manual: escribinos. Una entrega exitosa reinicia el contador.

Reconciliación

Los webhooks se pueden perder: tu endpoint estuvo caído más allá de los reintentos, o la suscripción se desactivó. La contraparte es consultar por fecha de modificación:

GET /integrations/v1/orders?updatedSince=2026-08-11T00:00:00.000Z

Devuelve solo tus órdenes, paginadas. Corriéndolo periódicamente cerrás cualquier hueco. No lo trates como opcional: es lo que hace que tu sistema converja aunque una entrega se pierda.

Referencia: GET /orders.

Recomendaciones

  • Respondé 200 antes de procesar. Encolá y procesá aparte.
  • Deduplicá por X-Drop-Event-Id, no por el contenido.
  • No asumas orden de llegada. Un reintento puede llegar después de un evento más nuevo; usá occurredAt para descartar los atrasados.
  • Tolerá campos nuevos. Agregarlos al payload no es un cambio incompatible.
  • Guardá el signing secret como una credencial, no en el repositorio.

En esta página