API de Subida de Documentos

Endpoint único para depositar albaranes y documentación de transporte en PDF, organizados por carpeta de cliente. Autenticación por token Bearer. Todas las respuestas son JSON UTF-8.

Base URLhttps://deca.infonetconsultores.com
ProtocoloHTTPS · JSON
AuthBearer token en cabecera Authorization
Versión1.0

Autenticación

Toda petición debe incluir la cabecera Authorization con un token Bearer estático. La comparación se hace con hash_equals(), resistente a ataques de temporización.

Authorization: Bearer <TU_TOKEN>
SituaciónRespuesta
Cabecera ausente o mal formada401 + WWW-Authenticate: Bearer
Token presente pero incorrecto403 (queda registrado en el log con la IP)

Apache con CGI/FastCGI: la cabecera Authorization se pierde antes de llegar a PHP salvo que el .htaccess la reinyecte. Si recibes 401 con un token válido, revisa que AllowOverride All esté activo en el vhost.

En Nginx + PHP-FPM el .htaccess no aplica: añade fastcgi_param HTTP_AUTHORIZATION $http_authorization; al bloque del server.

Endpoint

POST /subir.php Sube un PDF a la carpeta del cliente

Crea la carpeta del cliente si no existe y guarda el PDF con el nombre indicado. La escritura es atómica: se escribe primero en un fichero temporal y luego se renombra, de modo que nunca queda un PDF a medias en disco.

Cabeceras

CabeceraTipoValor
AuthorizationstringobligatoriaBearer <token>
Content-Typestringrecomendadaapplication/json — si se omite o es distinta, el cuerpo se lee como formulario ($_POST)

Cuerpo de la petición

Se acepta application/json (recomendado) o application/x-www-form-urlencoded / multipart/form-data con los mismos nombres de campo.

CampoTipoDescripción
clientestringobligatorio Nombre del cliente. Se convierte a un slug que da nombre a la carpeta: se quitan acentos, se pasa a minúsculas y se sustituye todo lo no alfanumérico por guiones. Máximo 100 caracteres tras el saneado.
"Transportes Pérez, S.L." → carpeta transportes-perez-s-l
nombre_ficherostringobligatorio Nombre con el que se guardará el PDF. Se toma solo el basename, se filtran los caracteres a [A-Za-z0-9._-] y se fuerza la extensión .pdf. Máximo 150 caracteres.
pdf_base64stringobligatorio Contenido del PDF codificado en Base64. Se admiten saltos de línea y espacios (se eliminan) y el formato Data URI data:application/pdf;base64,…. Máximo 20 MB ya decodificado.

Esquema

{
  "cliente":        "Transportes Perez SL",
  "nombre_fichero": "ALB-2026-0001.pdf",
  "pdf_base64":     "JVBERi0xLjQKMSAwIG9iajw8L1R5cGUv..."
}

Ejemplos de petición

# El PDF se codifica al vuelo y se inserta en el JSON
curl -X POST https://deca.infonetconsultores.com/subir.php \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"cliente\":\"Transportes Perez SL\",
       \"nombre_fichero\":\"ALB-2026-0001.pdf\",
       \"pdf_base64\":\"$(base64 -w0 albaran.pdf)\"}"

Respuesta correcta

201
Created — fichero guardado
El PDF se ha escrito en documentos/<cliente>/<fichero> con permisos 0644.
{
  "ok":      true,
  "cliente": "transportes-perez-sl",
  "fichero": "ALB-2026-0001.pdf",
  "ruta":    "documentos/transportes-perez-sl/ALB-2026-0001.pdf",
  "bytes":   152340
}
CampoTipoDescripción
okbooleanSiempre true en la respuesta 201.
clientestringSlug realmente usado como carpeta, ya saneado.
ficherostringNombre final tras el saneado y el forzado de extensión.
rutastringRuta relativa del fichero dentro del directorio de subidas.
bytesintegerTamaño del PDF decodificado, en bytes.

Errores

Todos los errores devuelven {"ok": false, "error": "<mensaje>"} con el código HTTP correspondiente.

{
  "ok":    false,
  "error": "El contenido decodificado no es un PDF."
}

Catálogo completo

400
Bad Request — datos de entrada inválidos
Agrupa varios casos distintos; el campo error los distingue.
"El cuerpo no es un JSON válido."
"Falta el parámetro \"cliente\"." · "Falta el parámetro \"nombre_fichero\"." · "Falta el parámetro \"pdf_base64\"."
"Data URI mal formado en \"pdf_base64\"."
"El campo \"pdf_base64\" no contiene base64 válido."
"El nombre de cliente no contiene caracteres válidos." · "El nombre de fichero no contiene caracteres válidos."
"Ruta de destino no permitida."
403
Forbidden — token incorrecto
La cabecera existe pero el token no coincide. El intento se registra en subidas.log junto con la IP de origen.
"Token no válido."
413
Payload Too Large
El PDF decodificado supera los 20 MB. Ojo: el JSON viaja un ~33 % más grande por el Base64, así que post_max_size debe ser mayor (32 MB en el .htaccess).
"El fichero supera el máximo de 20971520 bytes."
415
Unsupported Media Type — no es un PDF
El Base64 se decodifica bien pero el contenido no empieza por la firma %PDF-. Evita que se suban ejecutables o scripts renombrados.
"El contenido decodificado no es un PDF."
500
Internal Server Error — fallo de disco
Casi siempre son permisos de escritura en el directorio de subidas, o disco lleno.
"No se pudo crear el directorio base de subidas." · "No se pudo crear la carpeta del cliente."
"No se pudo escribir el fichero en disco." · "No se pudo mover el fichero a su destino final."

Saneado de nombres

Ningún dato del cliente llega en crudo al sistema de ficheros. Estas son las transformaciones exactas:

Carpeta de cliente

EntradaCarpeta resultante
Transportes Pérez, S.L.transportes-perez-s-l
LOGÍSTICA DEL SURlogistica-del-sur
../../etcetc
!!!400 — no quedan caracteres válidos

Nombre de fichero

EntradaFichero resultante
ALB-2026-0001.pdfALB-2026-0001.pdf sin cambios
albarán nº5.pdfalbar_n_5.pdf
facturafactura.pdf extensión forzada
../../../shell.phpshell.php.pdf
.htaccesshtaccess.pdf

Además del filtrado, tras crear la carpeta se comprueba con realpath() que la ruta resuelta siga estando dentro del directorio base. Si no, se responde 400. Es la red de seguridad final contra el path traversal.

Límites y validación

ParámetroValorDónde se cambia
Tamaño máximo20 MB (20971520 bytes) decodificadoMAX_FILE_BYTES · subir.php:36
SobrescrituraDesactivada → 409ALLOW_OVERWRITE · subir.php:39
Directorio base./documentosBASE_UPLOAD_DIR · subir.php:33
Log./subidas.logLOG_FILE · subir.php:42
Longitud slug cliente100 caracteresslugCliente()
Longitud nombre fichero150 caracteresnombreFicheroSeguro()
Cuerpo HTTP máximo32 MBpost_max_size · .htaccess

Orden de validación

El endpoint corta en el primer fallo, en este orden:

1.  Método POST ................... 405
2.  Cabecera Authorization ........ 401
3.  Token válido (hash_equals) .... 403
4.  JSON parseable ................ 400
5.  Los tres campos presentes ..... 400
6.  Base64 decodificable .......... 400
7.  Tamaño <= 20 MB ............... 413
8.  Firma %PDF- ................... 415
9.  Nombres saneables ............. 400
10. mkdir carpeta cliente ......... 500
11. realpath dentro de la base .... 400
12. No existe ya el fichero ....... 409
13. Escritura + rename atómico .... 500
     
     201 Created

Seguridad

MedidaImplementación
Timing attackhash_equals() en la comparación del token
Path traversalbasename() + whitelist [A-Za-z0-9._-] + verificación con realpath()
Ficheros ocultosltrim($nombre, '.') impide crear .htaccess y similares
Byte nuloSe eliminan los \0 antes de tocar el sistema de ficheros
Tipo de contenidoVerificación de la firma %PDF-, no de la extensión
Escritura parcialTemporal + rename() atómico; se limpia el temporal si algo falla
Auditoríasubidas.log registra subidas correctas y fallos de autenticación con IP

Pendiente de decidir: tal como está desplegado, la carpeta documentos/ cuelga del directorio web. Cualquiera que adivine https://deca.infonetconsultores.com/documentos/cliente/fichero.pdf puede descargar el PDF sin token.

Si los albaranes son confidenciales: mueve BASE_UPLOAD_DIR fuera del docroot (p. ej. dirname(__DIR__).'/documentos_privados') y sirve las descargas desde un PHP que valide el Bearer. Lo mismo aplica a subidas.log.

Conversor PDF → Base64

El fichero no sale de tu navegador: la conversión es local con FileReader. Útil para preparar el cuerpo JSON de Postman.

Arrastra un PDF aquí o haz clic para elegirlo

Probar el endpoint

Lanza una petición real contra subir.php. El token no se guarda en ningún sitio: vive solo en este campo mientras la pestaña esté abierta.