This Python script fetches ad account activity logs from Meta's (Facebook) Marketing API and exports them to Google Sheets. It captures all account changes, modifications, and events within a specified timeframe for audit and tracking purposes.
- Requirements
- Configuration
- Features
- Script Workflow
- Functions Reference
- Meta API Integration
- Google Sheets Integration
- Data Fields
- Error Handling
- Usage Examples
pip install requests gspread oauth2clientRequired Libraries:
requests- HTTP requests to Meta APIgspread- Google Sheets API wrapperoauth2client- Google OAuth authenticationjson- JSON data handlingdatetime- Date/time operations
- Meta Business Account with ad account access
- Meta App with Marketing API permissions
- Access Token with
ads_readpermission - Google Cloud Platform service account with Sheets API enabled
access_token = '' # Your Meta access token
ad_account_id = '' # Format: act_XXXXXXXXXX
meta_url = f"https://graph.facebook.com/v20.0/{ad_account_id}/activities"Getting Access Token:
- Go to Meta for Developers
- Create or select your app
- Navigate to Tools > Graph API Explorer
- Generate token with
ads_readpermission - Copy the access token
Ad Account ID Format:
- Find in Meta Ads Manager URL
- Format:
act_1234567890123456 - Include the
act_prefix
SERVICE_ACCOUNT_FILE = '' # Path to JSON key file
SPREADSHEET_ID = '' # From Google Sheets URL
SHEET_NAME = 'Sheet1' # Target worksheet nameGoogle Sheets ID Location:
https://docs.google.com/spreadsheets/d/[SPREADSHEET_ID]/edit
- Captures all ad account changes
- Records user actions and modifications
- Tracks campaign, ad set, and ad changes
- Documents budget adjustments
- Logs status changes (pause/resume)
- Automatically handles paginated responses
- Fetches all activities in date range
- No manual intervention required
- Actor information (who made changes)
- Application details (where changes originated)
- Timestamps (when changes occurred)
- Object information (what was changed)
- Event type classification
- Clears previous data
- Writes structured data with headers
- Single sheet update operation
- Ready for analysis and reporting
- Prints request URLs
- Shows response status codes
- Displays API responses
- Helpful for troubleshooting
1. Define Time Range
↓
2. Call get_ad_activities()
├─ Build API request
├─ Fetch first page
├─ Loop through pagination
└─ Collect all activities
↓
3. Call process_ad_activities()
├─ Create headers
├─ Extract activity fields
└─ Format as 2D array
↓
4. Call write_to_google_sheets()
├─ Authenticate
├─ Clear existing data
└─ Write new data
↓
5. Complete
Fetches ad activity logs from Meta API.
Parameters:
start_time(datetime): Start of date rangeend_time(datetime): End of date range
Returns:
list: Array of activity dictionaries
Process:
- Converts datetime to ISO 8601 format
- Builds API request with parameters
- Handles pagination automatically
- Collects all activities across pages
- Returns complete activity list
API Request Example:
params = {
'access_token': 'YOUR_TOKEN',
'since': '2024-06-08T00:00:00',
'until': '2024-06-07T23:59:59',
'fields': 'actor_id,actor_name,...'
}Pagination Handling:
while url:
response = requests.get(url, params=params)
data = response.json()
all_activities.extend(data.get('data', []))
url = data.get('paging', {}).get('next') # Get next page URLConverts raw API data to spreadsheet format.
Parameters:
ad_activities(list): Raw activity data from API
Returns:
list: 2D array ready for Google Sheets
Process:
- Creates header row
- Iterates through each activity
- Extracts relevant fields
- Builds data rows
- Returns formatted array
Output Structure:
[
['Actor ID', 'Actor Name', ...], # Header row
['123456', 'John Doe', ...], # Data row 1
['789012', 'Jane Smith', ...] # Data row 2
]Writes data to Google Sheets.
Parameters:
data(list): 2D array of data to write
Process:
- Authenticates with Google API
- Opens target spreadsheet
- Clears existing content
- Writes new data starting at A1
Authentication:
scope = [
'https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive'
]
creds = ServiceAccountCredentials.from_json_keyfile_name(
SERVICE_ACCOUNT_FILE,
scope
)
client = gspread.authorize(creds)Main execution function.
Parameters:
start_time(datetime): Report start date/timeend_time(datetime): Report end date/time
Workflow:
- Fetches ad activities
- Processes data
- Writes to Google Sheets
Current: v20.0
meta_url = "https://graph.facebook.com/v20.0/{ad_account_id}/activities"Note: Update version number as Meta releases new API versions.
Activities Endpoint:
GET /{ad-account-id}/activities
Purpose: Returns activity history for the ad account.
fields = [
'actor_id', # User/app ID that performed action
'actor_name', # Name of actor
'application_id', # App ID if action via app
'application_name', # App name
'date_time_in_timezone', # Localized timestamp
'event_time', # Unix timestamp
'event_type', # Type of event
'extra_data', # Additional event data
'object_id', # ID of modified object
'object_name', # Name of modified object
'object_type', # Type (campaign, ad set, ad)
'translated_event_type' # Human-readable event type
]| Event Type | Description |
|---|---|
create |
New object created |
update |
Object modified |
delete |
Object deleted |
pause |
Object paused |
resume |
Object resumed |
budget_change |
Budget modified |
bid_change |
Bid strategy changed |
status_change |
Status modified |
Meta API Rate Limits:
- Standard: 200 calls per hour per user
- App-level: Varies by app tier
- Account-level: Based on ad spend
Best Practices:
- Limit time ranges to reduce API calls
- Use pagination efficiently
- Cache results when possible
- Handle rate limit errors gracefully
-
Create Service Account:
- Go to Google Cloud Console
- Enable Google Sheets API
- Create service account
- Download JSON key
-
Share Spreadsheet:
- Open target Google Sheet
- Click "Share"
- Add service account email
- Grant "Editor" access
-
Configure Script:
SERVICE_ACCOUNT_FILE = 'path/to/service-account.json'
SPREADSHEET_ID = 'your_spreadsheet_id'
SHEET_NAME = 'Sheet1'Clear and Replace:
sheet.clear() # Remove all existing data
sheet.update('A1', data) # Write from cell A1Alternative - Append:
# If you want to preserve existing data
sheet.append_rows(data, value_input_option='RAW')| Column | Description | Example |
|---|---|---|
| Actor ID | User or app ID that made change | 1234567890 |
| Actor Name | Name of person/app | John Doe |
| Application ID | ID of app used (if applicable) | 9876543210 |
| Application Name | Name of application | Meta Business Suite |
| Date/Time in Timezone | Localized timestamp | 2024-06-08 14:30:45 PDT |
| Event Time | Unix timestamp | 1686248445 |
| Event Type | Action performed | update |
| Extra Data | Additional JSON details | {"old_value": "100", "new_value": "150"} |
| Object ID | ID of changed object | 23848234234 |
| Object Name | Name of object | Summer Sale Campaign |
| Object Type | Type of object | campaign |
| Translated Event Type | Human-readable type | Campaign Updated |
Error Detection:
if response.status_code != 200:
print(f"Error: {response.content.decode()}")
response.raise_for_status()Common HTTP Errors:
| Status Code | Meaning | Solution |
|---|---|---|
400 |
Bad Request | Check parameters and date format |
401 |
Unauthorized | Verify access token is valid |
403 |
Forbidden | Check permissions on ad account |
404 |
Not Found | Verify ad account ID is correct |
429 |
Rate Limited | Implement retry with backoff |
500 |
Server Error | Retry request after delay |
Invalid Token:
{
"error": {
"message": "Invalid OAuth access token",
"type": "OAuthException",
"code": 190
}
}Solution: Generate new access token from Meta for Developers.
Common Issues:
- Permission Denied: Service account needs editor access
- Spreadsheet Not Found: Check SPREADSHEET_ID
- Worksheet Not Found: Verify SHEET_NAME exists
- Quota Exceeded: Check Google API quotas
from datetime import datetime, timedelta
start_time = datetime.now() - timedelta(days=7)
end_time = datetime.now()
main(start_time, end_time)start_time = datetime(2024, 6, 1) # June 1, 2024
end_time = datetime(2024, 6, 30) # June 30, 2024
main(start_time, end_time)yesterday = datetime.now() - timedelta(days=1)
start_time = yesterday.replace(hour=0, minute=0, second=0)
end_time = yesterday.replace(hour=23, minute=59, second=59)
main(start_time, end_time)from datetime import datetime
now = datetime.now()
start_time = now.replace(day=1, hour=0, minute=0, second=0)
end_time = now
main(start_time, end_time)Current Code:
start_time = datetime(2024, 6, 8)
end_time = datetime(2024, 6, 7) # ERROR: End before start!This is incorrect! End time should be after start time.
Corrected:
start_time = datetime(2024, 6, 7) # June 7
end_time = datetime(2024, 6, 8) # June 8ISO 8601 Format:
start_time.isoformat()
# Returns: '2024-06-08T00:00:00'With Timezone:
from datetime import timezone
start_time = datetime(2024, 6, 8, tzinfo=timezone.utc)# Run daily at 1 AM
0 1 * * * /usr/bin/python3 /path/to/meta_activities.py- Open Task Scheduler
- Create Basic Task
- Trigger: Daily at 1:00 AM
- Action: Start a program
- Program:
python.exe - Arguments:
C:\path\to\meta_activities.py
- Program:
if __name__ == "__main__":
# Yesterday's activities
yesterday = datetime.now() - timedelta(days=1)
start_time = yesterday.replace(hour=0, minute=0, second=0)
end_time = yesterday.replace(hour=23, minute=59, second=59)
main(start_time, end_time)# Use environment variables
import os
access_token = os.getenv('META_ACCESS_TOKEN')- Store outside project directory
- Use restrictive file permissions (600)
- Never commit to version control
- Rotate periodically
# Service account keys
*.json
# Environment files
.env
The script already includes debugging output:
print(f"Request URL: {response.url}")
print(f"Response Status Code: {response.status_code}")
print(f"Response Content: {response.content.decode()}")# Test basic API access
response = requests.get(
f"https://graph.facebook.com/v20.0/me",
params={'access_token': access_token}
)
print(response.json())# Check ad account permissions
response = requests.get(
f"https://graph.facebook.com/v20.0/{ad_account_id}",
params={
'access_token': access_token,
'fields': 'id,name,account_status'
}
)
print(response.json())Possible Causes:
- No activities in date range
- Incorrect date format
- Permissions issue
- Wrong ad account ID
Solution:
# Add debug output
activities = get_ad_activities(start_time, end_time)
print(f"Total activities found: {len(activities)}")Check:
- Service account has editor access
- Sheet name matches exactly (case-sensitive)
- Spreadsheet ID is correct
- No API quota issues
Proprietary - For use with Meta Marketing API integration only.
For issues:
- Check debug output in console
- Verify access token validity (they expire)
- Confirm ad account ID format
- Test Google Sheets connection separately
- Review Meta API changelog for breaking changes