Fix Airtable Expiring Attachment URLs - Get Permanent File Links
You stored an Airtable attachment URL in your app. Maybe you saved it in a database column, rendered it in a frontend template, or passed it along to another service. Everything worked. Then, a few hours later, the image is broken. The PDF link returns an error. The file your user uploaded is gone, not deleted from Airtable, just unreachable from the URL you saved.
If you have hit this, you are not alone. This is one of the most common complaints in the Airtable developer community. Airtable changed how attachment URLs work in early 2024, and the result is that every attachment URL now expires within a couple of hours. If your application depends on these URLs, and most Airtable-powered apps do, you have a ticking clock on every single file link.
The good news: there is a clean fix. You download the attachment while the URL is still valid, re-upload it to a permanent file host, and use the permanent URL everywhere instead. This guide walks through the problem in detail, gives you a working Python script, and shows you how to automate the entire process with Make, Zapier, and n8n.
Why Airtable Attachment URLs Expire
Before February 2024, Airtable attachment URLs were static. You could grab the URL from the API response, store it anywhere, and it would work indefinitely. Developers built entire applications around this behavior: image galleries pulling from Airtable, document portals, client-facing dashboards with uploaded files.
Then Airtable switched to signed URLs. A signed URL includes a temporary authentication token as a query parameter. It looks something like this:
https://v5.airtableusercontent.com/v3/u/33/33/someTimestamp/someToken/photo.jpg
That token is valid for roughly two hours. After that, the URL returns an authorization error. The file still exists in your Airtable base, you can see it in the Airtable UI, but the URL you stored in your code no longer works.
Airtable made this change as a security measure. Signed URLs ensure that only authenticated requests can access attachments, preventing someone from guessing or scraping file URLs and downloading private data. It is a reasonable security decision, but it fundamentally breaks any workflow that treats Airtable attachment URLs as permanent links.
The impact is significant:
- Cached URLs break. If your app stores attachment URLs in a database, cache, or local storage, those links go dead within hours.
- Embedded images disappear. Any image tag pointing to an Airtable attachment URL will show a broken image after the URL expires.
- Shared links stop working. If you sent an Airtable attachment URL to a client, partner, or external system, it will fail when they try to access it later.
- Webhook payloads become stale. If you process Airtable webhooks asynchronously, the attachment URLs in the payload may already be expired by the time your worker picks them up.
The official Airtable recommendation is to fetch a fresh URL from the API every time you need to access an attachment. For some use cases, that works. But if you are rendering images on a public webpage, embedding files in emails, or passing URLs to third-party services that will access them later, fetching a fresh URL on every request is impractical or impossible.
The Fix: Permanent CDN URLs with FilePost
The solution is straightforward: download the Airtable attachment while the signed URL is still valid, upload it to a permanent file hosting service, and use the permanent URL going forward.
FilePost is a file upload API built for exactly this kind of use case. You POST a file, you get back a permanent CDN URL. The URL does not expire. The file is stored on Cloudflare's CDN, so it loads fast globally. You can embed it, share it, store it in a database, and it will keep working.
Here is the basic flow with curl:
# Upload a file to FilePost
curl -X POST https://filepost.dev/v1/upload \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@photo.jpg"
Response:
{
"url": "https://cdn.filepost.dev/abc123/photo.jpg",
"file_id": "abc123",
"name": "photo.jpg",
"size": 45321
}
That url field is your permanent link. It is served from a CDN, it does not expire, and it works anywhere a URL works: HTML image tags, API responses, email bodies, PDFs, mobile apps, you name it.
The process for fixing Airtable URLs is:
- Read the Airtable record to get the current (temporary) attachment URL
- Download the file from that URL before it expires
- Upload the file to FilePost to get a permanent CDN URL
- Store the permanent URL back in your Airtable record (in a separate text field) or in your application's database
You can do this as a one-time batch migration for existing records, or set it up as an automation that runs every time a new attachment is added. Let's start with the batch approach.
Python Script: Batch Fix Airtable Attachments
This script uses the pyairtable library to iterate through every record in an Airtable table, download any attachments, upload them to FilePost, and write the permanent URLs back to the record. It is a complete, working script you can run as-is after filling in your credentials.
import requests
from pyairtable import Api
# --- Configuration ---
AIRTABLE_TOKEN = "your_airtable_personal_access_token"
AIRTABLE_BASE_ID = "appXXXXXXXXXXXXXX"
AIRTABLE_TABLE_NAME = "Your Table Name"
# The field name that contains Airtable attachments
ATTACHMENT_FIELD = "Photos"
# A text/URL field where we will store the permanent URLs
PERMANENT_URL_FIELD = "Permanent URLs"
FILEPOST_API_KEY = "your_filepost_api_key"
FILEPOST_UPLOAD_URL = "https://filepost.dev/v1/upload"
# --- Setup ---
api = Api(AIRTABLE_TOKEN)
table = api.table(AIRTABLE_BASE_ID, AIRTABLE_TABLE_NAME)
def upload_to_filepost(file_bytes, filename):
"""Upload a file to FilePost and return the permanent CDN URL."""
response = requests.post(
FILEPOST_UPLOAD_URL,
headers={"X-API-Key": FILEPOST_API_KEY},
files={"file": (filename, file_bytes)},
)
response.raise_for_status()
return response.json()["url"]
def download_file(url):
"""Download a file from a URL and return the bytes."""
response = requests.get(url)
response.raise_for_status()
return response.content
def process_record(record):
"""Process a single Airtable record: download attachments, upload
to FilePost, and update the record with permanent URLs."""
record_id = record["id"]
fields = record["fields"]
attachments = fields.get(ATTACHMENT_FIELD, [])
if not attachments:
return
# Skip if we already have permanent URLs for this record
existing_urls = fields.get(PERMANENT_URL_FIELD, "")
if existing_urls:
print(f" Skipping {record_id} - already has permanent URLs")
return
permanent_urls = []
for attachment in attachments:
filename = attachment["filename"]
temp_url = attachment["url"]
print(f" Downloading: {filename}")
file_bytes = download_file(temp_url)
print(f" Uploading to FilePost: {filename}")
permanent_url = upload_to_filepost(file_bytes, filename)
permanent_urls.append(permanent_url)
print(f" Permanent URL: {permanent_url}")
# Write the permanent URLs back to Airtable as a comma-separated list
table.update(record_id, {PERMANENT_URL_FIELD: ", ".join(permanent_urls)})
print(f" Updated record {record_id} with {len(permanent_urls)} permanent URL(s)")
def main():
print("Fetching records from Airtable...")
records = table.all()
print(f"Found {len(records)} records")
for i, record in enumerate(records, 1):
print(f"\nProcessing record {i}/{len(records)}: {record['id']}")
try:
process_record(record)
except Exception as e:
print(f" Error processing {record['id']}: {e}")
continue
print("\nDone. All attachments have been migrated to permanent URLs.")
if __name__ == "__main__":
main()
Before running this script, install the dependencies:
pip install pyairtable requests
A few notes on the script:
- It skips records that already have permanent URLs. This makes it safe to run multiple times. If the script fails halfway through, just run it again and it will pick up where it left off.
- It stores URLs as a comma-separated string. For records with multiple attachments, all permanent URLs are stored in a single text field. You could also use a Long Text field or store them as JSON.
- Run it promptly. Since Airtable URLs expire in about two hours, the script needs to download each file before its URL expires. For large tables, this is usually not a problem because the script fetches fresh URLs from the API as it iterates. But if you have thousands of records, consider processing in batches.
Automate with Make (Integromat)
If you want every new Airtable attachment to automatically get a permanent URL, without running a script, Make is one of the easiest tools for the job. Here is the workflow step by step.
Step 1: Watch Records Trigger
Add an Airtable - Watch Records module. Configure it to watch your table and trigger when new records are created or when the attachment field is updated. Set the trigger to poll every 5 or 15 minutes depending on how quickly you need permanent URLs generated.
Step 2: Download the Attachment
Add an HTTP - Get a File module. Set the URL to the Airtable attachment URL from the trigger output. In Make, this is typically mapped as {{1.Photos[].url}} (replace "Photos" with your attachment field name). This module downloads the file into Make's internal binary storage.
If a record has multiple attachments, use an Iterator module between the trigger and the download step to process each attachment individually.
Step 3: Upload to FilePost
Add an HTTP - Make a Request module with the following configuration:
- URL:
https://filepost.dev/v1/upload - Method: POST
- Headers:
X-API-Key= your FilePost API key - Body type: Multipart/form-data
- Fields: Add a field named
file, set it to File, and map the binary data from the previous HTTP module
The response will contain the permanent CDN URL in the url field.
Step 4: Update the Airtable Record
Add an Airtable - Update a Record module. Set the Record ID to the record from the trigger, and update your "Permanent URL" text field with the value from the FilePost response: {{3.data.url}} (adjust the module number to match your scenario).
Once this scenario is active, every new attachment in your Airtable base will automatically get a permanent CDN URL within minutes.
Automate with Zapier
Zapier supports a similar workflow. The main difference is that Zapier handles file downloads differently: it uses a built-in "Hydrate" feature for file URLs rather than a separate download step.
Step 1: Trigger - New or Updated Record in Airtable
Choose the Airtable - New or Updated Record trigger. Select your base, table, and configure the trigger to watch for changes to the attachment field. Zapier will poll for new and updated records automatically.
Step 2: Action - Upload File via Webhooks by Zapier
Since FilePost does not have a native Zapier integration yet, use the Webhooks by Zapier - POST action. Configure it as follows:
- URL:
https://filepost.dev/v1/upload - Payload Type: Form
- Headers:
X-API-Key | your_filepost_api_key - File: Map the Airtable attachment URL. Zapier will download the file from the Airtable URL and forward it in the POST request.
Zapier's file handling will take care of downloading the attachment from the temporary Airtable URL and sending it as a multipart upload to FilePost.
Step 3: Action - Update Record in Airtable
Add an Airtable - Update Record action. Use the record ID from the trigger step, and set your "Permanent URL" field to the url value from the FilePost webhook response.
Turn on the Zap and every new Airtable attachment will be mirrored to a permanent CDN URL. For records with multiple attachments, you may need to use Zapier's Looping feature or a Formatter step to process each attachment individually.
Fix Your Airtable URLs
Get permanent CDN URLs for all your Airtable attachments. Free plan includes 30 uploads/month.
Get Your Free API KeyAutomate with n8n
n8n is the best option if you want full control over the automation logic, especially if you are self-hosting. Here is the workflow.
Step 1: Airtable Trigger
Add an Airtable Trigger node configured to poll your table for new or updated records. Set the Trigger Field to the field that changes when attachments are added (often a "Last Modified" formula field that references your attachment field). Set the poll interval to 5 minutes.
Step 2: Extract Attachment URLs
Airtable returns attachments as an array of objects. Add a Function node to extract the attachment URLs and filenames:
const attachments = $input.first().json.fields["Photos"] || [];
return attachments.map(att => ({
json: {
filename: att.filename,
url: att.url,
recordId: $input.first().json.id
}
}));
This outputs one item per attachment, so the rest of the workflow processes each file individually.
Step 3: Download the File
Add an HTTP Request node. Set the URL to {{ $json.url }} (the temporary Airtable attachment URL). Set the Response Format to File so n8n stores the downloaded content as binary data.
Step 4: Upload to FilePost
Add another HTTP Request node configured for the FilePost upload:
- Method: POST
- URL:
https://filepost.dev/v1/upload - Headers:
X-API-Key= your FilePost API key - Body Content Type: Multipart Form Data
- Send Binary Data: enabled
- Binary Property:
data - Parameter Name:
file
Alternatively, if you have installed the n8n-nodes-filepost community node, you can use the native FilePost node with the Upload File operation. It handles all of the above configuration automatically.
Step 5: Update Airtable Record
Add an Airtable node with the Update operation. Set the Record ID to {{ $json.recordId }} (passed through from the Function node), and set your "Permanent URL" field to {{ $json.url }} from the FilePost response.
Activate the workflow and it will run continuously, converting every new Airtable attachment into a permanent CDN URL. Since n8n can be self-hosted, this is also a good option if you have data residency requirements or want to avoid sending files through third-party automation services.
Best Practices for Managing Airtable Attachments
Whichever method you choose, whether the Python script, Make, Zapier, or n8n, here are a few tips to keep things running smoothly:
- Use a separate field for permanent URLs. Do not overwrite the Airtable attachment field. Keep the original attachment in Airtable (it is still useful for viewing files in the Airtable UI) and store the permanent FilePost URL in a dedicated text or URL field. Your application code should read from the permanent URL field instead of the attachment field.
- Process attachments immediately. Airtable URLs expire in roughly two hours. If you are using a polling trigger, set a short interval (5 minutes) to make sure you download the file before its URL expires. If you are processing webhooks asynchronously, handle them promptly.
- Handle multiple attachments per record. An Airtable attachment field can hold multiple files. Your automation needs to iterate through all of them and store all the permanent URLs. The Python script above handles this by storing a comma-separated list. In Make or n8n, use an iterator or split-out node.
- Add error handling. If a download fails because the URL already expired, log the error and flag the record for manual review. Do not silently skip it. In the Python script, the try/except block catches errors per record so one failure does not stop the entire batch.
- Manage your FilePost plan limits. The free tier gives you 30 uploads per month. If you have a large Airtable base with thousands of attachments, you may want to start with the Starter plan (500 uploads/month, $9/mo) or Pro plan (5,000 uploads/month, $29/mo) for the initial migration, then drop to a smaller plan for ongoing new attachments.
Frequently Asked Questions
Why do Airtable attachment URLs expire?
Airtable uses signed URLs for attachment access. These URLs contain a temporary authentication token that expires after approximately two hours. This is a security measure Airtable implemented in early 2024 to prevent unauthorized access to files. Before this change, Airtable attachment URLs were permanent, but the new behavior means any URL you store in your app, database, or frontend will stop working within hours.
How do I get permanent URLs for Airtable files?
Download the Airtable attachment using its temporary URL, then re-upload it to a permanent file hosting service like FilePost. FilePost returns a CDN-backed URL that never expires. You can do this manually with a Python script, or automate it with Make, Zapier, or n8n so every new Airtable attachment automatically gets a permanent URL.
Can I automate fixing Airtable expiring URLs?
Yes. You can set up automations in Make (Integromat), Zapier, or n8n that trigger whenever an Airtable record is created or updated. The automation downloads the attachment using the temporary URL, uploads it to FilePost to get a permanent CDN URL, and writes the permanent URL back to a field in your Airtable record. This runs automatically for every new attachment.
Does FilePost work with Airtable?
Yes. FilePost is a file upload API that accepts any file via a simple POST request and returns a permanent CDN URL. You can use it with Airtable by downloading attachments from Airtable's temporary URLs and re-uploading them to FilePost. This works via direct API calls, Python scripts, or no-code automation tools like Make, Zapier, and n8n.
Stop Fixing Broken Links
Upload once, get a permanent URL. Works with any Airtable workflow. No credit card required.
Get Your Free API Key