Skip to content
Airtable integration

Use FXMacroData in Airtable

Record each economic release in a table the moment it is published, keep macro series, FX rates, commodities, release calendars, and COT positioning current on a schedule, or import years of history into a base. Pick the workflow that fits how your team uses Airtable.

What you can do

Option Best for Runs in
A. Release alerts A new row for every release as it is published, pushed by FXMacroData. Airtable automation with a webhook trigger
B. Scheduled refresh Keeping the latest rows of one series, pair, commodity, calendar, or COT report current. Airtable automation, Run a script action
C. History import Loading up to 5,000 historical rows into a table on demand. Scripting extension
D. Sync API table A read-only synced table refreshed from your own scheduler, up to 10,000 rows. Python script and the Airtable Sync API
E. AI assistants Asking an assistant to pull data and write it into a base in plain language. Claude, ChatGPT, Cursor, or another MCP client

Free USD data

Without an API key, USD indicators, the USD release calendar, and USD COT positioning are available for the last 90 days, with a 15-minute delay after each release. A subscription key unlocks full history.

Subscription data

Other currencies, FX rates, commodities, real-time delivery, and webhook alerts need an API key from a subscription.

Key handling

The scripts send your key in the X-API-Key header. Anyone who can edit the automation or open the extension can read its settings, so share bases accordingly.

Option A

Release alerts into a table with webhooks

FXMacroData posts one flat JSON record to an Airtable automation each time a matching release is published. Every top-level field becomes a token you can map straight into table columns. Webhook subscriptions need an active paid API key.

  1. Create the table. Add columns for the fields you want to keep, for example Key (single line text), Headline (single line text), Value (number), Previous (number), Unit (single line text), Period (date), Published at (date with time, GMT), and Source (URL).
  2. Create the automation. Open Automations in your base, create an automation, and choose the trigger When webhook received. Copy the webhook URL Airtable shows. It looks like https://hooks.airtable.com/workflows/v1/genericWebhook/app.../wfl.../wtr....
  3. Send a sample record. Airtable learns the field names from the first request it receives. Post the example payload below to your webhook URL, then click Test trigger in Airtable.
  4. Create the FXMacroData subscription. In API management, open Webhooks, choose the Airtable destination, paste the webhook URL, optionally limit it to the currencies and indicators you want, and click Create webhook. You can also create it through the API with "destination": "airtable", as shown below.
  5. Map the fields. Add a Create record action, pick your table, and insert tokens from the trigger body into each column: record_key into Key, headline into Headline, value into Value, and so on. To keep one row per release, use a Find records action on Key equal to record_key followed by Update record, and create the record only when nothing is found.
  6. Turn the automation on. The next matching release adds a row.

Create the subscription with the API

curl -X POST "https://api.fxmacrodata.com/v1/webhooks/subscriptions" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.airtable.com/workflows/v1/genericWebhook/appXXXX/wflXXXX/wtrXXXX",
    "destination": "airtable",
    "currencies": ["usd", "eur"],
    "indicators": ["inflation", "policy_rate"]
  }'

The URL must be the automation's webhook URL with no query string. The Airtable destination takes no destination_options. Deliveries are not signed: the webhook URL is the credential, so keep it private. See webhook destinations for retries and delivery logs.

Example payload

{
  "event_id": "usd_inflation_1789129800",
  "event": "announcement",
  "record_key": "usd:inflation:2026-08-31",
  "currency": "USD",
  "indicator": "inflation",
  "name": "Inflation (CPI)",
  "period": "2026-08-31",
  "value": 3.4,
  "value_text": "3.4 %YoY",
  "previous": 3.4,
  "previous_text": "3.4 %YoY",
  "unit": "%YoY",
  "published_at": "2026-09-11T12:30:00Z",
  "latency_ms": null,
  "headline": "USD Inflation (CPI): 3.4 %YoY",
  "source_url": "https://www.bls.gov/news.release/archives/cpi_09112026.htm",
  "series_url": "https://fxmacrodata.com/api-data-docs/usd/inflation"
}

To send it as your sample record, save it as sample.json and run:

curl -X POST "https://hooks.airtable.com/workflows/v1/genericWebhook/appXXXX/wflXXXX/wtrXXXX" \
  -H "Content-Type: application/json" \
  --data @sample.json

Payload fields

Field Type Description
event_idtextUnique ID of the release event. Repeated deliveries of the same event carry the same ID.
eventtextAlways announcement.
record_keytextStable key per release, currency:indicator:period. Use it to find and update an existing row.
currencytextUppercase currency code, for example USD.
indicatortextIndicator slug, for example inflation.
nametextDisplay name of the indicator.
perioddateReference period of the observation, YYYY-MM-DD.
valuenumberPublished value, for Number fields.
value_texttextPublished value with its unit, for text fields.
previousnumberPrevious value, when one exists.
previous_texttextPrevious value with its unit.
unittextUnit of the series, for example %YoY.
published_atdate and timeOfficial publication time in UTC, ISO 8601, for example 2026-09-11T12:30:00Z.
latency_msnumberMilliseconds from publication to capture when measured, otherwise empty.
headlinetextOne-line summary such as USD Inflation (CPI): 3.4 %YoY.
source_urlURLOfficial release page from the publisher.
series_urlURLFXMacroData page for the series.

Option B

Scheduled refresh with an automation script

An automation reads the most recent rows of one dataset on a schedule and writes them into a table. Rows are matched on a Key column, so each run updates what it wrote before instead of adding duplicates.

  1. Create a table with a single line text field called Key plus the columns for your dataset from the table below. Columns you leave out are skipped.
  2. Create an automation with the trigger At a scheduled time, for example every hour or every weekday morning.
  3. Add the action Run a script and paste the script below.
  4. On the left of the script editor, add the input variables listed below.
  5. Click Test, check the rows it wrote, and turn the automation on.
Input variableValue
apiKeyYour FXMacroData API key. Leave empty for free USD data.
datasetindicator, forex, commodity, calendar, or cot
currencyFor indicator, calendar, and cot, for example usd
indicatorFor indicator, for example inflation
base, quoteFor forex, for example eur and usd
commodityFor commodity, for example gold
tableNameThe table to write into
rowsOptional. Most recent rows to read, default 20, maximum 100
DatasetColumns written
indicatorDate, Value, Previous, Change, Released at (UTC)
forexDate, Rate
commodityDate, Value
calendarRelease, Scheduled at (UTC)
cotDate, Non-commercial net, Open interest

Find indicator slugs in the API reference.

automation-refresh.js

Download .js
// FXMacroData -> Airtable: scheduled refresh for an automation.
//
// Use in an Airtable automation: trigger "At a scheduled time", action
// "Run a script". Add these input variables on the left of the script editor:
//
//   apiKey     your FXMacroData API key (leave empty for USD-only free data)
//   dataset    indicator | forex | commodity | calendar | cot
//   currency   e.g. usd            (indicator, calendar, cot)
//   indicator  e.g. inflation      (indicator)
//   base       e.g. eur            (forex)
//   quote      e.g. usd            (forex)
//   commodity  e.g. gold           (commodity)
//   tableName  the table to write into
//   rows       optional, most recent rows to read (default 20, max 100)
//
// The table needs a single-line text field called "Key" plus the columns
// listed in COLUMNS below for your dataset. Rows are matched on Key, so a
// rerun updates what it wrote before instead of adding duplicates.
//
// Docs: https://fxmacrodata.com/documentation/airtable?utm_source=airtable&utm_medium=referral&utm_campaign=airtable-scripts&utm_content=automation

const API = 'https://api.fxmacrodata.com';
const config = input.config();
const dataset = String(config.dataset || 'indicator').trim().toLowerCase();
const lower = v => String(v || '').trim().toLowerCase();
const upper = v => String(v || '').trim().toUpperCase();
const num = v => (typeof v === 'number' && Number.isFinite(v) ? v : null);
const iso = s => (typeof s === 'number' && Number.isFinite(s) ? new Date(s * 1000).toISOString() : null);

const DATASETS = {
    indicator: {
        path: () => `/v1/announcements/${lower(config.currency)}/${lower(config.indicator)}?fields=compact`,
        key: r => `${upper(config.currency)} ${lower(config.indicator)} ${r.date}`,
        cells: r => ({
            Date: r.date,
            Value: num(r.val),
            Previous: num(r.previous_value),
            Change: num(r.change_from_previous),
            'Released at (UTC)': iso(r.announcement_datetime),
        }),
    },
    forex: {
        path: () => `/v1/forex/${lower(config.base)}/${lower(config.quote)}?`,
        key: r => `${upper(config.base)}${upper(config.quote)} ${r.date}`,
        cells: r => ({Date: r.date, Rate: num(r.val)}),
    },
    commodity: {
        path: () => `/v1/commodities/${lower(config.commodity)}?`,
        key: r => `${lower(config.commodity)} ${r.date}`,
        cells: r => ({Date: r.date, Value: num(r.val)}),
    },
    calendar: {
        path: () => `/v1/calendar/${lower(config.currency)}`,
        key: r => r.calendar_event_id || `${lower(config.currency)}_${r.release}_${r.announcement_datetime}`,
        cells: r => ({Release: r.release || null, 'Scheduled at (UTC)': iso(r.announcement_datetime)}),
    },
    cot: {
        path: () => `/v1/cot/${lower(config.currency)}?`,
        key: r => `${upper(config.currency)} COT ${r.date}`,
        cells: r => ({Date: r.date, 'Non-commercial net': num(r.noncommercial_net), 'Open interest': num(r.open_interest)}),
    },
};

const def = DATASETS[dataset];
if (!def) {
    throw new Error(`dataset must be one of: ${Object.keys(DATASETS).join(', ')}`);
}

const rowsWanted = Math.min(Math.max(parseInt(config.rows, 10) || 20, 1), 100);
let url = API + def.path();
if (dataset !== 'calendar') {
    url += `${url.endsWith('?') ? '' : '&'}limit=${rowsWanted}`;
}
const headers = {Accept: 'application/json'};
if (config.apiKey) {
    headers['X-API-Key'] = String(config.apiKey).trim();
}

const response = await fetch(url, {headers});
const body = await response.json().catch(() => null);
if (!response.ok) {
    const detail = body && (body.detail || body.error);
    throw new Error(`FXMacroData HTTP ${response.status}: ${detail || 'request failed'}`);
}

const table = base.getTable(config.tableName);
const fieldNames = new Set(table.fields.map(f => f.name));
const query = await table.selectRecordsAsync({fields: ['Key']});
const existing = new Map(query.records.map(r => [r.getCellValueAsString('Key'), r.id]));

const creates = [];
const updates = [];
const seen = new Set();
for (const row of body.data || []) {
    const key = def.key(row);
    if (!key || seen.has(key)) continue;
    seen.add(key);
    const fields = {Key: key};
    for (const [name, value] of Object.entries(def.cells(row))) {
        if (fieldNames.has(name)) fields[name] = value;
    }
    if (existing.has(key)) {
        updates.push({id: existing.get(key), fields});
    } else {
        creates.push({fields});
    }
}

for (let i = 0; i < creates.length; i += 50) {
    await table.createRecordsAsync(creates.slice(i, i + 50));
}
for (let i = 0; i < updates.length; i += 50) {
    await table.updateRecordsAsync(updates.slice(i, i + 50));
}

output.set('created', creates.length);
output.set('updated', updates.length);

Option C

Full history import with the Scripting extension

The Scripting extension runs an interactive import: it asks which dataset you want, pages through the history, and creates or updates rows keyed on a Key field. Missing columns are created for you when you have permission to add fields.

  1. In your base, open Extensions, add the Scripting extension, and paste the script below.
  2. Open the extension settings and enter your FXMacroData API key, or leave it empty to import free USD data.
  3. Click Run, choose an economic indicator, FX rate, commodity, release calendar, or COT positioning, answer the prompts, and pick the table to write into.

Requests are sent with remoteFetchAsync, so they go from Airtable's servers rather than your browser. Each run imports up to 5,000 rows. The API key is stored in the extension settings, which every collaborator who can open the extension can read.

scripting-extension-import.js

Download .js
// FXMacroData -> Airtable: interactive history import for the Scripting extension.
//
// Paste into a Scripting extension and press Run. It asks what to import,
// pages through the full history, and creates or updates rows keyed on a
// "Key" field. Missing columns are created when you have permission.
//
// Requests go through remoteFetchAsync, which sends them from Airtable's
// servers rather than your browser.
//
// The API key is stored in the extension's settings, which every collaborator
// who can open this extension can read. Leave it empty to import the free USD
// data only.
//
// Docs: https://fxmacrodata.com/documentation/airtable?utm_source=airtable&utm_medium=referral&utm_campaign=airtable-scripts&utm_content=scripting

const settings = input.config({
    title: 'FXMacroData import',
    description: 'Imports FXMacroData history into a table. Get an API key at https://fxmacrodata.com/subscribe',
    items: [input.config.text('apiKey', {label: 'FXMacroData API key (optional for USD)'})],
});

const API = 'https://api.fxmacrodata.com';
const lower = v => String(v || '').trim().toLowerCase();
const upper = v => String(v || '').trim().toUpperCase();
const num = v => (typeof v === 'number' && Number.isFinite(v) ? v : null);
const iso = s => (typeof s === 'number' && Number.isFinite(s) ? new Date(s * 1000).toISOString() : null);

const TEXT = {type: 'singleLineText'};
const DATE = {type: 'date', options: {dateFormat: {name: 'iso'}}};
const DATETIME = {
    type: 'dateTime',
    options: {dateFormat: {name: 'iso'}, timeFormat: {name: '24hour'}, timeZone: 'utc'},
};
const number = precision => ({type: 'number', options: {precision}});

const DATASETS = {
    'Economic indicator': {
        ask: async () => ({
            currency: await input.textAsync('Currency (e.g. usd, eur, jpy)'),
            indicator: await input.textAsync('Indicator slug (e.g. inflation, gdp, policy_rate)'),
        }),
        path: p => `/v1/announcements/${lower(p.currency)}/${lower(p.indicator)}?fields=compact&`,
        paged: true,
        columns: {Date: DATE, Value: number(4), Previous: number(4), Change: number(4), 'Released at (UTC)': DATETIME},
        key: (r, p) => `${upper(p.currency)} ${lower(p.indicator)} ${r.date}`,
        cells: r => ({
            Date: r.date,
            Value: num(r.val),
            Previous: num(r.previous_value),
            Change: num(r.change_from_previous),
            'Released at (UTC)': iso(r.announcement_datetime),
        }),
    },
    'FX rate': {
        ask: async () => ({
            base: await input.textAsync('Base currency (e.g. eur)'),
            quote: await input.textAsync('Quote currency (e.g. usd)'),
        }),
        path: p => `/v1/forex/${lower(p.base)}/${lower(p.quote)}?`,
        paged: true,
        columns: {Date: DATE, Rate: number(6)},
        key: (r, p) => `${upper(p.base)}${upper(p.quote)} ${r.date}`,
        cells: r => ({Date: r.date, Rate: num(r.val)}),
    },
    Commodity: {
        ask: async () => ({
            commodity: await input.buttonsAsync('Commodity', [
                'gold', 'silver', 'platinum', 'oil_wti', 'oil_brent', 'natural_gas',
            ]),
        }),
        path: p => `/v1/commodities/${p.commodity}?`,
        paged: true,
        columns: {Date: DATE, Value: number(4)},
        key: (r, p) => `${p.commodity} ${r.date}`,
        cells: r => ({Date: r.date, Value: num(r.val)}),
    },
    'Release calendar': {
        ask: async () => ({currency: await input.textAsync('Currency (e.g. usd)')}),
        path: p => `/v1/calendar/${lower(p.currency)}`,
        paged: false,
        columns: {Release: TEXT, Name: TEXT, 'Scheduled at (UTC)': DATETIME, Importance: TEXT},
        key: (r, p) => r.calendar_event_id || `${lower(p.currency)}_${r.release}_${r.announcement_datetime}`,
        cells: r => ({
            Release: r.release || null,
            Name: r.name || null,
            'Scheduled at (UTC)': iso(r.announcement_datetime),
            Importance: r.event_importance || null,
        }),
    },
    'COT positioning': {
        ask: async () => ({currency: await input.textAsync('Currency (e.g. jpy)')}),
        path: p => `/v1/cot/${lower(p.currency)}?`,
        paged: true,
        columns: {Date: DATE, 'Open interest': number(0), 'Non-commercial net': number(0), 'Leveraged funds net': number(0)},
        key: (r, p) => `${upper(p.currency)} COT ${r.date}`,
        cells: r => ({
            Date: r.date,
            'Open interest': num(r.open_interest),
            'Non-commercial net': num(r.noncommercial_net),
            'Leveraged funds net': num(r.leveraged_funds_net),
        }),
    },
};

const choice = await input.buttonsAsync('What do you want to import?', Object.keys(DATASETS));
const def = DATASETS[choice];
const params = await def.ask();
const maxRows = Math.min(parseInt(await input.textAsync('How many recent rows? (max 5000)'), 10) || 500, 5000);
const table = await input.tableAsync('Write into which table?');

const headers = {Accept: 'application/json'};
if (settings.apiKey) {
    headers['X-API-Key'] = String(settings.apiKey).trim();
}

async function getJson(url) {
    const response = await remoteFetchAsync(url, {headers});
    const body = await response.json().catch(() => null);
    if (!response.ok) {
        const detail = body && (body.detail || body.error);
        throw new Error(`FXMacroData HTTP ${response.status}: ${detail || 'request failed'}`);
    }
    return body;
}

const rows = [];
if (def.paged) {
    let offset = 0;
    while (rows.length < maxRows) {
        const body = await getJson(`${API}${def.path(params)}limit=100&offset=${offset}`);
        const data = body.data || [];
        rows.push(...data.slice(0, maxRows - rows.length));
        const page = body.pagination || {};
        if (!data.length || page.has_more !== true) break;
        offset = typeof page.next_offset === 'number' ? page.next_offset : offset + data.length;
    }
} else {
    rows.push(...((await getJson(API + def.path(params))).data || []).slice(0, maxRows));
}

const wanted = {Key: TEXT, ...def.columns};
for (const [name, spec] of Object.entries(wanted)) {
    if (!table.getFieldByNameIfExists(name)) {
        if (!table.hasPermissionToCreateField(name, spec.type, spec.options)) {
            throw new Error(`Add a "${name}" field to ${table.name}, or ask a base creator to run this script.`);
        }
        await table.createFieldAsync(name, spec.type, spec.options || null);
    }
}

const query = await table.selectRecordsAsync({fields: ['Key']});
const existing = new Map(query.records.map(r => [r.getCellValueAsString('Key'), r.id]));
const creates = [];
const updates = [];
const seen = new Set();
for (const row of rows) {
    const key = def.key(row, params);
    if (!key || seen.has(key)) continue;
    seen.add(key);
    const fields = {Key: key, ...def.cells(row)};
    if (existing.has(key)) updates.push({id: existing.get(key), fields});
    else creates.push({fields});
}

for (let i = 0; i < creates.length; i += 50) await table.createRecordsAsync(creates.slice(i, i + 50));
for (let i = 0; i < updates.length; i += 50) await table.updateRecordsAsync(updates.slice(i, i + 50));

output.markdown(`Imported **${rows.length}** rows into **${table.name}**: ${creates.length} new, ${updates.length} updated.`);

Option D

Synced table with the Airtable Sync API

Airtable's Sync API turns an uploaded CSV into a read-only synced table. fxmd_airtable_sync.py reads one FXMacroData dataset, writes it as CSV, and posts it to your table's Sync API endpoint. Run it from cron, Task Scheduler, or a CI job to keep the table current. It uses only the Python standard library.

The Airtable Sync API is available on the Airtable Business and Enterprise Scale plans, and setting it up needs creator permission on the base.
  1. In Airtable, choose + Add or import, then Sync API, then Set up.
  2. Create a personal access token for that base with the data.records:write and schema.bases:write scopes.
  3. Run the script once with --sync-url set to the endpoint in the cURL example Airtable shows.
  4. Back in Airtable, pick the field types and choose Key as the unique ID. Leave the deletion setting on deleting records that are not in the latest request, so the table mirrors the most recent upload.
  5. Schedule the same command to refresh the table.
export FXMD_API_KEY=YOUR_API_KEY        # optional for USD indicators, calendar and COT
export AIRTABLE_TOKEN=YOUR_AIRTABLE_TOKEN

python fxmd_airtable_sync.py indicator --currency usd --indicator inflation \
  --sync-url https://api.airtable.com/v0/appXXXX/tblXXXX/sync/XXXX

python fxmd_airtable_sync.py forex --base eur --quote usd --start-date 2020-01-01 \
  --sync-url https://api.airtable.com/v0/appXXXX/tblXXXX/sync/XXXX

# Preview the CSV without uploading
python fxmd_airtable_sync.py calendar --currency usd --dry-run

Datasets: indicator, forex, commodity, calendar, and cot. Optional flags: --start-date, --end-date, and --max-rows (default 5,000). Airtable accepts up to 10,000 rows and 2 MB per upload, and 20 Sync API requests per 5 minutes per base.

Option E

AI assistants with both MCP servers

Connect an MCP client such as Claude, ChatGPT, or Cursor to the FXMacroData MCP server and to Airtable's official MCP server. The assistant can then read FXMacroData series and write them into a base in one conversation.

FXMacroData MCP server

https://mcp.fxmacrodata.com/mcp

USD data works without a key. Sign in with your FXMacroData account for other currencies and real-time data. Setup for each client is in the MCP server guide.

Airtable MCP server

https://mcp.airtable.com/mcp

Airtable's own server signs in with OAuth and is available on all Airtable plans.

Example request:

Get the last 24 months of USD inflation and the Fed policy rate from FXMacroData,
then add them to the "Macro" table in my "Research" Airtable base,
one row per month with Date, Inflation and Policy rate columns.

Related docs

Questions about an Airtable setup? Contact info@fxmacrodata.com.

AI Answer-Ready

Key Facts

Page
Airtable
Section
Documentation
Canonical URL
https://fxmacrodata.com/documentation/airtable
Source
FXMacroData editorial and official publisher references
Last Updated
See page metadata

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

What is this page about? This page explains Airtable with directly usable context for trading, research, and API workflows.

What source should be cited? Use the canonical URL and the listed source field; cite official publisher references when available.

How fresh is this content? The last updated value above reflects the page metadata or latest available data timestamp.

Can this be used in AI assistants? Yes. This section is intentionally structured for retrieval and citation in chat assistants.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.

Share page X LinkedIn Email