Website screenshots in Airtable

Airtable stores images in attachment fields, and an attachment can be added from any public image URL: Airtable downloads the file and keeps its own copy. So a website screenshot is one API call away. Below are two copy-paste scripts: one that fills a screenshot for every row of a table, and one automation that captures each new record as it arrives.

How it works

The script asks Shotmint for a signed screenshot URL (GET /v1/screenshot/url, free) and writes it into the attachment field as { url, filename }. Airtable then downloads that URL, which renders the page. Your API key only goes to Shotmint, never into a cell. The image that ends up in the field is a copy that Airtable stores, so it's a snapshot of the page at that moment.

Cost: 1 credit per screenshot. The same URL and size requested again within 24 hours comes from cache and costs nothing, and a failed render (dead domain, timeout) is refunded.

Setup

Get a key with 100 free renders at /pricing (no signup). In your table, you need a field with the URLs (URL or text type; bare domains like example.com work) and an empty Attachment field for the screenshots.

1. Fill the whole table (Scripting extension)

Open Extensions → Add an extension → Scripting, replace the code with the script below, and pick the table, the two fields and the device in the settings panel, then paste your key. Click Run. Rows that already have a screenshot are skipped, so you can run it again after adding new URLs. Updates are written 50 records at a time, the most Airtable accepts per call.

// Shotmint: website screenshots into an Airtable attachment field.
// Airtable: Extensions -> Add an extension -> Scripting, paste this, click Run.
const config = input.config({
  title: 'Website screenshots',
  description: 'Captures the page in the URL field and saves it in the attachment field. Rows that already have a screenshot are skipped.',
  items: [
    input.config.table('table', { label: 'Table' }),
    input.config.field('urlField', { parentTable: 'table', label: 'URL field' }),
    input.config.field('shotField', { parentTable: 'table', label: 'Attachment field for the screenshot' }),
    input.config.text('apiKey', { label: 'Shotmint API key' }),
    input.config.select('device', {
      label: 'Device',
      options: [{ label: 'Desktop 1280x800', value: 'desktop' }, { label: 'Mobile 390x844', value: 'mobile' }],
    }),
  ],
});

const API = 'https://shotmint.creasework.com';

async function screenshotUrl(pageUrl) {
  if (!pageUrl.toLowerCase().startsWith('http')) pageUrl = 'https://' + pageUrl;
  const p = { url: pageUrl, block_ads: 'true', width: '1280', height: '800' };
  if (config.device === 'mobile') Object.assign(p, { width: '390', height: '844', mobile: 'true' });
  const res = await remoteFetchAsync(API + '/v1/screenshot/url?' + new URLSearchParams(p), {
    headers: { 'X-API-Key': config.apiKey.trim() },
  });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error || 'HTTP ' + res.status);
  return body.url;
}

const query = await config.table.selectRecordsAsync({ fields: [config.urlField, config.shotField] });
const todo = query.records.filter(r => r.getCellValueAsString(config.urlField).trim() && !r.getCellValue(config.shotField));
output.text(`${todo.length} record(s) to capture`);

let updates = [];
for (const record of todo) {
  const pageUrl = record.getCellValueAsString(config.urlField).trim();
  try {
    const url = await screenshotUrl(pageUrl);
    const filename = pageUrl.replace(/^https?:\/\//, '').replace(/[^a-z0-9.-]+/gi, '_') + '.png';
    updates.push({ id: record.id, fields: { [config.shotField.id]: [{ url, filename }] } });
  } catch (e) {
    output.text(`${pageUrl}: ${e.message}`);
  }
  if (updates.length === 50) { await config.table.updateRecordsAsync(updates); updates = []; }
}
if (updates.length) await config.table.updateRecordsAsync(updates);
output.text('Done. Airtable downloads each screenshot into the attachment field within a few seconds.');

The script uses remoteFetchAsync, not fetch: extensions run in your browser, and remoteFetchAsync sends the request through Airtable's servers so the browser's cross-origin rules don't block it. Note that the key in the settings panel can be seen by anyone who can edit the extension, so use a key just for this base.

2. Screenshot each new record (automation)

To capture pages as they're added (a form, a sync, an import), create an automation. Trigger: When a record is created, or When a record matches conditions with "URL is not empty" if the URL is filled in later. Action: Run a script. In the left panel add two input variables: recordId (the trigger record's Airtable record ID) and url (its URL field). Then paste:

// Shotmint: screenshot a record's URL when it's created (Airtable automation).
// Trigger: "When a record is created" (or "matches conditions": URL is not empty).
// Action: "Run a script" with input variables recordId (Airtable record ID) and url (the URL field).
const API_KEY = 'YOUR_SHOTMINT_KEY';
const TABLE = 'Sites';             // table name
const SHOT_FIELD = 'Screenshot';   // attachment field

const { recordId, url } = input.config();
if (!url) throw new Error('The record has no URL');
const pageUrl = url.trim().toLowerCase().startsWith('http') ? url.trim() : 'https://' + url.trim();
const params = new URLSearchParams({ url: pageUrl, width: '1280', height: '800', block_ads: 'true' });

const res = await fetch('https://shotmint.creasework.com/v1/screenshot/url?' + params, {
  headers: { 'X-API-Key': API_KEY },
});
const body = await res.json();
if (!res.ok) throw new Error('Shotmint: ' + (body.error || res.status));

await base.getTable(TABLE).updateRecordAsync(recordId, {
  [SHOT_FIELD]: [{ url: body.url, filename: 'screenshot.png' }],
});
output.set('screenshotUrl', body.url);

Automation scripts run on Airtable's servers, so plain fetch works there. Change TABLE and SHOT_FIELD to your names. For a phone-sized capture, use width: '390', height: '844', mobile: 'true'. If the render fails, the script throws and the automation's run history shows Shotmint's error message.

Options

Any of these can go in the parameters of either script (they're the ones a signed URL carries):

ParameterEffect
width, heightViewport size in px (default 1280×800)
full_page: 'true'The whole scrolling page instead of the first screen
mobile: 'true'Phone emulation (mobile viewport and touch events)
dark_mode: 'true'Prefers-color-scheme: dark
block_ads: 'true'Blocks ads and common cookie banners
delay: '2000'Waits 2 s after load, for animations
hide_selectorsCSS selectors to hide, e.g. a chat widget
Comma-separated: '#chat, .popup'

What we tested

Airtable scripts only run inside Airtable, so we ran both scripts above, unchanged, in Node.js with stand-ins for input, output, base, the table methods and remoteFetchAsync, against the live API with a test key. The extension skipped rows with an existing screenshot and rows without a URL, and every URL it wrote downloaded as a real PNG: 1280×800 for desktop, 390×844 for mobile, including a long Wikipedia article. An unreachable domain gave a URL that returns a 400 error instead of an image, so Airtable has nothing to attach for that row. We haven't run the scripts inside Airtable itself.

Good uses

For other tools, see all integrations: Google Sheets, n8n, Make and Zapier, and more.

Try it with 100 free renders

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).

Get a free API key Full API reference