API Portas

Documentación para subir, listar y descargar archivos de Consultora Portas

v1 · REST · JSON · Bearer auth
ⓘ Este valor se usa en todos los ejemplos. Edítelo y los códigos de abajo se actualizan automáticamente. El path /portas/api es el prefijo fijo del API; podría cambiar el dominio en el futuro.
▶ Probar la API en vivo (playground)

1. Introducción

La API Portas tiene dos objetivos:

  1. Subir archivos PDF/JPG de una carpeta operativa del despachante.
  2. Obtener los entregables consolidados de esa carpeta — paquetes PDF unidos y ZIPs generados automáticamente a partir de los archivos subidos — más los archivos individuales por separado.

Cada entregable y cada archivo individual queda registrado con un token público de descarga. Ese token permite armar URLs clickeables que se pueden publicar tal cual en el sitio del proveedor para que los clientes finales descarguen los documentos.

Flujo recomendado: el proveedor consulta GET /v1/operaciones/{id_carpeta}/paquetes con su API key → la respuesta trae entregables (PDFs unidos, ZIPs) y archivos (individuales) → cada uno con su download_url → esas URLs se publican en su sitio.

2. Autenticación

Toda llamada al API (excepto el healthcheck y las URLs públicas de descarga por token) requiere el header Authorization: Bearer <api-key>.

Authorization: Bearer mkp_TUKEYPRIVADA1234567890abcdef

La API key se entrega de forma privada al proveedor por canal seguro. Nunca se publica en código frontend ni en URLs visibles. Trátela como una contraseña.

Scopes: cada API key tiene un conjunto de permisos (read, upload, delete). Si su key no incluye un scope necesario, el endpoint responde 403 forbidden_scope.

3. Convenciones

4. Glosario

TérminoSignificadoEjemplo
id_carpetaIdentificador completo de una carpeta operativa, formato anio.id_despachante.nro_interno.2026.04.98765
anioAño (4 dígitos) de la carpeta.2026
id_despachanteID interno del despachante (2 chars).04
nro_internoNúmero de carpeta interno del despachante.98765
sub_carpetaCarpeta lógica donde se clasifica el archivo. Ver lista de valores válidos en sección 6.Gastos, Factura_Portas
familiaGrupo de sub_carpetas que se consolida en un entregable.gastos, f3101, factura_portas
entregableArchivo final generado por el server (PDF unido o ZIP) a partir de los archivos de una familia."Gastos REF 2026_04_98765.pdf"
id_archivoID interno secuencial del archivo individual en la base.34511
download_tokenToken público de 64 hex que permite descargar sin auth.4f2e2ae78613…
hash_sha1Hash SHA1 del contenido del archivo (40 hex, minúsculas).3a52ce780950d4d969792a2559cd519d7ee8c727
idempotency_keyClave opcional (≤80 chars, alfanumérica + _-) que evita duplicación en reintentos de subida.upload_2026_04_98765_001

5.1 Healthcheck

GET/v1sin auth

Devuelve estado del servicio. Útil para verificar conectividad.

curl "https://api.portascloud.ar/v1"
$resp = file_get_contents('https://api.portascloud.ar/v1'); print_r(json_decode($resp, true));
const r = await fetch('https://api.portascloud.ar/v1'); console.log(await r.json());
import requests print(requests.get('https://api.portascloud.ar/v1').json())

Respuesta

{ "ok": true, "service": "portas-api", "version": "v1", "time": "2026-05-15T10:23:45-03:00" }

5.2 Paquetes de una carpeta ★ ENDPOINT PRINCIPAL

GET/v1/operaciones/{id_carpeta}/paquetesscope: read

Devuelve los entregables consolidados de la carpeta (PDFs unidos, ZIPs) y los archivos individuales. Cada uno con un download_url público clickeable. Este es el endpoint que el proveedor consume para armar la página del cliente final.

curl -H "Authorization: Bearer TU_API_KEY" \ "https://api.portascloud.ar/v1/operaciones/2026.04.98765/paquetes"
<?php $base = 'https://api.portascloud.ar'; $apiKey = 'TU_API_KEY'; $ic = '2026.04.98765'; $ch = curl_init("$base/v1/operaciones/$ic/paquetes"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); foreach ($data['entregables'] as $e) { if ($e['estado'] === 'ready') { echo '<a href="' . htmlspecialchars($e['download_url']) . '">' . htmlspecialchars($e['nombre']) . '</a><br>'; } }
const base = 'https://api.portascloud.ar'; const apiKey = 'TU_API_KEY'; const ic = '2026.04.98765'; const r = await fetch(`${base}/v1/operaciones/${ic}/paquetes`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await r.json(); for (const e of data.entregables) { if (e.estado === 'ready') { document.body.insertAdjacentHTML('beforeend', `<a href="${e.download_url}">${e.nombre}</a><br>`); } }
import requests base = 'https://api.portascloud.ar' api_key = 'TU_API_KEY' ic = '2026.04.98765' r = requests.get(f'{base}/v1/operaciones/{ic}/paquetes', headers={'Authorization': f'Bearer {api_key}'}) data = r.json() for e in data['entregables']: if e['estado'] == 'ready': print(e['nombre'], '->', e['download_url'])

Respuesta

{ "ok": true, "id_carpeta": "2026.04.98765", "meta": { "referencia": "26001IC04098765W", "despacho": null, "id_cliente": 123, "actualizado_en": "2026-05-15 10:23:45" }, "entregables": [ { "familia": "f3101", "tipo": "pdf", "estado": "ready", "nombre": "F3101 REF 26001IC04098765W.pdf", "tamanio_bytes": 5941860, "fuente_count": 23, "generado_en": "2026-05-15 10:24:01", "download_url": "https://api.portascloud.ar/v1/de/6998cc4a120afc8f3306b10d44b7184d3b37c6ab68b25261116361b7a6fc7230" }, { "familia": "gastos", "tipo": "pdf", "estado": "ready", "nombre": "Gastos REF 26001IC04098765W.pdf", "tamanio_bytes": 1346528, "fuente_count": 7, "generado_en": "2026-05-15 10:24:03", "download_url": "https://api.portascloud.ar/v1/de/8a3f1b…" }, { "familia": "gastos", "tipo": "zip", "estado": "pending", "nombre": null, "download_url": null } ], "archivos": [ { "id_archivo": 34511, "sub_carpeta": "Gastos", "nombre": "Scan00001.pdf", "tamanio_bytes": 166200, "hash_sha1": "3a52ce780950d4d969792a2559cd519d7ee8c727", "fecha_archivo": null, "fecha_subida": "2026-05-15 10:20:11", "download_url": "https://api.portascloud.ar/v1/d/4f2e2ae78613eaa6…" } ] }

Estados posibles de un entregable

EstadoSignificado
pendingEncolado para procesar. Esperando debounce (30s sin uploads en la familia) y siguiente tick del worker (cada 1 min).
processingGenerándose ahora mismo.
readyListo. download_url disponible.
failedFalló (ver campo error). El worker reintenta automáticamente hasta 5 veces.
Si llama al endpoint y los entregables están en pending, espere 30-90 segundos y vuelva a consultar.

5.3 Descarga pública de un entregable (SIN AUTH)

GET/v1/de/{download_token}público

Devuelve el binario del entregable (PDF unido o ZIP) directamente, con headers de descarga. Esta es la URL que aparece en download_url dentro de cada entregable en el endpoint anterior.

Seguridad: el token son 64 caracteres hex aleatorios. No revela el nombre del archivo ni permite inferir otros tokens. Es permanente (no expira). Si se filtra, solo afecta a ese entregable; el resto del catálogo queda protegido.

Ejemplo

# Click directo desde browser, <a href>, curl, etc: https://api.portascloud.ar/v1/de/6998cc4a120afc8f3306b10d44b7184d3b37c6ab68b25261116361b7a6fc7230

5.4 Descarga pública de un archivo individual (SIN AUTH)

GET/v1/d/{download_token}público

Igual que la anterior pero para los archivos individuales (los Scan, PDFs sueltos). URL aparece en download_url dentro de archivos[].

Ejemplo

https://api.portascloud.ar/v1/d/4f2e2ae78613eaa635d483e52c9f32e90781b5362ab4df7b6df2d5fe9114146b
Note la diferencia de path: /v1/de/ (con "e") para entregables, /v1/d/ para archivos individuales.

5.5 Listar archivos individuales

GET/v1/archivosscope: read

Alternativa al endpoint principal cuando solo se necesita la lista cruda de archivos individuales con filtros (por año, cliente, hash, etc.) sin los entregables.

Parámetros de query

ParámetroTipoRequisitoDescripción
id_carpetastringopcionalFiltrar por carpeta (2026.04.98765).
aniointopcionalFiltrar por año.
id_clienteint / listaopcionalFiltrar por cliente final. Acepta uno o varios separados por coma (84,1167), para quien gestiona más de una empresa. Si viene pero no tiene ningún entero válido → 400 id_cliente_invalido.
hash_sha1stringopcionalFiltrar por hash exacto.
limitintopcionalDefault 100, tope 500.
offsetintopcionalPaginación.
curl -H "Authorization: Bearer TU_API_KEY" \ "https://api.portascloud.ar/v1/archivos?id_carpeta=2026.04.98765"
$ch = curl_init('https://api.portascloud.ar/v1/archivos?id_carpeta=2026.04.98765'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer TU_API_KEY'], ]); $data = json_decode(curl_exec($ch), true); foreach ($data['rows'] as $f) print_r($f);
const r = await fetch('https://api.portascloud.ar/v1/archivos?id_carpeta=2026.04.98765', { headers: { 'Authorization': 'Bearer TU_API_KEY' } }); const data = await r.json(); for (const f of data.rows) console.log(f.nombre_archivo, f.download_url);
import requests r = requests.get('https://api.portascloud.ar/v1/archivos', params={'id_carpeta': '2026.04.98765'}, headers={'Authorization': 'Bearer TU_API_KEY'}) for f in r.json()['rows']: print(f['nombre_archivo'], f['download_url'])

Respuesta

{ "ok": true, "count": 2, "limit": 100, "offset": 0, "rows": [ { "id_archivo": 34511, "id_carpeta": "2026.04.98765", "sub_carpeta": "Gastos", "nombre_archivo": "Scan00001.pdf", "path_relativo": "2026/04/98765/Gastos/Scan00001.pdf", "tamanio_bytes": 166200, "hash_sha1": "3a52ce78…c727", "fecha_subida": "2026-05-15 10:20:11", "fecha_archivo": null, "estado": "OK", "download_url": "https://api.portascloud.ar/v1/d/4f2e2ae78613eaa6…" } ] }

5.6 Obtener metadatos de un archivo

GET/v1/archivos/{id}scope: read

curl -H "Authorization: Bearer TU_API_KEY" \ "https://api.portascloud.ar/v1/archivos/34511"

5.7 Verificar archivos por hash

POST/v1/archivos/verificarscope: read

Recibe hasta 500 hashes SHA1. Para cada uno indica si ya existe en el catálogo. Útil antes de un batch de upload.

curl -X POST \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"hashes":["3a52ce780950d4d969792a2559cd519d7ee8c727"]}' \ "https://api.portascloud.ar/v1/archivos/verificar"
$body = json_encode(['hashes' => ['3a52ce78…c727']]); $ch = curl_init('https://api.portascloud.ar/v1/archivos/verificar'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer TU_API_KEY', 'Content-Type: application/json', ], ]); print_r(json_decode(curl_exec($ch), true));
const r = await fetch('https://api.portascloud.ar/v1/archivos/verificar', { method: 'POST', headers: { 'Authorization': 'Bearer TU_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ hashes: ['3a52ce78…c727'] }), }); console.log(await r.json());
import requests r = requests.post('https://api.portascloud.ar/v1/archivos/verificar', json={'hashes': ['3a52ce78…c727']}, headers={'Authorization': 'Bearer TU_API_KEY'}) print(r.json())

5.8 Subir un archivo

POST/v1/archivosscope: uploadmultipart/form-data

Campos

CampoTipoRequisitoDescripción
archivofileobligatorioBinario (PDF por default, según whitelist).
id_carpetastringobligatorioFormato 2026.04.98765.
sub_carpetastringopcionalVer sección 6. Default: raíz.
hash_sha1string (40 hex)opcionalSi difiere del archivo recibido → 422.
fecha_archivostringopcionalFecha lógica del documento (ISO o parseable).
id_clienteintopcionalCliente final asociado.
idempotency_keystringopcionalEvita duplicación en reintentos.

Comportamiento ante colisión

curl -X POST \ -H "Authorization: Bearer TU_API_KEY" \ -F "id_carpeta=2026.04.98765" \ -F "sub_carpeta=Gastos" \ -F "archivo=@/ruta/local/Factura_001.pdf" \ "https://api.portascloud.ar/v1/archivos"
<?php $ch = curl_init('https://api.portascloud.ar/v1/archivos'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer TU_API_KEY'], CURLOPT_POSTFIELDS => [ 'id_carpeta' => '2026.04.98765', 'sub_carpeta' => 'Gastos', 'archivo' => new CURLFile('/ruta/local/Factura_001.pdf'), 'idempotency_key' => 'upload_2026_04_98765_001', ], ]); $resp = json_decode(curl_exec($ch), true); curl_close($ch); echo $resp['download_url'];
// Desde un <input type="file"> en el navegador const file = document.querySelector('#fileInput').files[0]; const fd = new FormData(); fd.append('id_carpeta', '2026.04.98765'); fd.append('sub_carpeta', 'Gastos'); fd.append('archivo', file); const r = await fetch('https://api.portascloud.ar/v1/archivos', { method: 'POST', headers: { 'Authorization': 'Bearer TU_API_KEY' }, body: fd, }); const data = await r.json(); console.log(data.download_url);
import requests with open('/ruta/local/Factura_001.pdf', 'rb') as f: r = requests.post( 'https://api.portascloud.ar/v1/archivos', headers={'Authorization': 'Bearer TU_API_KEY'}, data={ 'id_carpeta': '2026.04.98765', 'sub_carpeta': 'Gastos', }, files={'archivo': f}, ) print(r.json()['download_url'])

Respuesta exitosa

{ "ok": true, "id_archivo": 34512, "nombre_archivo": "Factura_001.pdf", "path_relativo": "2026/04/98765/Gastos/Factura_001.pdf", "hash_sha1": "3a52ce78…c727", "tamanio_bytes": 153042, "download_url": "https://api.portascloud.ar/v1/d/4f2e2ae7…", "ya_existia": false, "estado": "OK" }
Después de subir: si el archivo va a una sub_carpeta que forma parte de una familia (ver sección 6), el server marca el entregable correspondiente como pending. El worker espera 30s de quietud y luego lo regenera. Vas a verlo en estado ready dentro de ~1-2 minutos.

5.9 Baja de archivo

DELETE/v1/archivos/{id}scope: delete

Baja lógica (estado='BAJA'). El archivo queda en disco pero deja de aparecer en listados y descargas devuelven 404.

6. Subcarpetas válidas

El campo sub_carpeta al subir un archivo determina en qué carpeta lógica se guarda y a qué familia de entregable contribuye. Los nombres se normalizan (case-insensitive, acentos opcionales).

Subcarpetas que contribuyen a un entregable

sub_carpeta canónicaAliases aceptadosFamilia de entregableTipos generados
GastosgastosPDF + ZIP
Factura_PortasFactura Portasfactura_portasPDF unido
OM_DeclaracionOM Declaracion y sobre contenedor, 1f3101 (1ra parte)PDF unido
Factura_ComercialFactura comercial formulario de valor, 2f3101 (2da parte)PDF unido
Conocimiento_EmbarqueConocimiento de embarque, 3f3101 (3ra parte)PDF unido
Cert_OrigenCertificado de origen, 4f3101 (4ta parte)PDF unido
Terceros_Organismos3ros organismos y demas documentos, 5f3101 (5ta parte)PDF unido
DJAIdjaiPDF unido
Certificados_ImportacionCertificado de importacioncert_importacionPDF unido
PDF_ProvisorioPDF provisoriopdf_provisorioPDF (passthrough / unido si hay varios)

Otras subcarpetas usuales (sin entregable)

Estas sub_carpetas se guardan en la carpeta pero no contribuyen a ningún entregable consolidado:

Si subís un archivo con una sub_carpeta que no está en la primera tabla, se guarda sin problemas pero no dispara la regeneración de un entregable. Aparece solo en la lista archivos del endpoint principal.

7. Códigos de error

Toda respuesta de error tiene este formato:

{ "ok": false, "error": "codigo_corto", "message": "Descripción humana", "detalle": { ... opcional } }
400 upload_error · request malformado
401 missing_auth · falta Bearer
401 invalid_token · key inválida
403 forbidden_scope · scope insuficiente
404 not_found · recurso/endpoint inexistente
415 extension_not_allowed
422 missing_id_carpeta
422 invalid_hash · hash_sha1 mal formado
422 hash_mismatch · hash cliente ≠ servidor
422 invalid_fecha_archivo
422 invalid_idempotency_key
500 internal_error · error inesperado
500 file_missing · DB sin binario en disco

8. Flujo completo: armar la página del cliente

Caso de uso típico del proveedor: en su web, dado un id_carpeta, mostrar los entregables consolidados (links principales) y opcionalmente la lista completa de archivos individuales (vista detallada).

<?php $base = 'https://api.portascloud.ar'; $apiKey = 'TU_API_KEY'; $ic = $_GET['carpeta'] ?? '2026.04.98765'; $ch = curl_init("$base/v1/operaciones/$ic/paquetes"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ?> <h1>Carpeta <?= htmlspecialchars($ic) ?></h1> <h2>Paquetes para descargar</h2> <ul> <?php foreach ($data['entregables'] as $e): ?> <li> <?php if ($e['estado'] === 'ready'): ?> <a href="<?= htmlspecialchars($e['download_url']) ?>"> <?= htmlspecialchars($e['nombre']) ?> </a> (<?= number_format($e['tamanio_bytes'] / 1024, 1) ?> KB) <?php else: ?> <em><?= htmlspecialchars($e['nombre'] ?? $e['familia']) ?> (procesándose)</em> <?php endif; ?> </li> <?php endforeach; ?> </ul> <details> <summary>Ver todos los archivos individuales (<?= count($data['archivos']) ?>)</summary> <ul> <?php foreach ($data['archivos'] as $a): ?> <li> <a href="<?= htmlspecialchars($a['download_url']) ?>"> <?= htmlspecialchars($a['nombre']) ?> </a> <small>[<?= htmlspecialchars($a['sub_carpeta']) ?>]</small> </li> <?php endforeach; ?> </ul> </details>
async function mostrarCarpeta(idCarpeta) { const base = 'https://api.portascloud.ar'; const apiKey = 'TU_API_KEY'; const r = await fetch(`${base}/v1/operaciones/${idCarpeta}/paquetes`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); const data = await r.json(); // Sección principal: entregables const ulE = document.getElementById('entregables'); ulE.innerHTML = data.entregables.map(e => e.estado === 'ready' ? `<li><a href="${e.download_url}">${e.nombre}</a></li>` : `<li><em>${e.familia}.${e.tipo} (${e.estado})</em></li>` ).join(''); // Sección secundaria: archivos individuales const ulA = document.getElementById('archivos'); ulA.innerHTML = data.archivos.map(a => `<li><a href="${a.download_url}">${a.nombre}</a> [${a.sub_carpeta}]</li>` ).join(''); } mostrarCarpeta('2026.04.98765');
import requests base = 'https://api.portascloud.ar' api_key = 'TU_API_KEY' ic = '2026.04.98765' r = requests.get(f'{base}/v1/operaciones/{ic}/paquetes', headers={'Authorization': f'Bearer {api_key}'}) data = r.json() print('=== Entregables ===') for e in data['entregables']: if e['estado'] == 'ready': print(f"{e['nombre']:50} {e['download_url']}") else: print(f" ({e['familia']}.{e['tipo']} en {e['estado']})") print(f"\n=== Archivos individuales ({len(data['archivos'])}) ===") for a in data['archivos']: print(f"[{a['sub_carpeta']:20}] {a['nombre']:40} {a['download_url']}")
# Consulta cruda + extracción con jq: curl -sH "Authorization: Bearer TU_API_KEY" \ "https://api.portascloud.ar/v1/operaciones/2026.04.98765/paquetes" \ | jq '.entregables[] | select(.estado=="ready") | "\(.nombre) -> \(.download_url)"'
Pro tip: el endpoint /v1/operaciones/{id_carpeta}/paquetes es idempotente y rápido (~50ms). Se lo puede llamar en cada pageview del cliente — no es necesario cachear. Si se desea cachear, hágalo con TTL corto (1-2 min) para que los entregables nuevos aparezcan rápido cuando pasan de pending a ready.