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.
- 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), andSource(URL). - 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.... - 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.
- 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. - Map the fields. Add a Create record action, pick your table, and insert tokens from the trigger body into each column:
record_keyinto Key,headlineinto Headline,valueinto Value, and so on. To keep one row per release, use a Find records action on Key equal torecord_keyfollowed by Update record, and create the record only when nothing is found. - 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_id | text | Unique ID of the release event. Repeated deliveries of the same event carry the same ID. |
event | text | Always announcement. |
record_key | text | Stable key per release, currency:indicator:period. Use it to find and update an existing row. |
currency | text | Uppercase currency code, for example USD. |
indicator | text | Indicator slug, for example inflation. |
name | text | Display name of the indicator. |
period | date | Reference period of the observation, YYYY-MM-DD. |
value | number | Published value, for Number fields. |
value_text | text | Published value with its unit, for text fields. |
previous | number | Previous value, when one exists. |
previous_text | text | Previous value with its unit. |
unit | text | Unit of the series, for example %YoY. |
published_at | date and time | Official publication time in UTC, ISO 8601, for example 2026-09-11T12:30:00Z. |
latency_ms | number | Milliseconds from publication to capture when measured, otherwise empty. |
headline | text | One-line summary such as USD Inflation (CPI): 3.4 %YoY. |
source_url | URL | Official release page from the publisher. |
series_url | URL | FXMacroData 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.
- Create a table with a single line text field called
Keyplus the columns for your dataset from the table below. Columns you leave out are skipped. - Create an automation with the trigger At a scheduled time, for example every hour or every weekday morning.
- Add the action Run a script and paste the script below.
- On the left of the script editor, add the input variables listed below.
- Click Test, check the rows it wrote, and turn the automation on.
| Input variable | Value |
|---|---|
apiKey | Your FXMacroData API key. Leave empty for free USD data. |
dataset | indicator, forex, commodity, calendar, or cot |
currency | For indicator, calendar, and cot, for example usd |
indicator | For indicator, for example inflation |
base, quote | For forex, for example eur and usd |
commodity | For commodity, for example gold |
tableName | The table to write into |
rows | Optional. Most recent rows to read, default 20, maximum 100 |
| Dataset | Columns written |
|---|---|
indicator | Date, Value, Previous, Change, Released at (UTC) |
forex | Date, Rate |
commodity | Date, Value |
calendar | Release, Scheduled at (UTC) |
cot | Date, Non-commercial net, Open interest |
Find indicator slugs in the API reference.
automation-refresh.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.
- In your base, open Extensions, add the Scripting extension, and paste the script below.
- Open the extension settings and enter your FXMacroData API key, or leave it empty to import free USD data.
- 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
// 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.
- In Airtable, choose + Add or import, then Sync API, then Set up.
- Create a personal access token for that base with the
data.records:writeandschema.bases:writescopes. - Run the script once with
--sync-urlset to the endpoint in the cURL example Airtable shows. - Back in Airtable, pick the field types and choose
Keyas 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. - 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.