đź“„ Automating PDF Downloads from Smartsheet Reports
Hi everyone,
I manage hundreds of documents generated across multiple sheets in Smartsheet. To keep things organized, I use reports to consolidate them — but I often get asked to ensure these documents are also backed up in SharePoint for redundancy.
Unfortunately, Smartsheet doesn’t currently offer a built-in way to selectively download only final PDFs from reports or sheets. If you try to download attachments from a row, you’ll get everything — images, intermediate files, and other miscellaneous items — not just the final deliverable.
In my case, each row might contain multiple files, but the one I care about is the final PDF generated via Document Builder. What I really needed was a way to filter attachments by file type (e.g., PDF) and download just those.
Since there’s no off-the-shelf solution for this, I built one.
🛠️ What I Built
I created a Python script that:
- Connects to a Smartsheet Report
- Identifies the source sheets and rows
- Filters attachments by file type (PDF only)
- Downloads them into a local folder
- Logs the process (including skipped rows and errors)
It’s been working great for me, and I thought I’d share it in case it helps others.
🚀 How to Use It
- Install Python (if you haven’t already)
- Replace the following in the script:
ACCESS_TOKEN
 → your Smartsheet API tokenREPORT_ID
 → your Smartsheet Report ID
- Run the script — it will download all PDFs from the report into a folder and generate a log file.
đź’ˇ Feature Suggestion for Smartsheet
It would be amazing if Smartsheet added a native feature to:
- Filter attachments by file type (e.g., PDF, DOCX, PNG)
- Download selected types directly from reports or sheets
Until then, feel free to use or adapt this script for your own workflows.
Let me know if you have questions or improvements — happy to collaborate!
****************************SCRIPT BELOW***************************************************
import smartsheet
import requests
import os
import time
from collections import defaultdict
ACCESS_TOKEN = 'YOUR API TOKEN'
REPORT_ID = 'YOUR REPORT ID'
DOWNLOAD_FOLDER = 'pdf_attachments'
LOG_FILE = 'download_log.txt'
os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
smartsheet_client = smartsheet.Smartsheet(ACCESS_TOKEN)
total_rows = 0
rows_with_attachments = 0
pdf_found = 0
pdf_downloaded = 0
skipped_rows = []
filename_counter = defaultdict(int)
def download_with_retry(url, file_path, attachment_name, log):
global pdf_downloaded
retries = 0
while retries < 2:
response = requests.get(url)
if response.status_code == 200:
with open(file_path, 'wb') as f:
f.write(response.content)
log.write(f"[Downloaded] {attachment_name}\n")
print(f"[Downloaded] {attachment_name}")
pdf_downloaded += 1
break
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
log.write(f"[Rate Limit] Retrying after {retry_after} seconds...\n")
print(f"[Rate Limit] Retrying after {retry_after} seconds...")
time.sleep(retry_after)
elif response.status_code == 502:
log.write(f"[502 Error] Bad Gateway while downloading {attachment_name}. Retrying in 10 seconds...\n")
print(f"[502 Error] Bad Gateway while downloading {attachment_name}. Retrying in 10 seconds...")
time.sleep(10)
retries += 1
else:
log.write(f"[Error] Failed to download: {attachment_name} (HTTP {response.status_code})\n")
print(f"[Error] Failed to download: {attachment_name} (HTTP {response.status_code})")
break
with open(LOG_FILE, 'w', encoding='utf-8') as log:
log.write("PDF Download Log\n================\n")
report = smartsheet_client.Reports.get_report(REPORT_ID)
for row in report.rows:
total_rows += 1
sheet_id = row.sheet_id
row_id = row.id
log.write(f"\nProcessing row {row_id} from sheet {sheet_id}\n")
print(f"Processing row {row_id} from sheet {sheet_id}")
try:
attachments = smartsheet_client.Attachments.list_row_attachments(sheet_id, row_id).data
except smartsheet.exceptions.ApiError as e:
status_code = getattr(e.response, 'status_code', None)
if status_code == 429:
retry_after = int(e.response.headers.get('Retry-After', 5))
log.write(f"[Rate Limit] Retrying after {retry_after} seconds...\n")
print(f"[Rate Limit] Retrying after {retry_after} seconds...")
time.sleep(retry_after)
try:
attachments = smartsheet_client.Attachments.list_row_attachments(sheet_id, row_id).data
except:
log.write(f"[Error] Failed again to list attachments for row {row_id} in sheet {sheet_id}\n")
print(f"[Error] Failed again to list attachments for row {row_id} in sheet {sheet_id}")
skipped_rows.append((sheet_id, row_id))
continue
elif status_code == 502:
log.write(f"[502 Error] Bad Gateway while listing attachments for row {row_id}. Retrying in 10 seconds...\n")
print(f"[502 Error] Bad Gateway while listing attachments for row {row_id}. Retrying in 10 seconds...")
time.sleep(10)
try:
attachments = smartsheet_client.Attachments.list_row_attachments(sheet_id, row_id).data
except:
log.write(f"[Error] Failed again to list attachments for row {row_id} in sheet {sheet_id}\n")
print(f"[Error] Failed again to list attachments for row {row_id} in sheet {sheet_id}")
skipped_rows.append((sheet_id, row_id))
continue
else:
log.write(f"[Error] Failed to list attachments for row {row_id} in sheet {sheet_id}\n")
print(f"[Error] Failed to list attachments for row {row_id} in sheet {sheet_id}")
skipped_rows.append((sheet_id, row_id))
continue
if attachments:
rows_with_attachments += 1
for attachment in attachments:
if attachment.attachment_type == 'FILE' and attachment.mime_type == 'application/pdf':
pdf_found += 1
try:
file_info = smartsheet_client.Attachments.get_attachment(sheet_id, attachment.id)
file_url = file_info.url
base_name, ext = os.path.splitext(attachment.name)
filename_counter[attachment.name] += 1
if filename_counter[attachment.name] > 1:
unique_name = f"{base_name}_{filename_counter[attachment.name]}{ext}"
else:
unique_name = attachment.name
file_path = os.path.join(DOWNLOAD_FOLDER, unique_name)
download_with_retry(file_url, file_path, unique_name, log)
except smartsheet.exceptions.ApiError as e:
status_code = getattr(e.response, 'status_code', None)
if status_code == 429:
retry_after = int(e.response.headers.get('Retry-After', 5))
log.write(f"[Rate Limit] Retrying after {retry_after} seconds...\n")
print(f"[Rate Limit] Retrying after {retry_after} seconds...")