Endpoints que replican el sector móvil del sistema viejo
(portas.com.ar, app Symfony
smatphone) para que importas.com.ar
lo implemente de nuevo
El sistema viejo de Portas tiene una aplicación móvil (Symfony 1.4, app
smatphone) con 18 módulos y tres perfiles de usuario distintos.
Esta API expone esa misma funcionalidad como endpoints REST para que la reimplemente
importas.com.ar.
Convive con la API Web (escritorio) en el mismo servicio y con la misma API key. Hay solapamiento deliberado: movimientos y comprobantes existen en las dos, porque en el sistema viejo también son pantallas distintas con filtros distintos.
Son dos cosas separadas y hacen falta las dos:
| Qué | Dónde va | Para qué sirve |
|---|---|---|
| API key | Header Authorization: Bearer <key> |
Autentica a la aplicación. Es el secreto. Sin ella: 401. |
| id_usuario | Query string (o body en POST/PATCH/PUT) | Dice quién hace la llamada. Queda como usuario de alta / modificación en las tablas de Portas y en el log. |
Desde el 2026-09-08 id_usuario es el id_usuario de
freewaynet.usuarios, el padrón de FreeWayNet (el mismo id que queda en
id_usuario_alta de cada tabla). Importas mantiene un diccionario de sus usuarios
contra ese padrón y es responsable de mandar el id correcto. La tabla del móvil viejo,
web_clientes_usuarios, queda solo para el Symfony viejo mientras siga en uso.
Portas no valida la clave: el login lo hace importas contra sus propias tablas.
Los endpoints exigen scope movil:read o movil:write según
el caso. Por ahora también se aceptan web:read / web:write,
para que la key que importas ya tiene en producción sirva sin cambios.
GET /v1/movil/instalacion (sección 12) es
el único endpoint que no pide id_usuario: es justamente de
donde la app lo obtiene.
| Código | HTTP | Cuándo |
|---|---|---|
id_usuario_requerido | 400 | No vino, o no es un entero |
usuario_inexistente | 404 | No existe ese id en usuarios |
usuario_inactivo | 403 | activo <> 1 |
persona_no_habilitada | 403 | La operación es de otro perfil (ver 3) |
id_transportista_invalido | 400 | Vino pero no es un entero |
Qué ve la llamada no sale del usuario sino de los parámetros, igual que en la API Web. Importas decide qué parámetro manda para cada usuario suyo:
| Persona | Se decide por | Alcance | Credenciales del viejo |
|---|---|---|---|
transportista |
Viene id_transportista |
Nada en las pantallas de cliente. Lo suyo: remito firmado (11) y sus cargas
(GET /v1/web/oficios-externos?id_transportista=240, API Web) |
Movil, Transporte |
cliente |
Viene id_cliente (uno o varios: ?id_cliente=84,1167) |
Solo las operaciones de esos clientes | Movil, Cliente |
empleado |
No viene ninguno de los dos | Todas las operaciones (personal de Portas) | Movil, Empleado |
Si vienen los dos parámetros manda id_transportista. La persona
proveedor del viejo no tiene equivalente todavía.
web_clientes_usuarios y
siguen esperando el id_usuario_web de esa tabla. Quedan así hasta que se
defina dónde viven esos datos.
Quién es el usuario y qué menú mostrarle. Es el equivalente de "abrir la sesión" del móvil viejo: se llama una vez al entrar.
/v1/movil/usuario?id_usuario=123{
"ok": true,
"usuario": {
"id_usuario_web": 123,
"id_cliente": 1167,
"persona": "cliente",
"usuario": "operaciones@ferrero.com",
"nombre": "Juan Perez",
"cargo": "Comercio Exterior",
"razon_social": "FERRERO ARGENTINA S.A.",
"web_carpeta": "ferrero",
"id_proveedor_cliente": 0,
"id_transportista": 0,
"interno": null,
"notificaciones": { "impo": true, "expo": false }
},
"credenciales": ["Movil", "Cliente"],
"notificaciones": {
"impo": true, "expo": false, "oficializacion": true,
"finalizacion_carga": true, "permiso_cumplido": false,
"arribo_mercaderia": true, "facturacion": true, "reclamo": false,
"vencimiento_simi_djai": false, "vencimiento_temporal": false,
"vencimiento_forzoso": false, "digitalizacion": true,
"reintegro_aprobado": false, "coordinacion_transporte": false
}
}
credenciales viene con los nombres que usan los security.yml del
sistema viejo, para que el menú se pueda armar con el mismo criterio.
La pantalla "Perfil": casillas de notificación y cambio de clave. Las dos partes son opcionales — se manda lo que se quiere cambiar.
/v1/movil/usuario{
"id_usuario": 123,
"notificaciones": { "facturacion": false, "arribo_mercaderia": true },
"clave_actual": "laDeAhora",
"clave_nueva": "unaNuevaDe8+"
}
Es un PATCH de verdad: las casillas que no se mandan quedan como estaban. (El formulario viejo apagaba todo lo que no viniera, que es correcto para un form completo y peligroso para una API.)
| Código | HTTP | Cuándo |
|---|---|---|
nada_para_cambiar | 422 | No vino ni notificaciones ni clave_nueva |
notificacion_desconocida | 422 | Casilla que no existe (la respuesta lista las válidas) |
clave_corta | 422 | Menos de 8 caracteres |
clave_actual_incorrecta | 403 | No coincide con la vigente |
clave sin hashear. Mientras los dos sistemas convivan no se puede cambiar el
formato sin romper el login del viejo, así que esta API hace lo mismo — la
clave nunca sale en una respuesta, pero el almacenamiento sigue siendo plano. El paso a
bcrypt queda para cuando se apague el sistema viejo.
La grilla de eventos de la pantalla "Perfil": una columna para importación y otra para exportación.
/v1/movil/notificaciones/eventos?id_usuario=123{
"ok": true,
"eventos": {
"importacion": [
{ "id_evento": 12, "nombre": "Arribo de mercadería", "sector": "Operaciones",
"categoria": "", "color": "#3498db", "suscripto": true }
],
"exportacion": [ ... ]
}
}
/v1/movil/notificaciones/eventos{
"id_usuario": 123,
"eventos": [
{ "id_evento": 12, "operacion": 1, "suscripto": false },
{ "id_evento": 15, "operacion": 2, "suscripto": true }
]
}
operacion: 1 = importación, 2 = exportación. Es idempotente: se
puede repetir el mismo PUT sin efectos raros.
web_clientes_usuarios_eventos guarda exclusiones: si hay fila, al
usuario NO se le avisa, y el default es recibir todo. Está al revés de lo que
sugiere el nombre, y equivocarse deja a un cliente sin avisos o lo inunda. La API expone
suscripto (true = le llega) y hace la traducción del lado del servidor.
Registro del token de notificaciones push. Reemplaza /registrar_movil.
/v1/movil/dispositivos{ "id_usuario": 123, "token": "fcm-token-del-dispositivo", "so": "android" }
so: android, ios o web. Se escriben las
dos cosas que escribe el viejo: el token vigente en web_clientes_usuarios
(que es el que lee FreeWayNet para mandar el aviso) y el historial en
web_clientes_usuarios_token.
/v1/movil/dispositivos?id_usuario=123Lista los dispositivos registrados. El token se devuelve parcial
(abc123…xyz789): alcanza para reconocerlo y no sirve para mandar push.
/v1/movil/dispositivos{ "id_usuario": 123, "token": "fcm-token-del-dispositivo" }
Da de baja el dispositivo. Si era el vigente, también lo apaga en el usuario — si no, FreeWayNet le seguiría mandando avisos a un teléfono desvinculado.
Módulo ultimos_mov: la cronología de hitos y eventos de las
operaciones que el usuario puede ver.
/v1/movil/movimientos?id_usuario=123&via=sea&canal=rojo&page=1&limit=25{
"ok": true,
"items": [
{
"id_carpeta": "2026.04.94961",
"legajo": 94961,
"referencia": "PO-88213",
"factura": "A-0001-00012345",
"fecha": "2026-08-20 14:32:00",
"id_hito": 4,
"hito": "Oficialización",
"evento": "Despacho oficializado",
"observacion": ""
}
],
"paginado": { "pagina": 1, "limite": 25, "total": 412, "paginas": 17 }
}
Acepta todos los filtros comunes. limit va de 1 a 200
(25 por defecto).
Módulo comp_adeudados: lo que el cliente todavía debe.
/v1/movil/comprobantes-adeudados?id_usuario=123&estado=vencidosestado: todos (default), vencidos,
a_vencer. Más todos los filtros comunes.
{
"ok": true,
"items": [
{
"id_comprobante": "A-0001-00012345",
"legajo": 94961,
"id_carpeta": "2026.04.94961",
"operacion": "22001EC01030375C",
"referencia": "PO-88213",
"mercaderia": "Chocolate",
"factura_proveedor": "INV-2210",
"fecha": "2026-07-15",
"vencimiento": "2026-08-14",
"total": 154320.55,
"total_comprobante": 480000.00,
"vencido": true,
"dias_vencido": 10,
"cliente": ""
}
],
"estado": "vencidos",
"resumen": {
"comprobantes": 12, "monto": 1854320.55,
"vencidos": 5, "monto_vencido": 640210.00, "mas_antiguo": "2026-05-02"
},
"paginado": { "pagina": 1, "limite": 25, "total": 12, "paginas": 1 }
}
total es el SALDO, no el importe del comprobante. Es lo que
queda por cobrar, ya descontadas las imputaciones, y es lo que cuadra con la cuenta
corriente de FreeWayNet. El valor original del comprobante va aparte, en
total_comprobante. Usar el segundo para mostrar deuda infla el total.
cliente viene con la razón social solo cuando la llamada es de un
empleado (que ve varios clientes); para un cliente siempre es la propia y va vacío.
operacion acá NO es "importación / exportación".
La vista view_cli_comprobantes_adeudados devuelve en esa columna el
número de despacho (por ejemplo 22001EC01030375C), no el
tipo de operación. El nombre viene heredado de la vista y es el mismo valor que
devuelve /v1/web/comprobantes. Para separar impo de expo está el filtro
tipo=import|export, que se resuelve contra carpetas.operacion.
Los dos tablones de novedades. Misma forma para los dos.
/v1/movil/circulares?id_usuario=123&q=aduana/v1/movil/circulares/{id}?id_usuario=123/v1/movil/newsletters?id_usuario=123/v1/movil/newsletters/{id}?id_usuario=123El listado no trae el cuerpo (son textos largos con HTML): solo título,
fecha, link y palabras clave. El detalle agrega texto y
texto_html. q busca en título y palabras clave.
todos_los_clientes / id_clientes: si alguna
circular estaba pensada para un cliente puntual, hoy la ven todos. Se replicó el
comportamiento actual para no cambiar en la migración lo que la gente ve, pero los
campos se devuelven en cada item por si hay que filtrar.
Reemplaza las tres pantallas de fotos del móvil viejo (Foto Precinto, Inspección Mercadería y Subir Remito del transportista). Una sola llamada hace lo que antes eran dos pasos (subir y "Notificar"): crea el evento en la operación, guarda las fotos y deja la notificación al cliente pendiente.
/v1/movil/legajos/2026.04.94979/fotos multipart/form-data| Campo | Descripción | |
|---|---|---|
id_usuario | obligatorio | Usuario de FreeWayNet que hace la carga; queda en
id_usuario_alta del evento (ver 2). |
id_transportista | para remito | Hace que la llamada sea de persona
transportista: solo puede subir tipo=remito y solo sobre sus cargas. Sin él,
la llamada es de empleado. |
tipo | obligatorio | precinto → evento 176 FOTO PRECINTO (empleado)inspeccion → evento 188 INSPECCIÓN FÍSICA CANAL ROJO (empleado)remito → evento 240 FOTO REMITO (transportista de esa carga, o empleado)Con id_transportista solo se admite remito. |
fotos[] | 1 a 10 | Imágenes jpg / png / webp, hasta 12 MB cada una. Se normalizan en el servidor:
orientación corregida, lado mayor 1600 px, JPEG. También se acepta un campo
simple foto. |
observacion | opcional | Texto libre (hasta 2000). Para inspeccion es lo que se registra como
observación del evento ("Todo ok.") y permite crear el evento sin foto,
como el viejo. Para precinto y remito el título se arma solo:
FOTO PRECINTO (94979) - 7381ST. |
id_registro | opcional | Para agregar fotos a un evento creado por una llamada anterior del mismo tipo (lo devuelve la respuesta) en vez de crear otro. Si se omite, cada llamada crea un evento nuevo — el mail al cliente igual agrupa todo lo de la carpeta. |
curl -X POST "https://api.portascloud.ar/v1/movil/legajos/2026.04.94979/fotos?id_usuario=123" \
-H "Authorization: Bearer $API_KEY" \
-F tipo=precinto \
-F "fotos[]=@IMG_20260828_110608.jpg" \
-F "fotos[]=@IMG_20260828_110612.jpg"
# remito firmado por el transportista
curl -X POST "https://api.portascloud.ar/v1/movil/legajos/2026.04.94979/fotos?id_usuario=1022&id_transportista=240" -H "Authorization: Bearer $API_KEY" -F tipo=remito -F "fotos[]=@remito_firmado.jpg"
Respuesta 201:
{
"ok": true,
"id_carpeta": "2026.04.94979",
"tipo": "precinto",
"id_evento": 176,
"id_registro": 812345,
"evento_nuevo": true,
"observacion": "FOTO PRECINTO (94979) - 7381ST",
"fotos": [
{ "id_archivo": 601234, "nombre_archivo": "IMG_20260828_110608.jpg",
"tamanio_bytes": 190935, "download_url": "https://api.portascloud.ar/v1/d/<token>",
"ya_existia": false }
],
"notificacion": "pendiente"
}
Qué pasa después. Las fotos quedan en el storage de Portas
(descargables ya por download_url), el Sincronizador las lleva a la red de Portas
a la carpeta del evento (FreeWayNet las muestra con el clip) y el circuito de notificaciones
le avisa al cliente con las fotos, respetando sus preferencias por evento — igual que
cuando se cargaban desde el móvil viejo.
| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | Un cliente intenta subir; el tipo no es para su persona. |
| 403 | carpeta_ajena | Transportista sobre una carga que no es suya: no es el transportista del embarque ni tiene un remito (N/E) a su nombre en esa carpeta — el mismo criterio con el que se arma su listado. |
| 404 | carpeta_inexistente / evento_inexistente | |
| 409 | carpeta_anulada | |
| 415 | extension_not_allowed / no_es_imagen | No es jpg/png/webp o el archivo está corrupto. |
| 422 | tipo_invalido / sin_fotos / demasiadas_fotos | |
| 400 | upload_error | Foto por encima del límite de tamaño o incompleta. |
La app no tiene login. Al instalarse genera un identificador propio (un UUID v4)
y lo guarda en el teléfono. En cada arranque lo consulta acá y
recibe el id_usuario vinculado a ese teléfono, que es el que después
manda en el resto de los endpoints (por ejemplo, las posiciones de la sección 13).
/v1/movil/instalacion?id_instalacion=b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11| Parámetro | Detalle | |
|---|---|---|
id_instalacion | obligatorio | UUID de 36 caracteres con guiones
(8-4-4-4-12 hexadecimales). Mayúsculas o minúsculas da igual: se
guarda en minúsculas. |
No lleva id_usuario. Sirve la misma API key de la app que se usa para las
posiciones GPS.
// Primera vez que arranca: el UUID no existía y se registró
{ "ok": true, "id_instalacion": "b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11", "id_usuario": null, "nuevo": true }
// Registrado pero Portas todavía no lo vinculó a un usuario
{ "ok": true, "id_instalacion": "b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11", "id_usuario": null, "nuevo": false }
// Vinculado: la app ya puede trabajar
{ "ok": true, "id_instalacion": "b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11", "id_usuario": 123, "nuevo": false }
| Campo | Detalle |
|---|---|
id_usuario | El id_usuario de freewaynet.usuarios
vinculado a este teléfono, o null si todavía no se vinculó. |
nuevo | true solo en la llamada que registró el UUID.
Informativo. |
id_usuario vacío. Sistemas de Portas lo
vincula al usuario correspondiente y, desde el siguiente arranque (o la siguiente consulta),
el endpoint devuelve el id_usuario. No hay pantalla de login ni clave.
id_usuario sea null, no enviar datos y volver a consultar este
endpoint (reintento periódico o botón), sin obligar a cerrar la app.id_usuario guardado, para tomar
cualquier cambio de vinculación.| HTTP | Código | Cuándo |
|---|---|---|
| 400 | id_instalacion_invalido | Falta id_instalacion o no tiene formato UUID. |
| 401 | API key ausente o inválida. | |
| 403 | missing_scope | La key no tiene el scope de la app móvil. |
La app captura la posición del teléfono y la envía para poder ver en un mapa,
en tiempo real, dónde está el personal de Portas que trabaja en el puerto. Cada
teléfono es de un solo usuario: id_usuario va una vez, en la
raíz. Solo para personal de Portas (sin id_cliente ni
id_transportista).
/v1/movil/posiciones{
"id_usuario": 123,
"id_dispositivo": "b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11",
"version_app": "1.0.3",
"puntos": [
{
"lat": -34.581234,
"lng": -58.370456,
"fecha_hora": "2026-09-21T10:15:00-03:00",
"precision_m": 8.5,
"velocidad_kmh": 4.2,
"rumbo": 270,
"altitud_m": 12.0,
"bateria": 76,
"origen": "gps",
"es_simulada": false,
"id_carpeta": "2026.04.94961"
}
]
}
| Campo | Detalle | |
|---|---|---|
lat, lng | obligatorio | Grados decimales (WGS84). Se rechaza 0,0. |
fecha_hora | obligatorio | Momento de la captura (no del envío), ISO 8601
con offset o Z. Sin offset se toma como hora argentina. |
precision_m | muy recomendado | Radio de error en metros que informa el sistema operativo (accuracy). Es lo que permite descartar los puntos malos al dibujar. |
velocidad_kmh, rumbo, altitud_m | opcional | Rumbo en grados, 0 = norte. |
bateria | opcional | Porcentaje 0-100. |
origen | opcional | gps, red o fusionado. |
es_simulada | opcional | true si el sistema operativo marca la ubicación como mock. |
id_carpeta | opcional | El legajo en el que está trabajando el usuario, si la app lo sabe. |
id_dispositivo, version_app | opcional (raíz) | Un identificador de instalación generado por la app. Informativo. |
id_usuario y
fecha_hora que uno ya recibido se cuenta como duplicado y no se vuelve a guardar,
así que ante un timeout se puede reenviar el lote completo sin riesgo.
{
"ok": true,
"recibidos": 3,
"insertados": 2,
"duplicados": 0,
"rechazados": [ { "indice": 1, "motivo": "'fecha_hora' esta en el futuro" } ]
}
Un punto inválido no rechaza el lote: va a rechazados (con su
posición en la lista) y el resto se guarda. Con un 200 la app puede borrar
todo el lote de su cola local, incluidos los rechazados — reenviarlos daría el
mismo resultado. Se rechazan: coordenadas fuera de rango o 0,0, fecha ilegible, fecha a más de
10 minutos en el futuro o de más de 30 días. Los opcionales fuera de rango se guardan
vacíos sin rechazar el punto.
POST con la lista entera → solo con un HTTP 200
vaciar la lista. Ante cualquier otra cosa (sin conexión, timeout, 5xx) la lista se conserva
y se reintenta en el próximo ciclo con los puntos nuevos agregados. Si la lista supera los
500 puntos, enviarla en tandas de 500 y quitar de la lista cada tanda confirmada.
/v1/movil/posiciones/config?id_usuario=123Las opciones de captura, para poder cambiarlas sin publicar una versión nueva de la app:
cada cuánto capturar y el horario laboral del usuario. Pedirla al iniciar
la app y después cada refrescar_config_min minutos; si no hay conexión,
seguir con la última que se obtuvo.
{
"ok": true,
"id_usuario": 123,
"rastreo_activo": true,
"intervalo_movimiento_seg": 300,
"intervalo_quieto_seg": 300,
"intervalo_envio_seg": 300,
"precision_max_m": null,
"refrescar_config_min": 60,
"max_puntos_por_envio": 500,
"zona_horaria": "America/Argentina/Buenos_Aires",
"fecha_servidor": "2026-09-21T10:16:05-03:00",
"horario_propio": false,
"horario": [
{ "dia_semana": 1, "dia": "lunes", "desde": "08:00", "hasta": "18:00" },
{ "dia_semana": 2, "dia": "martes", "desde": "08:00", "hasta": "18:00" }
],
"en_horario_ahora": true
}
| Campo | Detalle |
|---|---|
rastreo_activo | false = no capturar nada (apagado general o de ese usuario). |
intervalo_movimiento_seg / intervalo_quieto_seg | Segundos entre capturas según el
teléfono se esté moviendo o no. Si la app no distingue, usar intervalo_movimiento_seg. |
intervalo_envio_seg | Cada cuánto intentar enviar la lista acumulada. |
precision_max_m | Si no es null, la app puede descartar las capturas con error mayor a esos metros. |
horario | Franjas en las que se captura. dia_semana: 1 = lunes … 7 = domingo.
Puede haber varias franjas el mismo día (horario cortado). Un día sin franjas = no se
captura. desde inclusive, hasta exclusive, en la hora de zona_horaria. |
horario_propio | true si el usuario tiene un horario particular; false si es el general. |
en_horario_ahora / fecha_servidor | Ya resuelto con el reloj del servidor, para detectar un teléfono con la hora mal puesta. |
Fuera del horario laboral la app no debe capturar (ni guardar en la lista). Lo que
ya estaba en la lista sí se puede enviar fuera de horario. Si igual llega un punto capturado
fuera de horario, el POST lo guarda (no es un error ni va a
rechazados); el horario se aplica al mostrar: /posiciones/ultimas y
/posiciones dejan afuera esos puntos, salvo que se pidan con
incluir_fuera_de_horario=1. Cada punto devuelto trae en_horario.
/v1/movil/posiciones/ultimas?id_usuario=123&max_minutos=30La última posición conocida de cada usuario: lo que dibuja el mapa en vivo.
id_usuario es opcional y aquí es el usuario que se busca: con él,
items trae solo su última posición (o viene vacío si no reportó o lo último
es de fuera de horario); sin él, la de todos.
max_minutos (opcional) deja afuera a los que no reportan desde hace más de eso.
{
"ok": true,
"fecha_servidor": "2026-09-21T10:16:05-03:00",
"items": [
{
"id_usuario": 123, "nombre": "JUAN PEREZ",
"lat": -34.581234, "lng": -58.370456,
"fecha_hora": "2026-09-21T10:15:00-03:00",
"precision_m": 8.5, "velocidad_kmh": 4.2, "rumbo": 270, "bateria": 76,
"origen": "gps", "es_simulada": false, "id_carpeta": "2026.04.94961",
"id_dispositivo": "b1f6c0e2-...", "version_app": "1.0.3",
"fecha_recepcion": "2026-09-21T10:15:04-03:00",
"segundos_sin_reportar": 65
}
]
}
/v1/movil/posiciones?id_usuario=123&id_usuario_consultado=45&desde=2026-09-21T08:00:00&hasta=2026-09-21T18:00:00&precision_max=50El recorrido de un usuario, en orden cronológico. id_usuario es quien consulta;
id_usuario_consultado, de quién es el recorrido. Sin fechas devuelve lo que va
del día. Rango máximo 31 días y 5000 puntos ("truncado": true si
había más). precision_max descarta los puntos con error mayor a esos metros.
| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | La llamada trae id_cliente o id_transportista. |
| 422 | puntos_requeridos / demasiados_puntos | Falta la lista o trae más de 500. |
| 400 | id_usuario_consultado_requerido / fecha_invalida / rango_invalido /
rango_excedido / precision_max_invalida / max_minutos_invalido |
Reemplaza las pantallas Rendir, Historial Rendiciones, Estado de Rendiciones,
Pedir Fondo e Historial Pedido Fondo del menú del empleado. Son solo para
personal de Portas, es decir, llamadas sin id_cliente ni id_transportista.
Cada empleado ve y carga solo lo suyo.
Hay dos tipos de registro, que la API distingue con tipo:
tipo | Qué es | Legajo | Concepto |
|---|---|---|---|
rendicion | Un gasto que el empleado ya hizo y rinde | obligatorio | Uno de /conceptos |
vale | Un pedido de fondos: plata que el empleado pide antes de gastar | opcional | Siempre RETIRO VALE; lo pone la API |
estado | Significa |
|---|---|
I Informado | Esperando aprobación. Todo nace en este estado. |
A Aprobado | Lo aprobó Portas. |
C Rechazado | Portas no lo aprobó. |
P Pendiente | Solo aparece en registros viejos; la API nunca lo crea. Se muestra junto con los I. |
Es la misma en el listado, en el detalle y en la respuesta del alta. Trae todo lo que muestra la tarjeta: el legajo con su operación, vía, canal y cliente; quién aprobó; y los topes. Con eso la app dibuja la tarjeta sin hacer otras llamadas.
{
"id_registro": 63699,
"tipo": "rendicion",
"estado": "I", "estado_texto": "Informado",
"en_orden_de_pago": false, "id_comprobante": null,
"concepto": { "id_evento": 114, "nombre": "PESADA" },
"importe": 90000.00,
"observacion": "Hablamos con aduana para liberar sin pesar...",
"fecha": "2026-10-19 12:10:00",
"posicion_gps": "-34.5981,-58.3702",
"solicitante": { "id_usuario": 4, "nombre": "MIGUEL SARAVIA" },
"aprobacion": { "id_usuario": null, "nombre": null, "fecha": null },
"legajo": {
"id_carpeta": "2026.04.95692", "nro_orden": 95692,
"operacion": "impo", "via": "maritima", "canal": "verde",
"cliente": "GESTAMP BAIRES S.A."
},
"topes": {
"concepto": 40000.00, "carpeta": 12000.00,
"rendido_concepto_en_carpeta": 90000.00, "rendido_carpeta": 103000.00,
"supera_tope_concepto": true, "supera_tope_carpeta": true
}
}
| Campo | Detalle |
|---|---|
importe | Lo rendido, en una rendición, o lo pedido, en un vale. Siempre positivo. |
en_orden_de_pago / id_comprobante | Portas ya lo incluyó en una Orden de Pago. Desde ese momento no se puede modificar. |
legajo | null en un vale sin legajo.
via puede ser aerea, maritima, terrestre o ferrocarril.
canal puede ser verde, naranja, rojo, o null
si el legajo todavía no tiene canal. |
topes | Solo en rendiciones con legajo.
|
posicion_gps | "lat,lng" que mandó la app al cargar, o null. |
/v1/movil/rendiciones/conceptos?id_usuario=88&id_carpeta=2026.04.95916Devuelve la lista para el combo de concepto al cargar una rendición. Depende del legajo, porque los topes son distintos para importación y exportación. Un vale no la necesita.
{
"ok": true,
"legajo": { "id_carpeta": "2026.04.95916", "nro_orden": 95916, "operacion": "impo", "cliente": "MOTOCAR IMPORTS S.A." },
"conceptos": [
{ "id_evento": 177, "nombre": "VIAJE NICOLAS", "tope": 0, "propio": true },
{ "id_evento": 111, "nombre": "PRESENTACION / CANAL", "tope": 5000, "propio": false },
{ "id_evento": 113, "nombre": "SALIDA", "tope": 6000, "propio": false }
],
"nota": "El tope solo precarga el importe: no bloquea el alta."
}
tope sirve para precargar el importe al elegir el concepto, como hacía el
sistema viejo. El empleado lo puede cambiar, y un importe mayor al tope no se rechaza.
Los conceptos con propio: true son los que solo ve ese usuario; van primero en la lista.
/v1/movil/rendiciones?id_usuario=88&tipo=rendicion&vista=pendientes&page=1| Parámetro | Detalle |
|---|---|
tipo | rendicion o vale. Si no viene, devuelve los dos. |
vista |
|
desde / hasta | AAAA-MM-DD, sobre la fecha de carga. hasta incluye ese día. |
id_carpeta | Devuelve solo los de ese legajo. |
page / limit | Paginado. limit vale 25 por defecto y como máximo 100. |
{ "ok": true, "vista": "pendientes", "items": [ { ...registro... } ],
"paginado": { "pagina": 1, "limite": 25, "total": 93, "paginas": 4 } }
Cada pantalla del menú viejo se arma así:
| Pantalla | Parámetros |
|---|---|
| Rendir | tipo=rendicion&vista=pendientes |
| Historial Rendiciones | tipo=rendicion&vista=historial |
| Pedir Fondo | tipo=vale&vista=pendientes |
| Historial Pedido Fondo | tipo=vale&vista=historial |
/v1/movil/rendiciones/{id_registro}?id_usuario=88Devuelve un registro: { "ok": true, "rendicion": { ...registro... } }. Si es de otro usuario
responde 404, igual que si no existiera.
/v1/movil/rendiciones/saldo?id_usuario=88Devuelve el estado de cuenta del empleado; es la pantalla Estado de Rendiciones.
{ "ok": true, "saldo": {
"vales_aprobados": 42368670.55, "rendido_aprobado": 41048649.36,
"saldo": 1320021.19,
"vales_pendientes": 0, "rendido_pendiente": 1159500,
"excluye": "MOVILIDAD CABIFY (evento 115)" } }
saldo es lo aprobado en vales menos lo aprobado en rendiciones. Si es positivo,
el empleado recibió plata que todavía no rindió. Los campos *_pendientes
son lo que todavía espera aprobación. Usa el mismo criterio que el reporte de cuenta
corriente de Portas.
/v1/movil/rendiciones header obligatorio: Idempotency-Key// Rendición
{ "id_usuario": 88, "tipo": "rendicion", "id_carpeta": "2026.04.95916", "id_evento": 111,
"importe": 5000, "observacion": "Canal rojo", "posicion_gps": "-34.5981,-58.3702" }
// Vale (pedido de fondos): sin concepto y con legajo opcional
{ "id_usuario": 4, "tipo": "vale", "importe": 150000, "observacion": "Pesadas y movilidad" }
| Campo | Detalle | |
|---|---|---|
tipo | obligatorio | rendicion o vale. |
importe | obligatorio | Mayor que 0, con hasta 2 decimales. Se acepta como número
o como texto; "1234,50" también sirve. |
id_carpeta | obligatorio en una rendición | El legajo tiene que existir. |
id_evento | obligatorio en una rendición | Uno de los que devuelve /conceptos
para ese legajo y ese usuario. En un vale se ignora. |
observacion | opcional | Hasta 200 caracteres. |
posicion_gps | opcional | "lat,lng". |
La fecha la pone el servidor. Responde 201 con el registro, que ya queda en estado
I, y con la lista mails: los aprobadores de Portas reciben un mail que les
avisa que hay algo nuevo para aprobar.
428. Una rendición duplicada se paga dos veces.
/v1/movil/rendiciones/{id_registro}{ "id_usuario": 88, "importe": 4500, "observacion": "Corrijo el importe" }
Corrige un registro propio mientras espera aprobación.
id_carpeta, id_evento, importe,
observacion o posicion_gps. El tipo no se puede cambiar.DELETE: lo que se cargó mal lo rechaza el aprobador.| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | La llamada trae id_cliente o id_transportista. |
| 404 | rendicion_inexistente | El registro no existe o es de otro usuario. |
| 404 | carpeta_inexistente | El legajo no existe. |
| 409 | en_orden_de_pago | Se quiso corregir un registro que ya está en una Orden de Pago. |
| 409 | ya_resuelto | Se quiso corregir algo ya aprobado o rechazado, o que se resolvió mientras se corregía. |
| 422 | tipo_invalido, importe_invalido, concepto_invalido,
falta_id_carpeta, observacion_larga, posicion_gps_invalida,
vista_invalida | Un dato falta o no sirve. |
| 428 | falta_idempotency_key | Alta sin el header Idempotency-Key. |
Reemplaza Pendientes Liberación y la pantalla Liberar Mercadería del
sistema viejo. Sirve para liberar una carga sin escanear el QR de la nota de entrega:
el empleado carga el canal, el inicio y el fin de carga. Es solo para personal de Portas, es decir,
llamadas sin id_cliente ni id_transportista.
Cuando se carga el fin de carga, el cliente recibe exactamente los mismos avisos que con el QR: la notificación «MERCADERIA LIBERADA» a sus usuarios suscriptos y los mails (incluidos los de recepción y descarga de Ferrero).
/v1/movil/cargas?id_usuario=88&vista=pendientes| Parámetro | Valores | Qué hace |
|---|---|---|
vista | pendientes (por defecto) o liberadas |
pendientes: con inicio de carga y sin fin de carga. liberadas: con fin de carga. |
dias | 1 a 30 | Ventana hacia atrás sobre el inicio (pendientes, 5 por defecto, como el viejo) o sobre el fin de carga (liberadas, 3 por defecto). |
todas | 0 (por defecto) o 1 |
Por defecto solo las cargas en las que el usuario es empleado de carga (primero o segundo).
Con 1, las de todos. |
legajo | número de orden | Busca ese legajo sin mirar fechas ni asignación: es para liberar una carga que no aparece en la lista. Puede devolver más de una carpeta (el mismo número en años distintos). |
Devuelve hasta 100 cargas, cada una con esta forma:
{
"id_carpeta": "2026.04.95860",
"legajo": 95860,
"operacion": "impo",
"via": "Terrestre",
"cliente": "...",
"referencia": "MM091026 4500880866",
"mercaderia": "RESINA PET",
"oficializacion": "2026-10-19",
"despacho": "26001IC05022834P",
"responsable": "AGUSTIN WUTHRICH",
"estado": "Carga Liberada",
"canal": { "id": 1, "nombre": "Verde" },
"fechas": {
"control_documentacion": null, "verificacion": null, "aviso_carga": "2026-10-20 09:00:00",
"inicio_carga": "2026-10-20 10:15:00", "finalizacion_carga": null
},
"empleados": [ { "id_usuario": 88, "nombre": "NICOLAS ALVARO ARCE" } ]
}
/v1/movil/cargas/{id_carpeta}?id_usuario=88Una carga, con la misma forma, más un bloque liberacion que dice qué admite hoy,
para que la app arme la pantalla sin repetir las reglas:
"liberacion": {
"liberada": false,
"puede_elegir_canal": true,
"canales": ["verde", "naranja", "rojo", "sin_canal"],
"requiere_canal": true,
"requiere_inicio": false,
"ezeiza_impo_aereo": false,
"fechas_desde": "2026-10-11 15:30",
"fechas_hasta": "2026-10-21 15:30"
}
/v1/movil/cargas/{id_carpeta}/liberacion{ "id_usuario": 88, "canal": "verde", "inicio": "2026-10-21 10:15", "finalizacion": "2026-10-21 15:20" }
Todos los campos son opcionales, pero hay que mandar al menos uno. Se puede liberar en una sola llamada o en dos (primero el inicio, después el fin).
verde, naranja, rojo o sin_canal.
Solo se puede cargar si la carpeta no tiene canal o si la vía es aérea
(puede_elegir_canal).AAAA-MM-DD HH:MM, hora de Argentina. Valen hasta 10 días para
atrás y no a futuro. Lo que ya estaba cargado no se pisa: un inicio que ya existía se
ignora y se informa en ignorados.sin_canal explícito) e inicio de carga. El fin no puede ser anterior al inicio.
En Ezeiza impo aéreo, como con el QR, si falta el inicio se toma igual al fin.Respuesta:
{
"cambios": ["canal", "finalizacion"],
"ignorados": [],
"avisos": { "notificaciones": 3, "mails": [ { "para": "...", "asunto": "MERCADERIA LIBERADA (95860) - ...", "enviado": true, "detalle": "" } ] },
"carga": { ...la carga actualizada, con su bloque liberacion... }
}
409 ya_liberada con la carga como quedó, y no repite los avisos. Un
reintento después de un corte de red recibe ese mismo 409: la app puede tomarlo como éxito.
| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | La llamada trae id_cliente o id_transportista. |
| 404 | carpeta_inexistente | El legajo no existe. |
| 409 | ya_liberada | La carga ya tiene fin de carga. |
| 409 | canal_ya_cargado | La carpeta ya tiene canal y no es aérea. |
| 409 | carpeta_anulada, carpeta_interna, sin_datos_de_carga |
La carpeta no admite liberación. |
| 422 | falta_canal, falta_inicio, finalizacion_anterior |
Falta un dato para finalizar o las fechas no cierran. |
| 422 | sin_datos, canal_invalido, fecha_invalida,
fecha_fuera_de_rango, vista_invalida, dias_invalido,
legajo_invalido | Un dato falta o no sirve. |
Reemplaza la pantalla Texto de la carátula del sistema viejo: notas libres del personal en puerto sobre un legajo (demoras, problemas en la terminal, lo que pasó en la carga). Cada nota es un evento 154 «OBSERVACIÓN PUERTO» del legajo y se ve en FreeWayNet con su autor. Es un evento interno: el cliente no lo ve ni recibe aviso. Por eso es solo para personal de Portas.
/v1/movil/legajos/{id_carpeta}/textos?id_usuario=4Las notas del legajo, de la más nueva a la más vieja (hasta 200), de todos los autores:
{
"ok": true,
"id_carpeta": "2026.04.95692",
"legajo": 95692,
"textos": [
{ "id_registro": 847210, "fecha": "2026-10-19 13:48:01",
"texto": "Día 19-10 finalmente se están liberando los últimos contenedores...",
"autor": { "id_usuario": 4, "nombre": "MIGUEL SARAVIA" } }
]
}
/v1/movil/legajos/{id_carpeta}/textos{ "id_usuario": 4, "texto": "Se presentaron los camiones a las 9 hs..." }
Responde 201 con la nota creada ("repetido": false). El texto admite hasta 4000
caracteres; la fecha es la del servidor.
200 con la nota que ya estaba y
"repetido": true. En el sistema viejo un doble toque dejó la misma nota hasta 6 veces.
| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | La llamada trae id_cliente o id_transportista. |
| 404 | carpeta_inexistente | El legajo no existe. |
| 409 | carpeta_interna | Carpeta de un despachante interno (40, 96, 97). |
| 409 | carpeta_anulada | Solo al grabar: la carpeta está anulada. |
| 422 | texto_vacio, texto_largo | Falta el texto o pasa de 4000 caracteres. |
Reemplaza Comprobantes (Cheques) e Historial Comprobantes del sistema viejo: las Órdenes de Pago en las que el usuario figura como empleado, con los cheques y los gastos por legajo. Es solo consulta y solo para personal de Portas: cada empleado ve únicamente sus O/P. El botón «informar» y la carga de gastos sobre la O/P del viejo no se usan, así que no se migran.
/v1/movil/comprobantes-odp?id_usuario=4&vista=historial| Parámetro | Valores | Qué hace |
|---|---|---|
vista | historial (por defecto) o pendientes |
historial: las rendidas. pendientes: las que no tienen fecha de rendición. |
desde, hasta | AAAA-MM-DD | Sobre la fecha de la O/P. |
q | texto | Parte del número de O/P, como el filtro del viejo. |
legajo | número de orden | Las O/P con algún gasto en ese legajo. |
page, limit | limit de 1 a 100 (25 por defecto) | Paginado, de la más nueva a la más vieja. |
historial. Las pendientes que quedan son de 2013 a 2022, en general con total 0:
conviene no mostrar esa pestaña, o mostrarla aparte como «antiguas».
{
"ok": true,
"vista": "historial",
"items": [
{ "id_comprobante": "O/P X0001-00048573", "numero": "X0001-00048573",
"fecha": "2026-10-14", "fecha_rendicion": "2026-10-14", "rendido": true,
"total": 607560, "beneficiario": "",
"cheques": ["027-0001-86119430"], "gastos": 1, "legajos": [95822] }
],
"paginado": { "pagina": 1, "limite": 25, "total": 6673, "paginas": 267 }
}
/v1/movil/comprobantes-odp/{numero}?id_usuario=4Una O/P completa. En la base el número es O/P X0002-00029330; en la URL va sin el
«O/P », como lo devuelve el campo numero: X0002-00029330.
{
"ok": true,
"comprobante": {
"id_comprobante": "O/P X0002-00029330", "numero": "X0002-00029330",
"fecha": "2026-10-09", "fecha_rendicion": "2026-10-09", "rendido": true,
"total": 40520, "beneficiario": "",
"cheques": [ { "nro_cheque": "027-0001-86119430", "fecha": "2026-10-14", "diferido": "2026-10-14",
"importe": 607560, "cuenta": "Cta Cte SuperVielle S.A." } ],
"gastos": [
{ "item": 1, "id_carpeta": "2026.04.94885", "legajo": 94885, "concepto": "MOVILIDAD",
"importe": 1150, "observacion": "", "id_rendicion": 63273 }
]
}
}
id_rendicion es la rendición de la que salió el gasto (sección
14), o null si se cargó directo en la O/P.
| HTTP | Código | Cuándo |
|---|---|---|
| 403 | persona_no_habilitada | La llamada trae id_cliente o id_transportista. |
| 404 | comprobante_inexistente | La O/P no existe, está anulada o es de otro empleado. |
| 422 | numero_invalido, vista_invalida, fecha_invalida,
legajo_invalido | Un dato no sirve. |
Los aceptan /movil/movimientos y /movil/comprobantes-adeudados
(y los listados de las fases siguientes). Son los mismos catorce filtros que el
Symfony viejo repetía en cada módulo.
| Parámetro | Valores | Qué filtra |
|---|---|---|
tipo | import / export | Importación o exportación |
legajo | número | Número de legajo (sufijo del id_carpeta) |
via | air / sea / ground | Vía de transporte |
canal | green / orange / red | Canal aduanero (rojo agrupa 3, 4 y 6) |
mercaderia | texto | Nombre de la mercadería (parcial) |
referencia | texto | Referencia del cliente (parcial) |
factura | texto | Factura del proveedor (parcial) |
comprobante | texto | Número de comprobante (parcial) |
despacho | texto | Número de despacho completo (parcial) |
destinacion | código | Destinación aduanera |
of_desde / of_hasta | fecha | Fecha de oficialización |
fin_desde / fin_hasta | fecha | Fecha de finalización de carga |
id_cliente | 84 o 84,1167 | Acota el alcance a esos clientes |
page / limit | enteros | Paginado |
Las fechas se aceptan como AAAA-MM-DD o DD/MM/AAAA. Un filtro mal
escrito se ignora en silencio, igual que en el sistema viejo.
view_consulta_caratula_resumen, que se resuelve en vivo
y es la parte cara de la consulta. Solo se une cuando alguno de ellos viene: pedirlos sin
necesidad hace más lenta la página.
Formato único, igual que el resto de la API:
{ "ok": false, "error": "usuario_inexistente", "message": "No existe el usuario web 999" }
| HTTP | Significa |
|---|---|
| 400 | Falta o está mal un parámetro estructural (típicamente id_usuario) |
| 401 | API key ausente o inválida |
| 403 | Scope insuficiente, usuario inactivo, persona no habilitada, clave incorrecta |
| 404 | El usuario o el recurso no existe |
| 422 | El pedido se entiende pero los datos no sirven |
| 502 | No se pudo llegar a la base de la LAN de Portas |
Esta tabla recorre el menú del empleado del sistema viejo, punto por punto, con lo que ya existe y lo que falta. Los endpoints planificados todavía no existen: la ruta y los campos son una propuesta y pueden cambiar al implementarlos. El uso que se menciona es el del sistema viejo en las últimas 7 semanas (del 31/08 al 20/10/2026).
| Menú viejo | Estado | Endpoint(s) |
|---|---|---|
| Cronograma Cargas | existe | GET /v1/cronograma (documentado en la API de Datos) |
| Carpetas: lista, búsqueda y carátula | existe | GET /v1/legajos y GET /v1/legajos/{id} (API de Datos) |
| Carpetas: fotos de precinto e inspección | existe | Sección 11 |
| Rendir, Pedir Fondo, sus historiales y Estado de Rendiciones | existe | Sección 14 |
| Mi Perfil | existe | Secciones 4 y 5 |
| Pendientes Liberación y Liberar Mercadería | existe | Sección 15 |
| Carpetas: texto del legajo | existe | Sección 16 |
| Comprobantes (Cheques) e Historial Comprobantes | existe | Sección 17 |
| Viajes y Viajes Realizados | a confirmar | Ver A |
| Endpoint planificado | Qué hace | |
|---|---|---|
| GET | /v1/movil/viajes?id_usuario=&vista=en_curso|realizados | Devuelve los viajes del usuario. |
| GET | /v1/movil/viajes/destinos y /v1/movil/viajes/motivos |
Devuelven los combos: los destinos (entidades como terminales, depósitos o la aduana) y los motivos (BUSCAR PRECINTOS, TIRO CANAL, etc.). |
| POST | /v1/movil/viajes |
Inicia un viaje. Recibe { id_usuario, id_entidad, gps, carpetas: [ { id_carpeta, id_motivo, observacion } ] }. |
| PATCH | /v1/movil/viajes/{id} | Finaliza o cancela un viaje. Recibe { id_usuario, estado, gps }. |
Parte de lo que hacían los viajes ya lo cubre el GPS en vivo (sección 13).
| Qué | Estado |
|---|---|
| Avisos push al teléfono | Los tokens ya se registran (sección 7), pero hoy nadie envía push: el sistema viejo usaba una API de Google que se dio de baja. Se define aparte. |
| Generar el PDF con QR de la nota de entrega | Pendiente, con prioridad baja: tuvo 1 uso en 7 semanas. |
| Audio, foto genérica, landing cost, reclamos, comunicador y chat | No se usan, así que no se migran. |