Constaia
Integraciones

Google Apps Script

Valida documentos de Google Drive desde Google Sheets con Apps Script y UrlFetchApp, guarda la clave en Script Properties y escribe el veredicto en la hoja.

Con Google Apps Script puedes analizar ficheros de Google Drive desde una hoja de cálculo: cada fila apunta a un fichero, el script lo envía a Constaia en multipart/form-data y escribe el veredicto y los campos extraídos en la misma fila. El script se ejecuta en los servidores de Google, así que la clave no llega al navegador.

Requisitos

  • Una hoja de Google Sheets y ficheros en Google Drive (JPEG, PNG, WEBP, HEIC o PDF, hasta 20 MB).
  • Una clave de test ck_test_… del panel. Consulta Autenticación.

Preparar la hoja

Crea una pestaña llamada Documentos con estas columnas en la fila 1:

ABCDEFG
ID de DriveTipo esperadoVeredictoMotivosNúmeroCaducidadID de análisis

Rellena la columna A con el ID de cada fichero (la parte de la URL de Drive entre /d/ y /view) y la B con el tipo esperado, por ejemplo es_dni. Los tipos disponibles están en el catálogo.

Instalar el script

Guarda la clave en Script Properties

En la hoja, abre Extensiones → Apps Script. En el editor, ve a Configuración del proyecto → Propiedades del script y añade la propiedad CONSTAIA_API_KEY con tu clave ck_test_…. Así la clave no aparece en el código ni se copia si alguien duplica el script.

Pega el código

Sustituye el contenido de Code.gs por:

Code.gs
const API = "https://api.constaia.com/v1";
const SHEET = "Documentos";

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("Constaia")
    .addItem("Analizar filas pendientes", "analyzePending")
    .addItem("Actualizar análisis en curso", "refreshPending")
    .addToUi();
}

function apiKey_() {
  const key = PropertiesService.getScriptProperties().getProperty("CONSTAIA_API_KEY");
  if (!key) throw new Error("Falta la propiedad del script CONSTAIA_API_KEY");
  return key;
}

function call_(path, params) {
  const response = UrlFetchApp.fetch(API + path, {
    ...params,
    headers: { Authorization: "Bearer " + apiKey_() },
    muteHttpExceptions: true,
  });
  const code = response.getResponseCode();
  const body = JSON.parse(response.getContentText());
  if (code >= 400) {
    const e = body.error || {};
    throw new Error(`${e.code}: ${e.message} (${e.request_id})`);
  }
  return body;
}

function analyzeDriveFile_(fileId, expect) {
  const blob = DriveApp.getFileById(fileId).getBlob();
  return call_("/analyze", {
    method: "post",
    // Un objeto con un Blob se envía como multipart/form-data.
    payload: {
      file: blob,
      options: JSON.stringify({ expect, language: "es", metadata: { drive_file_id: fileId } }),
    },
  });
}

function value_(field) {
  return field && field.value != null ? field.value : "";
}

function writeResult_(sheet, row, analysis) {
  const fields = analysis.fields || {};
  const verdict = analysis.verdict;
  sheet.getRange(row, 3, 1, 5).setValues([[
    verdict ? verdict.status : analysis.status,
    verdict ? verdict.reasons.filter((r) => r.severity !== "info").map((r) => r.message).join("\n") : "",
    value_(fields.document_number),
    value_(fields.expiry_date),
    analysis.id,
  ]]);
}

function analyzePending() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET);
  const rows = sheet.getDataRange().getValues();
  for (let i = 1; i < rows.length; i++) {
    const [fileId, expect, verdict] = rows[i];
    if (!fileId || verdict) continue;
    try {
      writeResult_(sheet, i + 1, analyzeDriveFile_(fileId, expect || undefined));
    } catch (err) {
      sheet.getRange(i + 1, 3, 1, 2).setValues([["error", err.message]]);
    }
    Utilities.sleep(200);
  }
}

function refreshPending() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET);
  const rows = sheet.getDataRange().getValues();
  for (let i = 1; i < rows.length; i++) {
    const status = rows[i][2];
    const id = rows[i][6];
    if (!id || (status !== "queued" && status !== "processing")) continue;
    try {
      writeResult_(sheet, i + 1, call_("/analyses/" + encodeURIComponent(id), { method: "get" }));
    } catch (err) {
      sheet.getRange(i + 1, 4).setValue(err.message);
    }
  }
}

Guarda, recarga la hoja y usa el menú Constaia → Analizar filas pendientes. La primera vez Google te pedirá autorizar el acceso a Drive, a la hoja y a servicios externos.

Revisa los resultados

La columna C recibe Válido, No válido o Revisar como texto (valid, invalid, review) y la D los motivos que no son informativos, en el idioma de language. Con formato condicional sobre la columna C puedes colorear las filas. Los campos disponibles por tipo están en GET /v1/document-types (por ejemplo, es_dni tiene document_number, birth_date, expiry_date…).

Análisis largos y resultados asíncronos

Si un análisis no termina en 30 s (PDFs de muchas páginas), la API responde 202 y la columna C queda en queued o processing con el ID de análisis en la G. Constaia → Actualizar análisis en curso consulta GET /v1/analyses/{id} para esas filas. Para hacerlo automáticamente, crea un activador por tiempo (Activadores → Añadir activador → refreshPending → Según tiempo, por ejemplo cada 5 minutos).

Por qué no recibir webhooks con doPost

Una aplicación web de Apps Script (doPost(e)) no puede leer las cabeceras de la petición, así que no puede verificar la firma de los webhooks (webhook-id, webhook-timestamp, webhook-signature). Además, Apps Script responde a los POST con una redirección y Constaia no sigue redirecciones: la entrega se consideraría fallida y se reintentaría. Si aun así la usas, trata el cuerpo solo como un aviso: toma data.id, comprueba que empieza por an_ y vuelve a leer el análisis con GET /v1/analyses/{id} y tu clave, que es la fuente de verdad, y haz que la escritura en la hoja sea idempotente. Para hojas de cálculo, el activador por tiempo de arriba es más sencillo y fiable.

Probar en modo test

Con una clave ck_test_… no se consumen créditos y la respuesta depende del nombre del fichero en Drive (el nombre del Blob). Sube cualquier imagen real a Drive con estos nombres:

FicheroTipo esperadoColumna C
dni_valid.jpges_dniVálido número 12345678Z, caducidad 2031-03-12
dni_expired.jpges_dniNo válido motivo de caducidad (not_expired, severidad error): "Caducado el 15/06/2020."
blurry.jpges_dniRevisar motivo de calidad insuficiente (low_quality)

También puedes renombrar el Blob en el código con blob.setName("dni_valid.jpg"). Más escenarios en Modo test.

Seguridad

  • La clave va en Script Properties, nunca en el código ni en una celda de la hoja. Quien pueda editar el proyecto de Apps Script puede ver las propiedades: comparte la hoja en modo lectura con quien no deba ver la clave.
  • Usa la clave de test mientras pruebas y cambia la propiedad a ck_live_… cuando pases a producción.
  • La hoja guarda los datos extraídos. Escribe solo los campos que necesites y limita quién tiene acceso. Constaia borra el fichero al terminar con storage: "none" (por defecto); más en Almacenamiento y privacidad.

Límites

  • Constaia: 20 MB por fichero, PDFs de hasta 30 páginas en síncrono (200 con async: true) y 2 peticiones por segundo por clave en el plan gratuito (10 en el de pago). El script procesa las filas una a una con una pausa, así que no llega al límite; ante un 429 la respuesta incluye Retry-After. Ver Límites de uso.
  • Apps Script tiene cuotas propias: tiempo máximo por ejecución y número diario de llamadas con UrlFetchApp, que dependen de tu tipo de cuenta. Con muchas filas, procesa por tandas o usa lotes.

Siguientes pasos

En esta página