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 de qué usuario es la llamada. Decide qué ve y qué puede hacer. |
id_usuario es el id_usuario_web de
freewaynet.web_clientes_usuarios — el mismo padón de usuarios que
usa el móvil viejo. Portas no valida la clave: el login lo hace
importas contra sus propias tablas, y en cada llamada nos dice quién es.
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.
| Código | HTTP | Cuándo |
|---|---|---|
id_usuario_requerido | 400 | No vino, o no es un entero |
usuario_inexistente | 404 | No existe ese id_usuario_web |
usuario_inactivo | 403 | activo <> 1 |
interno_inactivo | 403 | Empleado cuyo usuario interno fue dado de baja |
persona_no_habilitada | 403 | La operación es de otro perfil |
Del id_usuario sale la persona, con las mismas reglas y en el
mismo orden que el executeIngresar del Symfony viejo:
| Persona | Se decide por | Alcance | Credenciales del viejo |
|---|---|---|---|
cliente |
Existe la fila en clientes |
Solo su id_cliente |
Movil, Cliente |
proveedor |
Tiene id_proveedor_cliente |
id_cliente = 0 (nada, igual que hoy) |
Movil, Cliente |
transportista |
Tiene id_transportista |
Nada en estas pantallas (tiene las suyas, Fase 3) | Movil, Transporte |
empleado |
Ninguna de las anteriores | Todas las operaciones | Movil, Empleado (+ Aprueba_fondos / Aprueba_rendicion) |
El parámetro opcional id_cliente (uno o varios separados por coma:
?id_cliente=84,1167) acota ese alcance, nunca lo amplía.
Le sirve sobre todo al empleado, que ve todo y quiere mirar un cliente.
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 | Como en todos los endpoints móviles (puede ir en la query string). |
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) |
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"
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. |
| 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. |
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 |
| Fase | Módulo viejo | Qué trae |
|---|---|---|
| 2 | carpetas |
Hecho: foto de precinto, foto de inspección y remito del transportista (sección 11). Pendiente: fotos y audio genéricos, texto, liberar mercadería, generación y lectura de QR |
| 3 | cargas, transp_oficios |
Pendientes de carga y liberación; búsqueda y subida de remito del transportista |
| 4 | rendiciones, gastos_odp, comprobantes_odp |
Rendiciones, pedidos de fondos, aprobaciones, topes, eventos y tableros |
Todas se montan sobre la misma base: id_usuario, las mismas personas y el mismo
juego de filtros.