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.
Dónde llegamos
| Tramo | Alcance |
|---|---|
| Retiro | Buenos Aires, Córdoba y Rosario, con flota propia. |
| Entrega | Todo el país. Una vez que retiramos el paquete, lo llevamos a cualquier punto de Argentina. |
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.
| Servicio | Cuándo aplica |
|---|---|
Prime | Entrega el mismo día. |
Flex | Entrega en 2 a 3 días. |
Route | Cobertura nacional. |
Cómo empezar
Si vas a integrar a medida, el camino es este:
- 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.
- 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.
- Te habilitamos el ambiente de desarrollo y seguís esta guía.
- Cuando el circuito completo te funciona, te habilitamos producción.
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.ary 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_ides el campo que los mantiene bajo la misma cuenta. - Los datos de los ejemplos son ficticios. Los números de tracking de prueba son
ALFCM1520609yALFBO1519004.
Autenticación
La API usa tokens JWT. Se piden una vez, se guardan y se reutilizan hasta que vencen.
Flujo
| Paso | Acción | Datos |
|---|---|---|
| 01 | Registrar usuario | phone, email, name, password |
| 02 | Hacer login | phone, password, source |
| 03 | Recibir el token | JWT válido ~11 horas |
| 04 | Usarlo en cada request | x-access-token: {token} |
Contenido del token
El token es un JWT estándar. Si lo decodificás, vas a encontrar:
{
"id": "3515555999",
"customer_id": "b7c2d419-3f80-4a15-9e6d-0c48ab52f731",
"role": "USER",
"expiresIn": 43661,
"iat": 1784818338,
"exp": 1784861999
}| Campo | Qué es |
|---|---|
id | Teléfono del usuario. |
customer_id | Empresa asociada, si tiene datos cargados. |
role | Rol de la cuenta. |
expiresIn | Segundos de vida del token. |
iat | Cuándo se emitió, en formato Unix. |
exp | Cuándo vence, en formato Unix. |
Mapa de endpoints
Los ocho endpoints de la API, en el orden en que se usan.
| Endpoint | Método | Para qué sirve | Token |
|---|---|---|---|
/api/users/register | POST | Crear una cuenta | No |
/api/users/login | POST | Obtener token | No |
/api/orders/calculator/www | POST | Cotizar un envío | No |
/api/orders/integrations/confirm-order | POST | Crear la orden | Sí |
/api/orders/confirm/{id}/{sender_code} | GET | Avisar que está listo para retirar | No |
/api/orders/label/{ids}/{formato} | GET | Descargar el PDF de la etiqueta | Sí, en la URL |
/api/orders/tracking/{id} | GET | Consultar estado | No |
/api/orders/{id}/cancel | POST | Cancelar la orden | Sí |
Registro de usuario
Crea una cuenta de usuario en la plataforma. Es el primer paso si todavía no tenés credenciales.
/api/users/registersin tokenEncabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Accept | Obligatorio | application/json |
Content-Type | Obligatorio | application/json |
Cuerpo de la solicitud
{
"phone": "3515555999",
"email": "usuario@example.com",
"name": "Mi Empresa S.A.",
"password": "MiPassword123"
}Campos y validaciones
| Campo | Tipo | Obligatorio | Validación | Ejemplo |
|---|---|---|---|---|
phone | String | Obligatorio | Exactamente 10 dígitos numéricos. Único en el sistema. | 3515555999 |
email | String | Obligatorio | Formato RFC 5322. Máximo 254 caracteres. Único en el sistema. | usuario@example.com |
name | String | Obligatorio | Mínimo 1 carácter. Acepta números y caracteres especiales. | Mi Empresa S.A. |
password | String | Obligatorio | Mínimo 6 caracteres, máximo 500. Sin requisitos de complejidad. | MiPassword123 |
CURL
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
{
"expiresIn": 43661,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHBpcmVzSW4iOjQzNjYxLCJpYXQiOjE3ODQ4MTgzMzh9…",
"full_confirmation": null,
"customer": null,
"mustAcceptTerm": false,
"forceAcceptTerm": false
}Campos de respuesta
| Campo | Tipo | Descripción |
|---|---|---|
token | String (JWT) | Token de sesión, ya utilizable. Dura ~11 horas. |
expiresIn | Number | Segundos hasta que el token expire. |
customer | Object | null | Queda en null hasta que el usuario cargue los datos de empresa en el portal web. |
mustAcceptTerm | Boolean | Indica si hay términos pendientes de aceptación. |
Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 400 | INVALID-DATA | Parámetros inválidos o faltantes | Campo vacío o formato inválido: phone con menos de 10 dígitos, email sin @, password de menos de 6 caracteres |
| 409 | DUPLICATED-DATA | El usuario ya existe | El phone o el email ya están registrados |
| 500 | — | Error interno | Error del servidor |
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.
/api/users/loginsin tokenEncabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Accept | Obligatorio | application/json |
Content-Type | Obligatorio | application/json |
Cuerpo de la solicitud
{
"phone": "3515555999",
"password": "MiPassword123",
"source": "API"
}Campos
| Campo | Tipo | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|---|
phone | String | Obligatorio | Teléfono registrado, 10 dígitos. | 3515555999 |
password | String | Obligatorio | Contraseña de la cuenta. | MiPassword123 |
source | String | Obligatorio | Origen de la solicitud. | API |
CURL
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
{
"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
| Campo | Tipo | Descripción |
|---|---|---|
token | String (JWT) | Token de autenticación. Válido ~11 horas (43.661 segundos). |
expiresIn | Number | Segundos hasta el vencimiento. |
customer_id | UUID | null | Identificador de la empresa asociada. |
customer | Object | null | Datos completos de la empresa: CUIT, condición de facturación, dirección. |
address | Object | null | Dirección del usuario, ya geocodificada. |
Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 400 | MISSING-DATA | Faltan datos | No mandaste phone o password |
| 402 | LOGIN-ERROR | Usuario incorrecto | El phone no tiene formato válido: tienen que ser 10 dígitos |
| 402 | LOGIN-ERROR | Clave incorrecta | La contraseña no coincide, o el teléfono no está registrado |
| 500 | — | Error interno | Error del servidor |
Clave 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.
token y reutilizalo hasta que expire. Pedir uno nuevo en cada request funciona, pero es innecesario: dura unas 11 horas.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.
/api/orders/calculator/wwwsin tokenEncabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Accept | Obligatorio | application/json |
Content-Type | Obligatorio | application/json |
Origin | Recomendado | https://dev-ar.e-pick.com.ar |
Cuerpo de la solicitud
{
"package": {
"long": 10,
"width": 12,
"height": 11,
"weight": "1.2",
"value": 5000
},
"sender": {
"postal_code": "5000"
},
"addressee": {
"postal_code": "1425"
}
}{
"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
| Campo | Tipo | Obligatorio | Validación | Ejemplo |
|---|---|---|---|---|
long | Number | Obligatorio | Largo en cm. Acá se valida el volumen total, no cada lado por separado. | 10 |
width | Number | Obligatorio | Ancho en cm. | 12 |
height | Number | Obligatorio | Alto en cm. | 11 |
weight | String | Number | Obligatorio | Máximo 25 kg. Con 25 la cotización sale; con 25,1 devuelve isValid:false. | "1.2" |
value | Number | Obligatorio | Valor declarado del contenido. Sin tope. | 5000 |
- 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
| Campo | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|
sender.postal_code | Obligatorio | Código postal de retiro, 4 dígitos válidos. | 5000 |
addressee.postal_code | Obligatorio | Código postal de entrega, 4 dígitos válidos. | 1425 |
CURL
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
{
"isValid": true,
"price": 11060,
"equivalence": 1,
"eta": [3, 4],
"service": "E-PICK",
"package": "Route"
}{
"isValid": false,
"price": 0,
"equivalence": 0,
"eta": [0, 0]
}Campos de respuesta
| Campo | Tipo | Descripción |
|---|---|---|
isValid | Boolean | true = precio válido. false = alguna validación falló. |
price | Number | Precio del envío en pesos argentinos. |
equivalence | Number | Cuá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. |
eta | Array [min, max] | Días estimados de entrega. |
service | String | E-PICK cuando la entrega es nuestra. Cualquier otro valor es el servicio tercerizado que la hace. No lo hardcodees. |
package | String | Qué 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
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.| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 200 | isValid:false | El paquete no se puede enviar | Peso mayor a 25 kg o volumen total excedido |
| 422 | CALCULATOR.POSTAL_CODE | Error en campo "addressee_postal_code": 9999 | El código postal no existe. El mensaje dice cuál de los dos falló. |
| 400 | ERROR | Falta un objeto del cuerpo | No enviaste package, sender o addressee |
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.
/api/orders/integrations/confirm-orderrequiere tokenx-access-token válido, este endpoint devuelve 401. El token sale del login y dura ~11 horas.Encabezados
| Encabezado | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|
Accept | Obligatorio | Formato aceptado | application/json |
Content-Type | Obligatorio | Tipo de contenido | application/json |
x-access-token | Obligatorio | Token JWT obtenido en el login | eyJ0eXAiOiJKV1Qi… |
x-client-key | Opcional | Clave de cliente. Solo si te asignamos una; si no, omitilo. | 11111111-2222-3333-4444-555555555555 |
Cuerpo de la solicitud
{
"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
| Campo | Tipo | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|---|
webhook | String (URL) | Opcional | URL 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 |
id | String | Opcional | Tu identificador interno de la orden. Vuelve en la respuesta y en cada webhook. | 87821 |
creator_id | String | Recomendado | Teléfono del usuario registrado. Define de quién es la orden. | 3515555999 |
- Mandalo siempre. Garantiza que la orden quede bajo tu cuenta.
- Si no lo mandás: la orden se asocia al
sender.phonesi ese número está registrado. Si no lo está, la orden entra igual pero sin usuario ni cliente asociado. - Varias sucursales: mismo
creator_id, distintosender.phonepor punto de retiro.
Objeto package
| Campo | Tipo | Obligatorio | Validación |
|---|---|---|---|
long | Number | Obligatorio | Máximo 50 cm |
width | Number | Obligatorio | Máximo 50 cm |
height | Number | Obligatorio | Máximo 50 cm |
weight | Number | Obligatorio | Máximo 25 kg. Si se supera: error 422. |
value | Number | Obligatorio | Valor declarado del contenido |
Objetos sender y addressee
Los dos objetos llevan exactamente los mismos campos.
| Campo | Tipo | Obligatorio | Validación |
|---|---|---|---|
postal_code | String | Obligatorio | Tiene que ser un CP argentino válido. Si no, error 400. |
name | String | Obligatorio | Nombre de la persona o empresa |
phone | String | Obligatorio | Teléfono de contacto |
email | String | Obligatorio | Email de contacto |
street | String | Obligatorio | Nombre de la calle |
number | String | Obligatorio | Altura |
city | String | Obligatorio | Ciudad |
province | String | Obligatorio | Provincia |
extra | String | Opcional | Piso, departamento, unidad |
info | String | Opcional | Indicaciones para el repartidor, por ejemplo "sin timbre, llamar" |
CURL
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"
}
}'PAYED, sin pago previo. Si no, entran en PENDING y hay que abonarlas con el link de MercadoPago que viene en cho_url.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
{
"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
| Campo | Tipo | Descripción |
|---|---|---|
id | String | Identificador de la orden. Es el que usás para tracking y cancelación. Guardalo. |
status | String | Estado inicial: PAYED en cuenta corriente, PENDING si hay que pagar. |
integration_identificator | String | El info.id que mandaste. Te sirve para cruzar la orden con tu sistema. |
phisical_number | String | Número físico de la guía, el que va impreso en la etiqueta. |
sender_code / addressee_code | String | Códigos de 7 caracteres de remitente y destinatario. |
total | Number | Precio total del envío. |
cho_url | String (URL) | Link de MercadoPago para que tu cliente pague. |
qr_image | String (base64) | Código QR de pago, listo para mostrar o imprimir. |
account, prime, priority, package_id y algunos otros): son de uso interno de E-PICK y podés ignorarlos.Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 401 | COMMON.UNAUTHORIZED | Unauthorized action | Token faltante, inválido o vencido |
| 422 | NO-DATA | There is no sender, addressee or package info | Falta alguno de los tres objetos del cuerpo |
| 422 | CALCULATOR.PACKAGE | El paquete excede el peso máximo = 25kg. Por favor reducir o armar múltiples paquetes | weight mayor a 25 kg |
| 422 | CALCULATOR.PACKAGE | El paquete excede el tamaño máximo = 50cm. * 50cm. * 50cm. Por favor reducir o armar múltiples paquetes | Algún lado mayor a 50 cm |
| 400 | ERROR | Invalid addressee postal code | El código postal de entrega no existe |
| 400 | ERROR | Invalid sender postal code | El código postal de retiro no existe o está fuera de cobertura |
- Guardá el
idde la orden. - Compartí
cho_urloqr_imagecon tu cliente para que abone. - Seguí el envío con el endpoint de tracking usando ese mismo
id.
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.
/api/orders/confirm/{id}/{sender_code}públicosender_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ámetro | Tipo | Obligatorio | De dónde sale | Ejemplo |
|---|---|---|---|---|
{id} | String | Obligatorio | El id que devolvió crear orden. | ALFCM1520651 |
{sender_code} | String | Obligatorio | El sender_code que devolvió crear orden: 7 caracteres. | A1B2C3D |
Encabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Accept | Obligatorio | application/json |
CURL
curl --location 'https://dev-ar.e-pick.com.ar/api/orders/confirm/ALFCM1520651/A1B2C3D' \
--header 'Accept: application/json'Respuesta 200
{
"sTitle": "CONFIRMASTE TU ENVIO",
"sContent": "En 1 a 3 días hábiles pasaremos a buscar tu paquete código ALFCM1520651."
}| Campo | Tipo | Descripción |
|---|---|---|
sTitle | String | Título del mensaje, listo para mostrar. |
sContent | String | El aviso para el remitente, con el plazo de retiro y el código del paquete. |
200, no buscando un campo en el cuerpo. Los textos llegan en español.Cuándo se puede confirmar
| Estado de la orden | Se puede confirmar | Detalle |
|---|---|---|
PENDING | No | Todavía no está paga. |
PAYED | Sí | Es el único estado que admite confirmación. |
CONFIRMED | No | Ya está confirmada. |
COLLECTED y posteriores | No | El paquete ya salió. |
PAYED, así que confirmar es el paso siguiente inmediato a crearlas.Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 400 | ERROR | Su codigo de verificación es incorrecto | El sender_code no corresponde a esa orden. La orden queda como estaba. |
| 400 | ERROR | No se puede confirmar un paquete por segunda vez | La orden ya estaba confirmada |
sender_code esté mal.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.
/api/orders/label/{ids}/{formato}token en la URL?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ámetro | Obligatorio | Valor | Ejemplo |
|---|---|---|---|
{ids} | Obligatorio | Uno 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} | Obligatorio | normal para hoja A4, thermal para impresora térmica de 10×15 cm. | normal |
token | Obligatorio | El token JWT del login, como query parameter. | ?token=eyJ0eXAiOiJKV1Qi… |
Cuándo se puede imprimir
PAYED o CONFIRMED. Antes de pagarse no hay etiqueta, y una vez que el paquete salió tampoco tiene sentido reimprimirla.CURL
curl --location --output etiqueta.pdf \
'https://dev-ar.e-pick.com.ar/api/orders/label/ALFCM1520609/normal?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…'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é devuelve | Detalle |
|---|---|
| Código | 200 |
| Cuerpo | El PDF, en binario |
| Tamaño | ~125 KB por etiqueta |
Los dos formatos
| Formato | Para qué | Tamaño típico |
|---|---|---|
normal | Hoja A4, para impresora común. | ~125 KB por etiqueta |
thermal | 10×15 cm, para etiquetadora térmica. | ~140 KB por etiqueta |
Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 400 | — | No se puede imprimir | La orden no está en PAYED ni en CONFIRMED |
| 401 | — | No autorizado | Token faltante, inválido o vencido en el query parameter |
{
"code": "ERROR",
"message": "Para imprimir la/s orden/es deben estar Pagadas"
}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.
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.
/api/orders/tracking/{id}públicoLos 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ámetro | Tipo | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|---|
{id} | String | Obligatorio | Identificador de la orden, el que devolvió el endpoint de creación. | ALFCM1520609 |
Encabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Accept | Obligatorio | application/json |
CURL
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.
{
"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."
}{
"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"
}{
"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."
}{
"stage": "traveling",
"serviceTracking": "https://seguimiento.transportista.com/paquetes/123456789",
"stageType": "ok",
"message": "PAQUETE EN CAMINO. 😉"
}{
"stage": "destination",
"serviceTracking": false,
"stageType": "ok",
"message": "PAQUETE ENTREGADO. 🥳 Entregamos este paquete el 13/08/2026"
}{
"stage": "origin",
"serviceTracking": false,
"stageType": "light",
"message": "PAQUETE CANCELADO. Esta solicitud se encuentra cancelada desde el 18/08/2026"
}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
| Campo | Tipo | Descripción |
|---|---|---|
stage | String | Tramo del recorrido: origin, traveling o destination. |
serviceTracking | Boolean | 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. |
stageType | String | Cómo viene el envío: ok, dark, light o error. Ver la tabla de abajo. |
message | String | Texto listo para mostrar al destinatario. |
stageType
Es el campo que conviene usar para decidir qué mostrar. Son cuatro valores:
| stageType | Significa | Qué mostrarle a tu cliente |
|---|---|---|
ok | El envío avanza con normalidad. | Verde. Todo en orden. |
dark | Falta que alguien haga algo antes de que el envío siga. | Amarillo. Hay una acción pendiente. |
error | El 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. |
light | Cancelado, o el código no existe. | Gris. Ciclo cerrado. |
error 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:
| stage | stageType | Situación |
|---|---|---|
origin | dark | Pagado, esperando que el remitente confirme. |
origin | ok | Confirmado, con fecha de retiro. |
origin | error | Retiro pausado o intento de retiro fallido. |
origin | light | Cancelado, o el código no existe. |
traveling | ok | Retirado y en tránsito, o ya en manos del servicio tercerizado. |
traveling | error | Intento de entrega fallido. |
destination | ok | Entregado. |
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:
{
"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"
}{
"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."
}{
"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."
}{
"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."
}stage y stageType para tu lógica, y mostrá el message tal cual.Cuando el paquete no aparece
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.{
"stage": "origin",
"serviceTracking": false,
"stageType": "light",
"message": "PAQUETE INEXISTENTE. No registramos paquetes con el código de operación ingresado."
}stageType: "light" se usa tanto para una orden cancelada como para un código inexistente. Para distinguirlas hay que mirar el message.Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 404 | — | {"errors": "Item ID: tracking Not Found"} | Llamaste al endpoint sin id en la URL |
| 500 | — | Error interno | Error 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:
https://dev-ar.e-pick.com.ar/tracking?id=ALFCM1520609Cancelar orden
Cancela una orden que todavía no fue entregada. La cancelación es definitiva: una orden cancelada no se puede reactivar.
/api/orders/{id}/cancelrequiere tokenx-access-token válido.Parámetro de URL
| Parámetro | Tipo | Obligatorio | Descripción | Ejemplo |
|---|---|---|---|---|
{id} | String | Obligatorio | Identificador de la orden a cancelar. | ALFBO1519004 |
Encabezados
| Encabezado | Obligatorio | Valor |
|---|---|---|
Content-Type | Obligatorio | application/json |
x-access-token | Obligatorio | Token JWT del login |
CURL
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
La orden pasa a CANCELED y, si configuraste webhook, te llega la acción CANCELED.
Estados que admiten cancelación
| Estado | Se puede cancelar | Detalle |
|---|---|---|
PENDING | Sí | Esperando pago |
PAYED | Sí | Pagada, antes del retiro |
CONFIRMED | Sí | Confirmada, en programa de retiro |
COLLECTED | Sí | Recolectada, antes de entregarla a un tercero |
DELIVERED-TO-SERVICE | No | Ya está en poder del servicio tercerizado |
DELIVERED | No | Ciclo terminado |
Errores
| HTTP | Código | Mensaje | Causa |
|---|---|---|---|
| 401 | COMMON.UNAUTHORIZED | Unauthorized action | Token faltante, inválido o vencido |
| 400 | NO-DATA | Order not found | La orden no existe, o ya está cancelada |
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.Estados de una orden
Una orden recorre siempre la misma secuencia. Cada estado se corresponde con un tramo del recorrido físico del paquete.
| Estado | stage | stageType | Qué significa | Qué 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 |
| PAYED | origin | dark | Pago acreditado. Esperando que el remitente confirme que está embalado. | Confirmar la orden |
| CONFIRMED | origin | ok | Embalada y en programa de retiro. | Nada, sigue solo |
| COLLECTED | traveling | ok | Retirada físicamente, en organización interna. | Nada |
| DELIVERED-TO-SERVICE | traveling | ok | Entregada al servicio tercerizado para la distribución final. | Mostrar el link de seguimiento del tercero |
| DELIVERED | destination | ok | Entregada al destinatario. Ciclo cerrado. | Nada |
| CANCELED | origin | light | Cancelada por el cliente o por el sistema. | Nada, no se revierte |
PENDING → PAYED → CONFIRMED → COLLECTED → DELIVERED-TO-SERVICE → DELIVERED
↓
CANCELEDPENDING: la orden nace en PAYED.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.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.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á 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.
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.
{
"info": {
"webhook": "https://tu-dominio.com/webhooks/epick",
"id": "87821",
"creator_id": "3515555999"
}
}Cómo llega la notificación
| Detalle | Valor |
|---|---|
| Método | POST |
| Content-Type | application/json |
| Accept | application/json, text/plain, */* |
| User-Agent | axios/0.21.4 |
| Timeout | 5 segundos. Si tardás más, lo damos por fallido. |
| Reintentos | Durante 24 horas. Si no respondemos un 200, seguimos intentando; pasadas las 24 horas esa notificación se pierde. |
| Autenticación | Ninguna. No mandamos token, API key ni firma. |
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.
| Campo | Tipo | Puede ser null | Qué trae |
|---|---|---|---|
order_id | String | No | El identificador de la orden en E-PICK. Es tu clave para cruzar. |
integration_identificator | String | Sí | El info.id que mandaste al crear la orden. Viene en null si no lo enviaste. |
estado | String | Sí | 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. |
idEstado | Number | Sí | Código numérico de la acción, del 1 al 14. Cuatro acciones no tienen número y llegan en null. |
created_at | ISO 8601 | No | Cuándo ocurrió el movimiento, en UTC. |
short_description | String | Sí | Motivo en pocas palabras. Solo en estados de excepción. |
long_description | String | Sí | El mismo motivo explicado, listo para mostrarle a tu cliente. |
coords | Object | Sí | {lat, lng} de dónde ocurrió el movimiento. |
eta_start | String HH:MM:SS | Sí | Inicio de la ventana horaria estimada. Es una hora, no una fecha, y está en hora local de Argentina. |
eta_end | String HH:MM:SS | Sí | Fin de la ventana horaria estimada, también en hora local de Argentina. |
delivered_to_name | String | Sí | Nombre de quien recibió el paquete. |
delivered_to_dni | String | Sí | Documento de quien recibió. |
delivered_to_relation | String | Sí | Vínculo con el destinatario: por ejemplo si recibió un familiar, el encargado del edificio o un compañero de trabajo. |
created_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.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.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.
| idEstado | Acción | Etapa | Qué ocurrió | Campos que completa |
|---|---|---|---|---|
1 | CREATED | Alta | La orden entró al sistema. | — |
2 | PAYED | Alta | Pago acreditado. En cuenta corriente llega apenas se crea la orden. | — |
3 | CONFIRMED-TO-PICKUP | Retiro | El remitente confirmó que el paquete está embalado y listo. | — |
null | PLANNED-TO-PICKUP | Retiro | El retiro entró en la planificación del día. | — |
4 | SCHEDULED-TO-PICKUP | Retiro | Ya hay ventana horaria de retiro. | eta_start, eta_end |
5 | PICKUP-SUSPENDED-BY-CUSTOMER | Retiro | El remitente pidió suspender el retiro. | eta_start, eta_end |
6 | NOT-PICKEDUP | Retiro | Se intentó el retiro y no se concretó. El motivo viene en las descripciones. | eta_start, eta_end, coords, short_description, long_description |
7 | PICKEDUP | Retiro | Paquete retirado. | — |
null | INVENTORY-ON-BASE | Tránsito | El paquete está en una de nuestras bases. | coords |
8 | ON-DELIVERED-SERVICE | Tránsito | El paquete salió a la calle para entregarse. | — |
14 | TRANSFERED-TO-SERVICE | Tránsito | El paquete pasó a un servicio tercerizado para la entrega final. | short_description, long_description |
null | WITH-SIZE-DIFFERENCES | Tránsito | Al medir el paquete no coincidió con lo declarado. | short_description, long_description |
null | PLANNED-TO-DELIVERY | Entrega | La entrega entró en la planificación del día. | — |
9 | SCHEDULED-TO-DELIVERY | Entrega | Ya hay ventana horaria de entrega. | eta_start, eta_end |
11 | DELIVERY-SUSPENDED-BY-CUSTOMER | Entrega | El destinatario pidió suspender la entrega. | eta_start, eta_end |
10 | NOT-DELIVERED | Entrega | Se intentó la entrega y no se concretó. El motivo viene en las descripciones. | eta_start, eta_end, coords, short_description, long_description |
12 | DELIVERED | Entrega | Paquete entregado. | — |
13 | CANCELED | Cierre | Orden cancelada. | — |
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.
{
"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
}{
"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
}{
"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
}{
"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
- Aceptar
POSTcon 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_atcon 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
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})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.Códigos de error
Todos los códigos que puede devolver la API, en un solo lugar.
Códigos HTTP
| HTTP | Nombre | Qué pasó | Qué hacer |
|---|---|---|---|
| 200 | OK | Solicitud exitosa | Procesá la respuesta. En cotización, revisá igual isValid. |
| 201 | Created | Recurso creado | Guardá el id de la orden |
| 400 | Bad Request | Parámetros inválidos o faltantes | Revisá formatos y campos obligatorios |
| 401 | Unauthorized | Token faltante, inválido o vencido | Hacé login de nuevo |
| 402 | Payment Required | Credenciales incorrectas en el login | Verificá phone y password |
| 404 | Not Found | La orden no existe | Verificá el id |
| 409 | Conflict | Usuario o email ya registrados | Usá otros datos o recuperá la cuenta |
| 422 | Unprocessable Entity | Datos válidos pero fuera de las reglas de negocio | Peso mayor a 25 kg o dimensión mayor a 50 cm |
| 500 | Internal Server Error | Error nuestro | Reintentá; si persiste, escribinos |
| 503 | Service Unavailable | Mantenimiento | Reintentá en unos minutos |
Códigos propios de E-PICK
| Código | HTTP | Dónde aparece | Significa | Solución |
|---|---|---|---|---|
INVALID-DATA | 400 | Register, Login | Datos inválidos o faltantes | phone de 10 dígitos, email RFC 5322, password de 6+ caracteres |
DUPLICATED-DATA | 409 | Register | El usuario ya existe | Usá otro phone o email |
MISSING-DATA | 400 | Login | Falta phone o password | Enviá todos los campos |
LOGIN-ERROR | 402 | Login | Usuario o clave incorrectos | Verificá las credenciales |
CALCULATOR.PACKAGE | 422 | Cotización, Crear orden | El paquete no pasa la validación | Máximo 50 cm por lado y 25 kg |
CALCULATOR.POSTAL_CODE | 422 | Cotización, Crear orden | Código postal inválido | Usá un CP argentino de 4 dígitos |
COMMON.UNAUTHORIZED | 401 | Crear orden, Cancelar | Token faltante, inválido o vencido | Pedí un token nuevo |
NO-DATA | 400 / 422 | Crear orden, Cancelar | Falta un objeto del cuerpo, o la orden no existe | El 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.
{
"code": "ERROR",
"message": "Para imprimir la/s orden/es deben estar Pagadas"
}| Campo | Tipo | Para qué te sirve |
|---|---|---|
code | String | El identificador del error. Es lo que tu código evalúa. |
message | String | La 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. |
{"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.
| Cliente | User-Agent por defecto | Resultado |
|---|---|---|
Python urllib | Python-urllib/3.x | Bloqueado |
Python requests | python-requests/2.x | Pasa |
Node axios | axios/1.x | Pasa |
PHP Guzzle | GuzzleHttp/7 | Pasa |
curl | curl/8.x | Pasa |
Java OkHttp | okhttp/4.x | Pasa |
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.401 en producción casi siempre es un token vencido, no un problema de permisos. Antes de escalar, probá renovarlo.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álculo | Fórmula |
|---|---|
| Peso total | suma de (peso × cantidad) |
| Volumen total | suma de (largo × ancho × alto × cantidad) |
| Lado equivalente | raíz cúbica del volumen total |
| Dimensiones finales | largo = ancho = alto = lado equivalente |
Ejemplo
Producto 1: cantidad 2 peso 1.2 kg 15 × 8 × 17 cm
Producto 2: cantidad 3 peso 0.7 kg 10 × null × 20 cm1. 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 kg0.1 cm en lugar de cero: un cero anula el volumen de todo el producto.Restricciones
| Restricción | Límite | Si se supera |
|---|---|---|
| Dimensión de un producto individual | 100 cm | Cotización isValid:false u orden con error 422 |
| Dimensión del bulto final | 50 cm | Error 422 CALCULATOR.PACKAGE |
| Peso total | 25 kg | Error 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:
volumen del bulto = largo × ancho × alto
equivalence = volumen ÷ 15.552
precio = tarifa × equivalenceSi 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.
| Medidas | Volumen | ÷ 15.552 | equivalence | Precio |
|---|---|---|---|---|
| 18 × 27 × 32 cm | 15.552 cm³ | 1,00 | 1 | $ 12.646 |
| 50 × 25 × 25 cm | 31.250 cm³ | 2,0094 | 2.01 | $ 25.418 |
| 50 × 50 × 25 cm | 62.500 cm³ | 4,0188 | 4.02 | $ 50.837 |
| 50 × 50 × 50 cm | 125.000 cm³ | 8,0376 | 8.04 | $ 101.674 |
| 53 × 50 × 50 cm | 132.500 cm³ | 8,5199 | rechazado | isValid: false |
isValid:false por más que la cuenta dé un número.- 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
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 };
}Ejemplo completo
El recorrido completo en un solo archivo: registro, login, cotización, creación y seguimiento.
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"])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}.
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
| Ambiente | URL | Estado |
|---|---|---|
| Desarrollo | https://dev-ar.e-pick.com.ar | En uso para toda la integración |
| Producción | https://www.e-pick.com.ar | Se habilita cuando terminás de probar |
Cómo seguir
- Registrá un usuario de prueba.
- Corré el flujo completo: cotización, orden, etiqueta y tracking.
- Implementá el webhook y verificá que recibís las acciones.
- Avisanos para pasar a producción.