Keep your content plan in Airtable and post from it: a script in the Scripting extension sends a record's video and caption to Reelspan, then writes the request_id and the results back to the record. There is no Reelspan app for Airtable; the script calls the REST API.
Set up the table
Create a table called Posts with these fields:
| Field | Type | Holds |
|---|---|---|
Caption |
Long text | The caption (YouTube uses it as the title) |
Video |
Attachment | The video file |
Networks |
Multiple select | Network ids: tiktok, youtube, instagram, x, sandbox… |
Profile |
Single line text | The Reelspan profile, for example my-brand |
Request ID |
Single line text | Filled in by the script |
Status |
Single line text | Filled in by the script |
The multiple-select options must be Reelspan's network ids exactly; see Networks. Add sandbox to try the flow without a connected account.
Add the script
- Create an API key under Developers → API keys. Use a test key (
rs_test_…) while you set this up: posts go through every check but nothing reaches the networks. - In the base, open Extensions, add Scripting, and paste the script below.
- Open the script's settings and paste the key into Reelspan API key.
const settings = input.config({
title: 'Post to Reelspan',
description: 'Publishes a record’s video to the networks in its Networks field.',
items: [input.config.text('apiKey', { label: 'Reelspan API key' })],
});
const table = base.getTable('Posts');
const record = await input.recordAsync('Pick the post to publish', table);
if (!record) throw new Error('No record picked');
const video = record.getCellValue('Video')?.[0];
const networks = (record.getCellValue('Networks') ?? []).map((n) => n.name);
if (!video || networks.length === 0) throw new Error('The record needs a video and at least one network');
// the video endpoint takes multipart/form-data; Airtable's remote fetch can't send FormData, so build the body by hand
const fields = [
['user', record.getCellValueAsString('Profile')],
...networks.map((n) => ['platform[]', n]),
['title', record.getCellValueAsString('Caption')],
['video', video.url],
['async_upload', 'true'],
];
const boundary = `reelspan${Date.now()}`;
const body =
fields.map(([name, value]) => `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`).join('') +
`--${boundary}--\r\n`;
const res = await remoteFetchAsync('https://api.reelspan.com/v1/posts/video', {
method: 'POST',
headers: {
Authorization: `Bearer ${settings.apiKey}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Idempotency-Key': `airtable-${record.id}`,
},
body,
});
const json = await res.json();
if (!res.ok) {
await table.updateRecordAsync(record, { Status: `Error: ${json.message}` });
throw new Error(json.message);
}
await table.updateRecordAsync(record, { 'Request ID': json.request_id, Status: 'Posting' });
output.markdown(`Sent to ${networks.join(', ')}. Request ID: \`${json.request_id}\``);Select Run, pick a record, and the script fills in Request ID and Status.
A few things the script relies on:
remoteFetchAsyncsends the request from Airtable's servers. A plainfetchfrom the extension runs in your browser, and the Reelspan API doesn't accept browser requests from other sites.- The video goes as a URL. The script sends the attachment's URL in the
videofield and Reelspan downloads the file from there, so nothing is uploaded from your computer. Idempotency-Keyis the record's ID, so running the script twice on the same record never posts twice. See Retrying safely.
Other fields from Post a video, such as youtube_title or scheduled_date, go in as more [name, value] pairs. For text-only posts, send JSON to /v1/posts/text instead; see Post text.
Check the results
Add a second script that reads the status of the picked record:
const settings = input.config({
title: 'Check Reelspan status',
items: [input.config.text('apiKey', { label: 'Reelspan API key' })],
});
const table = base.getTable('Posts');
const record = await input.recordAsync('Pick the post to check', table);
const requestId = record?.getCellValueAsString('Request ID');
if (!requestId) throw new Error('This record has no Request ID yet');
const res = await remoteFetchAsync(`https://api.reelspan.com/v1/posts/status?request_id=${encodeURIComponent(requestId)}`, {
headers: { Authorization: `Bearer ${settings.apiKey}` },
});
const json = await res.json();
if (!res.ok) throw new Error(json.message);
const lines = json.results.map((r) => `${r.platform}: ${r.success ? r.post_url ?? 'done' : `failed (${r.error_message})`}`);
await table.updateRecordAsync(record, { Status: `${json.status} (${json.completed}/${json.total})` });
output.markdown(lines.join('\n\n') || 'No network has finished yet.');status is pending or in_progress while networks are still working, then completed (at least one network published) or failed. Only networks that have finished appear in results. With a test key, finished posts have no post_url. See Post status.
To have results arrive on their own, set a webhook that points at an Airtable automation's When webhook received trigger, or at a tool such as n8n that updates the record.
Keep the key safe
Script settings are visible to everyone who can edit the base. Use a key with only the Publish and Read scopes, and one key per base, so you can revoke it without touching anything else. See Authentication.