Legajos y usuarios: consulta y alta/actualización, en reemplazo de la conexión directa a la base y de las llamadas posicionales a stored procedures
https://api.portascloud.ar. Se pueden probar en vivo desde el playground.
/portas/api es fijo.
La API de Datos expone por REST lo que hoy www.portas.com.ar (escritorio / móvil /
importas) hace conectándose directo a la MySQL de FreeWayNet. Dos objetivos:
SELECT sobre las vistas vw_* (lectura) o el CALL al sp_*
(escritura). Sincrónico: el alta devuelve el id_carpeta en la misma respuesta.
Toda llamada requiere Authorization: Bearer <api-key>.
datos:read para los GET y el login; datos:write para
alta/actualización/evento y alta/actualización de usuarios. Sin el scope necesario → 403.
"ok": true. Error: "ok": false + error + message (+ detalle).YYYY-MM-DD / YYYY-MM-DD HH:MM:SS, zona Argentina). Importes y kilos viajan como número (no string).GET con querystring. Escritura: POST/PATCH con application/json.PATCH los campos ausentes no se tocan.Idempotency-Key (o el campo idempotency_key); un reintento con la misma clave devuelve el resultado original sin duplicar.id_carpeta = AAAA.DD.NNNNN (ej. 2026.04.98765), máximo 15 caracteres.GET/v1/legajos/{id_carpeta} — scope: datos:read
Devuelve la carátula completa del legajo desde vw_carpetas (la misma vista que consume portas.com.ar),
con los nombres ya resueltos (cliente, vía, mercadería, país, terminal como texto, no solo IDs),
más contenedores[] y eventos[] (que no están en la vista).
Incluye además cuatro campos calculados, iguales a los que muestra portas.com.ar:
tipo ("Importación" / "Exportación", derivado de operacion),
estado (texto del ciclo de vida: Apertura de Legajo → SIMI Oficializada / Carga Arribada →
Oficializada → Carga Liberada → Facturada → Cobrada, o Anulada / Anulada Aduana),
responsable (nombre del responsable principal asignado al legajo) y
en_curso (booleano con el mismo criterio del filtro "En Curso" del escritorio de portas.com.ar:
true mientras no haya fecha_finalizacion_carga ni fecha_ingreso_deposito;
ambas fechas crudas también vienen en carpeta).
carpeta trae ~107 campos)404 legajo_no_encontrado.GET/v1/legajos — scope: datos:read
| Parámetro | Tipo | Descripción |
|---|---|---|
anio | int | Filtra por año. |
id_cliente | int / lista | Filtra por cliente. Acepta uno o varios separados por coma (84,1167): un usuario puede gestionar más de una empresa. Ausente = todos. Si viene pero no tiene ningún entero válido → 400 id_cliente_invalido. |
operacion | int | 1=Impo, 2=Expo. |
en_curso | int | 1 = solo en curso (sin fecha_finalizacion_carga ni fecha_ingreso_deposito, igual que el filtro "En Curso" del escritorio); 0 = solo entregados; ausente = todas. |
mercaderia | int / lista | Filtra por mercadería: id_mercaderia del catálogo /v1/catalogos/mercaderias. Acepta uno o varios separados por coma (1181,875). Ausente = todas. Si viene pero no tiene ningún entero válido → 400 mercaderia_invalida. |
tipo_mercaderia | int / lista | Legajos cuyo tipo de mercadería sea alguno de estos. Se toma el tipo cargado en el legajo; si el legajo no lo tiene, el tipo principal de su mercadería. Mal escrito → 400 tipo_mercaderia_invalido. |
grupo | int / lista | Lo mismo, por grupo (1 a 4, ver tabla de grupos). grupo=1 = legajos de producto terminado. |
referencia | string | Búsqueda parcial sobre la referencia del cliente: referencia=MARCO encuentra "B11 430 6X2R MARCOPOLO". No distingue mayúsculas de minúsculas. Los caracteres % y _ se buscan tal cual, no como comodines. |
fecha_desde / fecha_hasta | date | Rango sobre fecha_oficializacion, en formato YYYY-MM-DD y con ambos extremos incluidos. Se puede mandar una sola de las dos, o las dos. Formato inválido → 400 fecha_invalida; fecha_desde posterior a fecha_hasta → 400 rango_invalido. |
incluir_sin_oficializar | int | 1 = suma al resultado los legajos todavía sin oficializar (fecha_oficializacion en null), que por definición no caen dentro de ningún rango. Default 0. Solo tiene efecto junto con fecha_desde/fecha_hasta. |
limit / offset | int | Default 100, tope 500 / paginación (ver más abajo). |
incluir_sin_oficializar: los legajos sin oficializar son muchos y de todos los años, no solo del período pedido. En la base de julio de 2026 el rango solo devuelve 232 legajos, y con este parámetro en 1 pasa a 14.395. Usarlo únicamente cuando se quiere "lo del período más todo lo que sigue pendiente"; para un listado por período, dejarlo apagado.fecha_llegada es el arribo real; si viene null, usar fecha_esperada_buque como estimada (equivale a COALESCE(fecha_llegada, fecha_esperada_buque), el criterio del escritorio).La respuesta incluye los campos para paginar:
| Campo | Tipo | Descripción |
|---|---|---|
count | int | Cantidad de filas devueltas en esta página. |
total | int | Total de legajos que matchean los filtros (todas las páginas). Total de páginas = ceil(total / limit). |
has_more | bool | true si después de esta página quedan más registros (offset + count < total); false en la última página. |
Para recorrer un listado completo: pedir con offset=0 y repetir sumando offset = offset + limit mientras has_more sea true. Para un paginador de UI: usar total para dibujar los números de página y pedir cada página con offset = (pagina - 1) * limit. El uso simple de siempre (un solo request con limit, sin offset) sigue funcionando igual.
Las listas anidadas del legajo, por separado (la carátula sale del compuesto 4.1). Ambas scope: datos:read.
| Método | Endpoint | Devuelve |
|---|---|---|
| GET | /v1/legajos/{id_carpeta}/contenedores | Lista de contenedores |
| GET | /v1/legajos/{id_carpeta}/eventos | Eventos del legajo |
GET/v1/catalogos/{nombre} — scope: datos:read
Valores válidos para resolver los IDs que piden las escrituras (mapean a las vw_* de FreeWayNet).
{nombre} | Columnas |
|---|---|
vias | id_via, nombre |
mercaderias FILTROS | id_mercaderia, nombre, carga_peligrosa, id_tipo_principal, tipos[], grupos[] — acepta tipo_mercaderia, grupo e id_cliente (ver abajo) |
tipos_mercaderia | id_tipo_mercaderia, nombre, id_grupo, grupo |
unidades_negocio | id_unidad_negocio, nombre |
tipos_operacion_importaciones / ..._exportaciones | id_tipo_operacion, nombre |
paises | id_pais, nombre, codigo_maria |
terminales | id_entidad, razon_social |
transportistas | id_transportista, razon_social |
envases | id_envase, nombre |
companias_aereas | id_compania_aerea, nombre |
agencias_maritimas | id_entidad, razon_social |
lineas_maritimas | id_entidad, razon_social |
monedas | id_moneda, nombre, abreviatura, simbolo |
clientes | id_cliente, razon_social, nro_cuit (solo activos; para filtros y detalle ver 4.7) |
Cada mercadería tiene uno o más tipos (los de /v1/catalogos/tipos_mercaderia), y uno de ellos es el
principal. Por ejemplo TAPAS es materia prima (principal) y también producto terminado.
Cada tipo pertenece a su vez a un grupo, que es la agrupación que Portas usa para las notificaciones de arribo a depósito:
id_grupo | Grupo | Tipos que lo forman (id_tipo_mercaderia) |
|---|---|---|
| 1 | Producto terminado | 5 PRODUCTO TERMINADO |
| 2 | Materia prima | 1 INSUMO · 7 MATERIA PRIMA · 8 EMBALAJES |
| 3 | Sorpresas | 6 MUESTRAS · 9 SORPRESAS |
| 4 | Repuestos | 2 REPUESTO |
null | (sin grupo) | 3 MAQUINARIA · 4 HELADERA |
Los tres son opcionales y se combinan libremente (se exigen todos los que vengan). Sin ninguno devuelve el catálogo completo. Los nueve tipos, con el grupo de cada uno, se obtienen de GET /v1/catalogos/tipos_mercaderia.
| Parámetro | Tipo | Descripción |
|---|---|---|
tipo_mercaderia | int / lista | Si viene, deja solo las mercaderías que tengan al menos uno de estos tipos (7, o 1,7,8). Cuenta cualquiera de sus tipos, no solo el principal. |
grupo | int / lista | Atajo por grupo: grupo=2 equivale a tipo_mercaderia=1,7,8. Valores 1 a 4; otro → 400 grupo_invalido. Si viene junto con tipo_mercaderia se exigen los dos. |
id_cliente | int / lista | Solo las mercaderías del cliente: las que aparecen en algún legajo suyo, más las que se le asignaron al darlas de alta (por el POST de abajo o desde FreewayNet). Sin este filtro devuelve las ~4.000 del catálogo completo. |
Un filtro mal escrito (grupo=abc) devuelve 400, nunca el catálogo entero. Una mercadería sin tipos cargados tiene tipos: [] y no sale en ningún filtro por tipo o grupo.
Seguimiento marítimo de los contenedores en tránsito: ETA, posición actual del buque
(lat/lng), trayectoria del viaje y eventos por puerto (Load, Discharge, Gate out…). Los datos los
alimenta FreeWayNet consultando la API de tracking (Searates) cada 8 horas y quedan en la tabla
eta_contenedores; esta API los expone en lectura.
| Método | Endpoint | Devuelve |
|---|---|---|
| GET | /v1/legajos/{id_carpeta}/eta | Todos los contenedores en seguimiento del legajo |
| GET | /v1/contenedores/{nro}/eta | Un contenedor puntual + sus carpetas vinculadas |
| GET | /v1/contenedores/eta | Todos los contenedores en seguimiento, de todos los clientes (uso administrativo — ver más abajo) |
| Parámetro | Tipo | Descripción |
|---|---|---|
detalle | int | 0 = respuesta liviana (sin locaciones ni trayectoria). Default: 1. |
posicion y trayectoria pueden venir null / vacías si el proceso de tracking
todavía no obtuvo datos para ese contenedor. trayectoria es una lista de pares
[lat, lng] lista para dibujar como polilínea en un mapa (ver el ejemplo con mapa en el playground).
Contenedor no seguido → 404 contenedor_no_encontrado.
GET/v1/contenedores/eta — scope: admin:read
Devuelve todos los contenedores en seguimiento con el cliente y el
interno de cada uno, sin filtrar por cliente. Pensado para pantallas internas de Portas
(una torre de control de la flota marítima), no para clientes finales: por eso usa el scope
admin:read y no datos:read.
| Parámetro | Tipo | Descripción |
|---|---|---|
id_cliente | int / lista | Devuelve solo los contenedores de ese cliente. Acepta uno o varios separados por coma (84,1167). Ausente o vacío = todos los clientes. Sin ningún entero válido → 400 id_cliente_invalido. |
detalle | int | 1 agrega locaciones y trayectoria a cada contenedor. Default: 0 (liviano). |
activos | int | 0 incluye también los contenedores que ya no se siguen. Default: 1. |
detalle=1: acá la trayectoria se devuelve para todos los contenedores a la vez,
así que la respuesta pasa de unas decenas de KB a varios MB. Conviene pedir el listado liviano para la grilla y
traer la ruta del contenedor puntual con /v1/contenedores/{nro}/eta cuando el usuario lo selecciona.
items y legajos[] lista todas sus carpetas (los campos id_carpeta /
interno del nivel superior son los de la primera). resumen.con_ruta viene
null cuando se pidió sin detalle=1, porque en ese modo la trayectoria no se arma
y el conteo no aplica.
GET/v1/cronograma — scope: datos:read
Datos para armar el calendario semanal de cargas que el cliente veía en el escritorio viejo
(/dashboard_cronograma). Devuelve una fila por legajo con actividad de carga en la ventana pedida,
con las fechas crudas, los flags ya calculados (IMO, inspección, foto de precinto, atención) y
las tarjetas resueltas con la misma lógica del sistema viejo, listas para ubicar en la grilla.
| Parámetro | Tipo | Descripción |
|---|---|---|
id_cliente | int / lista | Filtra los legajos de un cliente (la vista que ese cliente veía en el escritorio viejo). Acepta uno o varios separados por coma (84,1167), para quien gestiona más de una empresa: sigue siendo la vista cliente, con las cargas de todas juntas. Sin este parámetro devuelve todos los clientes (vista interna). Sin ningún entero válido → 400 id_cliente_invalido. |
dias | int | Ventana hacia atrás en días (máx. 90). Default: 30 con id_cliente —sea uno o varios— (como la vista cliente del viejo), 10 sin él (vista interna / TV). |
desde | date | Alternativa a dias: fecha de corte YYYY-MM-DD. Pisa a dias. |
dias/desde
solo corta hacia atrás. No se incluyen legajos anulados.
tarjetas[]: cada entrada es una tarjeta del calendario, ubicada por su fecha. Tipos: carga (siempre), verificacion y control_documentacion (solo si tienen fecha; en el viejo estas dos solo se mostraban en la vista del cliente).{id_carpeta} (Carga) / (Verif.) / (C.Doc.).tipo del legajo (Importación / Exportación). En la vista cliente el viejo mostraba despacho.etiqueta + despacho.numero en lugar de la razón social, y mercaderia en lugar del responsable.canal.color (verde / naranja / rojo).flags arriba. foto_precinto_pendiente=true → mostrar alerta de foto de precinto (aplica solo a Ferrero: Terrestre IC04/IC05 o Marítima canal rojo con carga iniciada).
Consulta de clientes sobre la vista vw_clientes (solo campos identificatorios: razón social, CUIT,
nro. importador/exportador, domicilio y localidad — sin datos comerciales ni contactos). Ambos scope: datos:read.
| Método | Endpoint | Devuelve |
|---|---|---|
| GET | /v1/clientes | Listado paginado (default: solo activos) |
| GET | /v1/clientes/{id_cliente} | Detalle de un cliente (incluye inactivos) |
| Parámetro | Tipo | Descripción |
|---|---|---|
q | string | Búsqueda parcial (LIKE) sobre razon_social, nro_cuit y nro_imp_exp. |
activo | int | todos | Ausente → solo activos (default). 0 → solo inactivos. todos → sin filtro. |
limit / offset | int | Paginado. Default 100, máx. 500. |
GET /v1/clientes/{id} lo devuelve igual (con "activo": false). Para poblar un combo
simple también está el catálogo liviano GET /v1/catalogos/clientes (ver 4.4).
POST/v1/legajos — scope: datos:write
Crea un legajo nuevo. Devuelve el id_carpeta generado (AAAA.DD.NNNNN) en la misma respuesta.
| Campo | Tipo | Requisito |
|---|---|---|
operacion | int (1=Impo, 2=Expo) | obligatorio |
tipo_operacion | string(2) | obligatorio |
via, mercaderia, unidad_negocio, tipo_mercaderia | int | obligatorio |
responsable | int | opcional (default 152) |
id_sistema | int | opcional (trazabilidad) |
idempotency_key | string | opcional |
id_cliente=1167, id_despachante='04', fecha_apertura=hoy.PATCH/v1/legajos/{id_carpeta} — scope: datos:write
Actualiza solo los campos enviados (los ausentes no se tocan). Reemplaza la llamada de 28 parámetros posicionales.
fecha_salida, fecha_esperada_buque, fecha_llegada,
fecha_inicio_forzoso, fecha_fin_forzoso, id_envase, kilos,
bultos, bultos_medidas, id_terminal, id_transportista,
camion, conocimiento_crt, fecha_mercaderia_disponible_origen,
registro_paquete, id_pais_origen_destino, nombre_vapor,
conocimiento_bl, contenedores, id_compania_aerea, guia_madre,
guia_hija, fecha_apertura, referencia, nro_factura,
importe_factura, id_moneda_factura, fecha_factura, id_sistema,
id_via, id_agencia_maritima, id_linea_maritima.
id_via: vía de transporte (id del catálogo vw_vias, el mismo que se envía en el alta).
Antes solo se podía fijar al crear el legajo; ahora también se puede actualizar.
Si el id no existe en el catálogo → 422 referencia_invalida.
id_agencia_maritima / id_linea_maritima: agencia y línea marítima del
embarque (ids de los catálogos agencias_maritimas y lineas_maritimas; columna
id_entidad). En la lectura del legajo vuelven como id_agencia_maritima +
agencia_maritima (nombre) e id_linea_maritima + linea_maritima.
Si el id no existe en el catálogo → 422 referencia_invalida.
contenedores: array de {id_envase, nro_contenedor, nro_precinto, kilos, bultos}.
Omitido → no toca. [] → borra todos. Con items → reemplaza la lista completa.
fecha_salida → evento 65; fecha_llegada → evento 48.POST/v1/legajos/{id_carpeta}/eventos — scope: datos:write
Registra un evento y devuelve el id_registro (paso previo a subir un archivo de evento al disco G:).
| Campo | Tipo | Requisito |
|---|---|---|
id_evento | int (3=Factura, 21=BL/CRT/AWB, 103=Packing List) | obligatorio |
fecha | datetime | obligatorio |
observacion | string | opcional |
POST/v1/legajos/{id_carpeta}/archivos — scope: upload — multipart/form-data
Sube un archivo desde portas.com.ar. El servidor lo guarda en el storage de DonWeb, lo deja descargable de inmediato (registrado como archivo del legajo) y lo encola para que el agente de la red de Portas lo baje al disco G: en la ruta correcta.
POST /v1/archivos (API de archivos): aquel publica lo que ya está
en el G: (lo usa el proceso interno). Este es el camino inverso: lo que entra por portas.com.ar
y tiene que terminar en el G:.
| Campo | Tipo | Requisito | Descripción |
|---|---|---|---|
archivo | file | obligatorio | El binario (PDF por default). |
id_evento + id_registro | int | destino A | Para un archivo de evento. El id_registro se obtiene antes con POST .../eventos. Destino: Eventos/{id_evento}/{id_registro}. Van juntos (uno solo → 422). |
sub_carpeta | string | destino B | Para un archivo de carpeta lógica (Gastos, Factura_Portas, …). Se usa si no viene el destino A. |
hash_sha1 | string(40) | opcional | Si difiere del archivo recibido → 422. |
fecha_archivo | string | opcional | Fecha lógica del documento. |
idempotency_key | string | opcional | Evita duplicación en reintentos. |
estado_g refleja la cola hacia el disco G:: pendiente → descargando →
colocado (el agente de la red lo bajó). El archivo ya es descargable por download_url aunque
todavía esté pendiente de llegar al G:.
POST/v1/catalogos/mercaderias — scope: datos:write
Crea una mercadería en el catálogo de FreewayNet, con sus tipos, y la deja asignada al cliente para que
GET /v1/catalogos/mercaderias?id_cliente=… la devuelva enseguida (antes había que esperar a que un legajo la usara).
| Campo | Tipo | Requisito |
|---|---|---|
nombre | string(50) | obligatorio — se guarda tal como viene |
tipos_mercaderia | int[] (o "6,5") | obligatorio — uno o más ids de /v1/catalogos/tipos_mercaderia |
principal | int | opcional — uno de los enviados; default el primero |
id_cliente | int | opcional — asigna la mercadería al cliente (recomendado: 1167) |
carga_peligrosa | bool | opcional (default false) |
idempotency_key | string | opcional |
200 con ya_existia: true y la mercadería existente tal cual está, tipos incluidos (no se modifican).
Si mandaron id_cliente, la asignación al cliente sí se registra.
Gestión de usuarios sobre vw_usuarios / sp_AltaUsuario / sp_ActualizarUsuario.
Los usuarios que crea portas.com.ar gestionan el campo password (con su propio esquema de codificación,
que la API no interpreta). El campo clave (de FreeWayNet) no se expone.
scope: datos:read. Filtros: activo, id_perfil, id_sector, q, email, limit, offset. No incluye password (no se vuelcan hashes en masa).
q busca con LIKE parcial sobre nombre, usuario y email
a la vez (ej. ?q=j.perez@portas.com.ar ya encuentra por email). email filtra por
igualdad exacta — útil para validar si existe un usuario con ese mail sin coincidencias parciales.
scope: datos:read. Devuelve el usuario completo, incluido password. Si no existe → 404 usuario_no_encontrado.
scope: datos:write. Único obligatorio: nombre. Campos: usuario, password, id_perfil, id_sector, email, activo y demográficos (direccion, telefonos, fecha_nacimiento, tipo_documento, nro_documento, estado_civil, nro_cuil, nro_legajo, nro_poder, fecha_ingreso, fecha_baja), más id_sistema.
scope: datos:write. Parcial (ausente = no tocar). Permite cambiar password. Si no existe → 404.
scope: datos:read. Valida credenciales server-side y devuelve los datos del usuario sin el hash.
password se compara por igualdad exacta: enviá el valor ya codificado
con el mismo esquema con que lo guardás (la API no hashea). Así no hace falta traerse el hash en cada login.
Formato común: { "ok": false, "error": "codigo", "message": "...", "detalle": { "sp_resultado": -31 } }
| SP | resultado | HTTP | error |
|---|---|---|---|
| Legajo (alta) | 1 | 201 | — |
-30…-36 | 422 | parametro_invalido | |
-50…-54 | 500 | error_interno | |
| Legajo (update) | -1 | 404 | legajo_no_encontrado |
-2 | 409 | legajo_anulado | |
-20…-25 | 422 | referencia_invalida | |
-34…-36 | 422 | dato_invalido | |
| Evento | -3 | 422 | evento_invalido |
-4 | 422 | fecha_invalida | |
| Usuario | -10 (alta) | 422 | parametro_faltante |
-1 (update) | 404 | usuario_no_encontrado |
GET /v1/catalogos/vias, /paises, etc.POST /v1/legajos → guardar el id_carpeta.PATCH /v1/legajos/{id_carpeta} (las veces que haga falta).POST /v1/legajos/{id_carpeta}/eventos → id_registro.POST /v1/legajos/{id_carpeta}/archivos (quedan descargables y bajan al disco G:). Para consultar lo ya publicado, la API de archivos.GET /v1/legajos/{id_carpeta} (datos) + GET /v1/operaciones/{id_carpeta}/paquetes (archivos).POST/PATCH /v1/usuarios para gestionar, POST /v1/usuarios/login para autenticar.