Airtable August 2, 2026 13 min read

Airtable document extraction — invoices and receipts into your base

Attach a PDF to an Airtable record and have the supplier, date, total and line items fill themselves in. There is no FlowParse app in the Airtable marketplace, so the integration is a Run a script automation calling our REST API — which works well for invoices and receipts and runs into a hard wall on long bank statements, because Airtable stops a script at 30 seconds. This page is the working script, the limits that are real, and the design to use when a document does not fit inside them.

FlowParse
flowparse.io

What this integration actually is

Airtable is where a lot of small finance operations actually live: an invoice tracker, a receipts base, a client-billing table with attachments on every record. The attachments are the problem — a PDF in an attachment field is a file, not data, so somebody still opens each one and types the supplier, the date and the total into the fields next to it.

This integration removes that step. An automation fires when a file is attached, a script sends the document to the extraction API, and the response is written straight back into the record's fields — with line items landing in a linked table if you want them. Nothing leaves Airtable except the document itself, and nothing needs a server.

It is one script, roughly forty lines including a base64 encoder you have to write yourself, and it is entirely within reach of anyone comfortable editing an Airtable automation. The document extraction API is the endpoint; this page is the Airtable-specific build, including the two limits that decide what you can send.

FlowParse
flowparse.io

What you should know before building

There is no FlowParse app in the Airtable marketplace, and no extension to install. The integration is the built-in *Run a script* automation action, which can call any HTTPS API. That action has no CORS restriction, unlike the Scripting extension, so calling an external API from it is a supported, ordinary thing to do.

Airtable stops a script at 30 seconds. That is the documented limit; Airtable has noted it is temporarily running scripts at 120 seconds while it tests raising the ceiling, but the number you should design against is 30. A `fetch` call inside the script also times out at 30 seconds independently. Our extraction is synchronous and can take longer than that on a big document, which is the whole reason the section below exists.

`btoa()` is not available in the automation script action, even though it exists in the Scripting extension. Since the API takes the document as base64, you write the encoder yourself — it is nine lines, it is in the script below, and it is the detail that costs most people an afternoon when they first try this.

The limits that decide what you can send

Two sets of limits meet in this integration: Airtable's script environment and our extraction time. Neither is negotiable, and together they draw a clear line between documents that work in an Airtable automation and documents that need a different route.

In practice a receipt photo, a one or two-page invoice and a short statement all complete well inside the window. A ten-page scanned bank statement usually will not — it needs vision on every page, and by the time it is done the script has already been terminated. Sending it anyway does not fail gracefully: you get a dead automation run and, because the request reached us, a billed extraction whose result you never see.

So the rule is simple: small documents go through the script; big ones go through a platform with a longer window, or through the browser. Deciding that up front is much cheaper than discovering it in production on the day someone attaches a year of statements.

LimitValueWhy it matters here
Airtable script execution30 seconds (documented; 120s under temporary test)The whole extraction has to finish inside it
Airtable fetch timeout30 secondsA slow document kills the call, not just the wait
Fetch calls per script50Plenty for one document, not for a loop over a table
Script memory512 MBBase64 of a large PDF is memory-hungry; keep files small
Record mutations15 per second, 50 per batchBatch line items and transactions, never one at a time
FlowParse file size20 MBRejected before extraction if larger
FlowParse extraction limit110 secondsLonger documents error out rather than hang

Who this suits

It suits people already running their finance admin in Airtable: an agency tracking supplier invoices, a small studio logging receipts against projects, a property manager keeping documents per unit. The base is the system of record, the attachment is already there, and all that is missing is the typing.

It suits bases where a human reviews things anyway. Airtable's grid and interfaces are genuinely good review surfaces — extracted values in fields, the original attachment one click away, a status field to move things along. That is a better review loop than most dedicated tools offer.

It suits it less if your documents are long bank statements, or if you are processing hundreds of files a day. For statements, bank statement to Excel in the browser is the honest answer; for volume, an automation platform with a proper queue — Make or n8n — will treat you better than a script with a thirty-second budget.

The design that works

The status field is the part people leave out and then regret. Without it there is no way to tell an unprocessed record from a failed one, no way to re-run just the failures, and no protection against the automation firing twice on the same document — which, since every extraction is billed, is a cost as well as a mess.

Trigger on a condition rather than on record creation. In a real base, someone creates the row, then drags the PDF in a few seconds later; a creation trigger fires against an empty attachment field and the script has nothing to fetch.

1

1. Trigger

When a record matches conditions — Attachment is not empty and Status is empty. More reliable than 'record created', which fires before the file finishes uploading.

2

2. Input variables

Pass recordId, the attachment URL, the filename and your API key into the script as config values.

3

3. Fetch the file

Attachment URLs expire, so download it inside the run rather than storing it anywhere.

4

4. Encode

Convert the bytes to base64 with your own encoder — btoa() is not there.

5

5. Call the API

One POST with the file and filename, Bearer key in the header.

6

6. Write back

Update the record's fields, and create linked records for line items in batches of 50.

7

7. Status

Set a status field on success and on failure. It is your queue, your retry marker and your audit trail.

Attachment URLs expire — fetch inside the run

Airtable attachment URLs are temporary. Since the change that introduced expiring links, a URL obtained through the API or an automation is guaranteed active for a couple of hours and then stops working. Any design that stores that URL and comes back to it later is quietly broken, and it breaks in the most annoying way — it works while you test, and fails a week later on a record nobody touched.

So download the file inside the same automation run that produced the URL, use the bytes immediately, and never persist the link in another field or another system. If something downstream needs the document again, it should ask Airtable for a fresh URL at that moment.

The same rule applies if you send the document elsewhere for extraction. Passing an expiring URL to a platform that will fetch it later is the single most common cause of intermittent failures in Airtable integrations. Send bytes, not links.

FlowParse
flowparse.io

The automation script, in full

This is the whole first half: read the inputs, fetch the attachment, encode it, call the API, and hand three outputs to the rest of the automation. Nothing here is FlowParse-specific except the URL and the header — swap those and the same skeleton calls any document API.

Note the encoder. Nine lines of standard base64, written out because the environment does not give you `btoa()`. It handles the padding cases correctly, which matters: a file encoded with sloppy padding decodes to a corrupt PDF and you get a 400 back with an unhelpful message about an unreadable file.

Keep the API key out of the script body if you can — an input variable sourced from a field in a locked, permission-restricted table is not perfect secret management, but it is considerably better than a key pasted into a script that every base collaborator can read and export.

Run a script — fetch, encode, extract
// Airtable → Automations → Run a script
// Input variables: recordId, fileUrl, fileName  (+ apiKey from a secret field or hard-coded)
const { recordId, fileUrl, fileName, apiKey } = input.config();

// Attachment URLs expire, so fetch the file inside this run — never store the URL.
const file = await fetch(fileUrl);
const bytes = new Uint8Array(await file.arrayBuffer());

// btoa() does NOT exist in the automation script action (only in the Scripting extension),
// so encode by hand. This is the whole encoder:
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let b64 = "";
for (let i = 0; i < bytes.length; i += 3) {
  const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
  b64 += A[b0 >> 2];
  b64 += A[((b0 & 3) << 4) | ((b1 === undefined ? 0 : b1) >> 4)];
  b64 += b1 === undefined ? "=" : A[((b1 & 15) << 2) | ((b2 === undefined ? 0 : b2) >> 6)];
  b64 += b2 === undefined ? "=" : A[b2 & 63];
}

const res = await fetch("https://flowparse.io/api/v1/extract", {
  method: "POST",
  headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
  body: JSON.stringify({ file: b64, filename: fileName }),
});

if (!res.ok) throw new Error("Extraction failed: " + res.status + " " + (await res.text()));
const body = await res.json();          // { type, pages, billedPages, data }
const fields = body.data.data;          // the document's own fields

output.set("type", body.type);          // "invoice" | "bank_statement" | "receipt" | ...
output.set("pages", body.pages);
output.set("json", JSON.stringify(fields));

Writing the fields back into the base

The second half of the script writes what came back. Map deliberately rather than dumping the JSON into a long text field: a base where Total is a number and Date is a date is a base you can group, roll up and chart. A base where everything is text is a base where you will do the same typing again later.

Two details save trouble. Dates come back as ISO strings, which Airtable date fields accept directly — do not reformat them into a local convention first. Amounts come back as numbers or numeric strings, so coerce with `Number()` and let a genuine null stay null rather than becoming a zero that pollutes every sum in the base.

Line items belong in their own table, linked back to the document record. Creating them in batches of fifty respects Airtable's mutation limits and finishes in a fraction of the time a per-record loop takes — which matters when the whole script has thirty seconds to live.

Run a script — map fields and line items
// Second half of the same script: write the fields back.
// fields = body.data.data from the call above.
const table = base.getTable("Invoices");

await table.updateRecordAsync(recordId, {
  "Supplier":   fields.supplier_name || "",
  "Invoice no": fields.invoice_number || "",
  "Date":       fields.invoice_date || null,     // ISO string, not a locale format
  "Currency":   fields.currency || "",
  "Net":        Number(fields.subtotal) || null,
  "Tax":        Number(fields.tax_amount) || null,
  "Total":      Number(fields.total) || null,
  "Status":     "Extracted",
});

// Line items go in their own table, linked back to this record.
const lines = base.getTable("Invoice lines");
const rows = (fields.line_items || []).map((l) => ({
  fields: {
    "Invoice":     [{ id: recordId }],
    "Description": l.description || "",
    "Qty":         Number(l.quantity) || null,
    "Unit price":  Number(l.unit_price) || null,
    "Amount":      Number(l.amount) || null,
  },
}));
// createRecordsAsync takes 50 at a time.
for (let i = 0; i < rows.length; i += 50) {
  await lines.createRecordsAsync(rows.slice(i, i + 50));
}

Line items are the reason to bother

A base that stores only invoice totals answers one question: how much did we spend. A base that stores line items answers the questions people actually ask — what did we buy, which project should carry it, is this supplier's pricing drifting, did we get charged for something we cancelled.

Airtable is unusually good at this shape of data because linked records and rollups are native. One invoice record, many line records, a rollup for the line sum, and a formula comparing that rollup to the extracted total gives you an arithmetic check on every document without writing a single validation rule.

That comparison is worth building on day one. If the line items sum to something other than the net, either the extraction misread a figure or the supplier's own document does not add up — and both are things you want to see before the invoice is paid rather than at year-end.

FlowParse
flowparse.io

Bank statements: where Airtable stops being the right home

You can extract a bank statement through this integration, and for a short one it works. Two things make it a poor default, though, and both are worth knowing before you design a base around it.

First, time: a multi-page statement — especially a scanned one — frequently needs longer than the script is allowed to run. Second, volume: one statement can produce hundreds of transaction rows, each of them a record creation, and a base filling with tens of thousands of transaction records will hit per-base record limits on your plan long before it hits anything interesting.

The pragmatic split most teams end up with: invoices and receipts live in Airtable, where the review workflow is genuinely useful, and statements go through the batch converter into Excel, where hundreds of rows are a spreadsheet's natural home. If a statement summary needs to be in the base, put the summary there — period, account, opening, closing, totals — and keep the transaction detail in the workbook.

What to do when a document does not fit in 30 seconds

There are three honest options and one bad one. The bad one is retrying the same document repeatedly in the hope it finishes faster — it will not, and each attempt is billed.

The best option for a document that genuinely needs to be processed automatically is to move the extraction call out of Airtable. Trigger the automation, send the record ID and the file to a platform with a longer execution window, let it call the API, and have it write the results back through Airtable's own REST API. Make and n8n both do this comfortably, and n8n has no execution ceiling worth worrying about.

The simplest option is to keep the document out of the automation entirely: convert it in the browser and attach the resulting workbook. Not everything needs to be automated, and a five-minute manual pass on the handful of large documents a month beats a fragile pipeline that fails on exactly those files.

RouteHandles long documentsCost of running it
Airtable Run a scriptNo — 30-second ceilingFree with your Airtable plan
Make scenario, writes back to AirtableYesOperations metered by Make
n8n workflow, writes back to AirtableYesYour own server, no per-task meter
Your own worker with a queueYes, and retries properlyEngineering time
Browser batch, attach the resultYes, any sizeA person, five minutes

Failures, and re-running only what failed

Three responses matter. A 422 means the document was read but held nothing extractable — a blurred photo, a blank page, a PDF that is really a letter — and it is not billed. A 429 means the page budget is gone. A 400 almost always means the base64 is wrong, which in this environment means the encoder or the padding.

Catch them and write the reason into a field. An automation that throws leaves a red mark in the run history and nothing on the record, so the person looking at the base cannot tell whether a document is unprocessed or unprocessable. A `Status` of *Failed* plus an `Error` field with the status code and message makes the queue self-explanatory.

Then re-running is trivial: a view filtered to *Failed*, and a manual automation or a second condition that picks those records up again once you have fixed whatever was wrong. That is the whole retry story, and it is better than an automatic one because a person decides when a document is worth paying to extract a second time.

Not extracting the same document twice

Every call is billed per page, and nothing on our side remembers that you sent this file yesterday. Airtable automations can and do fire more than once on the same record — an edit to another field, a re-run after an error, someone duplicating a row — so the protection has to live in your base.

The status field does most of it: trigger only when status is empty, and set it as the first mutation the script performs rather than the last. That leaves a small window, which in practice is fine for a base handling tens of documents a day and not fine for one handling thousands.

If duplicates would be expensive, store a fingerprint — filename plus size, or a hash if you are already computing one — in a field and check it before calling. It is ten extra lines and it makes the automation genuinely safe to re-run.

Checking the numbers rather than trusting them

Extraction is accurate, not infallible, and financial data deserves an arithmetic check rather than a confidence score. Airtable makes this pleasantly easy: rollups and formula fields can verify the document against itself, with no code and no extra API call.

For invoices: a rollup summing the linked line items, a formula comparing it to the extracted net, and another checking net plus tax against the total. Colour the mismatches and you have an exception view. For statements: opening balance plus the sum of transactions must equal the closing balance — the same check our engine runs, reproduced in the base where your team can see it.

That check is the difference between output that looks right and output you can defend. Where it fails, look at the document rather than the data — a surprising share of mismatches turn out to be the supplier's arithmetic rather than ours, and finding those is worth the exercise on its own.

FlowParse
flowparse.io

The review loop Airtable is genuinely good at

The reason to keep this in Airtable rather than a spreadsheet is the review surface. An interface with the extracted fields on the left and the attachment preview on the right, a status button, and a filtered queue of everything not yet approved, is a better invoice-review experience than most accounts-payable tools give you — and you can build it in an afternoon.

Give the reviewer only what needs a decision. Documents that extracted cleanly and pass their arithmetic do not need looking at; the queue should hold the ones that failed a check, the ones with a low-confidence field, and anything above whatever amount your policy says needs eyes.

Keep the original attached to the record. The extracted values are a working dataset and the supplier's or bank's document remains the evidence — and since we delete originals immediately after extraction, the copy in your base is the copy that exists.

FlowParse
flowparse.io

Airtable script versus the other routes

The comparison is really about where the work runs. The script is free, immediate and limited; the platforms cost money and remove the limits; your own code costs time and removes everything else. Most bases end up with the script for ordinary documents and one of the others for the awkward ones.

RouteBest whenMain constraint
Airtable Run a scriptInvoices and receipts, tens per day30-second script and fetch limits
ZapierYou already run Zaps and want no code at allWebhooks need a paid plan; per-task cost
MakeLong documents, visual builder, tighter budgetDocuments pass through Make
n8nVolume, or documents that must stay in your networkYou run the server
Power AutomateThe rest of the business is on Microsoft 365HTTP is a premium connector

Where the documents go

In this design the file travels from Airtable's storage to our API over TLS, is extracted, and the original is deleted immediately afterwards. Processing happens in the EU, extracted data is encrypted at rest, and no customer document is used to train models. The security page has the detail, including sub-processors.

Airtable's own permissions still apply to everything that lands in the base. If the extracted values include anything sensitive — account numbers on a statement, personal details on an expense claim — restrict the table and the interface accordingly, because the automation will happily write them into a view that everyone in the workspace can see.

There is no on-premise version of FlowParse. If your rule is that documents may never leave your infrastructure, this integration cannot satisfy it and neither can any of the hosted alternatives above — the honest answer is that we are not the right tool for that requirement.

The boundary, stated plainly

FlowParse is a document-extraction engine. There is no Airtable app, no marketplace extension, no sync integration and no callback — the API answers the request your script makes and nothing else. Anything event-driven here is Airtable reacting to Airtable.

It is not a bookkeeping system, not an approvals platform and not an archive. It does not code transactions to accounts, decide what is deductible, pay anyone or keep your documents. Those all stay in your base, your process and your accountant's hands.

It is also not a database. The output is data for you to store where you already work — which, if you are reading this page, is Airtable, and that is a perfectly good place for it as long as the volume stays in the range Airtable is built for.

A worked example: a studio's supplier base

A twelve-person design studio tracks supplier invoices in Airtable: one record per invoice, the PDF attached, fields for supplier, date, net, VAT, total and the project it belongs to. Before automating, the office manager keyed every one — about twenty-five a week, roughly ninety minutes.

The build took an afternoon. An automation triggers when the attachment is present and status is empty, the script extracts, the fields fill in, line items land in a linked table, and a rollup checks that the lines sum to the net. Project allocation stayed manual, because that is a judgement nobody wanted a machine making.

What changed is the shape of the work rather than its existence. Ninety minutes of typing became roughly fifteen minutes of reviewing an exceptions view — three or four documents a week that are scans, credit notes or a supplier whose layout puts the total in a place the model reads twice. The register is current daily instead of weekly, which turned out to matter more than the time saved.

A checklist before you turn it on

Test with the worst document you have, not the cleanest. A crisp digital invoice proves nothing — the ones that teach you something are the phone photo taken at an angle and the supplier whose PDF is four pages of terms with the numbers on page three.

1

Trigger on a condition

Attachment present and status empty — not on record creation, which fires too early.

2

Status field first

Write status before the API call, so a double-fire cannot double-bill.

3

Own encoder tested

Check a file that is not a multiple of three bytes — that is where padding bugs show up.

4

Failure path writes a reason

Status plus error message on the record, not just a red run in the history.

5

Fields typed properly

Numbers as numbers, dates as dates. Text fields are how you end up retyping later.

6

Arithmetic check in the base

Rollup of line items against the net; opening plus transactions against closing.

7

Big documents routed elsewhere

Anything long or heavily scanned should never enter the 30-second script.

8

Originals kept

We delete after extraction; the attachment in your base is the record.

Where to go next

The full endpoint reference, including validation and export, is in the API docs, and keys are issued from the API dashboard. For what comes back per document type, see the invoice parsing API, the receipt OCR API and the bank statement API.

If the thirty-second limit is the problem, the worked setups for Make, n8n, Zapier and Power Automate all show the same call in a platform with more room, and all integrations lists the cloud-storage routes too.

For statements specifically, the browser route is genuinely better: bank statement to Excel for one file, batch for a folder, and receipt scanner if the receipts base is the part you care about most.

Stop typing invoices into your base

One automation, one script, and every attachment fills in its own fields — with the line items linked and the arithmetic checked.

Frequently asked questions

Related