Guide
Gmail Labels API: Create and Manage
Gmail doesn't use folders. It uses labels, and a single message can carry multiple labels at once. The Gmail API exposes labels through the users.labels resource, which lets you create, list, update, and apply labels to messages programmatically. This guide covers the API approach with Python examples and the CLI alternative using folder commands.
Written by Qasim Muhammad Staff SRE
Command references used in this guide: nylas email folders list, nylas email folders create, and nylas email list.
What are Gmail labels and how does the API expose them?
A Gmail label is a tag attached to a message. Unlike traditional email folders, labels don't move messages to a separate location. A single message can have multiple labels simultaneously. The Gmail API represents labels as resources under users.labels, with each label having an id, name, type (system or user), and optional color properties. Gmail accounts start with 14 default system labels (the exact count varies by account configuration) and support up to 500 user-created labels.
According to the Gmail API documentation, labels fall into two categories: system labels (like INBOX, SENT, DRAFT, SPAM, TRASH) that can't be deleted or renamed, and user labels that you create and manage freely. System labels use uppercase IDs like INBOX. User labels get auto-generated IDs like Label_42. This distinction matters because the API rejects attempts to delete or rename system labels with a 400 error.
How do you list and create labels with the Gmail API?
Listing labels requires a GET request to users.labels.list. The response includes every label on the account, system and user alike. Creating a new label requires a POST to users.labels.create with a JSON body containing the label name and optional visibility settings. The API uses OAuth 2.0 with the gmail.labels scope, which is narrower than gmail.modify and doesn't grant access to message content.
The Python example below uses the google-api-python-client library (version 2.x), which handles OAuth token refresh and request serialization. Creating a label returns the new label object with its generated ID. The entire setup process takes about 15-20 minutes: creating a GCP project, enabling the Gmail API, configuring the OAuth consent screen, and downloading credentials. That's before writing any code.
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
# Assumes you have a valid OAuth token from the Google OAuth flow
creds = Credentials.from_authorized_user_file("token.json",
scopes=["https://www.googleapis.com/auth/gmail.labels"])
service = build("gmail", "v1", credentials=creds)
# List all labels
results = service.users().labels().list(userId="me").execute()
labels = results.get("labels", [])
for label in labels:
print(f"{label['id']:20s} {label['name']:30s} {label.get('type', 'user')}")
# Create a new label
new_label = service.users().labels().create(
userId="me",
body={
"name": "Project/Alpha",
"labelListVisibility": "labelShow",
"messageListVisibility": "show",
"color": {
"textColor": "#ffffff",
"backgroundColor": "#4a86e8"
}
}
).execute()
print(f"Created: {new_label['id']} — {new_label['name']}")Label names support forward slashes for nesting. Creating Project/Alpha displays as a nested label under Project in the Gmail web UI. The API supports a set of predefined color combinations for label backgrounds and text, documented in the users.labels reference. Custom hex values outside this set are rejected with a 400 error.
How do you apply and remove labels from messages?
Applying a label to a message is a POST to users.messages.modify with an addLabelIds array. Removing uses removeLabelIds. Both can be set in the same request, which is how Gmail implements “move to folder”: add the destination label and remove INBOX in one call. The modify endpoint requires the gmail.modify scope, which is broader than gmail.labels.
For batch operations, the Gmail API offers users.messages.batchModify, which applies label changes to up to 1,000 messages in a single request. This is 100x faster than individual modify calls when processing large mailboxes. A common pattern is searching for messages matching a query, collecting their IDs, and batch-modifying them. The quota cost is 50 units per batch call versus 5 units per individual call, according to the Gmail API usage limits.
# Apply a label to a single message
service.users().messages().modify(
userId="me",
id="MESSAGE_ID",
body={
"addLabelIds": ["Label_42"],
"removeLabelIds": ["INBOX"]
}
).execute()
# Batch apply a label to multiple messages (up to 1,000)
message_ids = ["msg_1", "msg_2", "msg_3"] # from a search result
service.users().messages().batchModify(
userId="me",
body={
"ids": message_ids,
"addLabelIds": ["Label_42"],
"removeLabelIds": []
}
).execute()
# Search for messages and label the results
results = service.users().messages().list(
userId="me",
q="from:billing@stripe.com after:2026/01/01"
).execute()
ids = [msg["id"] for msg in results.get("messages", [])]
if ids:
service.users().messages().batchModify(
userId="me",
body={"ids": ids, "addLabelIds": ["Label_billing"]}
).execute()
print(f"Labeled {len(ids)} messages")The batchModify endpoint is idempotent: applying a label that's already on a message doesn't produce an error or duplicate. Removing a label that isn't present is also a no-op. This makes it safe to run label-assignment scripts repeatedly without worrying about state management.
What is the difference between system and user labels?
System labels are built into every Gmail account and can't be created, deleted, or renamed through the API. Common system labels include INBOX, SENT, DRAFT, SPAM, TRASH, STARRED, IMPORTANT, UNREAD, CHAT, and the CATEGORY_* labels (PERSONAL, SOCIAL, UPDATES, FORUMS, PROMOTIONS). The exact set depends on account configuration. Each serves a specific function in Gmail's architecture.
User labels are the ones you create. They support nesting (via / in the name), custom colors, and visibility settings that control whether the label appears in the label list and message list. The API limits each account to 500 user labels. Attempting to create label 501 returns a 400 Bad Request error. The table below shows the key differences in API behavior.
| Behavior | System labels | User labels |
|---|---|---|
| ID format | Uppercase (INBOX, SENT) | Label_N (Label_42) |
| Create / delete | Not allowed (400 error) | Allowed (up to 500) |
| Rename | Not allowed | Allowed via update |
| Custom colors | No | Yes (preset combos) |
| Nesting | Fixed hierarchy | Via / separator |
A common mistake is treating the CATEGORY_* system labels like regular labels. These correspond to Gmail's tab categories (Primary, Social, Updates, Forums, Promotions). Messages can be in a category and have user labels simultaneously. Removing CATEGORY_PROMOTIONS from a message moves it to the Primary tab, which is a side effect that surprises developers who expected a simple label removal.
How do you manage Gmail labels from the CLI?
The Nylas CLI maps Gmail labels to its universal “folder” concept. Running nylas email folders list on a Gmail account returns both system and user labels with their IDs and names. Creating a folder on Gmail creates a user label. This abstraction means the same commands work on Gmail labels and Outlook folders without changing your script. No GCP project, no OAuth consent screen, no Python SDK. Authentication takes 2 minutes via nylas init.
The tradeoff is feature coverage. The API gives you a set of predefined color options (see the Label color reference), batch operations on 1,000 messages at once, and label visibility settings. The CLI gives you list, create, and JSON filtering. To find messages in a specific label, pipe nylas email list --json through jq and filter on the folders array. For advanced batch label management or color customization, the Gmail API is the right tool.
# List all Gmail labels (system + user) via the CLI
nylas email folders list
# List as JSON for scripting
nylas email folders list --json
# Create a new Gmail label
nylas email folders create "Project/Beta"
# Count messages in a specific label by filtering JSON output
nylas email list --json | jq '[.[] | select(.folders[] == "LABEL_ID")] | length'
# Count messages per label
nylas email folders list --json | \
jq -r '.[] | [.id, .name] | @tsv' | \
while IFS=$'\t' read -r id name; do
count=$(nylas email list --json 2>/dev/null | jq "[.[] | select(.folders[] == \"$id\")] | length")
printf "%-30s %s\n" "$name" "$count"
doneThe CLI approach shines when you need to work across providers. If you manage both Gmail and Outlook accounts, the folder commands abstract the difference between Gmail labels and Outlook folders behind one interface. The same nylas email folders list command returns a consistent JSON structure regardless of the underlying provider, which simplifies scripts that need to operate on multiple accounts.
Next steps
- Gmail API search queries — search syntax, operators, and filtering messages by label in queries
- Gmail API pagination and sync — page through large label results and delta sync with history ID
- List Gmail emails — fetch and filter Gmail messages from the terminal
- Full command reference — every flag and subcommand documented