API Portas — Móvil

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

Base URL: https://api.portascloud.ar  |  v1  |  Fases 1-2 + posiciones GPS + instalación + rendiciones — actualizado 2026-10-21

1. Qué es esta API

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.

Fase 1. Esta primera entrega cubre la identidad del usuario, sus preferencias de notificación, los dispositivos push y las tres pantallas de consulta del perfil Cliente. Las fases 2 a 4 (captura de fotos y QR, cargas y transporte, rendiciones y ODP) se agregan sobre la misma base.

2. Autenticación: API key + id_usuario

Son dos cosas separadas y hacen falta las dos:

QuéDónde vaPara 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.

El id_usuario no es un secreto — son enteros correlativos. Lo que protege a la API es la key del header. Quien tenga la key puede consultar como cualquier usuario, así que la key no puede viajar al navegador ni a la app del cliente final: las llamadas salen del backend de importas.

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.

Excepción: GET /v1/movil/instalacion (sección 12) es el único endpoint que no pide id_usuario: es justamente de donde la app lo obtiene.

Errores propios de id_usuario

CódigoHTTPCuándo
id_usuario_requerido400No vino, o no es un entero
usuario_inexistente404No existe ese id en usuarios
usuario_inactivo403activo <> 1
persona_no_habilitada403La operación es de otro perfil (ver 3)
id_transportista_invalido400Vino pero no es un entero

3. Personas y qué ve cada una

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:

PersonaSe decide porAlcanceCredenciales 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.

Endpoints legado — las secciones 4 a 7 (perfil, clave, casillas de notificación, dispositivos push) guardan sus datos en web_clientes_usuarios y siguen esperando el id_usuario_web de esa tabla. Quedan así hasta que se defina dónde viven esos datos.

4. GET /v1/movil/usuario

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.

GET /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.

5. PATCH /v1/movil/usuario

La pantalla "Perfil": casillas de notificación y cambio de clave. Las dos partes son opcionales — se manda lo que se quiere cambiar.

PATCH /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ódigoHTTPCuándo
nada_para_cambiar422No vino ni notificaciones ni clave_nueva
notificacion_desconocida422Casilla que no existe (la respuesta lista las válidas)
clave_corta422Menos de 8 caracteres
clave_actual_incorrecta403No coincide con la vigente
Claves en texto plano. El Symfony viejo guarda y compara 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.

6. Notificaciones por evento

La grilla de eventos de la pantalla "Perfil": una columna para importación y otra para exportación.

GET /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": [ ... ]
  }
}
PUT /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.

Por qué "suscripto" y no la tabla directa. En la base, 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.

7. Dispositivos (push)

Registro del token de notificaciones push. Reemplaza /registrar_movil.

POST /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.

GET /v1/movil/dispositivos?id_usuario=123

Lista los dispositivos registrados. El token se devuelve parcial (abc123…xyz789): alcanza para reconocerlo y no sirve para mandar push.

DELETE /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.

8. GET /v1/movil/movimientos

Módulo ultimos_mov: la cronología de hitos y eventos de las operaciones que el usuario puede ver.

GET /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).

9. GET /v1/movil/comprobantes-adeudados

Módulo comp_adeudados: lo que el cliente todavía debe.

GET /v1/movil/comprobantes-adeudados?id_usuario=123&estado=vencidos

estado: 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.

10. Circulares y newsletters

Los dos tablones de novedades. Misma forma para los dos.

GET /v1/movil/circulares?id_usuario=123&q=aduana
GET /v1/movil/circulares/{id}?id_usuario=123
GET /v1/movil/newsletters?id_usuario=123
GET /v1/movil/newsletters/{id}?id_usuario=123

El 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.

Una diferencia que conviene mirar. El móvil viejo lista todo lo publicado sin mirar 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.

11. POST /v1/movil/legajos/{id}/fotos

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.

POST /v1/movil/legajos/2026.04.94979/fotos   multipart/form-data
CampoDescripción
id_usuarioobligatorioUsuario de FreeWayNet que hace la carga; queda en id_usuario_alta del evento (ver 2).
id_transportistapara remitoHace que la llamada sea de persona transportista: solo puede subir tipo=remito y solo sobre sus cargas. Sin él, la llamada es de empleado.
tipoobligatorio 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.
observacionopcional 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_registroopcional 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.

HTTPCódigoCuándo
403persona_no_habilitadaUn cliente intenta subir; el tipo no es para su persona.
403carpeta_ajenaTransportista 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.
404carpeta_inexistente / evento_inexistente
409carpeta_anulada
415extension_not_allowed / no_es_imagenNo es jpg/png/webp o el archivo está corrupto.
422tipo_invalido / sin_fotos / demasiadas_fotos
400upload_errorFoto por encima del límite de tamaño o incompleta.

12. GET /v1/movil/instalacion — id_usuario del teléfono

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).

GET /v1/movil/instalacion?id_instalacion=b1f6c0e2-7a41-4c1d-9a55-0d3e8f2a6c11
ParámetroDetalle
id_instalacionobligatorioUUID 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.

Respuestas

// 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 }
CampoDetalle
id_usuarioEl id_usuario de freewaynet.usuarios vinculado a este teléfono, o null si todavía no se vinculó.
nuevotrue solo en la llamada que registró el UUID. Informativo.
Circuito. Portas instala la app en cada teléfono y la abre una vez: esa primera llamada registra el UUID con 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.
Qué tiene que hacer la app:

Errores

HTTPCódigoCuándo
400id_instalacion_invalidoFalta id_instalacion o no tiene formato UUID.
401API key ausente o inválida.
403missing_scopeLa key no tiene el scope de la app móvil.

13. Posiciones GPS del personal en la calle

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).

POST /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"
    }
  ]
}
CampoDetalle
lat, lngobligatorioGrados decimales (WGS84). Se rechaza 0,0.
fecha_horaobligatorioMomento de la captura (no del envío), ISO 8601 con offset o Z. Sin offset se toma como hora argentina.
precision_mmuy recomendadoRadio de error en metros que informa el sistema operativo (accuracy). Es lo que permite descartar los puntos malos al dibujar.
velocidad_kmh, rumbo, altitud_mopcionalRumbo en grados, 0 = norte.
bateriaopcionalPorcentaje 0-100.
origenopcionalgps, red o fusionado.
es_simuladaopcionaltrue si el sistema operativo marca la ubicación como mock.
id_carpetaopcionalEl legajo en el que está trabajando el usuario, si la app lo sabe.
id_dispositivo, version_appopcional (raíz)Un identificador de instalación generado por la app. Informativo.
Envío por lotes. En el puerto la señal se corta: la app debe guardar los puntos localmente y mandarlos juntos cuando vuelve (hasta 500 por llamada). El envío es idempotente: un punto con el mismo 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.

Ciclo de la lista en la app. Capturar → agregar a la lista local → intentar el 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.
GET /v1/movil/posiciones/config?id_usuario=123

Las 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
}
CampoDetalle
rastreo_activofalse = no capturar nada (apagado general o de ese usuario).
intervalo_movimiento_seg / intervalo_quieto_segSegundos entre capturas según el teléfono se esté moviendo o no. Si la app no distingue, usar intervalo_movimiento_seg.
intervalo_envio_segCada cuánto intentar enviar la lista acumulada.
precision_max_mSi no es null, la app puede descartar las capturas con error mayor a esos metros.
horarioFranjas 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_propiotrue si el usuario tiene un horario particular; false si es el general.
en_horario_ahora / fecha_servidorYa 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.

GET /v1/movil/posiciones/ultimas?id_usuario=123&max_minutos=30

La ú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
    }
  ]
}
GET /v1/movil/posiciones?id_usuario=123&id_usuario_consultado=45&desde=2026-09-21T08:00:00&hasta=2026-09-21T18:00:00&precision_max=50

El 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.

HTTPCódigoCuándo
403persona_no_habilitadaLa llamada trae id_cliente o id_transportista.
422puntos_requeridos / demasiados_puntosFalta la lista o trae más de 500.
400id_usuario_consultado_requerido / fecha_invalida / rango_invalido / rango_excedido / precision_max_invalida / max_minutos_invalido

14. Rendiciones y pedidos de fondos (empleado)

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:

tipoQué esLegajoConcepto
rendicionUn gasto que el empleado ya hizo y rindeobligatorio Uno de /conceptos
valeUn pedido de fondos: plata que el empleado pide antes de gastaropcional Siempre RETIRO VALE; lo pone la API
estadoSignifica
I InformadoEsperando aprobación. Todo nace en este estado.
A AprobadoLo aprobó Portas.
C RechazadoPortas no lo aprobó.
P PendienteSolo aparece en registros viejos; la API nunca lo crea. Se muestra junto con los I.
La aprobación no es parte de esta API. Los aprobadores de Portas aprueban o rechazan desde FreeWayNet, el sistema interno. La app carga, corrige mientras el registro espera aprobación y muestra el resultado: cuando cambia el estado, el registro pasa al historial.

Forma de un registro

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
  }
}
CampoDetalle
importeLo rendido, en una rendición, o lo pedido, en un vale. Siempre positivo.
en_orden_de_pago / id_comprobantePortas ya lo incluyó en una Orden de Pago. Desde ese momento no se puede modificar.
legajonull 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.
topesSolo en rendiciones con legajo.
  • concepto es el tope del concepto para la operación del legajo.
  • carpeta es el tope total de la carpeta según su vía y su canal, o null si no hay.
  • Los rendido_* suman los registros informados y aprobados del mismo legajo, incluido este.
  • Los supera_tope_* sirven para pintar la alerta.
posicion_gps"lat,lng" que mandó la app al cargar, o null.
GET /v1/movil/rendiciones/conceptos?id_usuario=88&id_carpeta=2026.04.95916

Devuelve 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.

GET /v1/movil/rendiciones?id_usuario=88&tipo=rendicion&vista=pendientes&page=1
ParámetroDetalle
tiporendicion o vale. Si no viene, devuelve los dos.
vista
  • pendientes (default): lo que espera aprobación, con lo último cargado arriba.
  • historial: lo aprobado y lo rechazado, con lo último resuelto arriba.
  • todas.
desde / hastaAAAA-MM-DD, sobre la fecha de carga. hasta incluye ese día.
id_carpetaDevuelve solo los de ese legajo.
page / limitPaginado. 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í:

PantallaParámetros
Rendirtipo=rendicion&vista=pendientes
Historial Rendicionestipo=rendicion&vista=historial
Pedir Fondotipo=vale&vista=pendientes
Historial Pedido Fondotipo=vale&vista=historial
GET /v1/movil/rendiciones/{id_registro}?id_usuario=88

Devuelve un registro: { "ok": true, "rendicion": { ...registro... } }. Si es de otro usuario responde 404, igual que si no existiera.

GET /v1/movil/rendiciones/saldo?id_usuario=88

Devuelve 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.

POST /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" }
CampoDetalle
tipoobligatoriorendicion o vale.
importeobligatorioMayor que 0, con hasta 2 decimales. Se acepta como número o como texto; "1234,50" también sirve.
id_carpetaobligatorio en una rendiciónEl legajo tiene que existir.
id_eventoobligatorio en una rendiciónUno de los que devuelve /conceptos para ese legajo y ese usuario. En un vale se ignora.
observacionopcionalHasta 200 caracteres.
posicion_gpsopcional"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.

Idempotency-Key es obligatorio. La app manda un valor nuevo (por ejemplo un UUID) por cada rendición que el usuario confirma. Si reintenta porque se cortó la señal, manda el mismo valor, y la API devuelve la respuesta original en vez de crear otra. Sin el header, responde 428. Una rendición duplicada se paga dos veces.
PATCH /v1/movil/rendiciones/{id_registro}
{ "id_usuario": 88, "importe": 4500, "observacion": "Corrijo el importe" }

Corrige un registro propio mientras espera aprobación.

Errores propios

HTTPCódigoCuándo
403persona_no_habilitadaLa llamada trae id_cliente o id_transportista.
404rendicion_inexistenteEl registro no existe o es de otro usuario.
404carpeta_inexistenteEl legajo no existe.
409en_orden_de_pagoSe quiso corregir un registro que ya está en una Orden de Pago.
409ya_resueltoSe quiso corregir algo ya aprobado o rechazado, o que se resolvió mientras se corregía.
422tipo_invalido, importe_invalido, concepto_invalido, falta_id_carpeta, observacion_larga, posicion_gps_invalida, vista_invalidaUn dato falta o no sirve.
428falta_idempotency_keyAlta sin el header Idempotency-Key.

15. Cargas y liberación sin QR (empleado)

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).

GET /v1/movil/cargas?id_usuario=88&vista=pendientes
ParámetroValoresQué hace
vistapendientes (por defecto) o liberadas pendientes: con inicio de carga y sin fin de carga. liberadas: con fin de carga.
dias1 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).
todas0 (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.
legajonú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" } ]
}
GET /v1/movil/cargas/{id_carpeta}?id_usuario=88

Una 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"
}
POST /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).

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... }
}
Si la carga se liberó mientras tanto por otra vía (el QR u otro teléfono), la API responde 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.

Errores propios

HTTPCódigoCuándo
403persona_no_habilitadaLa llamada trae id_cliente o id_transportista.
404carpeta_inexistenteEl legajo no existe.
409ya_liberadaLa carga ya tiene fin de carga.
409canal_ya_cargadoLa carpeta ya tiene canal y no es aérea.
409carpeta_anulada, carpeta_interna, sin_datos_de_carga La carpeta no admite liberación.
422falta_canal, falta_inicio, finalizacion_anterior Falta un dato para finalizar o las fechas no cierran.
422sin_datos, canal_invalido, fecha_invalida, fecha_fuera_de_rango, vista_invalida, dias_invalido, legajo_invalidoUn dato falta o no sirve.

16. Texto del legajo (empleado)

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.

GET /v1/movil/legajos/{id_carpeta}/textos?id_usuario=4

Las 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" } }
  ]
}
POST /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.

Doble envío. Si el mismo usuario manda el mismo texto en el mismo legajo dentro de los 2 minutos, no se graba otra vez: la respuesta es 200 con la nota que ya estaba y "repetido": true. En el sistema viejo un doble toque dejó la misma nota hasta 6 veces.

Errores propios

HTTPCódigoCuándo
403persona_no_habilitadaLa llamada trae id_cliente o id_transportista.
404carpeta_inexistenteEl legajo no existe.
409carpeta_internaCarpeta de un despachante interno (40, 96, 97).
409carpeta_anuladaSolo al grabar: la carpeta está anulada.
422texto_vacio, texto_largoFalta el texto o pasa de 4000 caracteres.

17. Comprobantes ODP del empleado

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.

GET /v1/movil/comprobantes-odp?id_usuario=4&vista=historial
ParámetroValoresQué hace
vistahistorial (por defecto) o pendientes historial: las rendidas. pendientes: las que no tienen fecha de rendición.
desde, hastaAAAA-MM-DDSobre la fecha de la O/P.
qtextoParte del número de O/P, como el filtro del viejo.
legajonúmero de ordenLas O/P con algún gasto en ese legajo.
page, limitlimit de 1 a 100 (25 por defecto)Paginado, de la más nueva a la más vieja.
Desde 2023 las O/P se rinden el mismo día que se emiten, así que la vista que sirve es 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 }
}
GET /v1/movil/comprobantes-odp/{numero}?id_usuario=4

Una 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.

Errores propios

HTTPCódigoCuándo
403persona_no_habilitadaLa llamada trae id_cliente o id_transportista.
404comprobante_inexistenteLa O/P no existe, está anulada o es de otro empleado.
422numero_invalido, vista_invalida, fecha_invalida, legajo_invalidoUn dato no sirve.

18. Filtros comunes de operación

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ámetroValoresQué filtra
tipoimport / exportImportación o exportación
legajonúmeroNúmero de legajo (sufijo del id_carpeta)
viaair / sea / groundVía de transporte
canalgreen / orange / redCanal aduanero (rojo agrupa 3, 4 y 6)
mercaderiatextoNombre de la mercadería (parcial)
referenciatextoReferencia del cliente (parcial)
facturatextoFactura del proveedor (parcial)
comprobantetextoNúmero de comprobante (parcial)
despachotextoNúmero de despacho completo (parcial)
destinacioncódigoDestinación aduanera
of_desde / of_hastafechaFecha de oficialización
fin_desde / fin_hastafechaFecha de finalización de carga
id_cliente84 o 84,1167Acota el alcance a esos clientes
page / limitenterosPaginado

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.

Los cuatro últimos filtros de la tabla (factura, despacho, destinación y las fechas) obligan a unir 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.

19. Errores

Formato único, igual que el resto de la API:

{ "ok": false, "error": "usuario_inexistente", "message": "No existe el usuario web 999" }
HTTPSignifica
400Falta o está mal un parámetro estructural (típicamente id_usuario)
401API key ausente o inválida
403Scope insuficiente, usuario inactivo, persona no habilitada, clave incorrecta
404El usuario o el recurso no existe
422El pedido se entiende pero los datos no sirven
502No se pudo llegar a la base de la LAN de Portas

20. Lo que falta: endpoints planificados

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ú viejoEstadoEndpoint(s)
Cronograma Cargasexiste GET /v1/cronograma (documentado en la API de Datos)
Carpetas: lista, búsqueda y carátulaexiste GET /v1/legajos y GET /v1/legajos/{id} (API de Datos)
Carpetas: fotos de precinto e inspecciónexisteSección 11
Rendir, Pedir Fondo, sus historiales y Estado de RendicionesexisteSección 14
Mi PerfilexisteSecciones 4 y 5
Pendientes Liberación y Liberar MercaderíaexisteSección 15
Carpetas: texto del legajoexisteSección 16
Comprobantes (Cheques) e Historial ComprobantesexisteSección 17
Viajes y Viajes Realizadosa confirmarVer A

A. Viajes (a confirmar con Portas)

En la base de Portas no se registra un viaje desde 2021: hay 29 en total. Las pantallas se siguen abriendo, pero nadie guarda nada. Si se decide migrarlas, esta es la propuesta.
Endpoint planificadoQué hace
GET/v1/movil/viajes?id_usuario=&vista=en_curso|realizadosDevuelve 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).

Fuera del menú del empleado

QuéEstado
Avisos push al teléfonoLos 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 entregaPendiente, con prioridad baja: tuvo 1 uso en 7 semanas.
Audio, foto genérica, landing cost, reclamos, comunicador y chatNo se usan, así que no se migran.