attio-ts-sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,27 +7,50 @@
7
7
  [![Context7](https://img.shields.io/badge/[]-Context7-059669)](https://context7.com/hbmartin/attio-ts-sdk)
8
8
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hbmartin/attio-ts-sdk)
9
9
 
10
- A modern, type-safe TypeScript SDK for the [Attio](https://attio.com) CRM API. Built with Zod v4 and a new Attio‑aware client layer that adds retries, error normalization, caching, and higher‑level helpers on top of the generated OpenAPI client.
11
-
12
- - **Create an Attio client in one line** (`createAttioClient({ apiKey })`)
13
- - **Retry & rate‑limit aware** (exponential backoff + `Retry-After`)
14
- - **Normalized errors** (consistent shape + optional suggestions for select/status mismatches)
15
- - **Record normalization** (handles inconsistent response shapes)
16
- - **Metadata caching** (attributes, select options, statuses)
17
- - **Pagination helpers** (`paginate` + cursor handling)
18
- - **Response helpers** (`assertOk`, `toResult`)
19
- - **Offset pagination support** (`paginateOffset`)
20
-
21
- You still have full access to the generated, spec‑accurate endpoints.
22
-
23
- ## Features
10
+ A modern, type-safe TypeScript SDK for the [Attio](https://attio.com) CRM API. Built with Zod v4 and a client layer that adds retries, error normalization, caching, and higher‑level helpers on top of the generated OpenAPI client.
24
11
 
25
12
  - **Full Attio API Coverage** - People, companies, lists, notes, tasks, meetings, webhooks, and more
13
+ - **Create a client in one line** - `createAttioClient({ apiKey })`
14
+ - **Retry & rate‑limit aware** - exponential backoff + `Retry-After`
15
+ - **Normalized errors** - consistent shape + optional suggestions for select/status mismatches
16
+ - **Record normalization** - handles inconsistent response shapes
17
+ - **Metadata caching** - attributes, select options, statuses
18
+ - **Pagination helpers** - `paginate` + `paginateOffset` + cursor handling
26
19
  - **Runtime Validation** - Every request and response validated with Zod v4 schemas
27
20
  - **Tree-Shakeable** - Import only what you need
28
21
  - **TypeScript First** - Complete type definitions generated from OpenAPI spec
29
- - **Attio-Aware Client** - Retries, normalized errors, caching, helpers
30
- - **Zero Config** - Sensible defaults, just add your API key
22
+
23
+ You still have full access to the generated, spec‑accurate endpoints.
24
+
25
+ ### See Also
26
+
27
+ - [attio-js](https://github.com/d-stoll/attio-js) - an alternative SDK generated with Speakeasy
28
+ - [attio-tui](https://github.com/hbmartin/attio-tui) - a TUI for using Attio built with the library
29
+
30
+ ## Table of Contents
31
+
32
+ - [Installing](#installing)
33
+ - [Getting Your API Key](#getting-your-api-key)
34
+ - [Usage](#usage)
35
+ - [Quick Start](#quick-start)
36
+ - [Recommended Pattern](#recommended-pattern)
37
+ - [Attio SDK](#attio-sdk)
38
+ - [Attio Convenience Layer](#attio-convenience-layer)
39
+ - [Value Helpers](#value-helpers)
40
+ - [Record Value Accessors](#record-value-accessors)
41
+ - [Schema Helpers](#schema-helpers)
42
+ - [Client Configuration](#client-configuration)
43
+ - [Error Handling](#error-handling)
44
+ - [Pagination Helpers](#pagination-helpers)
45
+ - [Caching](#caching)
46
+ - [Debug Hooks](#debug-hooks)
47
+ - [Metadata Helpers](#metadata-helpers)
48
+ - [Working with Records](#working-with-records)
49
+ - [Using Generated Endpoints Directly](#using-generated-endpoints-directly)
50
+ - [Managing Lists](#managing-lists)
51
+ - [Notes and Tasks](#notes-and-tasks)
52
+ - [Webhooks](#webhooks)
53
+ - [Development](#development)
31
54
 
32
55
  ## Installing
33
56
 
@@ -47,6 +70,19 @@ bun add attio-ts-sdk zod
47
70
 
48
71
  > **Note:** Zod v4 is a peer dependency - install it alongside the SDK.
49
72
 
73
+ ## Getting Your API Key
74
+
75
+ 1. Log in to your [Attio](https://attio.com) workspace.
76
+ 2. Navigate to **Workspace Settings → Developers** (or visit `https://app.attio.com/settings/developers` directly).
77
+ 3. Click **Create a new integration**, give it a name, and select the scopes your application needs.
78
+ 4. Copy the generated API token and store it securely (e.g. in an environment variable).
79
+
80
+ ```bash
81
+ export ATTIO_API_KEY="your-api-key-here"
82
+ ```
83
+
84
+ The SDK reads the key from whatever you pass to `createAttioClient({ apiKey })` — it does **not** read environment variables automatically, so you control exactly how the secret is loaded.
85
+
50
86
  ## Usage
51
87
 
52
88
  This SDK provides two layers:
@@ -115,10 +151,46 @@ const objects = assertOk(await getV2Objects({ client }));
115
151
  console.log(objects);
116
152
  ```
117
153
 
154
+ ### Attio SDK
155
+
156
+ `createAttioSdk` builds on top of the convenience layer and the generated endpoints to provide a single, namespaced object you can pass around your application. It binds the client once so you don't repeat `{ client }` on every call, and groups operations by resource.
157
+
158
+ ```typescript
159
+ import { createAttioSdk } from 'attio-ts-sdk';
160
+
161
+ const sdk = createAttioSdk({ apiKey: process.env.ATTIO_API_KEY });
162
+ ```
163
+
164
+ The returned `sdk` object exposes these namespaces:
165
+
166
+ | Namespace | Methods |
167
+ | --- | --- |
168
+ | `sdk.objects` | `list`, `get`, `create`, `update` |
169
+ | `sdk.records` | `create`, `update`, `upsert`, `get`, `delete`, `query` |
170
+ | `sdk.lists` | `list`, `get`, `queryEntries`, `addEntry`, `updateEntry`, `removeEntry` |
171
+ | `sdk.metadata` | `listAttributes`, `getAttribute`, `getAttributeOptions`, `getAttributeStatuses`, `schema` |
172
+
173
+ The underlying `AttioClient` is also available as `sdk.client` when you need to drop down to the generated endpoints.
174
+
175
+ ```typescript
176
+ const companies = await sdk.records.query({
177
+ object: 'companies',
178
+ filter: { attribute: 'name', value: 'Acme' },
179
+ });
180
+
181
+ const attributes = await sdk.metadata.listAttributes({
182
+ target: 'objects',
183
+ identifier: 'companies',
184
+ });
185
+
186
+ // Use the generated endpoints when you need full spec access
187
+ const { data } = await getV2Objects({ client: sdk.client });
188
+ ```
189
+
118
190
  ### Attio Convenience Layer
119
191
 
120
- The Attio helpers wrap the generated endpoints with retries, error normalization,
121
- record normalization, and opinionated defaults.
192
+ The standalone helper functions wrap the generated endpoints with retries, error normalization,
193
+ record normalization, and opinionated defaults. They are the same functions that `createAttioSdk` uses under the hood — use them directly when you prefer explicit `{ client }` threading.
122
194
 
123
195
  ```typescript
124
196
  import { createAttioClient, createRecord, listLists, searchRecords } from 'attio-ts-sdk';
@@ -143,6 +215,55 @@ const matches = await searchRecords({
143
215
  });
144
216
  ```
145
217
 
218
+ ### Value Helpers
219
+
220
+ The `value` namespace provides factory functions that build correctly shaped field-value arrays for record creation and updates. Each helper validates its input with Zod before returning, so typos and bad data fail fast at the call site rather than in the API response.
221
+
222
+ ```typescript
223
+ import { value } from 'attio-ts-sdk';
224
+ ```
225
+
226
+ | Helper | Signature | Description |
227
+ | --- | --- | --- |
228
+ | `value.string` | `(value: string) => ValueInput[]` | Non-empty string field. |
229
+ | `value.number` | `(value: number) => ValueInput[]` | Finite numeric field. |
230
+ | `value.boolean` | `(value: boolean) => ValueInput[]` | Boolean field. |
231
+ | `value.domain` | `(value: string) => ValueInput[]` | Domain field (non-empty string). |
232
+ | `value.email` | `(value: string) => ValueInput[]` | Email field (validated format). |
233
+ | `value.currency` | `(value: number, currencyCode?: string) => ValueInput[]` | Currency field. `currencyCode` is an optional ISO 4217 code (e.g. `"USD"`). |
234
+
235
+ ```typescript
236
+ const values = {
237
+ name: value.string('Acme Corp'),
238
+ domains: value.domain('acme.com'),
239
+ contact_email: value.email('hello@acme.com'),
240
+ employee_count: value.number(150),
241
+ is_customer: value.boolean(true),
242
+ annual_revenue: value.currency(50000, 'USD'),
243
+ };
244
+
245
+ await sdk.records.create({ object: 'companies', values });
246
+ ```
247
+
248
+ ### Record Value Accessors
249
+
250
+ `getValue` and `getFirstValue` extract attribute values from a record object. Pass an optional Zod schema to get typed, validated results.
251
+
252
+ ```typescript
253
+ import { getFirstValue, getValue } from 'attio-ts-sdk';
254
+
255
+ // Untyped — returns unknown
256
+ const name = getFirstValue(company, 'name');
257
+ const domains = getValue(company, 'domains');
258
+
259
+ // Typed — returns parsed values or throws on mismatch
260
+ import { z } from 'zod';
261
+
262
+ const nameSchema = z.object({ value: z.string() });
263
+ const typedName = getFirstValue(company, 'name', { schema: nameSchema });
264
+ // ^? { value: string } | undefined
265
+ ```
266
+
146
267
  ### Schema Helpers
147
268
 
148
269
  Create a schema from cached metadata and use accessors to reduce raw string keys:
@@ -159,20 +280,6 @@ const schema = await createSchema({
159
280
  const name = schema.getAccessorOrThrow('name').getFirstValue(company);
160
281
  ```
161
282
 
162
- ### Record Value Helpers
163
-
164
- ```typescript
165
- import { getFirstValue, getValue, value } from 'attio-ts-sdk';
166
-
167
- const values = {
168
- name: value.string('Acme'),
169
- domains: value.domain('acme.com'),
170
- };
171
-
172
- const name = getFirstValue(company, 'name');
173
- const domains = getValue(company, 'domains');
174
- ```
175
-
176
283
  ### Client Configuration
177
284
 
178
285
  ```typescript
@@ -189,7 +296,28 @@ const client = createAttioClient({
189
296
 
190
297
  ### Error Handling
191
298
 
192
- Errors are normalized to `AttioError` / `AttioApiError` / `AttioNetworkError`.
299
+ All errors thrown by the convenience layer and `createAttioSdk` are normalized into a hierarchy rooted at `AttioError`:
300
+
301
+ | Class | Default Code | When |
302
+ | --- | --- | --- |
303
+ | `AttioApiError` | *(from response)* | HTTP error responses (4xx / 5xx). Includes `response`, `requestId`, and optional `retryAfterMs`. |
304
+ | `AttioNetworkError` | *(from cause)* | Connection failures, DNS errors, timeouts. |
305
+ | `AttioRetryError` | `RETRY_ERROR` | All retry attempts exhausted. |
306
+ | `AttioResponseError` | `RESPONSE_ERROR` | Response body failed Zod validation. |
307
+ | `AttioConfigError` | `CONFIG_ERROR` | Invalid client configuration. |
308
+ | `AttioBatchError` | `BATCH_ERROR` | A batch operation partially or fully failed. |
309
+
310
+ Every `AttioError` carries these optional fields:
311
+
312
+ ```typescript
313
+ error.status // HTTP status code
314
+ error.code // machine-readable error code
315
+ error.requestId // Attio x-request-id header
316
+ error.retryAfterMs // parsed Retry-After (milliseconds)
317
+ error.suggestions // fuzzy-match suggestions for value mismatches (see below)
318
+ ```
319
+
320
+ #### Catching errors from the convenience layer
193
321
 
194
322
  ```typescript
195
323
  import { createAttioClient, createRecord, AttioError } from 'attio-ts-sdk';
@@ -203,18 +331,40 @@ try {
203
331
  values: { stage: [{ value: 'Prospectt' }] },
204
332
  });
205
333
  } catch (err) {
206
- const error = err as AttioError;
207
- console.log(error.status, error.code, error.requestId, error.suggestions);
334
+ if (err instanceof AttioError) {
335
+ console.log(err.status, err.code, err.requestId, err.suggestions);
336
+ } else {
337
+ // Re-throw if it's not an error we specifically handle
338
+ throw err;
339
+ }
208
340
  }
209
341
  ```
210
342
 
211
- If you use the generated endpoints directly, you can normalize and unwrap responses:
343
+ #### Smart suggestions for value mismatches
344
+
345
+ When an API error indicates a select option or status mismatch, the SDK automatically attaches a `suggestions` object with up to three fuzzy-matched alternatives:
346
+
347
+ ```typescript
348
+ error.suggestions
349
+ // {
350
+ // field: 'stage',
351
+ // attempted: 'Prospectt',
352
+ // bestMatch: 'Prospect',
353
+ // matches: ['Prospect', 'Prospecting', 'Closed']
354
+ // }
355
+ ```
356
+
357
+ #### Response helpers for generated endpoints
358
+
359
+ When using the generated endpoints directly, use `assertOk` or `toResult` to unwrap responses:
212
360
 
213
361
  ```typescript
214
362
  import { assertOk, toResult, getV2Objects } from 'attio-ts-sdk';
215
363
 
364
+ // Throws on error, returns the data payload
216
365
  const objects = assertOk(await getV2Objects({ client }));
217
366
 
367
+ // Returns a discriminated union { ok: true, value } | { ok: false, error }
218
368
  const result = toResult(await getV2Objects({ client }));
219
369
  if (result.ok) {
220
370
  console.log(result.value);
@@ -223,35 +373,109 @@ if (result.ok) {
223
373
  }
224
374
  ```
225
375
 
376
+ #### throwOnError mode
377
+
378
+ You can also opt into exceptions at the client level:
379
+
380
+ ```typescript
381
+ const client = createAttioClient({
382
+ apiKey: process.env.ATTIO_API_KEY,
383
+ throwOnError: true,
384
+ });
385
+
386
+ // Generated endpoints now throw instead of returning { error }
387
+ const { data } = await postV2ObjectsByObjectRecords({
388
+ client,
389
+ path: { object: 'companies' },
390
+ body: { data: { values: { name: [{ value: 'Test' }] } } },
391
+ });
392
+ ```
393
+
226
394
  ### Pagination Helpers
227
395
 
396
+ The SDK provides two pagination strategies. Use the one that matches the endpoint:
397
+
398
+ | Strategy | Helper | Endpoints |
399
+ | --- | --- | --- |
400
+ | **Offset-based** | `paginateOffset` | Record queries (`postV2ObjectsByObjectRecordsQuery`), list entry queries (`postV2ListsByListEntriesQuery`) |
401
+ | **Cursor-based** | `paginate` | Meetings (`getV2Meetings`), notes (`getV2Notes`), tasks (`getV2Tasks`), webhooks, and most `GET` list endpoints |
402
+
403
+ Both helpers automatically extract items and pagination metadata from raw API responses — pass the generated endpoint call directly and the helper does the rest.
404
+
405
+ > **Note:** The convenience functions `queryRecords` / `sdk.records.query` and `queryListEntries` / `sdk.lists.queryEntries` return a single page of unwrapped results. To collect **all** pages, use the pagination helpers with the generated endpoints as shown below.
406
+
407
+ #### Paginating record queries (offset-based)
408
+
228
409
  ```typescript
229
410
  import {
230
411
  createAttioClient,
231
- paginate,
232
412
  paginateOffset,
233
- getV2Meetings,
234
413
  postV2ObjectsByObjectRecordsQuery,
235
414
  } from 'attio-ts-sdk';
236
415
 
237
416
  const client = createAttioClient({ apiKey: process.env.ATTIO_API_KEY });
238
417
 
239
-
240
- const meetings = await paginate(async (cursor) => {
241
- const result = await getV2Meetings({
418
+ // Collect all companies matching a filter across every page
419
+ const allCompanies = await paginateOffset(async (offset, limit) => {
420
+ return postV2ObjectsByObjectRecordsQuery({
242
421
  client,
243
- query: { cursor },
422
+ path: { object: 'companies' },
423
+ body: {
424
+ offset,
425
+ limit,
426
+ filter: { attribute: 'name', value: 'Acme' },
427
+ sorts: [{ attribute: 'created_at', direction: 'desc' }],
428
+ },
244
429
  });
245
- return result;
246
430
  });
431
+ ```
247
432
 
248
- const offsetResults = await paginateOffset(async (offset, limit) => {
249
- const result = await postV2ObjectsByObjectRecordsQuery({
433
+ #### Paginating list entry queries (offset-based)
434
+
435
+ ```typescript
436
+ import { paginateOffset, postV2ListsByListEntriesQuery } from 'attio-ts-sdk';
437
+
438
+ const allEntries = await paginateOffset(async (offset, limit) => {
439
+ return postV2ListsByListEntriesQuery({
250
440
  client,
251
- path: { object: 'companies' },
252
- body: { offset, limit },
441
+ path: { list: 'sales-pipeline' },
442
+ body: {
443
+ offset,
444
+ limit,
445
+ filter: { attribute: 'stage', value: 'negotiation' },
446
+ },
253
447
  });
254
- return result;
448
+ });
449
+ ```
450
+
451
+ #### Paginating cursor-based endpoints
452
+
453
+ ```typescript
454
+ import { paginate, getV2Meetings } from 'attio-ts-sdk';
455
+
456
+ const allMeetings = await paginate(async (cursor) => {
457
+ return getV2Meetings({ client, query: { cursor } });
458
+ });
459
+ ```
460
+
461
+ #### Pagination options
462
+
463
+ Both helpers accept an options object to control limits:
464
+
465
+ ```typescript
466
+ // Offset-based options
467
+ const records = await paginateOffset(fetchPage, {
468
+ offset: 0, // starting offset (default: 0)
469
+ limit: 100, // items per page (default: 50)
470
+ maxPages: 5, // stop after N pages
471
+ maxItems: 200, // stop after N total items
472
+ });
473
+
474
+ // Cursor-based options
475
+ const meetings = await paginate(fetchPage, {
476
+ cursor: null, // starting cursor (default: null)
477
+ maxPages: 10, // stop after N pages
478
+ maxItems: 500, // stop after N total items
255
479
  });
256
480
  ```
257
481
 
@@ -538,35 +762,10 @@ const { data: webhook } = await postV2Webhooks({
538
762
  const { data: webhooks } = await getV2Webhooks({ client });
539
763
  ```
540
764
 
541
- ### Error Handling
542
-
543
- ```typescript
544
- import { postV2ObjectsByObjectRecords } from 'attio-ts-sdk';
765
+ ## See Also
545
766
 
546
- const result = await postV2ObjectsByObjectRecords({
547
- client,
548
- path: { object: 'companies' },
549
- body: { data: { values: { name: [{ value: 'Test' }] } } },
550
- });
551
-
552
- if (result.error) {
553
- console.error('API Error:', result.error);
554
- } else {
555
- console.log('Created:', result.data);
556
- }
557
-
558
- // Or use throwOnError for exceptions
559
- try {
560
- const { data } = await postV2ObjectsByObjectRecords({
561
- client,
562
- path: { object: 'companies' },
563
- body: { data: { values: { name: [{ value: 'Test' }] } } },
564
- throwOnError: true,
565
- });
566
- } catch (error) {
567
- console.error('Request failed:', error);
568
- }
569
- ```
767
+ - [attio-js](https://github.com/d-stoll/attio-js) - an alternative SDK generated with Speakeasy
768
+ - [attio-tui](https://github.com/hbmartin/attio-tui) - a TUI for using Attio built with this library
570
769
 
571
770
  ## Development
572
771
 
@@ -593,11 +792,6 @@ pnpm test
593
792
  pnpm build
594
793
  ```
595
794
 
596
- ### Releasing
597
-
598
- - Merge the automated Release PR created by Release Please
599
- - Manually run the "Release" workflow to publish to npm and JSR with provenance
600
-
601
795
  ## License
602
796
 
603
- MIT © Harold Martin
797
+ [MIT](LICENSE) © [Harold Martin](https://www.linkedin.com/in/harold-martin-98526971/)