Guide

Fix Google API 412 Errors (ETag, If-Match)

Why Google Calendar returns 412 Precondition Failed on stale ETags and what Gmail does instead. Exact answers for ETag, If-Match, If-None-Match, history.list, and syncToken across both Google APIs.

Written by Qasim Muhammad Staff SRE

Reviewed by Nick Barraclough

VerifiedCLI 3.1.1 · Gmail, Google Calendar · last tested April 26, 2026

Exact answers to the top Calendar and Gmail API queries

Google Calendar and Gmail handle conditional requests differently, and developers searching for ETag or If-Match answers often land on generic HTTP guides that don't reflect what each API actually documents. The following reference maps 7 common search queries to precise answers drawn from Google's own Calendar and Gmail API reference pages, not from general HTTP specs.

QueryShort answerSource
gmail api etagThe documented Gmail Message schema includes historyId but does not document an etag field.Message resource
gmail.users.messages.get if-none-matchThe users.messages.get method page documents path params and response shape, but it does not document If-None-Match or a 304 contract.users.messages.get
gmail api if-matchGmail's method docs do not document If-Match behavior for message or label endpoints, so do not rely on it as a Gmail-specific concurrency contract.Gmail REST reference
delete a label history.list gmailhistory.list documents label changes on messages, not a label resource deleted event. Refresh users.labels.list after deleting custom labels.history.list
google calendar api etag if-match 412Calendar's events.update and events.patch document If-Match and return 412 Precondition Failed when the ETag you send is stale. The Event resource documents an etag field.events.update
calendar api etagYes — the Event resource exposes a documented etag property you can pass back in If-Match for optimistic concurrency.Event resource
calendar api incremental syncCalendar uses syncToken on events.list, not Gmail's historyId. A 410 Gone response means the token expired and you need a full sync.Calendar sync guide

Calendar API: If-Match and 412 are documented

Google Calendar is the one Google Workspace API that fully documents optimistic concurrency via ETags. The Event resource exposes a documented etag property, and both events.update and events.patch accept the If-Match request header. When the ETag you send no longer matches the current resource version, the API returns 412 Precondition Failed — a documented contract, not undocumented behavior.

According to Google's Calendar API reference, a 412 means you fetched an event, modified it, and called events.patch with If-Match: <old-etag> while another client updated the same event. The Calendar API refuses to clobber the newer state. In apps with 2 or more concurrent editors, this scenario is common enough that retry logic should be built into any Calendar integration.

# 1. GET the event - the etag is part of the documented response body
curl -H "Authorization: Bearer $TOKEN" \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID" \
  | jq '{id, summary, etag, updated}'

# 2. PATCH with If-Match: <etag> for optimistic concurrency
curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "If-Match: $ETAG" \
  -H "Content-Type: application/json" \
  -d '{"summary": "Renamed event"}' \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID"

# Possible response statuses:
#   200 OK                    - your update won; a new etag is returned
#   412 Precondition Failed   - your etag is stale; refetch and retry
#   404 Not Found             - the event was deleted in the meantime

Calendar also documents incremental sync via syncToken, returned on events.list. This mechanism is separate from Gmail's historyId / history.list approach. When a syncToken expires, the Calendar API returns 410 Gone and requires a full resync — Gmail's equivalent fallback returns 404 instead. Despite sharing the Google branding, the two APIs use different sync vocabularies; mixing them in client code leads to silent failures.

Does users.messages.get return an etag field?

No. The Gmail Message resource does not include a documented etag property in its JSON schema. Google's reference lists 17 documented fields on the Message object — id, threadId, labelIds, snippet, historyId, and others — but etag is not among them. This distinction matters because developers searching for "gmail api etag" often assume Gmail mirrors Calendar's ETag contract.

According to Google's Gmail API documentation, historyId is the officially supported change marker for Gmail messages. If your HTTP client exposes an ETag in the response headers, that header is separate from the documented resource schema and should not be treated as a stable concurrency token.

curl -H "Authorization: Bearer $TOKEN" \
  "https://gmail.googleapis.com/gmail/v1/users/me/messages/$MESSAGE_ID?format=metadata&fields=id,threadId,labelIds,historyId,snippet" \
  | jq

# Documented response fields look like:
# {
#   "id": "18c7...",
#   "threadId": "18c7...",
#   "labelIds": ["INBOX", "UNREAD"],
#   "historyId": "442991",
#   "snippet": "Preview text..."
# }

What Gmail documents about If-Match, If-None-Match, 304, and 412

Gmail's method-level reference pages do not document conditional request headers on the endpoints developers search for most. While RFC 7232 defines If-Match, If-None-Match, 304 Not Modified, and 412 Precondition Failed as standard HTTP semantics, Google's Gmail API reference for at least 4 key endpoints — users.messages.get, users.messages.modify, users.labels.patch, and users.labels.delete — omits conditional header behavior entirely.

  • If-None-Match is not documented on users.messages.get.
  • If-Match is not documented on the Gmail message or label methods most people search for.
  • 304 and 412 are not described as part of the Gmail contract on those method pages.

In other words: use generic HTTP documentation as background knowledge, but build Gmail sync and concurrency around the behaviors Google actually documents for Gmail.

How history.list actually relates to label deletion

Gmail's history.list endpoint tracks message-level label changes, not changes to the label catalog itself. According to Google's Gmail sync guide, callers store the latest historyId and pass it as startHistoryId on subsequent calls. The API returns 4 documented change buckets — messagesAdded, messagesDeleted, labelsAdded, and labelsRemoved — all scoped to individual messages, not to the label definitions themselves.

When a startHistoryId is too old or invalid, Gmail returns 404 and requires a full sync. This fallback threshold depends on server-side retention and is not published as a fixed window. The practical effect: history.list tells you when a label was added to or removed from a message, but it does not emit an event when a label definition is created or deleted from the catalog.

curl -H "Authorization: Bearer $TOKEN" \
  "https://gmail.googleapis.com/gmail/v1/users/me/history?startHistoryId=$START_HISTORY_ID" \
  | jq '.history[] | {id, messagesAdded, messagesDeleted, labelsAdded, labelsRemoved}'

# If START_HISTORY_ID is too old, Gmail returns HTTP 404.
# At that point, do a full sync and store a newer historyId.

What to do after deleting a custom Gmail label

After calling users.labels.delete to remove a custom label, the correct follow-up is to refresh the label catalog explicitly — not to poll history.list for a deletion event. Gmail's history reference documents 4 change bucket types, and none of them cover label-definition deletions. Any sync logic that waits for a history event after deleting a label will wait indefinitely.

  1. Delete the custom label with the Labels API.
  2. Refresh the label catalog with users.labels.list.
  3. Reconcile any missing label IDs in your local cache.
  4. Continue using history.list for message-level changes.

The users.labels.list endpoint returns all labels in a single response — Gmail accounts can have up to 500 user-created labels according to Google's documentation. Comparing the returned label IDs against your cached list identifies which labels were deleted.

# Refresh the current label catalog after deleting a custom label
curl -H "Authorization: Bearer $TOKEN" \
  "https://gmail.googleapis.com/gmail/v1/users/me/labels" \
  | jq '.labels[] | {id, name, type}'

# Compare the returned IDs to your cached label list.
# Missing IDs are deleted labels.

The safer architecture for both Google APIs

Building reliable sync against Google Calendar and Gmail requires following each API's documented contract separately. Calendar documents 3 conditional response codes (200, 412, 410) for its ETag and syncToken flows. Gmail documents historyId with a 404 fallback for incremental sync. Mixing the two contracts — or assuming one API mirrors the other — causes silent data loss and stale-state bugs that are difficult to reproduce.

  • Gmail: use historyId and history.list for incremental sync; expect a 404 fallback when startHistoryId ages out; re-fetch the label catalog separately when label definitions change; avoid depending on undocumented ETag semantics for correctness.
  • Calendar: use the etag in the Event resource and pass it back in If-Match for optimistic concurrency on events.update and events.patch; expect 412 and re-fetch on conflict; use syncToken on events.list for incremental sync; expect 410 Gone when a token expires.
  • Both: store the change marker (historyId or syncToken) per user, surface it on retry, and treat the documented failure code (404 or 410) as the "do a full sync" signal.

For teams that don't want to maintain separate sync logic per provider, the Nylas API normalizes mail and calendar operations so one integration pattern covers Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP — eliminating the need to hand-roll each provider's sync edge cases independently.

Next steps