Constaia
Integrations

Google Apps Script

Validate Google Drive documents from Google Sheets with Apps Script and UrlFetchApp, keep the key in Script Properties and write the verdict to the sheet.

With Google Apps Script you can analyze Google Drive files from a spreadsheet: each row points to a file, the script sends it to Constaia as multipart/form-data and writes the verdict and extracted fields back to the same row. The script runs on Google's servers, so the key never reaches the browser.

Prerequisites

  • A Google Sheets spreadsheet and files in Google Drive (JPEG, PNG, WEBP, HEIC or PDF, up to 20 MB).
  • A test key ck_test_… from the dashboard. See Authentication.

Prepare the sheet

Create a tab called Documents with these columns in row 1:

ABCDEFG
Drive IDExpected typeVerdictReasonsNumberExpiryAnalysis ID

Fill column A with each file's ID (the part of the Drive URL between /d/ and /view) and column B with the expected type, for example es_dni. The available types are in the catalogue.

Install the script

Store the key in Script Properties

In the sheet, open Extensions → Apps Script. In the editor, go to Project Settings → Script Properties and add the property CONSTAIA_API_KEY with your ck_test_… key. That way the key is not in the code and is not copied when someone duplicates the script.

Paste the code

Replace the content of Code.gs with:

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

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("Constaia")
    .addItem("Analyze pending rows", "analyzePending")
    .addItem("Refresh running analyses", "refreshPending")
    .addToUi();
}

function apiKey_() {
  const key = PropertiesService.getScriptProperties().getProperty("CONSTAIA_API_KEY");
  if (!key) throw new Error("Missing script property 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",
    // An object containing a Blob is sent as multipart/form-data.
    payload: {
      file: blob,
      options: JSON.stringify({ expect, language: "en", 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);
    }
  }
}

Save, reload the sheet and use the menu Constaia → Analyze pending rows. The first time, Google asks you to authorize access to Drive, the sheet and external services.

Review the results

Column C gets Válido, No válido or Revisar as text (valid, invalid, review) and column D the non-informational reasons, in the language you asked for. Conditional formatting on column C lets you colour the rows. The fields available per type are in GET /v1/document-types (for example, es_dni has document_number, birth_date, expiry_date…).

Long analyses and asynchronous results

If an analysis does not finish within 30 s (PDFs with many pages), the API responds 202 and column C stays at queued or processing with the analysis ID in G. Constaia → Refresh running analyses calls GET /v1/analyses/{id} for those rows. To do it automatically, create a time-driven trigger (Triggers → Add trigger → refreshPending → Time-driven, for example every 5 minutes).

Why not receive webhooks with doPost

An Apps Script web app (doPost(e)) cannot read request headers, so it cannot verify the signature of webhooks (webhook-id, webhook-timestamp, webhook-signature). Also, Apps Script answers POST requests with a redirect and Constaia does not follow redirects: the delivery would be considered failed and retried. If you use it anyway, treat the body only as a notification: take data.id, check it starts with an_ and read the analysis again with GET /v1/analyses/{id} and your key, which is the source of truth, and make the sheet write idempotent. For spreadsheets, the time-driven trigger above is simpler and more reliable.

Test in test mode

With a ck_test_… key no credits are used and the response depends on the file name in Drive (the Blob's name). Upload any real image to Drive with these names:

FileExpected typeColumn C
dni_valid.jpges_dniVálido number 12345678Z, expiry 2031-03-12
dni_expired.jpges_dniNo válido expiry reason (not_expired, severity error)
blurry.jpges_dniRevisar insufficient quality reason (low_quality)

You can also rename the Blob in code with blob.setName("dni_valid.jpg"). More scenarios in Test mode.

Security

  • The key goes in Script Properties, never in the code or a sheet cell. Anyone who can edit the Apps Script project can see the properties: share the sheet as view-only with people who must not see the key.
  • Use the test key while testing and change the property to ck_live_… when you go to production.
  • The sheet stores the extracted data. Write only the fields you need and restrict who has access. Constaia deletes the file when it finishes with storage: "none" (the default); more in Storage and privacy.

Limits

  • Constaia: 20 MB per file, PDFs up to 30 pages synchronously (200 with async: true) and 2 requests per second per key on the free plan (10 on paid). The script processes rows one by one with a pause, so it stays below the limit; on a 429 the response includes Retry-After. See Rate limits.
  • Apps Script has its own quotas: maximum run time per execution and a daily number of UrlFetchApp calls, which depend on your account type. With many rows, process them in chunks or use batches.

Next steps

On this page