---
name: google-contacts-oauth-app
description: >-
  The Google Contacts / People API layer on top of the shared
  `google-cloud-console-oauth` skill — the People scope family and why none of
  it is restricted (no CASA assessment), the `contacts.readonly` / `contacts`
  least-privilege ladder, the "other contacts" corpus that explains why an app
  sees fewer people than the user does, Workspace directory people and the
  admin switch behind them, `personFields`/`readMask`, seven-day sync tokens,
  contact groups, and quota. Use when asked to get Google Contacts OAuth
  credentials, register or scope a People API app, decide between
  `contacts.readonly`, `contacts`, `contacts.other.readonly` and
  `directory.readonly`, judge whether a Contacts integration needs a security
  assessment, or explain why a connection returns almost no people. Read
  `google-cloud-console-oauth` first for the console mechanics; use the
  relevant product skill for other Google products.
---

# Google Contacts (People API) OAuth2 App Registration

The good news first, because it inverts the usual Google calculation: **no People API scope is restricted.** Whole-
address-book read and write are *sensitive* scopes — app verification, a justification, a demo video — and nothing
more. There is no CASA security assessment, no annual revalidation, no permitted-application-type gate. A Contacts
integration is one of the cheapest full-access Google integrations to get approved.

What costs time here instead is the **data model**. "Contacts" is not one corpus but three — saved contacts, "other
contacts", and the Workspace directory — each behind its own scope, its own method, and its own field mask. An app
that requests the obvious scope and calls the obvious method sees a fraction of the people the user sees in Gmail, and
the user reports it as a bug. That, the two-rung scope ladder, and seven-day sync tokens are what this file is for.
Everything else is shared.

## Built on: `google-cloud-console-oauth`

**Read `google-cloud-console-oauth` first, then this file.** It owns all the console mechanics, and this file does not
repeat them:

- Choosing or creating the Cloud project, enabling APIs, permissions, and what only a human can do in the console.
- Configuring the app on the Google Auth Platform (Branding, Audience, Data Access, Clients), creating a **Web
  application** client, and the redirect-URI rules with the platform's per-data-center callback list.
- The generic scope model: declaring every scope on Data Access, the non-sensitive / sensitive / restricted tiers, and
  the rule that the app's tier is its most sensitive scope.
- The client secret shown and downloadable **only once at creation**, and add-then-disable rotation.
- `Testing` vs `In production` — including the seven-day refresh-token expiry — the 100-user caps, verification, and
  the refresh-token rules (`access_type=offline`, `prompt=consent`, 100 tokens per account per client, six-month idle
  expiry).

If you are reading only this file, you are missing all of the above.

## Inputs to collect before you start

The base skill lists the console inputs. These are the Contacts-specific ones:

| Input | Notes |
| --- | --- |
| **Read-only or read-write?** | Decides `contacts.readonly` vs `contacts` — there is no narrower write scope (§1) |
| **Does the product need "other contacts"?** | A separate scope and separate methods, or the app sees far fewer people (§2, §3) |
| **Does it need the Workspace domain directory?** | A separate scope, Workspace-only, and an admin switch can turn it off (§2) |
| **Consumer Gmail users, one Workspace customer, or a multi-tenant connector?** | Directory features are meaningless for consumer accounts (§2) |
| **Contact groups: read, or also write?** | Group writes need the full `contacts` scope (§6) |
| **Incremental sync or full re-list each time?** | Sync tokens expire in seven days — decide the fallback now (§5) |
| **Expected contacts per user and sync frequency** | Quota is per-user and the full-sync quota cannot be raised (§7) |

## Quick Start

1. Work the base skill's console steps, and **enable the People API** on the project. A perfectly configured client
   against a project with the API disabled fails on the first call, not at authorization.
2. Settle the scope set before touching Data Access: §1 (the family and tiers), §2 (which corpus each unlocks).
3. Confirm with the owner that **sensitive-scope verification** is planned — brand, justifications, demo video. There
   is no security assessment to budget for (§1).
4. If the directory is in scope, confirm the customers are on Workspace and the admin sharing switch is on (§2).
5. Declare exactly the scopes you will request, and no more; request exactly what you declared.
6. Finish the base skill's capture, round-trip and handoff steps, adding the Contacts checks in §9.

## People API platform state (verified 2026-09-20 — re-verify before trusting)

- **The People API (`people.googleapis.com/v1`) is the only current API for a user's contacts.** The GData Contacts
  API v3 and its `https://www.google.com/m8/feeds` scope were **turned down on 19 January 2022** (§8).
- **No People API scope is on Google's restricted list.** That list covers Gmail, Drive, Fit, Chat, Data Portability,
  Photos Ambient and Health — checked scope by scope on this date. Contacts and directory scopes are **sensitive**:
  verification, yes; CASA, no.
- **Three corpora, three scopes**: saved contacts (`contacts` / `contacts.readonly`), "other contacts"
  (`contacts.other.readonly`), Workspace directory (`directory.readonly`). None implies another (§2).
- **`personFields` / `readMask` is required on every read.** Omitting it is a 400, not a default (§4).
- **Sync tokens expire seven days after the full sync**, with reason `EXPIRED_SYNC_TOKEN` (§5).
- **The first page of a full sync carries an extra quota that is fixed and cannot be increased**; exceeding it returns
  429 (§7).

If the scope tables or method list do not look like this, stop and report what you actually see.

## 1. The scope family and what each one costs

| Scope | Grants | Tier |
| --- | --- | --- |
| `https://www.googleapis.com/auth/contacts` | "See, edit, download, and permanently delete your contacts" — the only write scope | **Sensitive** |
| `https://www.googleapis.com/auth/contacts.readonly` | "See and download your contacts" | **Sensitive** |
| `https://www.googleapis.com/auth/contacts.other.readonly` | "See and download contact info automatically saved in your 'Other contacts'" — read-only, always | **Sensitive** |
| `https://www.googleapis.com/auth/directory.readonly` | "See and download your organization's Google Workspace directory" | **Sensitive** |
| `https://www.googleapis.com/auth/profile.emails.read` | Email addresses on the signed-in user's own profile | See the note below |
| `https://www.googleapis.com/auth/userinfo.email`, `.../userinfo.profile`, `openid` | Identity of the signed-in user | **Non-sensitive** |

**None of these is restricted.** That is the single most valuable fact in this file: the annual CASA security
assessment, the assurance-level negotiation and the permitted-application-type gate that dominate a Drive or Gmail
project simply do not apply to Contacts. What remains is ordinary sensitive-scope verification — a brand check, a
per-scope justification, and an unlisted demo video of the consent flow — which the base skill covers.

**The tier is still sensitive, not free.** An app requesting `contacts.readonly` goes through review, shows the
unverified-app screen until it passes, and is capped at 100 lifetime users while unverified. Plan verification into
launch.

**`profile.emails.read` is real but oddly documented.** It appears in the authorization block of the People API's own
method references (`people.get`, `people.getBatchGet`), but it is **not** on Google's public OAuth 2.0 Scopes
reference, which lists `user.emails.read` for the People API instead. Treat it as a live but legacy-flavoured scope:
before declaring it, paste the exact string into the Data Access picker and read the tier the console gives it, and if
the console does not recognise it, use `user.emails.read` or drop it — for the signed-in user's own address,
`userinfo.email` is non-sensitive and usually enough.

The other `user.*.read` profile scopes on the same family (`user.addresses.read`, `user.birthday.read`,
`user.emails.read`, `user.gender.read`, `user.organization.read`, `user.phonenumbers.read`) each unlock one field
group on the **signed-in user's own profile**. They do nothing for contacts, and each one added widens the review.
Request one only when a named feature needs that field.

### There is no narrow write scope

Drive has `drive.file`; Gmail has `gmail.send`. **Contacts has nothing equivalent.** The ladder is exactly two rungs:

- `contacts.readonly` — read everything in the address book.
- `contacts` — read, create, update **and permanently delete** everything in the address book.

An app that only needs to create contacts must still request the scope that can delete them all, and the consent
screen says so in those words. That is a product conversation to have before the first customer sees the screen, not a
console setting. If writes are occasional, consider whether a read-only connection plus an out-of-band write path
serves better than asking every customer for delete rights.

## 2. Three corpora, and why an app sees fewer people than the user

This is the defect report that arrives after launch: *"it only imported 40 of my 3,000 contacts."* Almost always,
nothing is broken.

| Corpus | What it is | Scope | Methods |
| --- | --- | --- | --- |
| **Saved contacts** ("connections") | What the user deliberately saved — the `myContacts` group and any groups they made | `contacts.readonly` or `contacts` | `people.connections.list`, `people.searchContacts`, `people.get` |
| **Other contacts** | Addresses Google auto-saved from the user's interactions, belonging to **no contact group** | `contacts.other.readonly` | `otherContacts.list`, `otherContacts.search`, `otherContacts.copyOtherContactToMyContactsGroup` |
| **Directory** | The Workspace domain's profiles and shared domain contacts | `directory.readonly` | `people.listDirectoryPeople`, `people.searchDirectoryPeople` |

**No scope implies another, and no method spans corpora.** `contacts.readonly` does not return other contacts.
`contacts.other.readonly` does not return saved contacts. `directory.readonly` returns neither. An app that requests
only `contacts.readonly` and calls only `connections.list` is behaving correctly and returning a small number, because
most people a consumer Gmail user "has" were never saved — they are other contacts, which is exactly what the user
sees in Gmail's autocomplete and at the Other contacts view in the Contacts UI.

**Other contacts are read-only, and deliberately thin.** Google restricts the fields the API will return for the
CONTACT source to `names`, `emailAddresses`, `phoneNumbers`, `photos` and `metadata` — full name, email, phone, and
nothing more. There is no create, update or delete; the only write is
`otherContacts.copyOtherContactToMyContactsGroup`, which promotes one into the saved address book (and therefore needs
the `contacts` scope for the destination). Do not design a sync that expects to enrich or edit them.

**Directory people need a Workspace account and an administrator's cooperation.** The directory methods are
domain-scoped: a consumer Gmail account has no directory, and the call returns nothing useful however the scopes look.
Google's own guide states that **domain administrators must enable external contact and profile sharing** for the API
to reach domain data — so a Workspace customer whose admin has that off gets an empty directory with a valid token and
a correctly declared scope. That is an admin-side fix, and admin-console changes of this kind can take up to 24 hours
to propagate. Pass sources explicitly: `DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE` for the org's users and
`DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT` for shared domain contacts — they are different populations and most apps want
both.

**There is no domain-wide "list every user's contacts" call.** Contacts are per-user: either each user authorizes, or
a service account with domain-wide delegation impersonates them one at a time. Nothing in the People API returns one
admin view of everyone's address books.

## 3. Choosing the read method

| Method | Use it for | Page size | Sync tokens |
| --- | --- | --- | --- |
| `people.connections.list` | The full saved address book, and incremental sync | 1–1000, default 100 | **Yes** (`requestSyncToken`) |
| `otherContacts.list` | The full "other contacts" corpus, and incremental sync | 1–1000, default 100 | **Yes** |
| `people.listDirectoryPeople` | The whole domain directory, and incremental sync | 1–1000, default 100 | **Yes** |
| `people.searchContacts` | A typeahead over saved contacts | default 10, **capped at 30** | No |
| `otherContacts.search` | A typeahead over other contacts | default 10, **capped at 30** | No |
| `people.searchDirectoryPeople` | A typeahead over the directory | 1–500, default 100 | No |
| `people.getBatchGet` | Hydrating known resource names | **max 200 names per call** | n/a |

Three things to know before wiring search into anything:

- **Search is prefix matching, not substring.** A person named "foo name" matches `f`, `fo`, `foo`, `foo n`, `nam` —
  and does **not** match `oo n`. Users type substrings and conclude the search is broken.
- **Search needs a warmup.** Google documents sending a search request with an **empty query first** to update the
  cache; without it, early results are stale or thin. Read `searchContacts` results only after the warmup.
- **Search is not a backfill.** Capped at 30 results with no sync token, it is a typeahead endpoint. Full ingestion
  belongs to the `list` methods.

`people.connections.list` only accepts `people/me` as the resource name — you list the authorizing user's connections
and nobody else's.

## 4. `personFields` and `readMask` — required, and not interchangeable

Every read takes a field mask, and **it is required**: `people.connections.list` and `people.getBatchGet` return a
**400** when `personFields` is missing. There is no default set.

- `people.connections.list`, `people.get`, `people.getBatchGet` spell it **`personFields`**.
- `otherContacts.list`, `otherContacts.search`, `people.searchContacts`, `listDirectoryPeople`,
  `searchDirectoryPeople` spell it **`readMask`**.

Same idea, different parameter name, and passing the wrong one is a 400 that reads like a typo because it is one. The
value is a comma-separated list from a fixed vocabulary (`addresses`, `ageRanges`, `biographies`, `birthdays`,
`calendarUrls`, `clientData`, `coverPhotos`, `emailAddresses`, `events`, `externalIds`, `genders`, `imClients`,
`interests`, `locales`, `locations`, `memberships`, `metadata`, `miscKeywords`, `names`, `nicknames`, `occupations`,
`organizations`, `phoneNumbers`, `photos`, `relations`, `sipAddresses`, `skills`, `urls`, `userDefined`).

Three traps:

- **The mask for other contacts is much shorter.** For `READ_SOURCE_TYPE_CONTACT` only `names`, `emailAddresses`,
  `phoneNumbers`, `photos` and `metadata` are accepted; `otherContacts.search` accepts only `names`,
  `emailAddresses`, `phoneNumbers` and `metadata`. A mask copied from `connections.list` fails here.
- **`sources` is not the same thing as the mask**, and the defaults differ per method. `otherContacts.list` will not
  accept `READ_SOURCE_TYPE_PROFILE` on its own — a profile source must be paired with the contact source.
- **Omitting `metadata` breaks sync.** Deletions and change tracking arrive inside metadata (§5); a mask that leaves
  it out gives a sync that silently never deletes anything.

Field masks are also the cheapest quota lever there is: ask for the field groups the product maps and no others.

## 5. Sync tokens and their seven-day expiry

`connections.list`, `otherContacts.list` and `listDirectoryPeople` all support incremental sync the same way: set
`requestSyncToken=true` on a full sync, read `nextSyncToken` **from the last page only**, and pass it as `syncToken`
next time.

What breaks in production:

- **Sync tokens expire seven days after the full sync.** An expired token fails with an `ErrorInfo` whose reason is
  `EXPIRED_SYNC_TOKEN` (HTTP 410 in Google's guides), and the only recovery is a full sync with no `syncToken`. Any
  connector syncing less often than weekly — or paused for a week by an outage, a rate limit or a disabled account —
  must treat the full re-sync as a normal path, not an error branch.
- **Deletions only appear in a sync response**, as a person with `PersonMetadata.deleted` set to `true`. Outside a
  sync, deleted contacts simply stop appearing, so a full-list-and-diff strategy must detect removals itself.
- **Every other parameter must match the first call.** When paginating or syncing, the field mask, sources, sort order
  and page size have to be identical to the call that produced the token. Changing the mask to map one new field
  invalidates the sync, so plan a full re-sync into any field-mapping change.
- **Writes propagate for several minutes** into sync results, and Google states plainly that **incremental syncs are
  not intended for read-after-write**. A write-then-sync test that shows nothing is usually just early.
- The **default sort order is `LAST_MODIFIED_ASCENDING`**; `FIRST_NAME_ASCENDING` and `LAST_NAME_ASCENDING` exist but
  change the pagination contract as above.

## 6. Contact groups

Groups are labels on contacts, exposed as their own resource, and they follow the same two-rung scope ladder:
`contacts.readonly` reads them, `contacts` writes them. `contactGroups.list` pages 1–1000 with a **default of 30** —
smaller than every other list default here, and easy to mistake for "the user only has 30 labels" — and it supports
sync tokens and a `groupFields` mask (`clientData`, `groupType`, `memberCount`, `metadata`, `name`).

The membership rules are where integrations trip:

- A group is `USER_CONTACT_GROUP` or `SYSTEM_CONTACT_GROUP`. **Only `contactGroups/myContacts` and
  `contactGroups/starred` accept member additions**; other system groups are deprecated and members can only be
  *removed* from them.
- `contactGroups.members.modify` takes at most **1000 resource names** across adds and removes combined.
- **Creating a group whose name duplicates an existing one returns 409.** A sync that re-creates groups by name needs
  to reconcile first.
- Saved contacts live in groups; **other contacts are by definition in none** (§2). Mapping "groups" onto a unified
  tags or lists concept works for the first corpus only.

## 7. Quota and per-user limits

**Google publishes no per-minute numbers for the People API.** There is no public quota table equivalent to Calendar's
or Drive's; the documentation speaks of request classes ("critical read requests", "critical write requests") without
values. The real limits for a project are the ones shown in the Cloud Console under the People API's quota page, which
is also where an increase is requested. Do not quote a figure from a blog post into a capacity plan — read the console.

What *is* documented, and matters:

- **The first page of a full sync has an additional quota, it is fixed, and it cannot be increased.** Exceeding it
  returns **429**. A connector that re-syncs many tenants on the same schedule, or that retries a failed full sync in
  a tight loop, will hit this and cannot buy its way out — stagger full syncs and back off.
- **Quota is enforced per end user as well as per project.** One heavy tenant can be throttled while every other
  customer is fine, which looks like a broken connection rather than a rate limit.
- **A quota failure does not always arrive as 429.** Google APIs also return **403 with reason `rateLimitExceeded` or
  `userRateLimitExceeded`** (and `RATE_LIMIT_EXCEEDED` in the newer error details) for quota. Treating that 403 as an
  authorization failure is the classic misdiagnosis: it triggers a pointless token refresh or, worse, disconnects a
  healthy customer. Map it to backoff.
- Page sizes are the other lever: 1000 per page on the list methods, 200 resource names per `getBatchGet`, 1000
  resource names per group-membership modify.

## 8. The old Contacts API — dead, except the part that is not

- **Contacts API v3 (GData, `https://www.google.com/m8/feeds`) was turned down on 19 January 2022.** Its single scope
  covered both personal contacts and directory data; the People API split that into `contacts` (personal) and
  `directory.readonly` (domain). Any runbook, library or scope string mentioning `m8/feeds` is describing a dead API.
- **Apps Script's Contacts service shut down on 31 January 2025**, replaced by the People API advanced service. Old
  internal automations may still be written against it.
- **The Domain Shared Contacts API survives.** Google's own migration guide states it is not affected by the Contacts
  API turndown, and it is still documented as current: a GData/XML, Workspace-admin-only API for **external** contacts
  shared across a domain, enabled from the Admin console. So "the old GData contacts APIs are gone" is not quite true,
  and if a customer's requirement is *shared external domain contacts* rather than users' personal contacts, the
  People API is not the right surface. Google warns explicitly against using it for a domain's own users — that
  produces duplicates.
- A separate **Contact Delegation API** exists for delegating one Workspace user's contacts to another. It is not part
  of the People API and does not change any scope decision here.

## Product fact — what the Contacts connector asks for

> **As of 2026-09-20, Unified.to's Google Contacts connector requests:**
>
> - **Read:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/profile.emails.read`,
>   `https://www.googleapis.com/auth/contacts.readonly`, `https://www.googleapis.com/auth/contacts.other.readonly`
> - **Write:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/contacts`
> - **Login/identity only:** `openid`, `profile`, `email`
>
> Three consequences worth reading twice:
>
> - **Nothing requested is restricted**, so an app registered for this connector needs sensitive-scope verification
>   and **no CASA assessment**. This is the cheap Google integration.
> - **The write configuration does not include `contacts.other.readonly`.** A connection made for writing therefore
>   cannot read the "other contacts" corpus at all — the §2 shortfall arrives as a product difference between a read
>   connection and a write connection, not as a bug.
> - **Neither configuration requests `directory.readonly`.** Workspace directory people are out of reach on this
>   connector today; asking for them is a connector change, not a console setting.
>
> Other connector-side behaviour to know: it ships **platform-owned Google OAuth credentials**, so a customer only
> needs their own Cloud client when they specifically want their own (own branding on the consent screen, their own
> verification posture, their own quota). It sends `access_type` and `prompt` on the authorize URL — the two
> parameters the base skill flags as the usual cause of a missing refresh token — exchanges and refreshes against
> Google's standard endpoints, and treats Google's 403 quota responses as rate limiting to back off from rather than
> as an authorization failure (§7). A **Google service-account credential** path also exists as an alternative to the
> user OAuth flow, requesting the full `contacts` scope, which is the domain-wide-delegation route rather than a
> per-user consent.
>
> Scope sets change. This note is dated, not live; confirm with the connector's owner before submitting anything.

## 9. Contacts-specific end-to-end checks

On top of the base skill's authorize → refresh round trip:

1. Make one real `people.connections.list` call with an explicit `personFields` and confirm the People API is enabled
   on the project and returns data.
2. Compare the count against what the user sees in the Contacts UI **before** calling it a bug — if the gap is large,
   check whether the missing people are other contacts (§2) and whether `contacts.other.readonly` was requested,
   declared *and* granted.
3. Inspect the granted `scope` in the token response against what was requested. Google's granular-consent screen lets
   a user approve contacts and decline other contacts; the result is a valid token that 403s on one of the three
   corpora, at the first call and not at consent.
4. Test the **seven-day sync path deliberately**: run a full sync, keep the token, and confirm the code takes the full
   re-sync branch on `EXPIRED_SYNC_TOKEN` rather than crashing. Do not wait a week to find out.
5. Confirm `metadata` is in the field mask and that a deleted contact comes back with `deleted: true` on the next
   incremental sync (§5).
6. If the directory matters, test with a **Workspace** account whose admin has contact and profile sharing enabled —
   a consumer account cannot prove anything here.
7. If writes are in scope, confirm the consent screen wording ("permanently delete") has been seen and accepted by
   whoever owns the customer relationship (§1).

| Symptom | Cause |
| --- | --- |
| Every People call fails although scopes look right | People API not enabled on the project |
| 400 on every read | Missing `personFields` / `readMask`, or the wrong one of the two for that method (§4) |
| Far fewer people than the user expects | Other contacts are a separate corpus and scope (§2) |
| Directory calls return nothing on a valid Workspace token | Admin has not enabled contact/profile sharing, or the sources were not passed (§2) |
| Directory calls return nothing on a consumer account | Consumer accounts have no directory (§2) |
| One of three read paths 403s, the others work | Granular consent dropped that scope (§9.3) |
| Sync suddenly fails after a quiet week | `EXPIRED_SYNC_TOKEN` — seven-day expiry, full re-sync required (§5) |
| Sync never removes deleted contacts | `metadata` missing from the field mask, or diffing without a sync token (§5) |
| Sync breaks right after a field-mapping change | Sync parameters must match the call that issued the token (§5) |
| 429 on the first page of a full sync | The fixed full-sync quota — stagger syncs; it cannot be increased (§7) |
| 403 `rateLimitExceeded` treated as an auth error | It is quota, not authorization — back off (§7) |
| Search misses obvious matches | Prefix matching only, and the warmup request was skipped (§3) |
| Group create returns 409 | A group of that name already exists (§6) |
| Anything referencing `m8/feeds` | Contacts API v3, dead since 19 January 2022 (§8) |

## Stop and ask

Beyond the base skill's list, hand back to a human when:

- The product needs to **create or update** contacts and nobody has accepted that the only write scope also grants
  permanent deletion, in those words, on the consent screen (§1).
- Someone plans around `directory.readonly` without confirming the customers are on **Workspace** and that their
  admins will enable contact and profile sharing (§2).
- A requirement turns out to be **shared external domain contacts** rather than users' personal contacts — that is the
  Domain Shared Contacts API and a different, admin-level integration (§8).
- A capacity plan needs real quota numbers: read them from the project's console quota page, or ask, rather than
  quoting a figure (§7).
- The sync design assumes tokens survive longer than seven days, or assumes a field-mask change is free (§5).
- `profile.emails.read` is wanted but the Data Access picker does not recognise it or labels it unexpectedly (§1).
- The People API scope tiers do not match the **platform state** section above.

## References

Contacts/People-specific only; the base skill carries the generic Google OAuth references. Every URL verified to
resolve on 2026-09-20.

- People API overview — https://developers.google.com/people
- Get ready to use the People API (enabling the API) — https://developers.google.com/people/v1/getting-started
- Read and manage contacts (sync behaviour, field masks) — https://developers.google.com/people/v1/contacts
- Read "Other contacts" — https://developers.google.com/people/v1/other-contacts
- Read the domain directory (admin sharing requirement, sources) — https://developers.google.com/people/v1/directory
- Read profiles — https://developers.google.com/people/v1/profiles
- Contacts API (v3) migration guide — turndown date, scope split, Domain Shared Contacts exception — https://developers.google.com/people/contacts-api-migration
- `people.connections.list` — page size, sync tokens, `EXPIRED_SYNC_TOKEN`, full-sync quota — https://developers.google.com/people/api/rest/v1/people.connections/list
- `people.get` — the full authorization-scope block for People reads — https://developers.google.com/people/api/rest/v1/people/get
- `people.getBatchGet` — 200-resource-name limit — https://developers.google.com/people/api/rest/v1/people/getBatchGet
- `people.searchContacts` — prefix matching, warmup, 30-result cap — https://developers.google.com/people/api/rest/v1/people/searchContacts
- `otherContacts.list` — readMask restrictions, sources rules — https://developers.google.com/people/api/rest/v1/otherContacts/list
- `otherContacts.search` — https://developers.google.com/people/api/rest/v1/otherContacts/search
- `people.listDirectoryPeople` — https://developers.google.com/people/api/rest/v1/people/listDirectoryPeople
- `people.searchDirectoryPeople` — https://developers.google.com/people/api/rest/v1/people/searchDirectoryPeople
- `contactGroups` resource (group types, system groups) — https://developers.google.com/people/api/rest/v1/contactGroups
- `contactGroups.list` — page size default 30, groupFields, sync tokens — https://developers.google.com/people/api/rest/v1/contactGroups/list
- `contactGroups.members.modify` — 1000-name limit, `myContacts` / `starred` — https://developers.google.com/people/api/rest/v1/contactGroups.members/modify
- Apps Script: migrate from the Contacts service to the People API — https://developers.google.com/apps-script/migration/contacts-people
- Domain Shared Contacts API (still current, Workspace admin, external contacts) — https://developers.google.com/admin-sdk/domain-shared-contacts
- Contact Delegation API — https://developers.google.com/admin-sdk/contact-delegation
