Source: https://cli.nylas.com/guides/gmail-labels-api

# 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](https://cli.nylas.com/authors/qasim-muhammad) Staff SRE

Updated May 23, 2026

> **TL;DR:** The Gmail API manages labels through `users.labels` endpoints. Gmail has roughly a dozen system labels (INBOX, SENT, TRASH, etc.) and supports up to 500 user-created labels. The Nylas CLI maps labels to folders, so `nylas email folders list` returns both system and user labels.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of May 23, 2026.

Command references used in this guide: [`nylas email folders list`](https://cli.nylas.com/docs/commands/email-folders-list), [`nylas email folders create`](https://cli.nylas.com/docs/commands/email-folders-create), and [`nylas email list`](https://cli.nylas.com/docs/commands/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](https://developers.google.com/gmail/api/reference/rest/v1/users.labels), 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.

```python
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](https://developers.google.com/gmail/api/reference/rest/v1/users.labels). 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.

```python
# 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](https://developers.google.com/gmail/api/reference/rest/v1/users.labels)), 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.

```bash
# 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"
  done
```

The 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](https://cli.nylas.com/guides/gmail-api-search-query) — search syntax, operators, and filtering messages by label in queries
- [Gmail API pagination and sync](https://cli.nylas.com/guides/gmail-api-pagination-sync) — page through large label results and delta sync with history ID
- [List Gmail emails](https://cli.nylas.com/guides/list-gmail-emails) — fetch and filter Gmail messages from the terminal
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
