Getting HttpError 400 when creating Gmail filter with Python

I’m working on a Python application to manage Gmail filters and running into issues when trying to create new filters. I can successfully read existing filters and labels, but creating new ones throws an error.

import os
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

def setup_gmail_filter():
    scopes = ['https://www.googleapis.com/auth/gmail.settings.basic']
    creds_path = 'client_credentials.json'
    token_path = 'gmail_token.json'

    creds = None
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, scopes)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            auth_flow = InstalledAppFlow.from_client_secrets_file(creds_path, scopes)
            creds = auth_flow.run_local_server(port=0)
        with open(token_path, 'w') as f:
            f.write(creds.to_json())

    gmail_service = build('gmail', 'v1', credentials=creds)
    new_filter = {
        'criteria': {'from': '[email protected]'}, 
        'action': {'removeLabelIds': ['SPAM']}
    }
    response = (
        gmail_service.users()
        .settings()
        .filters()
        .create(userId='me', body=new_filter)
        .execute()
    )
    return response

if __name__ == '__main__':
    setup_gmail_filter()

The error message says “Filter doesn’t have any actions” even though I’m specifying an action. Has anyone encountered this before? What’s the correct way to structure the filter action?

I ran into this exact same frustration about six months ago. The Gmail API documentation is pretty misleading on this point, but the real issue is that removeLabelIds alone doesn’t satisfy the API’s requirements for a complete action. What fixed it for me was adding markAsRead or markAsImportant to the action block. Your current filter structure is correct otherwise, just needs that extra action property. Try changing your action to something like ‘action’: {‘removeLabelIds’: [‘SPAM’], ‘markAsRead’: True} and it should go through without the 400 error. I also noticed that using removeLabelIds with system labels like SPAM can be finicky compared to custom labels you create yourself.

looks like removeLabelIds might not count as a “real” action for gmail api. try adding addLabelIds or forward/delete actions instead. i had similar issue and it worked when i used actual labels to add rather than just removing spam

The issue is that removeLabelIds with just ‘SPAM’ isn’t considered a valid action by the Gmail API. I encountered this exact problem last year when building a similar tool. You need to include at least one positive action alongside removal actions. Try modifying your filter to include addLabelIds with a custom label or add markAsRead: true to the action object. Something like 'action': {'removeLabelIds': ['SPAM'], 'markAsRead': true} should work. The API seems to require actions that actually do something beyond just removing default labels. Also make sure your OAuth scope includes gmail.modify if you plan to do more complex filter operations later.