Documentación de la API

Todo lo que necesitás para mover paquetes con E-PICK desde tu propio sistema: ocho endpoints, de la cotización a la entrega.

Base URLhttps://dev-ar.e-pick.com.arAmbienteDesarrolloVersión1.0ActualizadoAgosto 2026
El recorrido de una integración
01Cotizar
02Crear la orden
03Confirmar
04Imprimir la etiqueta
05Seguir el envío
06Entregado
Para evaluar

Antes de integrar

E-PICK es una empresa de logística de última milla en Argentina. Retiramos el paquete en el domicilio de quien envía y lo entregamos en el domicilio de quien recibe.

Puerta a puerta, sin excepciónTrabajamos únicamente puerta a puerta. No tenemos sucursales ni puntos de retiro o despacho: ni quien envía deja el paquete en un mostrador, ni quien recibe lo va a buscar a ningún lado. Si tu operación depende de que el cliente retire en una sucursal, este servicio no es para vos.

Dónde llegamos

TramoAlcance
RetiroBuenos Aires, Córdoba y Rosario, con flota propia.
EntregaTodo el país. Una vez que retiramos el paquete, lo llevamos a cualquier punto de Argentina.
Lo primero que conviene mirarEl que limita es el retiro, no la entrega. Si despachás desde Buenos Aires, Córdoba o Rosario, podés vender a todo el país. Si despachás desde otra provincia, todavía no llegamos a buscarte el paquete.

Los tres servicios

La cotización te devuelve cuál corresponde en el campo package. No se elige: lo determina la ruta entre el origen y el destino.

ServicioCuándo aplica
PrimeEntrega el mismo día.
FlexEntrega en 2 a 3 días.
RouteCobertura nacional.

Cómo empezar

Antes que nadaSi tu tienda corre sobre Tiendanube, Shopify, WooCommerce, Empretienda o VTEX, escribinos antes de escribir una sola línea de código. Puede que no necesites integrar nada.

Si vas a integrar a medida, el camino es este:

  1. Escribinos a info@e-pick.com.ar o desde el formulario de app.e-pick.com.ar. Un asesor responde en menos de 24 horas.
  2. Acordamos las condiciones comerciales. Ahí se define si tu cuenta va a operar con cuenta corriente — facturada, sin pago por envío — que es lo que cambia cómo se comporta la API al crear órdenes.
  3. Te habilitamos el ambiente de desarrollo y seguís esta guía.
  4. Cuando el circuito completo te funciona, te habilitamos producción.
Sobre registrarsePodés registrarte solo con el endpoint de registro y empezar a probar. Lo que no se resuelve por API es la parte comercial: las tarifas y la cuenta corriente se acuerdan con una persona.
Empezá por acá

Introducción

Esta es la guía completa para integrar tu sistema con la plataforma de envíos de E-PICK: crear órdenes, cotizarlas, seguirlas y cancelarlas.

Qué resuelve la API

  • Precio. Cotizar un envío por dimensiones, peso y códigos postales.
  • Órdenes. Crear el envío y obtener el número de guía.
  • Seguimiento. Consultar dónde está el paquete ahora, o recibir cada movimiento por webhook y armar la historia del envío.

Antes de empezar

  • Todos los ejemplos apuntan al ambiente de desarrollo. Producción es https://www.e-pick.com.ar y se habilita cuando terminás de probar.
  • Los tokens duran unas 11 horas. Renovalos con el endpoint de login.
  • Si tenés varios puntos de retiro, creator_id es el campo que los mantiene bajo la misma cuenta.
  • Los datos de los ejemplos son ficticios. Los números de tracking de prueba son ALFCM1520609 y ALFBO1519004.
Fundamentos

Autenticación

La API usa tokens JWT. Se piden una vez, se guardan y se reutilizan hasta que vencen.

Dónde hace faltaTodos los endpoints requieren token, con excepción de Cotización, Confirmar orden y Tracking.

Flujo

PasoAcciónDatos
01Registrar usuariophone, email, name, password
02Hacer loginphone, password, source
03Recibir el tokenJWT válido ~11 horas
04Usarlo en cada requestx-access-token: {token}

Contenido del token

El token es un JWT estándar. Si lo decodificás, vas a encontrar:

JSON
{
  "id": "3515555999",
  "customer_id": "b7c2d419-3f80-4a15-9e6d-0c48ab52f731",
  "role": "USER",
  "expiresIn": 43661,
  "iat": 1784818338,
  "exp": 1784861999
}
CampoQué es
idTeléfono del usuario.
customer_idEmpresa asociada, si tiene datos cargados.
roleRol de la cuenta.
expiresInSegundos de vida del token.
iatCuándo se emitió, en formato Unix.
expCuándo vence, en formato Unix.
RenovaciónNo hay endpoint de refresh: cuando el token vence, se hace login de nuevo. Un 401 en cualquier endpoint casi siempre significa token vencido.
Referencia rápida

Mapa de endpoints

Los ocho endpoints de la API, en el orden en que se usan.

EndpointMétodoPara qué sirveToken
/api/users/registerPOSTCrear una cuentaNo
/api/users/loginPOSTObtener tokenNo
/api/orders/calculator/wwwPOSTCotizar un envíoNo
/api/orders/integrations/confirm-orderPOSTCrear la ordenSí
/api/orders/confirm/{id}/{sender_code}GETAvisar que está listo para retirarNo
/api/orders/label/{ids}/{formato}GETDescargar el PDF de la etiquetaSí, en la URL
/api/orders/tracking/{id}GETConsultar estadoNo
/api/orders/{id}/cancelPOSTCancelar la ordenSí
Endpoint 01

Registro de usuario

Crea una cuenta de usuario en la plataforma. Es el primer paso si todavía no tenés credenciales.

POST/api/users/registersin token

Encabezados

EncabezadoObligatorioValor
AcceptObligatorioapplication/json
Content-TypeObligatorioapplication/json

Cuerpo de la solicitud

Formato de request
{
  "phone": "3515555999",
  "email": "usuario@example.com",
  "name": "Mi Empresa S.A.",
  "password": "MiPassword123"
}

Campos y validaciones

CampoTipoObligatorioValidaciónEjemplo
phoneStringObligatorioExactamente 10 dígitos numéricos. Único en el sistema.3515555999
emailStringObligatorioFormato RFC 5322. Máximo 254 caracteres. Único en el sistema.usuario@example.com
nameStringObligatorioMínimo 1 carácter. Acepta números y caracteres especiales.Mi Empresa S.A.
passwordStringObligatorioMínimo 6 caracteres, máximo 500. Sin requisitos de complejidad.MiPassword123

CURL

Registrar usuario
curl --location 'https://dev-ar.e-pick.com.ar/api/users/register' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "phone": "3515555999",
  "email": "usuario@example.com",
  "name": "Mi Empresa S.A.",
  "password": "MiPassword123"
}'

Respuesta 200

JSON
{
  "expiresIn": 43661,
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHBpcmVzSW4iOjQzNjYxLCJpYXQiOjE3ODQ4MTgzMzh9…",
  "full_confirmation": null,
  "customer": null,
  "mustAcceptTerm": false,
  "forceAcceptTerm": false
}

Campos de respuesta

CampoTipoDescripción
tokenString (JWT)Token de sesión, ya utilizable. Dura ~11 horas.
expiresInNumberSegundos hasta que el token expire.
customerObject | nullQueda en null hasta que el usuario cargue los datos de empresa en el portal web.
mustAcceptTermBooleanIndica si hay términos pendientes de aceptación.

Errores

HTTPCódigoMensajeCausa
400INVALID-DATAParámetros inválidos o faltantesCampo vacío o formato inválido: phone con menos de 10 dígitos, email sin @, password de menos de 6 caracteres
409DUPLICATED-DATAEl usuario ya existeEl phone o el email ya están registrados
500—Error internoError del servidor
Endpoint 02

Login y obtención de token

Devuelve el token JWT que necesitás para crear y cancelar órdenes. Es el único endpoint que hay que llamar de forma recurrente: el token vence.

POST/api/users/loginsin token

Encabezados

EncabezadoObligatorioValor
AcceptObligatorioapplication/json
Content-TypeObligatorioapplication/json

Cuerpo de la solicitud

Formato de request
{
  "phone": "3515555999",
  "password": "MiPassword123",
  "source": "API"
}

Campos

CampoTipoObligatorioDescripciónEjemplo
phoneStringObligatorioTeléfono registrado, 10 dígitos.3515555999
passwordStringObligatorioContraseña de la cuenta.MiPassword123
sourceStringObligatorioOrigen de la solicitud.API

CURL

Obtener token
curl --location 'https://dev-ar.e-pick.com.ar/api/users/login' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "phone": "3515555999",
  "password": "MiPassword123",
  "source": "API"
}'

Respuesta 200

JSON
{
  "id": "3515555999",
  "customer_id": "b7c2d419-3f80-4a15-9e6d-0c48ab52f731",
  "role": "USER",
  "expiresIn": 43579,
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
  "name": "Mi Empresa S.A.",
  "dni": null,
  "full_confirmation": null,
  "image": null,
  "email": "usuario@example.com",
  "address": {
    "street": "Av. Colón",
    "number": "500",
    "coords": {
      "lat": -31.4201,
      "lng": -64.1888,
      "formatted_address": "Av. Colón 500, Córdoba, Argentina"
    },
    "city": "Córdoba",
    "province": "Córdoba",
    "postal_code": "5000"
  },
  "phone": "3515555999",
  "customer": {
    "id": "b7c2d419-3f80-4a15-9e6d-0c48ab52f731",
    "name": "Mi Empresa S.A.",
    "cuit": "30123456789",
    "invoice_type": 1,
    "invoice_condition": 1,
    "address": "Av. Colón 500",
    "phone": "1145550000",
    "email": "usuario@example.com",
    "auto_invoice": false,
    "allow_account": true,
    "created_at": "2024-10-01T15:16:36.000Z",
    "updated_at": "2025-01-17T15:04:41.000Z"
  },
  "mustAcceptTerm": false,
  "forceAcceptTerm": false
}

Campos de respuesta

CampoTipoDescripción
tokenString (JWT)Token de autenticación. Válido ~11 horas (43.661 segundos).
expiresInNumberSegundos hasta el vencimiento.
customer_idUUID | nullIdentificador de la empresa asociada.
customerObject | nullDatos completos de la empresa: CUIT, condición de facturación, dirección.
addressObject | nullDirección del usuario, ya geocodificada.

Errores

HTTPCódigoMensajeCausa
400MISSING-DATAFaltan datosNo mandaste phone o password
402LOGIN-ERRORUsuario incorrectoEl phone no tiene formato válido: tienen que ser 10 dígitos
402LOGIN-ERRORClave incorrectaLa contraseña no coincide, o el teléfono no está registrado
500—Error internoError del servidor
Un mensaje, dos causasClave incorrecta significa las dos cosas: contraseña equivocada o teléfono que no existe. La API no los distingue a propósito, para que nadie pueda averiguar qué teléfonos están registrados probando de a uno.

Al depurar tenelo presente: si estás seguro de la contraseña, lo que puede estar mal es el teléfono.
Guardá el tokenGuardá el token y reutilizalo hasta que expire. Pedir uno nuevo en cada request funciona, pero es innecesario: dura unas 11 horas.
Endpoint 03

Cotización de precios

Calcula el precio de un envío a partir de las dimensiones, el peso y los códigos postales de origen y destino. Sirve para mostrarle el precio a tu cliente antes de confirmar nada.

POST/api/orders/calculator/wwwsin token

Encabezados

EncabezadoObligatorioValor
AcceptObligatorioapplication/json
Content-TypeObligatorioapplication/json
OriginRecomendadohttps://dev-ar.e-pick.com.ar

Cuerpo de la solicitud

Formato básico
{
  "package": {
    "long": 10,
    "width": 12,
    "height": 11,
    "weight": "1.2",
    "value": 5000
  },
  "sender": {
    "postal_code": "5000"
  },
  "addressee": {
    "postal_code": "1425"
  }
}
Con customer_id, para clientes con acuerdo comercial
{
  "info": {
    "customer_id": "b7c2d419-3f80-4a15-9e6d-0c48ab52f731"
  },
  "package": {
    "long": 10,
    "width": 12,
    "height": 11,
    "weight": "1.2",
    "value": 5000
  },
  "sender": {
    "postal_code": "5000"
  },
  "addressee": {
    "postal_code": "1425"
  }
}

Campos del paquete

CampoTipoObligatorioValidaciónEjemplo
longNumberObligatorioLargo en cm. Acá se valida el volumen total, no cada lado por separado.10
widthNumberObligatorioAncho en cm.12
heightNumberObligatorioAlto en cm.11
weightString | NumberObligatorioMáximo 25 kg. Con 25 la cotización sale; con 25,1 devuelve isValid:false."1.2"
valueNumberObligatorioValor declarado del contenido. Sin tope.5000
Qué valida y qué noLa cotización no valida cada lado por separado: un paquete de 80 × 12 × 11 cm cotiza sin problema. Lo que sí valida es el volumen total y el peso:
  • Volumen máximo: 125.000 cm³, el equivalente a un bulto de 50 × 50 × 50 cm. Con 132.500 cm³ ya devuelve isValid:false.
  • Peso máximo: 25 kg. Con 25 cotiza; con 25,1 no.

Por eso una cotización válida no garantiza que la orden se pueda crear: al crearla se aplican además los límites por dimensión. Si trabajás con bultos grandes, validalos vos antes de mostrarle un precio a tu cliente.

Campos de ubicación

CampoObligatorioDescripciónEjemplo
sender.postal_codeObligatorioCódigo postal de retiro, 4 dígitos válidos.5000
addressee.postal_codeObligatorioCódigo postal de entrega, 4 dígitos válidos.1425

CURL

Cotizar envío
curl --location 'https://dev-ar.e-pick.com.ar/api/orders/calculator/www' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Origin: https://dev-ar.e-pick.com.ar' \
--data '{
  "package": {
    "long": 10,
    "width": 12,
    "height": 11,
    "weight": "1.2",
    "value": 5000
  },
  "sender": {
    "postal_code": "5000"
  },
  "addressee": {
    "postal_code": "1425"
  }
}'

Respuesta 200

Cotización válida
{
  "isValid": true,
  "price": 11060,
  "equivalence": 1,
  "eta": [3, 4],
  "service": "E-PICK",
  "package": "Route"
}
Cotización inválida
{
  "isValid": false,
  "price": 0,
  "equivalence": 0,
  "eta": [0, 0]
}

Campos de respuesta

CampoTipoDescripción
isValidBooleantrue = precio válido. false = alguna validación falló.
priceNumberPrecio del envío en pesos argentinos.
equivalenceNumberCuántos paquetes estándar ocupa tu bulto. La tarifa se multiplica por este número, así que el price ya lo tiene aplicado. Ver cálculo de dimensiones.
etaArray [min, max]Días estimados de entrega.
serviceStringE-PICK cuando la entrega es nuestra. Cualquier otro valor es el servicio tercerizado que la hace. No lo hardcodees.
packageStringQué servicio corresponde a esa ruta: Prime el mismo día, Flex en 2 a 3 días, Route cobertura nacional. No se elige: lo determina el par origen-destino.

Errores

Leelo con atenciónHay dos formas de fallar y se detectan distinto. Si el paquete no pasa las reglas de negocio —peso o volumen— la respuesta es un 200 con isValid:false: el código HTTP no te avisa de nada. Si el problema está en los datos que mandaste, sí llega un código de error.

Revisá siempre isValid, aunque el status sea 200.
HTTPCódigoMensajeCausa
200isValid:falseEl paquete no se puede enviarPeso mayor a 25 kg o volumen total excedido
422CALCULATOR.POSTAL_CODEError en campo "addressee_postal_code": 9999El código postal no existe. El mensaje dice cuál de los dos falló.
400ERRORFalta un objeto del cuerpoNo enviaste package, sender o addressee
Endpoint 04

Crear orden de envío

Crea la orden de envío. Es el endpoint central de la integración y el único que combina token, validación de paquete y validación de direcciones.

POST/api/orders/integrations/confirm-orderrequiere token
AutenticaciónSin un x-access-token válido, este endpoint devuelve 401. El token sale del login y dura ~11 horas.

Encabezados

EncabezadoObligatorioDescripciónEjemplo
AcceptObligatorioFormato aceptadoapplication/json
Content-TypeObligatorioTipo de contenidoapplication/json
x-access-tokenObligatorioToken JWT obtenido en el logineyJ0eXAiOiJKV1Qi…
x-client-keyOpcionalClave de cliente. Solo si te asignamos una; si no, omitilo.11111111-2222-3333-4444-555555555555

Cuerpo de la solicitud

Formato completo
{
  "info": {
    "webhook": "https://tu-dominio.com/webhooks/epick",
    "id": "87821",
    "creator_id": "3515555999"
  },
  "package": {
    "long": 10,
    "width": 12,
    "height": 11,
    "weight": 1.2,
    "value": 5000
  },
  "sender": {
    "postal_code": "5000",
    "name": "Mi Tienda",
    "phone": "3515555999",
    "email": "tienda@example.com",
    "street": "Av. Colón",
    "number": "500",
    "city": "Córdoba",
    "province": "Córdoba",
    "extra": "Piso 2",
    "info": "Llamar antes de retirar"
  },
  "addressee": {
    "postal_code": "1425",
    "name": "Cliente Final",
    "phone": "1145678900",
    "email": "cliente@example.com",
    "street": "Av. Rivadavia",
    "number": "3000",
    "city": "Buenos Aires",
    "province": "Buenos Aires",
    "extra": "Dpto 1B",
    "info": "Llamar al timbre"
  }
}

Objeto info

CampoTipoObligatorioDescripciónEjemplo
webhookString (URL)OpcionalURL donde te avisamos cada acción sobre la orden. Tiene que ser una URL válida y accesible desde internet.https://tu-dominio.com/webhooks/epick
idStringOpcionalTu identificador interno de la orden. Vuelve en la respuesta y en cada webhook.87821
creator_idStringRecomendadoTeléfono del usuario registrado. Define de quién es la orden.3515555999
creator_id
  • Mandalo siempre. Garantiza que la orden quede bajo tu cuenta.
  • Si no lo mandás: la orden se asocia al sender.phone si ese número está registrado. Si no lo está, la orden entra igual pero sin usuario ni cliente asociado.
  • Varias sucursales: mismo creator_id, distinto sender.phone por punto de retiro.

Objeto package

CampoTipoObligatorioValidación
longNumberObligatorioMáximo 50 cm
widthNumberObligatorioMáximo 50 cm
heightNumberObligatorioMáximo 50 cm
weightNumberObligatorioMáximo 25 kg. Si se supera: error 422.
valueNumberObligatorioValor declarado del contenido

Objetos sender y addressee

Los dos objetos llevan exactamente los mismos campos.

CampoTipoObligatorioValidación
postal_codeStringObligatorioTiene que ser un CP argentino válido. Si no, error 400.
nameStringObligatorioNombre de la persona o empresa
phoneStringObligatorioTeléfono de contacto
emailStringObligatorioEmail de contacto
streetStringObligatorioNombre de la calle
numberStringObligatorioAltura
cityStringObligatorioCiudad
provinceStringObligatorioProvincia
extraStringOpcionalPiso, departamento, unidad
infoStringOpcionalIndicaciones para el repartidor, por ejemplo "sin timbre, llamar"

CURL

Crear orden
curl --location 'https://dev-ar.e-pick.com.ar/api/orders/integrations/confirm-order' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…' \
--data '{
  "info": {
    "webhook": "https://tu-dominio.com/webhooks/epick",
    "id": "87821",
    "creator_id": "3515555999"
  },
  "package": {
    "long": 10,
    "width": 12,
    "height": 11,
    "weight": 1.2,
    "value": 5000
  },
  "sender": {
    "postal_code": "5000",
    "name": "Mi Tienda",
    "phone": "3515555999",
    "email": "tienda@example.com",
    "street": "Av. Colón",
    "number": "500",
    "city": "Córdoba",
    "province": "Córdoba",
    "extra": "Piso 2",
    "info": "Llamar antes de retirar"
  },
  "addressee": {
    "postal_code": "1425",
    "name": "Cliente Final",
    "phone": "1145678900",
    "email": "cliente@example.com",
    "street": "Av. Rivadavia",
    "number": "3000",
    "city": "Buenos Aires",
    "province": "Buenos Aires",
    "extra": "Dpto 1B",
    "info": "Llamar al timbre"
  }
}'
Cuenta corrienteSi tenés un acuerdo comercial con cuenta corriente, las órdenes se crean directamente en PAYED, sin pago previo. Si no, entran en PENDING y hay que abonarlas con el link de MercadoPago que viene en cho_url.
Reintentos y duplicadosCada llamada crea un envío nuevo. No deduplicamos por info.id: si mandás dos veces la misma orden, se despachan dos paquetes y se facturan dos. Si se te corta la conexión y no sabés si llegó, no reintentes a ciegas: la orden puede haberse creado igual.

Respuesta 200 / 201

JSON
{
  "id": "ALFCM1520609",
  "account": "PENDING",
  "status": "PENDING",
  "sender_code": "A1B2C3D",
  "addressee_code": "E4F5G6H",
  "number": 1520609,
  "phisical_number": "2026AR00001520609",
  "integration_identificator": "87821",
  "total": 11060,
  "equivalence": 1,
  "package_id": "1",
  "sender_name": "Mi Tienda",
  "addressee_name": "Cliente Final",
  "sender_phone": "3515555999",
  "addressee_phone": "1145678900",
  "sender_postal_code": "5000",
  "addressee_postal_code": "1425",
  "height": 11,
  "long": 10,
  "weight": 1.2,
  "width": 12,
  "value": 5000,
  "created_at": "2026-07-23T15:01:40.171Z",
  "updated_at": "2026-07-23T15:01:40.171Z",
  "status_name": "Pendiente de Pago",
  "origin": "API",
  "paused": "NO",
  "confirmed_at": null,
  "payed_at": null,
  "delivered_at": null,
  "prime": false,
  "priority": false,
  "integration_webhook": "https://tu-dominio.com/webhooks/epick",
  "cho_url": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=1234567890-…",
  "mp_url": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=1234567890-…",
  "preference_id": "1234567890-abcd1234-…",
  "qr_image": "data:image/png;base64,iVBORw0KGgoAAAANS…"
}

Campos de respuesta

CampoTipoDescripción
idStringIdentificador de la orden. Es el que usás para tracking y cancelación. Guardalo.
statusStringEstado inicial: PAYED en cuenta corriente, PENDING si hay que pagar.
integration_identificatorStringEl info.id que mandaste. Te sirve para cruzar la orden con tu sistema.
phisical_numberStringNúmero físico de la guía, el que va impreso en la etiqueta.
sender_code / addressee_codeStringCódigos de 7 caracteres de remitente y destinatario.
totalNumberPrecio total del envío.
cho_urlString (URL)Link de MercadoPago para que tu cliente pague.
qr_imageString (base64)Código QR de pago, listo para mostrar o imprimir.
Campos que podés ignorarLa respuesta trae varios campos más que no figuran en esta tabla (account, prime, priority, package_id y algunos otros): son de uso interno de E-PICK y podés ignorarlos.

Errores

HTTPCódigoMensajeCausa
401COMMON.UNAUTHORIZEDUnauthorized actionToken faltante, inválido o vencido
422NO-DATAThere is no sender, addressee or package infoFalta alguno de los tres objetos del cuerpo
422CALCULATOR.PACKAGEEl paquete excede el peso máximo = 25kg. Por favor reducir o armar múltiples paquetesweight mayor a 25 kg
422CALCULATOR.PACKAGEEl paquete excede el tamaño máximo = 50cm. * 50cm. * 50cm. Por favor reducir o armar múltiples paquetesAlgún lado mayor a 50 cm
400ERRORInvalid addressee postal codeEl código postal de entrega no existe
400ERRORInvalid sender postal codeEl código postal de retiro no existe o está fuera de cobertura
Los límites del paqueteAcá se valida cada lado por separado: máximo 50 cm. La cotización no lo hace, así que un bulto de 80 × 50 × 50 cm cotiza bien y después falla al crear la orden. Si trabajás con bultos grandes, verificá los lados antes de prometerle un precio a tu cliente.
Qué hacer después
  1. Guardá el id de la orden.
  2. Compartí cho_url o qr_image con tu cliente para que abone.
  3. Seguí el envío con el endpoint de tracking usando ese mismo id.
Endpoint 05

Confirmar orden

Avisa que el paquete ya está embalado y listo para que lo retiremos. La orden pasa de PAYED a CONFIRMED y entra en el programa de retiro. Mientras no se confirme, el envío no se agenda.

GET/api/orders/confirm/{id}/{sender_code}público
Sin token, pero no es abiertoEste endpoint no lleva token. Lo que lo protege es el sender_code: sin ese código no se puede confirmar la orden. Tratalo como un secreto — no lo publiques en una URL compartida ni lo expongas en el frontend de tu sitio.

Parámetros de URL

ParámetroTipoObligatorioDe dónde saleEjemplo
{id}StringObligatorioEl id que devolvió crear orden.ALFCM1520651
{sender_code}StringObligatorioEl sender_code que devolvió crear orden: 7 caracteres.A1B2C3D

Encabezados

EncabezadoObligatorioValor
AcceptObligatorioapplication/json

CURL

Confirmar el envío
curl --location 'https://dev-ar.e-pick.com.ar/api/orders/confirm/ALFCM1520651/A1B2C3D' \
--header 'Accept: application/json'

Respuesta 200

JSON
{
  "sTitle": "CONFIRMASTE TU ENVIO",
  "sContent": "En 1 a 3 días hábiles pasaremos a buscar tu paquete código ALFCM1520651."
}
CampoTipoDescripción
sTitleStringTítulo del mensaje, listo para mostrar.
sContentStringEl aviso para el remitente, con el plazo de retiro y el código del paquete.
Cómo saber si salió bienLa respuesta es texto para mostrar, no datos: no trae el estado nuevo ni ningún campo que puedas evaluar. Si confirmaste bien, verificalo por el código HTTP 200, no buscando un campo en el cuerpo. Los textos llegan en español.

Cuándo se puede confirmar

Estado de la ordenSe puede confirmarDetalle
PENDINGNoTodavía no está paga.
PAYEDSíEs el único estado que admite confirmación.
CONFIRMEDNoYa está confirmada.
COLLECTED y posterioresNoEl paquete ya salió.
Cuenta corrienteSi tenés cuenta corriente, tus órdenes nacen en PAYED, así que confirmar es el paso siguiente inmediato a crearlas.

Errores

HTTPCódigoMensajeCausa
400ERRORSu codigo de verificación es incorrectoEl sender_code no corresponde a esa orden. La orden queda como estaba.
400ERRORNo se puede confirmar un paquete por segunda vezLa orden ya estaba confirmada
El orden de los chequeosEl chequeo de ya confirmada corre antes que el del código. Si la orden ya está confirmada, vas a recibir ese mensaje aunque además el sender_code esté mal.
Endpoint 06

Etiquetas

Devuelve el PDF de la etiqueta para pegar en el paquete. Podés pedir una o varias en el mismo PDF, en formato A4 o para impresora térmica.

GET/api/orders/label/{ids}/{formato}token en la URL
Rompe el patrón del resto de la APIAcá el token va en la URL, como query parameter ?token=…, no en el encabezado x-access-token como en el resto de la API. Tenelo en cuenta: un token en la URL queda escrito en los logs de tu servidor, en el historial del navegador y en el encabezado Referer si la abrís desde una página. No generes estos links en el frontend.

Parámetros de URL

ParámetroObligatorioValorEjemplo
{ids}ObligatorioUno o varios identificadores de orden, separados por coma sin espacios. Todas las etiquetas salen en un mismo PDF. Máximo 12 por pedido.ALFCM1520609,ALFBO1519004
{formato}Obligatorionormal para hoja A4, thermal para impresora térmica de 10×15 cm.normal
tokenObligatorioEl token JWT del login, como query parameter.?token=eyJ0eXAiOiJKV1Qi…
Cuántas por vezEl tope es de 12 órdenes por pedido. Si tenés que imprimir más, partilas en tandas de a 12. Un PDF de 12 etiquetas pesa alrededor de un mega y medio.
Antes de pedir un loteEl PDF no te dice qué órdenes contiene: es un documento para imprimir, sin datos que puedas leer desde el código. Si pedís doce y recibís el archivo, no tenés forma de verificar que estén las doce. Por eso conviene que revises el estado de cada orden antes de armar el lote y lleves vos la cuenta de qué esperabas.

Cuándo se puede imprimir

Estados válidosSolo se generan etiquetas de órdenes en PAYED o CONFIRMED. Antes de pagarse no hay etiqueta, y una vez que el paquete salió tampoco tiene sentido reimprimirla.

CURL

Una etiqueta en A4
curl --location --output etiqueta.pdf \
'https://dev-ar.e-pick.com.ar/api/orders/label/ALFCM1520609/normal?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…'
Dos etiquetas térmicas en un mismo PDF
curl --location --output etiquetas.pdf \
'https://dev-ar.e-pick.com.ar/api/orders/label/ALFCM1520609,ALFBO1519004/thermal?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…'

Respuesta 200

La respuesta no es JSON: es el PDF binario. Guardalo o mandalo a la impresora directamente; no intentes parsearlo. Una etiqueta pesa alrededor de 125 KB, así que un pedido de cincuenta órdenes son unos pocos megabytes.

Qué devuelveDetalle
Código200
CuerpoEl PDF, en binario
Tamaño~125 KB por etiqueta

Los dos formatos

FormatoPara quéTamaño típico
normalHoja A4, para impresora común.~125 KB por etiqueta
thermal10×15 cm, para etiquetadora térmica.~140 KB por etiqueta

Errores

HTTPCódigoMensajeCausa
400—No se puede imprimirLa orden no está en PAYED ni en CONFIRMED
401—No autorizadoToken faltante, inválido o vencido en el query parameter
Cuerpo del 400
{
  "code": "ERROR",
  "message": "Para imprimir la/s orden/es deben estar Pagadas"
}
Al imprimir en loteCuando falla, la respuesta no es un PDF. Si tu código asume que siempre recibe un PDF y guarda el cuerpo directo a un archivo, vas a terminar con un .pdf roto en el disco y sin saber por qué. Al imprimir en lote, verificá el código HTTP antes de guardar: si no es 200, no lo escribas.

Qué trae la etiqueta

Número de orden, código QR, remitente, y los datos del destinatario: nombre, domicilio completo y teléfono. Si el envío lo entrega un servicio tercerizado, además lleva su código de sucursal y su código de barras.

Datos personalesEl PDF contiene datos personales del destinatario. Tratalo como tal: no lo dejes en una carpeta pública ni en una URL adivinable, y borralo cuando ya no lo necesites.
Endpoint 07

Tracking de orden

Devuelve dónde está la orden en este momento. Es público: no lleva token, así que podés consultarlo desde el navegador de tu cliente sin exponer credenciales.

GET/api/orders/tracking/{id}público
Foto contra historiaTracking es una foto del instante, no un historial. Te dice dónde está el paquete ahora y nada más: no guarda ni devuelve lo que pasó antes. Si ayer hubo un intento de entrega fallido y hoy el paquete ya se entregó, la consulta de hoy dice entregado y de aquel intento no queda rastro.

Los webhooks son lo contrario: te llega cada acción en el momento en que ocurre. Si las vas guardando, armás la historia completa del envío — cuándo se programó el retiro, cuántas veces se intentó entregar, por qué no se pudo, quién lo pausó — que es justo lo que necesitás cuando un cliente reclama y hay que reconstruir qué pasó.

Por eso consultar tracking más seguido no reemplaza a los webhooks: por más que preguntes cada minuto, lo que ocurrió entre dos consultas no lo vas a ver nunca.

Parámetro de URL

ParámetroTipoObligatorioDescripciónEjemplo
{id}StringObligatorioIdentificador de la orden, el que devolvió el endpoint de creación.ALFCM1520609

Encabezados

EncabezadoObligatorioValor
AcceptObligatorioapplication/json

CURL

Consultar estado
curl --location 'https://dev-ar.e-pick.com.ar/api/orders/tracking/ALFCM1520609' \
--header 'Accept: application/json'

Respuesta 200

Una respuesta real por cada estado de la orden.

PAYED
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "dark",
  "message": "PAQUETE PAGADO .El 20/08/2026 se abonó este envío. Estamos esperando que quien envía lo confirme. No tenemos fecha estimada de entrega."
}
CONFIRMED
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "ok",
  "message": "PAQUETE EN PROCESO DE RETIRO. Lo buscaremos el 21/08/2026. Fecha estimada de entrega: 26/08/2026"
}
COLLECTED
{
  "stage": "traveling",
  "serviceTracking": false,
  "stageType": "ok",
  "message": "PAQUETE COLECTADO. Ya tenemos tu paquete! 🥳 Lo retiramos el 20/08/2026.\n                    Fecha estimada de entrega: 26/08/2026."
}
DELIVERED-TO-SERVICE
{
  "stage": "traveling",
  "serviceTracking": "https://seguimiento.transportista.com/paquetes/123456789",
  "stageType": "ok",
  "message": "PAQUETE EN CAMINO. 😉"
}
DELIVERED
{
  "stage": "destination",
  "serviceTracking": false,
  "stageType": "ok",
  "message": "PAQUETE ENTREGADO. 🥳 Entregamos este paquete el 13/08/2026"
}
CANCELED
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "light",
  "message": "PAQUETE CANCELADO. Esta solicitud se encuentra cancelada desde el 18/08/2026"
}
Sobre el mensajeEl message viene redactado para mostrárselo al destinatario tal cual, en español, con la fecha ya adentro del texto y a veces con emojis. Puede traer saltos de línea y espacios de más — mirá el ejemplo de COLLECTED: eso llega así. Si lo vas a mostrar en HTML, normalizá los espacios en blanco antes.

Campos de respuesta

CampoTipoDescripción
stageStringTramo del recorrido: origin, traveling o destination.
serviceTrackingBoolean | String (URL)false cuando entregamos nosotros. Si el envío pasó a un servicio tercerizado, trae la URL de seguimiento de ese servicio para que se la muestres a tu cliente.
stageTypeStringCómo viene el envío: ok, dark, light o error. Ver la tabla de abajo.
messageStringTexto listo para mostrar al destinatario.

stageType

Es el campo que conviene usar para decidir qué mostrar. Son cuatro valores:

stageTypeSignificaQué mostrarle a tu cliente
okEl envío avanza con normalidad.Verde. Todo en orden.
darkFalta que alguien haga algo antes de que el envío siga.Amarillo. Hay una acción pendiente.
errorEl envío está trabado: un retiro o una entrega que no se pudo concretar, o una pausa.Rojo. Mostrá el message: explica qué pasó y cuándo se reintenta.
lightCancelado, o el código no existe.Gris. Ciclo cerrado.
El nombre engañaerror no es un error técnico y no significa que la consulta haya fallado: la respuesta es un 200 perfectamente válido. Quiere decir que el paquete está trabado y necesita atención. Es el estado que más le importa a tu cliente, porque es el único donde algo salió distinto de lo planeado.

Combinaciones reales

Las nueve formas de respuesta que aparecieron al consultar órdenes reales de todos los estados:

stagestageTypeSituación
origindarkPagado, esperando que el remitente confirme.
originokConfirmado, con fecha de retiro.
originerrorRetiro pausado o intento de retiro fallido.
originlightCancelado, o el código no existe.
travelingokRetirado y en tránsito, o ya en manos del servicio tercerizado.
travelingerrorIntento de entrega fallido.
destinationokEntregado.

Cuando el envío se traba

Estas son las respuestas que vas a querer mostrarle a tu cliente con más detalle, porque explican por qué el paquete no avanza:

Retiro fallido, con reintento
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "error",
  "message": "PAQUETE EN PAUSA. El 12/08/2026 intentamos retirarlo pero la colecta no se pudo concretar con éxito. Volveremos a intentar retirarlo el 13/08/2026. Fecha estimada de entrega, 19/08/2026"
}
Pausado por el remitente
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "error",
  "message": "PAQUETE EN PAUSA. Quien envía pausó el retiro de este paquete el dia 11/08/2026. Estamos esperando su confirmación para programar la colecta. No tenemos fecha estimada de entrega."
}
Entrega fallida, con reintento
{
  "stage": "traveling",
  "serviceTracking": false,
  "stageType": "error",
  "message": "PAQUETE COLECTADO. Ya tenemos tu paquete! 🥳 Lo retiramos el 08/08/2026. El 12/08/2026 intentamos efectuar la entrega y no se pudo concretar con éxito. Volveremos a intentar entregarlo el 13/08/2026."
}
Entrega trabada, requiere acción del destinatario
{
  "stage": "traveling",
  "serviceTracking": false,
  "stageType": "error",
  "message": "PAQUETE COLECTADO. Ya tenemos tu paquete! 🥳 Lo retiramos el 08/08/2026. El 12/08/2026 intentamos efectuar la entrega y ocurrió un inconveniente que necesitamos sea solucionado por quien recibe para volver a programar la misma."
}
No parsees el mensajeFijate que el mensaje del último caso arrastra el texto del estado anterior y le suma el incidente: primero cuenta que el paquete fue retirado y después qué pasó con la entrega. Los mensajes se componen así, y las plantillas cambian. No los parsees: usá stage y stageType para tu lógica, y mostrá el message tal cual.

Cuando el paquete no aparece

El error más fácil de pasar por altoHay dos casos distintos que devuelven exactamente la misma respuesta: un código que no existe, y una orden en PENDING, es decir creada pero todavía sin pagar. Las órdenes impagas no son visibles en tracking. Los dos casos devuelven 200 — no 404 — con el mensaje PAQUETE INEXISTENTE, así que no se pueden distinguir desde afuera. Si validás por el código HTTP, un número mal tipeado por tu cliente te va a pasar como válido.
Código inexistente, u orden sin pagar
{
  "stage": "origin",
  "serviceTracking": false,
  "stageType": "light",
  "message": "PAQUETE INEXISTENTE. No registramos paquetes con el código de operación ingresado."
}
stageTypeOjo: stageType: "light" se usa tanto para una orden cancelada como para un código inexistente. Para distinguirlas hay que mirar el message.

Errores

HTTPCódigoMensajeCausa
404—{"errors": "Item ID: tracking Not Found"}Llamaste al endpoint sin id en la URL
500—Error internoError del servidor

Página pública de seguimiento

Además del endpoint, hay una página web de seguimiento que podés compartir tal cual por mail o WhatsApp:

URL pública
https://dev-ar.e-pick.com.ar/tracking?id=ALFCM1520609
Endpoint 08

Cancelar orden

Cancela una orden que todavía no fue entregada. La cancelación es definitiva: una orden cancelada no se puede reactivar.

POST/api/orders/{id}/cancelrequiere token
AutenticaciónIgual que al crear una orden, necesitás un x-access-token válido.

Parámetro de URL

ParámetroTipoObligatorioDescripciónEjemplo
{id}StringObligatorioIdentificador de la orden a cancelar.ALFBO1519004

Encabezados

EncabezadoObligatorioValor
Content-TypeObligatorioapplication/json
x-access-tokenObligatorioToken JWT del login

CURL

Cancelar orden
curl --location --request POST 'https://dev-ar.e-pick.com.ar/api/orders/ALFBO1519004/cancel' \
--header 'Content-Type: application/json' \
--header 'x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…'

Respuesta 200

Cuerpo vacíoLa cancelación exitosa devuelve HTTP 200 con el cuerpo vacío. No esperes un JSON de vuelta: si tu cliente HTTP intenta parsear la respuesta, va a fallar. Verificá el código de estado, no el cuerpo.

La orden pasa a CANCELED y, si configuraste webhook, te llega la acción CANCELED.

Estados que admiten cancelación

EstadoSe puede cancelarDetalle
PENDINGSíEsperando pago
PAYEDSíPagada, antes del retiro
CONFIRMEDSíConfirmada, en programa de retiro
COLLECTEDSíRecolectada, antes de entregarla a un tercero
DELIVERED-TO-SERVICENoYa está en poder del servicio tercerizado
DELIVEREDNoCiclo terminado

Errores

HTTPCódigoMensajeCausa
401COMMON.UNAUTHORIZEDUnauthorized actionToken faltante, inválido o vencido
400NO-DATAOrder not foundLa orden no existe, o ya está cancelada
Dos casos, un mismo errorCancelar una orden que ya estaba cancelada devuelve el mismo 400 Order not found que cancelar una que nunca existió: no se distinguen. Y fijate que es un 400, no un 404, aunque el mensaje diga not found.
Ciclo de vida

Estados de una orden

Una orden recorre siempre la misma secuencia. Cada estado se corresponde con un tramo del recorrido físico del paquete.

TramooriginEn origen: pago, confirmación y retiro
TramotravelingEn camino: recolectado o en poder del servicio tercerizado
TramodestinationEn destino: entregado
EstadostagestageTypeQué significaQué podés hacer
PENDING——Creada, esperando pago. Solo si no tenés cuenta corriente. No es visible en tracking: el endpoint responde PAQUETE INEXISTENTE hasta que se abone.Compartir el link de pago
PAYEDorigindarkPago acreditado. Esperando que el remitente confirme que está embalado.Confirmar la orden
CONFIRMEDoriginokEmbalada y en programa de retiro.Nada, sigue solo
COLLECTEDtravelingokRetirada físicamente, en organización interna.Nada
DELIVERED-TO-SERVICEtravelingokEntregada al servicio tercerizado para la distribución final.Mostrar el link de seguimiento del tercero
DELIVEREDdestinationokEntregada al destinatario. Ciclo cerrado.Nada
CANCELEDoriginlightCancelada por el cliente o por el sistema.Nada, no se revierte
Secuencia
PENDING → PAYED → CONFIRMED → COLLECTED → DELIVERED-TO-SERVICE → DELIVERED
                                                                 ↓
                                                            CANCELED
Cuenta corrienteCon cuenta corriente no existe el estado PENDING: la orden nace en PAYED.
stage y estado no van de la manoEl stage no se deduce del estado. Una orden en CONFIRMED aparece en origin si el retiro está agendado para otro día, y en traveling si es para hoy. Si necesitás saber en qué tramo está el paquete, leé el stage que devuelve el tracking en vez de derivarlo del estado.
No confundir con los webhooksEsta tabla es el status de la orden, el que devuelven crear orden y tracking. Los webhooks no informan esto: informan acciones, con un vocabulario propio de 18 valores mucho más detallado. Si vas a integrar por webhook, la tabla que te importa es la de esa sección, no ésta.
Notificaciones

Webhooks

Cada vez que ocurre una acción sobre la orden le hacemos un POST a la URL que nos diste. No es una versión más cómoda del tracking: es la única forma de tener la historia completa de un envío.

Guardá todo lo que te llegaTracking te dice dónde está el paquete ahora. Los webhooks te cuentan todo lo que le fue pasando, en el momento en que pasa.

Guardá cada notificación que recibas, aunque en ese momento no la uses. Es tu único registro: no exponemos ningún endpoint que devuelva el historial de una orden, así que lo que no guardaste no se puede recuperar después.
Acciones, no estadosEl webhook informa acciones: operaciones concretas que ocurren sobre el envío, como que se programó un retiro o que no se pudo entregar. No es lo mismo que el status de la orden, que es el que ves al crearla o consultarla por tracking. Son dos modelos distintos y con vocabularios distintos: no intentes mapear uno contra el otro campo por campo.

Cómo se configura

Se declara al crear la orden, en info.webhook. No hay panel ni endpoint aparte: la URL viaja con cada orden, así que podés usar la misma para todas o una distinta por orden.

En el body de crear orden
{
  "info": {
    "webhook": "https://tu-dominio.com/webhooks/epick",
    "id": "87821",
    "creator_id": "3515555999"
  }
}

Cómo llega la notificación

DetalleValor
MétodoPOST
Content-Typeapplication/json
Acceptapplication/json, text/plain, */*
User-Agentaxios/0.21.4
Timeout5 segundos. Si tardás más, lo damos por fallido.
ReintentosDurante 24 horas. Si no respondemos un 200, seguimos intentando; pasadas las 24 horas esa notificación se pierde.
AutenticaciónNinguna. No mandamos token, API key ni firma.
La causa número uno de fallasNo enviamos credenciales. Si tu endpoint está detrás de autenticación, va a rechazarnos con 401 y no te van a llegar las notificaciones: es, de lejos, la causa más común de webhooks que "no funcionan". Dejá la URL abierta y protegela con un path difícil de adivinar, validando el order_id contra tu base antes de hacer nada.

El payload

Son siempre estos 13 campos, en todos los estados. Lo que cambia es cuáles vienen completos y cuáles en null.

CampoTipoPuede ser nullQué trae
order_idStringNoEl identificador de la orden en E-PICK. Es tu clave para cruzar.
integration_identificatorStringSíEl info.id que mandaste al crear la orden. Viene en null si no lo enviaste.
estadoStringSíLa acción que ocurrió. Es el dato que dispara tu lógica. El campo se llama estado por compatibilidad, pero lo que transporta es una acción.
idEstadoNumberSíCódigo numérico de la acción, del 1 al 14. Cuatro acciones no tienen número y llegan en null.
created_atISO 8601NoCuándo ocurrió el movimiento, en UTC.
short_descriptionStringSíMotivo en pocas palabras. Solo en estados de excepción.
long_descriptionStringSíEl mismo motivo explicado, listo para mostrarle a tu cliente.
coordsObjectSí{lat, lng} de dónde ocurrió el movimiento.
eta_startString HH:MM:SSSíInicio de la ventana horaria estimada. Es una hora, no una fecha, y está en hora local de Argentina.
eta_endString HH:MM:SSSíFin de la ventana horaria estimada, también en hora local de Argentina.
delivered_to_nameStringSíNombre de quien recibió el paquete.
delivered_to_dniStringSíDocumento de quien recibió.
delivered_to_relationStringSíVínculo con el destinatario: por ejemplo si recibió un familiar, el encargado del edificio o un compañero de trabajo.
Dos zonas horarias en el mismo payloadcreated_at viene en UTC — termina en Z — pero eta_start y eta_end están en hora local de Argentina. Son dos referencias distintas en el mismo payload: si convertís el eta como si fuera UTC, le vas a mostrar al destinatario una ventana corrida tres horas.
Cuándo vienen completosLos tres campos delivered_to_* se completan al entregar, y solo cuando la persona que recibe nos da sus datos. Si la entrega se hace con código verificador, no se piden y llegan en null. Tratalos siempre como opcionales: una entrega válida puede no traerlos.
Detalle que rompe integracionesDentro de coords, lat y lng a veces llegan como número y a veces como string: -34.6037 en un estado y "-34.6037" en otro. Convertilos a número siempre, no confíes en el tipo que venga.

Las 18 acciones

Estas son las acciones que viajan en el campo estado, ordenadas según el recorrido del paquete. La última columna dice qué campos opcionales completa cada una.

idEstadoAcciónEtapaQué ocurrióCampos que completa
1CREATEDAltaLa orden entró al sistema.—
2PAYEDAltaPago acreditado. En cuenta corriente llega apenas se crea la orden.—
3CONFIRMED-TO-PICKUPRetiroEl remitente confirmó que el paquete está embalado y listo.—
nullPLANNED-TO-PICKUPRetiroEl retiro entró en la planificación del día.—
4SCHEDULED-TO-PICKUPRetiroYa hay ventana horaria de retiro.eta_start, eta_end
5PICKUP-SUSPENDED-BY-CUSTOMERRetiroEl remitente pidió suspender el retiro.eta_start, eta_end
6NOT-PICKEDUPRetiroSe intentó el retiro y no se concretó. El motivo viene en las descripciones.eta_start, eta_end, coords, short_description, long_description
7PICKEDUPRetiroPaquete retirado.—
nullINVENTORY-ON-BASETránsitoEl paquete está en una de nuestras bases.coords
8ON-DELIVERED-SERVICETránsitoEl paquete salió a la calle para entregarse.—
14TRANSFERED-TO-SERVICETránsitoEl paquete pasó a un servicio tercerizado para la entrega final.short_description, long_description
nullWITH-SIZE-DIFFERENCESTránsitoAl medir el paquete no coincidió con lo declarado.short_description, long_description
nullPLANNED-TO-DELIVERYEntregaLa entrega entró en la planificación del día.—
9SCHEDULED-TO-DELIVERYEntregaYa hay ventana horaria de entrega.eta_start, eta_end
11DELIVERY-SUSPENDED-BY-CUSTOMEREntregaEl destinatario pidió suspender la entrega.eta_start, eta_end
10NOT-DELIVEREDEntregaSe intentó la entrega y no se concretó. El motivo viene en las descripciones.eta_start, eta_end, coords, short_description, long_description
12DELIVEREDEntregaPaquete entregado.—
13CANCELEDCierreOrden cancelada.—
Llegan de a variosUn mismo movimiento puede disparar más de una acción en el mismo segundo: por ejemplo INVENTORY-ON-BASE y PICKEDUP juntas. No asumas una notificación por movimiento ni que llegan en orden.

Ejemplos

Payloads tomados de envíos reales. Los identificadores de orden y las coordenadas están reemplazados por valores de ejemplo; el resto es tal cual sale.

Acción simple: entregado
{
  "order_id": "ALFCM1520609",
  "integration_identificator": "87821",
  "idEstado": 12,
  "estado": "DELIVERED",
  "created_at": "2026-08-20T16:35:26.000Z",
  "short_description": null,
  "long_description": null,
  "coords": null,
  "eta_start": null,
  "eta_end": null,
  "delivered_to_name": null,
  "delivered_to_dni": null,
  "delivered_to_relation": null
}
Con ventana horaria
{
  "order_id": "ALFCM1520609",
  "integration_identificator": "87821",
  "idEstado": 9,
  "estado": "SCHEDULED-TO-DELIVERY",
  "created_at": "2026-08-20T16:24:56.000Z",
  "short_description": null,
  "long_description": null,
  "coords": null,
  "eta_start": "14:42:00",
  "eta_end": "17:02:00",
  "delivered_to_name": null,
  "delivered_to_dni": null,
  "delivered_to_relation": null
}
Entrega fallida, con motivo y ubicación
{
  "order_id": "ALFBO1519004",
  "integration_identificator": "87822",
  "idEstado": 10,
  "estado": "NOT-DELIVERED",
  "created_at": "2026-08-20T16:24:53.000Z",
  "short_description": "No pudimos encontrarte",
  "long_description": "No atendiste el timbre / puerta,",
  "coords": { "lat": -34.6037, "lng": -58.3816 },
  "eta_start": "12:06:00",
  "eta_end": "14:26:00",
  "delivered_to_name": null,
  "delivered_to_dni": null,
  "delivered_to_relation": null
}
Sin idEstado y con coords en string
{
  "order_id": "ALFBO1519004",
  "integration_identificator": "87822",
  "idEstado": null,
  "estado": "INVENTORY-ON-BASE",
  "created_at": "2026-08-20T16:26:37.000Z",
  "short_description": null,
  "long_description": null,
  "coords": { "lat": "-34.6037", "lng": "-58.3816" },
  "eta_start": null,
  "eta_end": null,
  "delivered_to_name": null,
  "delivered_to_dni": null,
  "delivered_to_relation": null
}

Qué tiene que hacer tu endpoint

Checklist
  • Aceptar POST con JSON en una URL accesible desde internet.
  • Responder 200 en menos de 5 segundos. Si tenés que procesar, encolá y respondé primero.
  • Ser idempotente. Reintentamos durante 24 horas, así que la misma acción puede llegarte varias veces.
  • Tolerar desorden. Compará created_at con lo que ya guardaste y descartá lo viejo.
  • No romperse con acciones nuevas. Si aparece una acción que no conocés, ignorala y devolvé 200; no devuelvas error. Vamos a seguir agregando acciones a medida que la operación se hace más fina.
  • Responder 200 también para órdenes que no reconocés, en vez de un error: si no, vamos a reintentar 24 horas por una orden que nunca vas a procesar.

Recibirlo

Python / Django
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import json

@csrf_exempt
@require_POST
def epick_webhook(request):
    data = json.loads(request.body)

    order_id = data.get("order_id")
    estado = data.get("estado")
    ocurrido = data.get("created_at")

    order = Order.objects.filter(epick_id=order_id).first()
    if not order:
        # orden desconocida: respondemos 200 igual, si no reintenta 24 horas
        return JsonResponse({"ok": True})

    # idempotencia: descartar notificaciones repetidas o fuera de orden
    if order.epick_updated_at and ocurrido <= order.epick_updated_at:
        return JsonResponse({"ok": True})

    order.estado = estado
    order.epick_updated_at = ocurrido

    # las acciones de excepción traen el motivo para mostrarle al cliente
    if estado in ("NOT-PICKEDUP", "NOT-DELIVERED"):
        order.motivo = data.get("long_description")

    if estado in ("SCHEDULED-TO-PICKUP", "SCHEDULED-TO-DELIVERY"):
        order.ventana = (data.get("eta_start"), data.get("eta_end"))

    order.save()
    return JsonResponse({"ok": True})
Regla simpleDevolvé 200 siempre que hayas recibido el mensaje, incluso si no vas a hacer nada con él. Cualquier otra respuesta nos hace reintentar durante 24 horas.
Referencia

Códigos de error

Todos los códigos que puede devolver la API, en un solo lugar.

Códigos HTTP

HTTPNombreQué pasóQué hacer
200OKSolicitud exitosaProcesá la respuesta. En cotización, revisá igual isValid.
201CreatedRecurso creadoGuardá el id de la orden
400Bad RequestParámetros inválidos o faltantesRevisá formatos y campos obligatorios
401UnauthorizedToken faltante, inválido o vencidoHacé login de nuevo
402Payment RequiredCredenciales incorrectas en el loginVerificá phone y password
404Not FoundLa orden no existeVerificá el id
409ConflictUsuario o email ya registradosUsá otros datos o recuperá la cuenta
422Unprocessable EntityDatos válidos pero fuera de las reglas de negocioPeso mayor a 25 kg o dimensión mayor a 50 cm
500Internal Server ErrorError nuestroReintentá; si persiste, escribinos
503Service UnavailableMantenimientoReintentá en unos minutos

Códigos propios de E-PICK

CódigoHTTPDónde apareceSignificaSolución
INVALID-DATA400Register, LoginDatos inválidos o faltantesphone de 10 dígitos, email RFC 5322, password de 6+ caracteres
DUPLICATED-DATA409RegisterEl usuario ya existeUsá otro phone o email
MISSING-DATA400LoginFalta phone o passwordEnviá todos los campos
LOGIN-ERROR402LoginUsuario o clave incorrectosVerificá las credenciales
CALCULATOR.PACKAGE422Cotización, Crear ordenEl paquete no pasa la validaciónMáximo 50 cm por lado y 25 kg
CALCULATOR.POSTAL_CODE422Cotización, Crear ordenCódigo postal inválidoUsá un CP argentino de 4 dígitos
COMMON.UNAUTHORIZED401Crear orden, CancelarToken faltante, inválido o vencidoPedí un token nuevo
NO-DATA400 / 422Crear orden, CancelarFalta un objeto del cuerpo, o la orden no existeEl message dice cuál de los dos es

Cómo llega un error

Los errores de la API vienen siempre con la misma forma: un código y un mensaje explicando la causa.

Formato de error
{
  "code": "ERROR",
  "message": "Para imprimir la/s orden/es deben estar Pagadas"
}
CampoTipoPara qué te sirve
codeStringEl identificador del error. Es lo que tu código evalúa.
messageStringLa causa, en español y redactada para una persona. Sirve para mostrársela a tu operador; no la uses como condición en tu código, porque el texto puede cambiar.
La excepciónHay una excepción: si llamás a una ruta que no existe —por ejemplo tracking sin pasarle el id— el error viene del enrutador, no de la API, y tiene otra forma: {"errors": "Item ID: tracking Not Found"}. Si te aparece ese formato, revisá la URL antes que los datos.

El 403 que no es nuestro

Si recibís un 403 cuyo cuerpo menciona Cloudflare, no llegaste a la API: te frenó nuestro WAF antes. Pasa cuando el cliente HTTP no manda un User-Agent propio.

ClienteUser-Agent por defectoResultado
Python urllibPython-urllib/3.xBloqueado
Python requestspython-requests/2.xPasa
Node axiosaxios/1.xPasa
PHP GuzzleGuzzleHttp/7Pasa
curlcurl/8.xPasa
Java OkHttpokhttp/4.xPasa
Cómo se resuelveLa solución es mandar un User-Agent que identifique tu aplicación, por ejemplo mi-tienda/1.0. Es buena práctica igual: si algún día tenemos que rastrear un problema tuyo en nuestros logs, es lo que te distingue.
El error más comúnUn 401 en producción casi siempre es un token vencido, no un problema de permisos. Antes de escalar, probá renovarlo.
Guía práctica

Cálculo de dimensiones

Si tu envío junta varios productos en un mismo paquete, tenés que mandarnos las dimensiones del bulto final, no las de cada producto. Así se calculan.

Fórmulas

CálculoFórmula
Peso totalsuma de (peso × cantidad)
Volumen totalsuma de (largo × ancho × alto × cantidad)
Lado equivalenteraíz cúbica del volumen total
Dimensiones finaleslargo = ancho = alto = lado equivalente

Ejemplo

Datos de entrada
Producto 1:  cantidad 2   peso 1.2 kg   15 × 8 × 17 cm
Producto 2:  cantidad 3   peso 0.7 kg   10 × null × 20 cm
Paso a paso
1. Peso total
  (1.2 × 2) + (0.7 × 3) = 2.4 + 2.1 = 4.5 kg

2. Volumen total
  Producto 1: 15 × 8 × 17 = 2040 cm³  →  × 2 = 4080 cm³
  Producto 2: 10 × 0.1 × 20 = 20 cm³  →  × 3 = 60 cm³
  Total: 4080 + 60 = 4140 cm³

3. Lado equivalente
  ∛4140 = 16.05 cm

4. Lo que mandás en package
  16.05 × 16.05 × 16.05 cm, 4.5 kg
El detalle que se pasa por altoCuando a un producto le falta una dimensión, usá 0.1 cm en lugar de cero: un cero anula el volumen de todo el producto.

Restricciones

RestricciónLímiteSi se supera
Dimensión de un producto individual100 cmCotización isValid:false u orden con error 422
Dimensión del bulto final50 cmError 422 CALCULATOR.PACKAGE
Peso total25 kgError 422 CALCULATOR.PACKAGE

Equivalencia de volumen

El campo equivalence de la cotización es el multiplicador que se aplica al precio según el volumen del bulto. Un paquete chico da 1. Cuanto más volumen ocupa, más alto: un bulto de 50 × 50 × 50 cm da 8.04, y el price de esa cotización es el precio base multiplicado por 8,04.

E-PICK cobra por volumen ocupado, no por peso. La unidad de referencia es un paquete estándar de 18 × 27 × 32 cm, o sea 15.552 cm³. La cuenta es directa:

La cuenta
volumen del bulto   = largo × ancho × alto
equivalence  = volumen ÷ 15.552
precio              = tarifa × equivalence

Si el bulto es más chico que el paquete estándar, la equivalencia es 1: un envío no se cobra menos que un paquete.

MedidasVolumen÷ 15.552equivalencePrecio
18 × 27 × 32 cm15.552 cm³1,001$ 12.646
50 × 25 × 25 cm31.250 cm³2,00942.01$ 25.418
50 × 50 × 25 cm62.500 cm³4,01884.02$ 50.837
50 × 50 × 50 cm125.000 cm³8,03768.04$ 101.674
53 × 50 × 50 cm132.500 cm³8,5199rechazadoisValid: false
El techoLa última fila marca el techo: el volumen máximo es 125.000 cm³, el equivalente a un bulto de 50 × 50 × 50 cm. Por encima de eso la cotización devuelve isValid:false por más que la cuenta dé un número.
Errores comunesTres cosas que conviene tener claras:
  • No es un entero. No lo trates como una cantidad de bultos.
  • El precio ya viene multiplicado. No lo vuelvas a multiplicar vos.
  • El peso no cambia el precio. El mismo bulto con 0,1 kg o con 24 kg cotiza igual: el peso solo define si el envío se puede hacer o no.

Implementación

JavaScript
function dimensionesDelBulto(productos) {
  let pesoTotal = 0;
  let volumenTotal = 0;

  for (const producto of productos) {
    for (const dim of [producto.largo, producto.ancho, producto.alto]) {
      if (dim && dim > 100) {
        return { error: 'Ningún producto puede superar 100 cm por lado' };
      }
    }

    const cantidad = producto.cantidad || 1;
    pesoTotal += (producto.peso || 0) * cantidad;

    // una dimensión ausente vale 0.1, nunca 0
    const largo = producto.largo || 0.1;
    const ancho = producto.ancho || 0.1;
    const alto  = producto.alto  || 0.1;
    volumenTotal += largo * ancho * alto * cantidad;
  }

  if (pesoTotal > 25) {
    return { error: 'El peso total supera los 25 kg' };
  }

  const lado = Math.round(Math.cbrt(volumenTotal) * 100) / 100;

  if (lado > 50) {
    return { error: 'El bulto resultante supera los 50 cm por lado' };
  }

  return { long: lado, width: lado, height: lado, weight: pesoTotal };
}
Código

Ejemplo completo

El recorrido completo en un solo archivo: registro, login, cotización, creación y seguimiento.

Flujo completo en Python
import requests

BASE = "https://dev-ar.e-pick.com.ar"

# ---------------------------------------------------------------- 1. Registro (una sola vez)
r = requests.post(f"{BASE}/api/users/register", json={
    "phone": "3515555999",
    "email": "mitienda@example.com",
    "name": "Mi Tienda S.A.",
    "password": "MiPassword123",
})
r.raise_for_status()

# ---------------------------------------------------------------- 2. Login
r = requests.post(f"{BASE}/api/users/login", json={
    "phone": "3515555999",
    "password": "MiPassword123",
    "source": "API",
})
r.raise_for_status()
token = r.json()["token"]

# ---------------------------------------------------------------- 3. Cotización
r = requests.post(f"{BASE}/api/orders/calculator/www", json={
    "package": {"long": 10, "width": 12, "height": 11, "weight": "1.2", "value": 5000},
    "sender": {"postal_code": "5000"},
    "addressee": {"postal_code": "1425"},
})
quote = r.json()
if not quote["isValid"]:
    raise SystemExit("Cotización inválida: revisar medidas y códigos postales")

print("Precio:", quote["price"], "ETA", quote["eta"])

# ---------------------------------------------------------------- 4. Crear la orden
r = requests.post(
    f"{BASE}/api/orders/integrations/confirm-order",
    headers={"x-access-token": token},
    json={
        "info": {
            "webhook": "https://tu-dominio.com/webhooks/epick",
            "id": "87821",
            "creator_id": "3515555999",
        },
        "package": {"long": 10, "width": 12, "height": 11, "weight": 1.2, "value": 5000},
        "sender": {
            "postal_code": "5000", "name": "Mi Tienda", "phone": "3515555999",
            "email": "tienda@example.com", "street": "Av. Colón", "number": "500",
            "city": "Córdoba", "province": "Córdoba",
            "extra": "Piso 2", "info": "Llamar antes de retirar",
        },
        "addressee": {
            "postal_code": "1425", "name": "Cliente Final", "phone": "1145678900",
            "email": "cliente@example.com", "street": "Av. Rivadavia", "number": "3000",
            "city": "Buenos Aires", "province": "Buenos Aires",
            "extra": "Dpto 1B", "info": "Llamar al timbre",
        },
    },
)
r.raise_for_status()
order = r.json()
order_id = order["id"]

print("Orden creada:", order_id, order["status"])

# ---------------------------------------------------------------- 5. Seguimiento
r = requests.get(f"{BASE}/api/orders/tracking/{order_id}")
print(r.json()["message"])
En producciónEl registro se hace una vez, no en cada envío. En producción vas a llamar solo login, crear orden y tracking.
Dudas comunes

Preguntas frecuentes

¿Cuánto dura el token?

Alrededor de 11 horas (43.661 segundos). No hay refresh: cuando vence, se hace login de nuevo.

¿Qué pasa si no mando creator_id?

La orden se asocia al sender.phone si ese número está registrado. Si no lo está, la orden entra igual pero sin usuario ni cliente asociado, y después es difícil reclamarla. Mandalo siempre.

¿Puedo tener varias sucursales de retiro?

Sí. Mismo creator_id para todas, distinto sender.phone por sucursal. Así las órdenes quedan bajo la misma cuenta pero salen de puntos distintos.

¿Cómo sé si una zona está cubierta?

Cotizá el envío: si la cotización vuelve con isValid: true, la ruta entre esos dos códigos postales se puede hacer. Tené en cuenta que hay códigos postales cubiertos para entrega pero no para retiro, así que la cotización es siempre entre un origen y un destino concretos.

¿Qué pasa si el paquete pesa más de 25 kg?

La cotización devuelve isValid:false. Si igual intentás crear la orden, recibís un 422 con el mensaje "El paquete excede peso máximo = 25kg".

¿Puedo usar el mismo webhook para todas las órdenes?

Sí. En el payload viene el id de la orden y el integration_identificator con tu número interno, así que siempre sabés de cuál se trata.

¿Qué significa equivalence?

Es el multiplicador que se aplica al precio según el volumen del bulto. Un paquete chico da 1; uno de 50 × 50 × 50 cm da 8.04. El price de la cotización ya viene multiplicado, así que no lo vuelvas a multiplicar. Y no siempre es un entero: no lo trates como una cantidad de paquetes.

¿Cómo cancelo una orden?

Con POST /api/orders/{id}/cancel y el token. Se puede hasta el estado COLLECTED; después ya está en la calle.

¿Tengo que generar las etiquetas?

Las genera E-PICK: vos las descargás en PDF con el endpoint de etiquetas, en A4 o en formato térmico, de a una o de a varias en el mismo archivo.

¿Cómo pruebo el flujo completo de punta a punta?

Si tu cuenta no es corriente, la orden queda en PENDING esperando el pago y no podés seguir. Escribinos a info@e-pick.com.ar y te pasamos la cuenta de desarrollo a cuenta corriente: a partir de ahí las órdenes entran directamente en PAYED y podés recorrer todo el circuito.

¿Cada cuánto puedo consultar el tracking?

Si es para responderle a un cliente que entra a tu sitio, consultalo cuando haga falta. Pero si lo que querés es seguir tu operación, consultarlo más seguido no es la solución: tracking devuelve una foto del momento, así que lo que pase entre dos consultas se pierde igual. Para eso están los webhooks, que te avisan de cada acción cuando ocurre. Una integración con webhooks bien implementados casi no necesita consultar el tracking.

¿El tracking necesita token?

No. Es público, así que podés llamarlo desde el navegador de tu cliente o compartir directamente https://dev-ar.e-pick.com.ar/tracking?id={id}.

Estamos acá

Contacto y soporte

Escribinos a info@e-pick.com.ar. Para que podamos ayudarte rápido, mandanos el id de la orden y la hora aproximada de la llamada.

Ambientes

AmbienteURLEstado
Desarrollohttps://dev-ar.e-pick.com.arEn uso para toda la integración
Producciónhttps://www.e-pick.com.arSe habilita cuando terminás de probar

Cómo seguir

  1. Registrá un usuario de prueba.
  2. Corré el flujo completo: cotización, orden, etiqueta y tracking.
  3. Implementá el webhook y verificá que recibís las acciones.
  4. Avisanos para pasar a producción.