Airtable Attachment Public URL: Fix Expiring File Links
If you pull an attachment from the Airtable API, the URL looks public. You can paste it in a browser, download the image, and pass it to another service. Then a few hours later, the same URL breaks.
That is expected behavior. Airtable attachment download URLs are temporary. Airtable's own support docs say these direct download URLs expire after a short time and should not be used as long-lived public file links. If you need a stable URL, the file needs to be copied somewhere else.
The pattern
- Read the attachment URL from Airtable while it is still valid.
- Download the file bytes from that temporary URL.
- Upload those bytes to FilePost with
multipart/form-data. - Store the returned permanent CDN URL back in Airtable.
Why Airtable Attachment URLs Break
Airtable attachments have two useful URL shapes:
- Attachment viewer URLs. These open inside Airtable and require access to the base or interface.
- Attachment download URLs. These can be opened directly, but they expire after a short time.
The direct download URL is the one developers usually grab from the API. It often lives on an airtableusercontent.com domain. It is useful for immediate processing, but it is not a CDN URL and it is not meant to be stored forever.
Airtable currently says download URLs stay active for at least 2 hours after receiving them. That is enough for an automation step. It is not enough for a website, client portal, email campaign, invoice PDF, or database field that needs to keep working next week.
The Fix: Store Your Own Public URL
The clean fix is to add a second field in Airtable, usually a URL or long text field, and store a permanent hosted copy there.
Example fields:
| Field | Type | Purpose |
|---|---|---|
Attachment |
Attachment | The original file in Airtable |
Public URL |
URL or long text | The permanent CDN URL returned by FilePost |
FilePost ID |
Single line text | Optional file ID for later deletion or lookup |
Python Script: Airtable Attachment to Permanent URL
This script reads records from Airtable, downloads the first attachment in a field, uploads it to FilePost, and writes the public URL back to Airtable.
import os
import requests
AIRTABLE_TOKEN = os.environ["AIRTABLE_TOKEN"]
AIRTABLE_BASE_ID = os.environ["AIRTABLE_BASE_ID"]
AIRTABLE_TABLE = "Assets"
AIRTABLE_ATTACHMENT_FIELD = "Attachment"
AIRTABLE_PUBLIC_URL_FIELD = "Public URL"
FILEPOST_API_KEY = os.environ["FILEPOST_API_KEY"]
def airtable_headers():
return {
"Authorization": f"Bearer {AIRTABLE_TOKEN}",
"Content-Type": "application/json",
}
def list_records():
url = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}/{AIRTABLE_TABLE}"
response = requests.get(url, headers=airtable_headers(), timeout=30)
response.raise_for_status()
return response.json()["records"]
def download_attachment(attachment):
response = requests.get(attachment["url"], timeout=60)
response.raise_for_status()
return response.content, attachment.get("filename", "airtable-file")
def upload_to_filepost(file_bytes, filename):
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": FILEPOST_API_KEY},
files={"file": (filename, file_bytes)},
timeout=120,
)
response.raise_for_status()
return response.json()
def update_airtable_record(record_id, public_url, file_id):
url = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}/{AIRTABLE_TABLE}/{record_id}"
payload = {
"fields": {
AIRTABLE_PUBLIC_URL_FIELD: public_url,
"FilePost ID": file_id,
}
}
response = requests.patch(url, headers=airtable_headers(), json=payload, timeout=30)
response.raise_for_status()
def main():
for record in list_records():
fields = record.get("fields", {})
if fields.get(AIRTABLE_PUBLIC_URL_FIELD):
continue
attachments = fields.get(AIRTABLE_ATTACHMENT_FIELD, [])
if not attachments:
continue
file_bytes, filename = download_attachment(attachments[0])
uploaded = upload_to_filepost(file_bytes, filename)
update_airtable_record(
record["id"],
uploaded["url"],
uploaded["file_id"],
)
print(f"{record['id']} -> {uploaded['url']}")
if __name__ == "__main__":
main()
Environment Variables
export AIRTABLE_TOKEN="pat..."
export AIRTABLE_BASE_ID="app..."
export FILEPOST_API_KEY="fh_..."
Run the script soon after the Airtable URL is generated. If the download URL has already expired, request the record from Airtable again to get a fresh attachment URL.
Automation Version
The same flow works in Zapier, Make, n8n, or Pipedream:
- Trigger on a new or updated Airtable record.
- Use the attachment download URL from the trigger payload.
- Download the file into the automation step.
- Upload the binary file to FilePost.
- Update the Airtable record with the returned
url.
The important detail is timing. Treat the Airtable URL as an input to the workflow, not the final URL you store or send to users.
When This Is Worth Doing
This pattern is useful when Airtable is acting as the internal database, but another system needs a stable public file URL:
- A public website that renders images from Airtable records
- A client portal that links to PDFs uploaded by your team
- An email automation that includes file links
- A CRM or support tool that needs attachment URLs without Airtable login
- An n8n or Zapier workflow that passes file URLs to another API
If everyone opening the file already has Airtable access, an Airtable viewer URL may be enough. If the file has to work outside Airtable, copy it to a file host and store the hosted URL.
Turn Airtable files into permanent URLs
Upload the file to FilePost and store the returned CDN URL back in Airtable. Free plan includes 300 uploads per month.
Get Your API Key