---
name: attio-oauth-app
description: Creates or signs in to an Attio account and registers an app in the Attio Developer console (build.attio.com) to obtain an OAuth2 client ID and client secret — with redirect URIs, the app-configured scope catalogue, the object/attribute data model, non-expiring tokens with no refresh token, workspace-level admin-only installs, API-created webhooks, and a safe credential handoff. Use when asked to get Attio OAuth credentials, create an Attio app or developer account, pick Attio scopes, rotate an Attio client secret, publish an Attio app to the App Store, or debug an Attio connection that authorizes fine but 403s on reads, cannot see custom attributes, or cannot register webhooks. For any other vendor's developer portal, use that vendor's skill instead.
---

# Attio OAuth2 App Registration

Get a working Attio OAuth2 client — an Attio account and workspace, an app in the Developer console, redirect URIs,
a scope selection, client ID and client secret — for a platform that connects many customers' Attio workspaces.

Four things are expensive to get wrong here, and none of them is the signup form:

1. **Scopes are not requested. They are configured.** Attio's authorize endpoint takes `client_id`, `response_type`,
   `redirect_uri` and `state` — and **no `scope` parameter at all**. Every customer grants exactly the scope set
   ticked in the app's **Scopes** tab. The app registration *is* the scope decision, and a missing scope does not
   fail at authorization; it fails later, as a permission error on one endpoint (§5).
2. **Reading the schema is a separate scope from reading the data.** `record_permission:read` grants records;
   `object_configuration:read` grants the objects and attributes that describe them — and Attio requires **both** on
   nearly every record endpoint. Of the ~36 scopes, `object_configuration:read` is required by more endpoints than
   any other. A connector with records but not object configuration reads nothing (§5, §6).
3. **The workspace defines its own objects and attributes.** Attio ships people and companies, offers deals, users
   and workspaces, and lets each customer add custom objects, custom lists and custom attributes of ~17 types. A
   scope grants a *capability*, not a schema — two customers who granted the identical scope set can return
   completely different shapes (§6).
4. **Tokens do not expire and there is no refresh token.** Attio states plainly that `exp` is always `null` "because
   Attio access tokens do not currently expire". There is nothing to refresh, and the only ways a token dies are
   revocation and uninstall — which arrive as a 401 with no warning (§7).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name** and **slug** | The slug is a unique identifier; Attio says pick one you're happy to keep (§3) |
| **Developer account name and logo** | Public-facing if you ever publish; name rules are strict (§3, §8) |
| **Which Attio account and workspace** owns the app | You need **admin** on it; prefer an isolated development workspace (§2) |
| **Redirect URIs** | Every callback host your platform serves; must match exactly (§4) |
| **Which objects and operations** the connector performs | Decides the scope selection — this is the whole job (§5) |
| **Webhooks wanted?** | Needs `webhook:read-write` and API-created webhooks (§9) |
| **Publish to the Attio App Store?** | Optional; OAuth works unpublished, with a warning (§8) |
| **Whether customers run custom objects / custom attributes** | Changes what "done" means for the connector (§6) |

## Quick Start

1. Confirm a **new** app is needed — a new client ID orphans every existing customer connection (§1).
2. Sign in to Attio as a workspace **admin**, ideally of an isolated development workspace (§2).
3. **Developer console → New app** at `https://build.attio.com`; set name and slug (§3).
4. **OAuth tab → enable the OAuth 2.0 toggle**, then add **every** redirect URI, exactly (§3, §4).
5. **Scopes tab** — tick the union your connector needs, and do not forget `object_configuration:read` (§5).
6. Read §6 before promising anyone the connector "syncs Attio" — the customer defines the schema.
7. Capture client ID and client secret from the OAuth tab (§7).
8. Plan for **non-expiring tokens with no refresh token**, and handle revocation as a 401 path (§7).
9. If you need push events, enable `webhook:read-write` and register webhooks through the API (§9).
10. Verify from a **second, unrelated workspace**, as an admin, and check `GET /v2/self` (§10).
11. Hand the credentials over — never commit them (§11).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The docs moved to `docs.attio.com`.** `developers.attio.com/docs/*` now **308-redirects to
  `https://docs.attio.com/`** — the docs home, not the equivalent page. `attio.com/developers/introduction`
  redirects the same way. Any bookmark or code comment pointing at `developers.attio.com` lands somewhere generic.
- **Apps are created in the Developer console at `https://build.attio.com`** — "New app" in the sidebar, then name
  and slug. OAuth is a per-app **toggle** on the app's OAuth tab; scopes are on its Scopes tab.
- **Endpoints, fixed for everyone:** authorize `https://app.attio.com/authorize`, token
  `https://app.attio.com/oauth/token`, revoke `https://app.attio.com/oauth/revoke`, API base `https://api.attio.com`
  (v2), identify `GET https://api.attio.com/v2/self`.
- **Revocation is new.** The revoke endpoint was added on **2026-09-11**. Anything written before that date will tell
  you Attio has no revocation endpoint.
- **The authorize call has no `scope` parameter.** Documented query parameters are `client_id` (required),
  `response_type=code` (required), `redirect_uri` (required, must exactly match a registered URI) and `state`
  (optional). **PKCE is not documented.**
- **Token exchange**: Attio's tutorial posts `application/x-www-form-urlencoded` with `grant_type`, `code`,
  `redirect_uri`, `client_id` and `client_secret`. The endpoint reference lists `client_id`, `client_secret`,
  `grant_type` and `code` as required and does **not** list `redirect_uri`. Follow the tutorial: send
  `redirect_uri`, form-encoded.
- **The documented token response is `access_token` and `token_type: "Bearer"` — nothing else.** No `expires_in`,
  no `refresh_token`, no `scope`.
- **Tokens do not expire.** Attio's identify endpoint states: "`exp` is always `null`, because Attio access tokens do
  not currently expire." Manually generated API keys likewise "do not expire, but can be deleted at any time."
- **Tokens are workspace-scoped, not user-scoped.** Introspection returns `sub` = the workspace ID, plus
  `workspace_id`, `workspace_name`, `workspace_slug` and `authorized_by_workspace_member_id` — the member who
  authorized it, recorded but not the scope boundary.
- **The scope catalogue is 18 resources × `:read` / `:read-write` = 36 strings** (§5). There is no bare `:write`.
- **Installing and managing apps and workspace integrations is an admin-only capability.** Members can view
  workspace settings but not change them.
- **Review is not a gate on connecting other workspaces.** Attio: "If your app uses OAuth, users can install it via
  the standard OAuth flow even before the app has been published. However, they will see a warning that the app has
  not yet been approved by the Attio team." App Store listing is optional, reviewed, and aims to complete within
  **1 week**.
- **Rate limits: 100 requests/second for reads, 25 requests/second for writes**, plus score-based limits on List
  records and List entries whose scores are **summed across all apps and access tokens** in the workspace over a
  10-second sliding window. Individual endpoints can be lower: **List notes has been capped at 10 req/s since
  2026-09-02** as a temporary stability measure, and create-call-recording is 1 req/s. Revoke is separately limited
  to 50 requests/minute per IP.
- **Webhooks are API-created for multi-customer integrations**, signed with `Attio-Signature`, HTTPS-only, 5-second
  timeout, and duplicate subscriptions are rejected with 409 (§9).
- **The App SDK is a different product** — TypeScript/React apps that run inside Attio, versioned on Attio's
  infrastructure and subject to code review. A REST/OAuth connector is not an App SDK app and does not get code
  reviewed; do not let SDK documentation set expectations for this run.

If the Developer console does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new client ID, and every existing customer authorization is bound to the old one** — every
customer re-authorizes. Reuse the existing app for: adding a redirect URI, changing the scope selection, rotating a
compromised secret, enabling webhooks, or diagnosing an authorization failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app
for a different product, or a deliberate environment split. Note the Attio-specific wrinkle: because scopes live on
the app and not on the authorize call, **changing the scope selection changes it for every future authorization at
once**. Attio does not document whether tokens already issued pick up newly added scopes or keep the set they were
granted (§5) — treat a scope addition as potentially requiring a re-authorization sweep until you have verified
otherwise against a real token. Say which path you are taking before you touch the console.

## 2. Account and workspace

- Sign in or sign up at Attio, then open the Developer console at `https://build.attio.com`.
- **You must be a workspace admin.** Attio lists "install and manage apps and workspace integrations" among the
  things only admins can do; members can view workspace settings but not edit them. API keys are likewise
  admin-only, under **Workspace settings → Developers**.
- **Use an isolated development workspace.** Attio recommends it, and says outright: "We are happy to provide your
  team with development workspaces as needed. Please reach out to `support@attio.com` or use the chat widget." Ask
  for one rather than testing against a production CRM full of real customer records.
- The **developer account** itself — the identity your app is published under — carries a name and a logo, and both
  are subject to App Store rules if you ever publish (§8). Setting them sensibly now costs nothing.

Hand control back for anything only a human can do: email verification, 2FA, accepting terms, being made an admin,
requesting a development workspace, or submitting the app for review. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Give
the user an ordered click path with the literal values to paste — the redirect URIs from §4 and the exact scope
strings from §5 — and continue once they report back with the client ID.

## 3. Create the app

**Developer console → New app.** Give the app a name and a **slug**; Attio calls the slug "a unique identifier for
your app, so pick one you're happy to keep." Then:

| Step | Detail |
| --- | --- |
| **OAuth tab** | **Enable OAuth 2.0 via the toggle at the top of the page.** Without this there is no client ID, no client secret and no flow. |
| **Redirect URIs** | Configured on the same OAuth tab. See §4. |
| **Scopes tab** | The whole permission model. See §5. |
| **Client ID / secret** | On the OAuth tab once OAuth is enabled. See §7. |
| **Share privately** | Overflow menu (three dots, top right) → *Share privately* for an install link (§8). |
| **Publish app** | Button top right, when and if you want an App Store listing (§8). |

There is no private-vs-public *distribution* setting to get wrong and no app-type fork: an OAuth app is installable
by any workspace that completes the flow from the moment OAuth is enabled. Publishing only affects discoverability
and the unapproved-app warning (§8).

## 4. Redirect URIs

Register **every** callback host your platform serves. Attio's rule is exact matching: the `redirect_uri` on the
authorize call "must exactly match one of the registered redirect URLs in your app's settings pages at
build.attio.com". For Unified.to these are one per data center; confirm the current list with the platform owner
rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Two practical notes:

- **Send `redirect_uri` again at the token exchange.** Attio's OAuth tutorial includes it in the form body, even
  though the endpoint reference does not list it among the required parameters. Sending it matches the worked
  example and costs nothing; omitting it relies on undocumented leniency.
- **Plain HTTP is accepted for local development** — Attio's own tutorial registers
  `http://localhost:3050/integrations/attio/callback`. Do not leave a localhost URI on a production app.

Attio does not publish a cap on the number of redirect URIs. Register the literal strings above and do not rely on
trailing-slash or subpath forgiveness.

## 5. Scopes — configured on the app, never requested

**This is the section that decides whether the connector works.** There is no `scope` parameter on the authorize
call. Attio says it directly: "When using an OAuth access token, the scopes are specified by configuring the scope
settings for your app in the Developer console." Whatever is ticked on the Scopes tab is what every customer grants,
today and retroactively for every future install.

The full catalogue — **18 resources, each as `:read` or `:read-write`.** There is no bare `:write`; `:read-write`
is the write level and includes reading.

| Scope family | What Attio says it grants |
| --- | --- |
| `record_permission:read` / `:read-write` | View, and optionally write, **records** |
| `object_configuration:read` / `:read-write` | View, and optionally write, the **configuration and attributes of objects** |
| `list_entry:read` / `:read-write` | View, and optionally write, the **entries in a list** |
| `list_configuration:read` / `:read-write` | View, and optionally write, the **configuration and attributes of lists** |
| `user_management:read` / `:read-write` | View **workspace members** |
| `note:read` / `:read-write` | View, and optionally write, **notes** |
| `task:read` / `:read-write` | View, and optionally write, **tasks** |
| `comment:read` / `:read-write` | View **comments (and threads)**, and optionally write comments |
| `meeting:read` / `:read-write` | View, and optionally write, **meetings** |
| `call_recording:read` / `:read-write` | View, and optionally write, **call recordings, transcripts and speakers** |
| `webhook:read` / `:read-write` | View, and optionally **manage, webhooks** |
| `file:read` / `:read-write` | View, and upload, **files** |
| `email:read` / `:read-write` | View **email metadata**. "Email content is never exposed." |
| `activity_configuration:read` / `:read-write` | Configuration and attributes of **activities** |
| `activity_record:read` / `:read-write` | **Activity records** |
| `sequence_unsubscribe:read` / `:read-write` | View, and optionally add to, the **sequence unsubscribe list** |
| `public_collection:read` / `:read-write` | Settings and information within **public collections** (legacy naming) |
| `private_collection:read` / `:read-write` | Settings and information of **all** collections "regardless of their access settings" |

### The `object_configuration` trap

This is the classic Attio failure, and it is not obvious from the scope names. **Records and the schema that
describes them are two different scopes, and record endpoints require both.** Straight from Attio's endpoint
reference:

| Endpoint | Required scopes |
| --- | --- |
| List records (`POST /v2/objects/{object}/records/query`) | `record_permission:read`, **`object_configuration:read`** |
| Get a record | `record_permission:read`, **`object_configuration:read`** |
| Create / update a record | `record_permission:read-write`, **`object_configuration:read`** |
| List objects (`GET /v2/objects`) | `object_configuration:read` |
| List attributes, when `target` is `objects` | `object_configuration:read` |
| List attributes, when `target` is `lists` | `list_configuration:read` |
| List notes | `note:read`, `object_configuration:read`, `record_permission:read` |
| List / create tasks | `task:read`(`-write`), `object_configuration:read`, `record_permission:read`, `user_management:read` |
| List / get list entries | `list_entry:read`, `list_configuration:read` |
| List workspace members | `user_management:read` |
| Create / update / delete a webhook | **`webhook:read-write`** |

Read that table as three rules:

1. **`object_configuration:read` is effectively mandatory.** It is required by more endpoints than any other scope,
   including plain record reads. Tick it even for a read-only integration. An app that grants records but not object
   configuration authorizes cleanly and then fails on the first list call.
2. **Lists are a parallel universe.** Records live under `object_configuration` + `record_permission`; list entries
   live under `list_configuration` + `list_entry`. Reading a list's attributes needs `list_configuration:read`, not
   `object_configuration:read`. Anything that models pipelines or processes as Attio lists needs the list pair too.
3. **Cross-resource scopes are common.** Tasks need four scopes. Notes need three. Copy the "Required scopes" line
   from each endpoint's reference page rather than guessing from the resource name.

`private_collection:*` grants access "regardless of their access settings" — it is the one a security-conscious
admin will look at hardest. Do not tick it unless the product genuinely requires seeing collections the authorizing
admin's own permissions would not reach.

> **As of 2026-09-20, this platform's Attio connector** correctly sends **no `scope` parameter** on the authorize
> call — which is right for Attio — and is a confidential client that does **not** use PKCE. Its internally recorded
> per-capability requirements amount to: `record_permission:read` / `:read-write`, `object_configuration:read` /
> `:read-write`, `list_configuration:read` / `:read-write`, `note:read` / `:read-write`, `task:read` /
> `:read-write`, `meeting:read`, and `user_management:read` / `:read-write` — and **no `webhook` scope at all**,
> even though it registers webhooks through the API, which Attio gates behind `webhook:read-write`. Treat that list
> as the floor, not the spec: because Attio ignores requested scopes entirely, only the Scopes tab matters, so tick
> the union of everything above **plus `webhook:read-write`** if webhooks are wanted. Confirm the exact current set
> with the connector's owner before ticking boxes.

## 6. The object and attribute data model — read this before promising anything

Attio is not a fixed-schema CRM, and this is the thing that most often makes an integration "work" in testing and
disappoint in production.

- **Objects are the tables.** Attio's own analogy: "In relational database terms, objects are roughly tables."
  **People and companies are enabled by default; deals, users and workspaces are optional**, and a customer can
  create **custom objects** on top. Only admins can enable and deactivate standard objects.
- **Records are the rows.** An instance of an object.
- **Attributes are the columns**, and "Some attributes, such as the name on a person, are system defined. Others,
  you define yourself, either in Attio's UI or over the API." Attribute types include text, number, select, status,
  rating, currency, date, timestamp, checkbox, domain, email address, phone number, location, personal name,
  record reference, actor reference and interaction.
- **Lists are a second axis.** Lists aggregate records into a process, and **lists carry their own attributes** —
  Attio's example is a deal that has extra attributes only relevant inside one list. A value can therefore live on
  the record, on the list entry, or both.

Four consequences for a connector that cannot know the shape in advance:

1. **Discover, do not assume.** `GET /v2/objects` and the attributes endpoints are how you learn what this workspace
   actually has. That is exactly why `object_configuration:read` is required alongside records (§5) — and why a
   connector that hardcodes `people`, `companies` and `deals` breaks the moment a customer's value lives on a custom
   object or a custom attribute.
2. **An object that is not enabled simply does not exist.** Deals, users and workspaces are optional. A perfectly
   valid workspace may have no deals object, and the right behaviour is a clear message, not a crash.
3. **Slugs versus IDs.** Objects, lists and attributes are addressable by slug or UUID; slugs are human-editable and
   IDs are not. Store IDs for anything you must resolve again later.
4. **The same scope grant returns different data per customer.** Two customers who tick the identical boxes can
   return entirely different attribute sets. "We support Attio" is a claim about capabilities, not about fields —
   say so before a customer infers otherwise.

## 7. Capture the credentials, and understand the token

From the app's **OAuth tab** in the Developer console:

- **Client ID** and **client secret**
- Endpoints, fixed and identical for everyone:
  - authorize — `https://app.attio.com/authorize`
  - token — `https://app.attio.com/oauth/token`
  - revoke — `https://app.attio.com/oauth/revoke`
  - REST API — `https://api.attio.com` (v2); identify — `GET https://api.attio.com/v2/self`

**The flow:**

```
GET  https://app.attio.com/authorize
       ?client_id=<client id>&response_type=code
       &redirect_uri=<exact registered URI>&state=<csrf nonce>

POST https://app.attio.com/oauth/token          Content-Type: application/x-www-form-urlencoded
       grant_type=authorization_code&code=<code>&redirect_uri=<same URI>
       &client_id=<client id>&client_secret=<client secret>

→ { "access_token": "...", "token_type": "Bearer" }
```

Then `Authorization: Bearer <access_token>` on every API call. Attio also accepts HTTP Basic with the token as the
username and a blank password, but recommends Bearer.

**Token behaviour — the part most OAuth checklists get wrong here:**

- **No expiry.** Attio: "`exp` is always `null`, because Attio access tokens do not currently expire." There is no
  `expires_in` in the documented response.
- **No refresh token.** None is documented in the token response, and there is no documented refresh grant. Any
  refresh machinery in a client is inert against Attio — harmless, but do not let its presence convince anyone that
  expiry is handled.
- **A token is workspace-scoped.** `sub` is the workspace ID. `authorized_by_workspace_member_id` records who
  clicked, but the grant is the workspace's.
- **Revocation is the real failure mode.** `POST https://app.attio.com/oauth/revoke` with the token in the
  `Authorization` header, or form-encoded as a `token` parameter; `204 No Content` on success, `400` if no token was
  found or if the header and body disagree. A customer uninstalling or revoking from their side produces the same
  outcome with no notice to you. Because tokens never expire, **a 401 means "gone", not "refresh me"** — do not
  build a refresh-and-retry loop that will never succeed.
- **`GET /v2/self` is the health check.** It needs no scopes, accepts any Attio token type, and returns `200` with
  `{"active": false}` — not an error — for unknown, revoked or deleted tokens. Poll it rather than inferring
  liveness from a business endpoint.

Rotating the client secret breaks every token exchange until the new value is deployed; existing access tokens are
not documented as being invalidated by rotation, but do not rely on that. Never rotate without explicit go-ahead.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the
transcript and can be rotated, then move on.

> **As of 2026-09-20, this platform's Attio connector** uses the authorize, token and API base URLs above, sends
> `client_id`, `redirect_uri`, `response_type` and `state` on the authorize call, and presents the result as a
> `Bearer` token. It treats the access token as non-expiring, which matches Attio. Two divergences from Attio's
> documented worked example are worth raising with the connector's owner rather than assuming they are fine: it
> posts the code exchange as **JSON** rather than `application/x-www-form-urlencoded`, and it **omits `redirect_uri`
> from the exchange body**. It also carries refresh-token configuration that Attio does not document. Confirm the
> current behaviour with the connector's owner — this is a snapshot, not a contract.

### The single-workspace alternative

If the user actually has one Attio workspace and no multi-tenant requirement, **an API key is the right answer and
this run produces no OAuth credentials at all.** Workspace settings → **Developers** → **+ New access token**, name
it, set its scopes. Admins only. "Tokens do not expire, but can be deleted at any time", and — unlike OAuth — the
**scopes of an existing key can be edited later**. Attio's scope catalogue is identical for both token types. Say
this out loud before building an OAuth app for a single-workspace use case.

## 8. Install, approval, and the App Store

Three things people conflate:

1. **Who installs (the customer's side).** Attio lists "install and manage apps and workspace integrations" as an
   **admin-only** capability, and the token is workspace-scoped. Expect a customer **admin** to be the one who
   completes your flow. A member who tries will not be able to grant workspace-wide access — plan the support
   message for that, because "the connect button did nothing" from a non-admin is a common ticket.
2. **Review is not a gate on connecting.** Attio: "If your app uses OAuth, users can install it via the standard
   OAuth flow even before the app has been published. However, they will see a warning that the app has not yet been
   approved by the Attio team." So a brand-new app can onboard real customers today; the cost is a scary warning on
   the consent screen. Publishing removes it. **Private invites** (overflow menu → *Share privately*) are an App
   SDK sharing mechanism and are not what an OAuth connector needs, though they carry the same unapproved warning.
3. **Publishing to the App Store is optional and reviewed.** Developer console → **Publish app**. Attio aims to
   complete reviews **within 1 week**. Requirements are specific and rejections are cheap to avoid:
   - **Developer account**: must not contain "Attio" or impersonate another company; no "dev"/"test" in the name;
     correctly capitalized; must have a logo.
   - **App name**: must not contain "Attio"; unofficial apps must not imply they are the official integration for a
     service; official apps match the service's exact name and capitalization; unofficial apps use title case.
   - **Listing text**: three sections — Overview, How It Works, Configure — each **100–3,000 characters**, English,
     high quality, headers must not end with a colon, and Attio concepts spelled "records", "objects", "lists",
     "people".
   - **Submission video**: initial submissions **must** include a screen recording walking through installation,
     authentication/configuration and core features — not a generic marketing video.
   - **Links**: HTTPS, and every link **must return 200**.
   - **Logos**: PNG, no transparency, **minimum 560×560**, 1:1, no rounded corners (Attio applies a 30% radius),
     with padding.
   - Code review applies to **App SDK** apps, not to a REST/OAuth connector running on your own infrastructure —
     for which Attio states there is "no versioning or review process on Attio's side" for shipping updates.

   Do not promise a customer an App Store listing before it is accepted, and do not invent compliance, security or
   data-retention claims on the submission — ask.

## 9. Webhooks

Attio has two ways to create webhooks, and only one works for a multi-customer connector.

- **API-created (what you want).** `POST /v2/webhooks`, scope **`webhook:read-write`**. Attio: "Creating webhooks
  over the API is essential for those building integrations for Attio that will operate for many customers." It is
  also the only way to use filters.
- **Settings-page-created.** Note Attio's own caveat: "webhooks created with tokens that were created through our
  OAuth sign up flow will not be shown in the developer settings page." So do not expect to eyeball a customer's
  OAuth-created webhooks in a UI.

The operational rules that cost the most when missed:

- **Verify `Attio-Signature`** (duplicated as `X-Attio-Signature` for legacy middleware): hex HMAC-SHA256 of the
  **raw request body**, interpreted as UTF-8, using the webhook secret returned when the webhook was created.
  Restringifying parsed JSON changes the bytes and breaks the signature. Attio recommends signature verification
  **over IP allowlisting**, though it does publish a fixed egress IP list for restrictive firewalls.
- **HTTPS only.** HTTP targets are refused.
- **Respond 2xx within 5 seconds.** Anything else retries up to **10 times with exponential backoff over roughly 3
  days**, after which the webhook is marked **degraded** and Attio emails you.
- **At-least-once delivery.** Deduplicate on the `Idempotency-Key` header, which is stable across retries.
- **Subscriptions must be unique** per (target URL, event type, filter) within a workspace, or the request is
  rejected `409 uniqueness_conflict`. Key order and operation order are ignored when comparing filters, and a `null`
  filter equals an empty `$and`. This bites when re-subscribing a customer without tearing the old one down.
- **Delivery is rate limited to 25 requests/second per target URL.** A single shared callback host across many
  customers is a shared budget.
- **Filters** are `$and`/`$or` over `field` (dot notation, e.g. `id.object_id`, `parent_object_id`, `actor.type`),
  `operator` (`equals` / `not_equals`) and `value`. Filters are only editable and viewable over the API.
- **Event coverage worth knowing:** `record.created` / `.updated` / `.deleted` / **`.merged`**, `note.created` /
  `.updated` / `.deleted` and a separate **`note-content.updated`** (Attio notes that body updates do *not* fire
  `note.updated`), `task.*`, `comment.*`, `list.*`, `list-entry.*`, `list-attribute.*`, `object-attribute.*`,
  `workspace-member.created`, `call-recording.created`.
- **V1 webhook events are deprecated** (`entry.created`, `entry-attribute.updated`, `entry.deleted`) in favour of
  the `list-entry.*` V2 equivalents with different payload shapes.

> **As of 2026-09-20, this platform's Attio connector** registers webhooks through the API, filtered by object ID,
> and keeps the returned webhook ID so it can delete the subscription later. Two things to raise with its owner:
> it does not record a `webhook` scope among its requirements even though `POST /v2/webhooks` needs
> `webhook:read-write`, and there is no sign that it verifies the `Attio-Signature` header on inbound deliveries.
> Its recorded rate-limit note also says 100 requests/second across the whole API, which predates both the 25/s
> write limit and the 10/s cap on listing notes.

## 10. Verify end-to-end

Testing in the workspace that owns the app proves little — you are an admin there and the schema is yours. Test the
path a customer takes:

1. Authorize from a **second, unrelated workspace**, through your platform's real connect flow, as that workspace's
   **admin**. Note the unapproved-app warning if the app is not published (§8) and confirm your product's copy
   prepares customers for it.
2. Confirm the token response really is just `access_token` and `token_type`, and that nothing in your client is
   waiting for an `expires_in` or a `refresh_token` that will never arrive.
3. Call `GET /v2/self`. Confirm `active: true`, and check `scope` — it is a **space-separated list of exactly what
   the app was configured with**. This is the fastest way to prove the Scopes tab matches reality.
4. Call `GET /v2/objects` and the attributes endpoint for one object. **If this 403s, `object_configuration:read` is
   missing** — and every record read will fail the same way (§5).
5. Read records from the second workspace. Then have that workspace add a **custom attribute** and confirm the
   connector neither crashes nor silently drops it (§6).
6. If the workspace has no deals object, confirm the connector reports that clearly rather than failing opaquely.
7. Exercise one write and confirm the `:read-write` scope is actually ticked — read and write are separate strings.
8. If webhooks are on: register one, confirm it appears via `GET /v2/webhooks`, verify a signature against the
   **raw** body, and re-run subscription to confirm you get a clean result rather than a `409`.
9. **Revoke** from the customer side, or via `POST /oauth/revoke`, and confirm your platform treats the resulting
   401 as "reconnect", not "refresh".
10. Drive a realistic sync and watch for `429`s — especially on notes (10/s) and on writes (25/s).

| Symptom | Cause |
| --- | --- |
| Authorization succeeds, then every record list fails with a permission error | `object_configuration:read` not ticked — it is required alongside `record_permission:read` (§5) |
| Reads work, writes fail | `:read-write` not ticked; there is no implicit upgrade from `:read` (§5) |
| List entries fail although records work | Lists need `list_configuration` + `list_entry`, a separate pair (§5) |
| `POST /v2/webhooks` refused | `webhook:read-write` missing from the app's scopes (§9) |
| Tasks or notes fail despite `task:read` / `note:read` | Those endpoints also require `object_configuration:read`, `record_permission:read`, and for tasks `user_management:read` (§5) |
| Changing the requested scopes in client code has no effect | Attio has no `scope` parameter — scopes come from the app's Scopes tab (§5) |
| Token exchange fails right after consent | `redirect_uri` not byte-identical to a registered URI, or omitted from the exchange (§4) |
| Authorize URL rejected before consent | `redirect_uri` does not exactly match a registered URI, or `response_type` is not `code` (§4) |
| Consent screen shows a scary "not approved by Attio" warning | The app is unpublished — expected, and not a blocker to connecting (§8) |
| A non-admin customer cannot complete the flow | Installing apps and integrations is admin-only, and the token is workspace-scoped (§8) |
| Token stops working with no warning; refresh attempts fail | Revoked or uninstalled. Tokens never expire, so 401 means gone — reconnect, do not refresh (§7) |
| Client waits forever for `expires_in` / `refresh_token` | Neither is in the documented token response (§7) |
| Custom fields a customer can see are missing from the sync | Attributes are per-workspace; discover them instead of hardcoding (§6) |
| "Deals is not enabled on this workspace" | Deals, users and workspaces are optional objects (§6) |
| `409 uniqueness_conflict` when subscribing | Same (target URL, event type, filter) already registered; tear down first (§9) |
| Webhook signature never matches | Signature computed over restringified JSON instead of the raw UTF-8 body (§9) |
| Webhooks stopped and an email arrived | Repeated non-2xx or >5s responses marked the webhook degraded after ~3 days of retries (§9) |
| Note edits never arrive | Body changes fire `note-content.updated`, not `note.updated` (§9) |
| Duplicate webhook deliveries | At-least-once delivery; deduplicate on `Idempotency-Key` (§9) |
| 429 on a modest sync | Writes are 25/s, listing notes is capped at 10/s, and record-query scores are summed across *all* apps and tokens in that workspace (§ Platform state) |
| An old doc link lands on a generic docs homepage | `developers.attio.com/docs/*` 308-redirects to `docs.attio.com/` (§ Platform state) |

## 11. Hand off — never commit the secret

- **Do not** write the client secret, an access token, a webhook secret or an API key into source control, a test, a
  fixture, a committed `.env`, a ticket, a PR body, or a chat channel. Attio says the same in its own tutorial:
  access tokens "are highly sensitive data" and production apps must encrypt them before storing. Values go to the
  user, for their secret store or console.
- If a code change is needed (a redirect host, form-encoding the exchange, sending `redirect_uri`, treating 401 as
  reconnect rather than refresh, signature verification), keep it secret-free and say what the human must set out of
  band.
- Close with: app name and slug, the Attio workspace and account that own it, the client ID, where the secret was
  delivered, the authorize/token/revoke endpoints, **the exact list of scope strings ticked on the Scopes tab**, the
  redirect URIs registered, whether webhooks are enabled and under which scope, whether the app is published or
  still showing the unapproved warning, and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the user actually has **one** workspace and an API key is the right
answer (that run produces no OAuth credentials at all); the scope selection needs to change on a live app, because
Attio does not document whether already-issued tokens pick up new scopes; `private_collection:*` is on the table,
because it reaches collections the authorizing admin's own permissions would not; a customer's admin must be the one
to install and only a member is available; the App Store submission asks for compliance, security, data-retention or
volume claims, or needs a demo video; the connector must handle customer-defined custom objects or attributes and
nobody has decided what "supported" means; or the Developer console does not match the **Platform state** section
above.

## References

- Developer platform overview — https://docs.attio.com/docs/overview
- Developer console (create and manage apps) — https://build.attio.com
- Connect an app to Attio through OAuth (the worked flow) — https://docs.attio.com/rest-api/tutorials/connect-an-app-through-oauth
- OAuth authorize endpoint — https://docs.attio.com/docs/oauth/authorize
- OAuth token endpoint — https://docs.attio.com/docs/oauth/token
- OAuth introspect endpoint — https://docs.attio.com/docs/oauth/introspect
- OAuth revoke endpoint — https://docs.attio.com/docs/oauth/revoke
- Identify the current token (`GET /v2/self`) — https://docs.attio.com/rest-api/endpoint-reference/meta/identify
- Authenticating requests (OAuth vs API key, scopes) — https://docs.attio.com/rest-api/guides/authentication
- REST API overview — https://docs.attio.com/rest-api/overview
- OpenAPI specification (authoritative scope catalogue and per-endpoint scopes) — https://api.attio.com/openapi/api
- How to access the OpenAPI spec — https://docs.attio.com/rest-api/endpoint-reference/openapi
- Objects and lists (the data model) — https://docs.attio.com/docs/objects-and-lists
- Users and workspaces (workspace members, workspace-scoped access) — https://docs.attio.com/docs/users-and-workspaces
- Slugs and IDs — https://docs.attio.com/docs/slugs-and-ids
- Attribute types — https://docs.attio.com/rest-api/attribute-types/attribute-types
- List objects (required scopes) — https://docs.attio.com/rest-api/endpoint-reference/objects/list-objects
- List attributes (objects vs lists scopes) — https://docs.attio.com/rest-api/endpoint-reference/attributes/list-attributes
- List records (required scopes) — https://docs.attio.com/rest-api/endpoint-reference/records/list-records
- Create a webhook — https://docs.attio.com/rest-api/endpoint-reference/webhooks/create-a-webhook
- Configuring webhooks (signing, retries, filters, egress IPs) — https://docs.attio.com/rest-api/guides/webhooks
- Handling rate limits — https://docs.attio.com/rest-api/guides/rate-limiting
- Paginating API results — https://docs.attio.com/rest-api/guides/pagination
- Filtering and sorting — https://docs.attio.com/rest-api/guides/filtering-and-sorting
- The publication lifecycle (develop, share privately, publish) — https://docs.attio.com/share/the-publication-lifecycle
- Private invites — https://docs.attio.com/share/private-invites
- Publishing to the App Store (review, ~1 week) — https://docs.attio.com/share/publishing-to-the-app-store
- App listing requirements — https://docs.attio.com/share/app-listings
- Logo requirements — https://docs.attio.com/share/logos
- Listing image requirements — https://docs.attio.com/share/listing-images
- Shipping updates (no review for REST API apps) — https://docs.attio.com/share/shipping-updates
- Code review requirements (App SDK only) — https://docs.attio.com/share/code-review
- REST API changelog — https://docs.attio.com/changelog/rest-api
- Documentation index (every page, machine readable) — https://docs.attio.com/llms.txt
- Help: manage members, admins and teams (admin-only capabilities) — https://attio.com/help/reference/workspace-settings-billing/manage-members-and-admins
- Help: generate an API key (single-workspace alternative) — https://attio.com/help/reference/apps/generating-an-api-key
- Help: manage standard objects — https://attio.com/help/reference/workspace/objects
- Help: apps in Attio — https://attio.com/help/academy/introduction/apps
- Public App Store — https://attio.com/apps
- Attio developer platform (marketing overview) — https://attio.com/platform/developers
