@proveanything/smartlinks 1.14.10 → 1.14.11

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.
@@ -1,4 +1,4 @@
1
- import { ContactResponse, ContactCreateRequest, ContactUpdateRequest, ContactListResponse, PublicContactUpsertRequest, PublicContactUpsertResponse, UserSearchResponse, ContactPatch, PublicGetMyContactResponse, PublicUpdateMyContactResponse, ContactSchemaResponse } from "../types";
1
+ import { ContactResponse, ContactCreateRequest, ContactUpdateRequest, ContactListResponse, ContactSearchParams, ContactSearchResponse, PublicContactUpsertRequest, PublicContactUpsertResponse, UserSearchResponse, ContactPatch, PublicGetMyContactResponse, PublicUpdateMyContactResponse, ContactSchemaResponse } from "../types";
2
2
  export declare namespace contact {
3
3
  function create(collectionId: string, data: ContactCreateRequest): Promise<ContactResponse>;
4
4
  function list(collectionId: string, params?: {
@@ -6,6 +6,7 @@ export declare namespace contact {
6
6
  offset?: number;
7
7
  includeDeleted?: boolean;
8
8
  }): Promise<ContactListResponse>;
9
+ function search({ collectionId, q, typeahead, email, phone, id, userId, tags, tagsAll, source, locale, createdFrom, createdTo, externalIdKey, externalIdValue, customFieldKey, customFieldValue, limit, offset, }: ContactSearchParams): Promise<ContactSearchResponse>;
9
10
  function get(collectionId: string, contactId: string, params?: {
10
11
  includeDeleted?: boolean;
11
12
  }): Promise<ContactResponse>;
@@ -19,6 +19,52 @@ export var contact;
19
19
  return request(path);
20
20
  }
21
21
  contact.list = list;
22
+ async function search({ collectionId, q, typeahead, email, phone, id, userId, tags, tagsAll, source, locale, createdFrom, createdTo, externalIdKey, externalIdValue, customFieldKey, customFieldValue, limit, offset, }) {
23
+ const query = new URLSearchParams();
24
+ if (q !== undefined)
25
+ query.set("q", q);
26
+ if (typeahead !== undefined)
27
+ query.set("typeahead", String(typeahead));
28
+ if (email !== undefined)
29
+ query.set("email", email);
30
+ if (phone !== undefined)
31
+ query.set("phone", phone);
32
+ if (id !== undefined)
33
+ query.set("id", id);
34
+ if (userId !== undefined)
35
+ query.set("userId", userId);
36
+ if (tags !== undefined) {
37
+ const arr = Array.isArray(tags) ? tags : tags.split(",").map(t => t.trim());
38
+ arr.forEach(t => query.append("tags", t));
39
+ }
40
+ if (tagsAll !== undefined) {
41
+ const arr = Array.isArray(tagsAll) ? tagsAll : tagsAll.split(",").map(t => t.trim());
42
+ arr.forEach(t => query.append("tagsAll", t));
43
+ }
44
+ if (source !== undefined)
45
+ query.set("source", source);
46
+ if (locale !== undefined)
47
+ query.set("locale", locale);
48
+ if (createdFrom !== undefined)
49
+ query.set("createdFrom", createdFrom);
50
+ if (createdTo !== undefined)
51
+ query.set("createdTo", createdTo);
52
+ if (externalIdKey !== undefined)
53
+ query.set("externalIdKey", externalIdKey);
54
+ if (externalIdValue !== undefined)
55
+ query.set("externalIdValue", externalIdValue);
56
+ if (customFieldKey !== undefined)
57
+ query.set("customFieldKey", customFieldKey);
58
+ if (customFieldValue !== undefined)
59
+ query.set("customFieldValue", customFieldValue);
60
+ if (limit !== undefined)
61
+ query.set("limit", String(limit));
62
+ if (offset !== undefined)
63
+ query.set("offset", String(offset));
64
+ const path = `/admin/collection/${encodeURIComponent(collectionId)}/contacts/search?${query.toString()}`;
65
+ return request(path);
66
+ }
67
+ contact.search = search;
22
68
  async function get(collectionId, contactId, params) {
23
69
  const query = new URLSearchParams();
24
70
  if ((params === null || params === void 0 ? void 0 : params.includeDeleted) !== undefined)
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.14.10 | Generated: 2026-05-16T17:03:23.904Z
3
+ Version: 1.14.11 | Generated: 2026-05-19T10:35:51.071Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -4460,6 +4460,40 @@ interface ContactListResponse {
4460
4460
  }
4461
4461
  ```
4462
4462
 
4463
+ **ContactSearchParams** (interface)
4464
+ ```typescript
4465
+ interface ContactSearchParams {
4466
+ collectionId: string
4467
+ q?: string
4468
+ typeahead?: boolean
4469
+ email?: string
4470
+ phone?: string
4471
+ id?: string
4472
+ userId?: string
4473
+ tags?: string | string[]
4474
+ tagsAll?: string | string[]
4475
+ source?: string
4476
+ locale?: string
4477
+ createdFrom?: string
4478
+ createdTo?: string
4479
+ externalIdKey?: string
4480
+ externalIdValue?: string
4481
+ customFieldKey?: string
4482
+ customFieldValue?: string
4483
+ limit?: number
4484
+ offset?: number
4485
+ }
4486
+ ```
4487
+
4488
+ **ContactSearchResponse** (interface)
4489
+ ```typescript
4490
+ interface ContactSearchResponse {
4491
+ items: Contact[]
4492
+ limit: number
4493
+ offset: number
4494
+ }
4495
+ ```
4496
+
4463
4497
  **PublicContactUpsertResponse** (interface)
4464
4498
  ```typescript
4465
4499
  interface PublicContactUpsertResponse {
@@ -8629,6 +8663,28 @@ Returns all proof type definitions. Proof types are templates that specify which
8629
8663
  **list**(collectionId: string,
8630
8664
  params?: { limit?: number; offset?: number; includeDeleted?: boolean }) → `Promise<ContactListResponse>`
8631
8665
 
8666
+ **search**({
8667
+ collectionId,
8668
+ q,
8669
+ typeahead,
8670
+ email,
8671
+ phone,
8672
+ id,
8673
+ userId,
8674
+ tags,
8675
+ tagsAll,
8676
+ source,
8677
+ locale,
8678
+ createdFrom,
8679
+ createdTo,
8680
+ externalIdKey,
8681
+ externalIdValue,
8682
+ customFieldKey,
8683
+ customFieldValue,
8684
+ limit,
8685
+ offset,
8686
+ }: ContactSearchParams) → `Promise<ContactSearchResponse>`
8687
+
8632
8688
  **get**(collectionId: string,
8633
8689
  contactId: string,
8634
8690
  params?: { includeDeleted?: boolean }) → `Promise<ContactResponse>`
@@ -0,0 +1,162 @@
1
+ # Contact Search
2
+
3
+ Admin-scoped endpoint for querying contacts within a collection. Supports
4
+ free-text search, type-ahead, identity lookup, structured filters, and JSONB
5
+ field queries. All parameters are optional and composable.
6
+
7
+ **Base path:** `GET /api/admin/:collectionId/contacts/search`
8
+
9
+ ---
10
+
11
+ ## Quick examples
12
+
13
+ ```ts
14
+ // Type-ahead while user types "joh" into a search box
15
+ await contact.search({ collectionId, q: "joh", typeahead: true })
16
+
17
+ // Find contacts by partial email
18
+ await contact.search({ collectionId, email: "acme.com" })
19
+
20
+ // Exact contact by UUID
21
+ await contact.search({ collectionId, id: "uuid-here" })
22
+
23
+ // All contacts tagged "vip" created this year
24
+ await contact.search({
25
+ collectionId,
26
+ tags: ["vip"],
27
+ createdFrom: "2026-01-01",
28
+ })
29
+
30
+ // Find by external ID (e.g. Shopify customer ID)
31
+ await contact.search({
32
+ collectionId,
33
+ externalIdKey: "shopify_id",
34
+ externalIdValue: "123456",
35
+ })
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Parameters
41
+
42
+ ### Free-text
43
+
44
+ | Param | Type | Description |
45
+ |---|---|---|
46
+ | `q` | `string` | General search term. Searches `first_name`, `last_name`, `display_name`, `company`, and all identity values (`email`, `phone`). When the value contains `@` only email identities are searched; when it matches a phone pattern only phone identities are searched. |
47
+ | `typeahead` | `boolean` | When `true`, uses prefix (`startsWith`) operators instead of substring (`contains`). Significantly cheaper per keystroke. Default limit becomes `10`; minimum `q` length is 2 characters. Use this for live search-as-you-type UIs. |
48
+
49
+ ### Identity filters
50
+
51
+ | Param | Type | Description |
52
+ |---|---|---|
53
+ | `email` | `string` | Partial match against normalised email identity values. Case-insensitive. |
54
+ | `phone` | `string` | Partial match against normalised phone identity values. |
55
+
56
+ ### Exact lookups
57
+
58
+ These short-circuit all text/filter logic and return at most one contact.
59
+
60
+ | Param | Type | Description |
61
+ |---|---|---|
62
+ | `id` | `string` (UUID) | Contact primary key. |
63
+ | `userId` | `string` | Firebase auth UID linked to the contact. |
64
+
65
+ ### Structured filters
66
+
67
+ All filters are ANDed with each other and with any text search.
68
+
69
+ | Param | Type | Description |
70
+ |---|---|---|
71
+ | `tags` | `string \| string[]` | Comma-separated string or repeated param. Contacts must have **any** of these tags. |
72
+ | `tagsAll` | `string \| string[]` | Contacts must have **all** of these tags. |
73
+ | `source` | `string` | Exact match on the `source` field. |
74
+ | `locale` | `string` | Exact match on the `locale` field (e.g. `"en-US"`). |
75
+ | `createdFrom` | `string` (ISO-8601) | Lower bound on `created_at`. |
76
+ | `createdTo` | `string` (ISO-8601) | Upper bound on `created_at`. |
77
+
78
+ ### JSONB field filters
79
+
80
+ Searches inside the `external_ids` or `custom_fields` JSONB columns by
81
+ key/value pair. Both params in a pair must be supplied together.
82
+
83
+ | Param | Type | Description |
84
+ |---|---|---|
85
+ | `externalIdKey` | `string` | Top-level key in `externalIds` (e.g. `"shopify_id"`). |
86
+ | `externalIdValue` | `string` | Expected value at that key. |
87
+ | `customFieldKey` | `string` | Top-level key in `customFields`. |
88
+ | `customFieldValue` | `string` | Expected value at that key. |
89
+
90
+ ### Pagination
91
+
92
+ | Param | Type | Default | Max |
93
+ |---|---|---|---|
94
+ | `limit` | `number` | `20` (`10` in typeahead mode) | `100` (`10` in typeahead mode) |
95
+ | `offset` | `number` | `0` | — |
96
+
97
+ ---
98
+
99
+ ## Response
100
+
101
+ ```ts
102
+ {
103
+ items: Contact[],
104
+ limit: number,
105
+ offset: number,
106
+ }
107
+ ```
108
+
109
+ Each `Contact` item follows the shape defined in `src/types/contact.ts`.
110
+
111
+ ---
112
+
113
+ ## SDK usage
114
+
115
+ ```ts
116
+ import { contact } from '@proveanything/smartlinks'
117
+
118
+ const results = await contact.search({
119
+ collectionId: "my-collection",
120
+ q: "jane",
121
+ tags: ["vip"],
122
+ limit: 20,
123
+ })
124
+
125
+ for (const c of results.items) {
126
+ console.log(c.displayName, c.email)
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Performance notes
133
+
134
+ The search endpoint is backed by **PostgreSQL trigram GIN indexes** via the
135
+ `pg_trgm` extension (migration `20260519000001`). This makes `ILIKE '%query%'`
136
+ scans fast even with millions of contacts per org.
137
+
138
+ **What is a trigram?** PostgreSQL decomposes every string into overlapping
139
+ 3-character windows ("trigrams") and stores them in an inverted index. A query
140
+ for `"acme"` is converted into trigrams `" ac", "acm", "cme", "me "` and the
141
+ index returns only rows containing those windows — so Postgres never scans rows
142
+ that can't match, regardless of whether the query is a prefix, suffix, or
143
+ middle-of-word substring.
144
+
145
+ **Type-ahead mode** (`typeahead: true`) is the recommended pattern for
146
+ keystroke-by-keystroke UI. It emits `LIKE 'query%'` (prefix) operators instead
147
+ of `LIKE '%query%'` (substring). Prefix scans are cheaper because they require
148
+ fewer trigram lookups, making them safe to call on every keypress. Minimum query
149
+ length is enforced at 2 characters server-side to prevent over-broad scans.
150
+
151
+ **Future scale path:** At very high volumes, routing `search` through
152
+ Elasticsearch would add typo-tolerance and relevance ranking. The API surface is
153
+ intentionally implementation-agnostic so the backing engine can be swapped
154
+ without client changes.
155
+
156
+ ---
157
+
158
+ ## Error codes
159
+
160
+ | Code | HTTP | Meaning |
161
+ |---|---|---|
162
+ | `SEARCH_REQUIRED` | 400 | No searchable parameter was provided. |
package/dist/openapi.yaml CHANGED
@@ -2586,6 +2586,113 @@ paths:
2586
2586
  description: Unauthorized
2587
2587
  404:
2588
2588
  description: Not found
2589
+ /admin/collection/{collectionId}/contacts/search:
2590
+ get:
2591
+ tags:
2592
+ - contact
2593
+ summary: contact.search
2594
+ operationId: contact_search
2595
+ security:
2596
+ - bearerAuth: []
2597
+ parameters:
2598
+ - name: collectionId
2599
+ in: path
2600
+ required: true
2601
+ schema:
2602
+ type: string
2603
+ - name: q
2604
+ in: query
2605
+ required: false
2606
+ schema:
2607
+ type: string
2608
+ - name: typeahead
2609
+ in: query
2610
+ required: false
2611
+ schema:
2612
+ type: string
2613
+ - name: email
2614
+ in: query
2615
+ required: false
2616
+ schema:
2617
+ type: string
2618
+ - name: phone
2619
+ in: query
2620
+ required: false
2621
+ schema:
2622
+ type: string
2623
+ - name: id
2624
+ in: query
2625
+ required: false
2626
+ schema:
2627
+ type: string
2628
+ - name: userId
2629
+ in: query
2630
+ required: false
2631
+ schema:
2632
+ type: string
2633
+ - name: source
2634
+ in: query
2635
+ required: false
2636
+ schema:
2637
+ type: string
2638
+ - name: locale
2639
+ in: query
2640
+ required: false
2641
+ schema:
2642
+ type: string
2643
+ - name: createdFrom
2644
+ in: query
2645
+ required: false
2646
+ schema:
2647
+ type: string
2648
+ - name: createdTo
2649
+ in: query
2650
+ required: false
2651
+ schema:
2652
+ type: string
2653
+ - name: externalIdKey
2654
+ in: query
2655
+ required: false
2656
+ schema:
2657
+ type: string
2658
+ - name: externalIdValue
2659
+ in: query
2660
+ required: false
2661
+ schema:
2662
+ type: string
2663
+ - name: customFieldKey
2664
+ in: query
2665
+ required: false
2666
+ schema:
2667
+ type: string
2668
+ - name: customFieldValue
2669
+ in: query
2670
+ required: false
2671
+ schema:
2672
+ type: string
2673
+ - name: limit
2674
+ in: query
2675
+ required: false
2676
+ schema:
2677
+ type: string
2678
+ - name: offset
2679
+ in: query
2680
+ required: false
2681
+ schema:
2682
+ type: string
2683
+ responses:
2684
+ 200:
2685
+ description: Success
2686
+ content:
2687
+ application/json:
2688
+ schema:
2689
+ $ref: "#/components/schemas/ContactSearchResponse"
2690
+ 400:
2691
+ description: Bad request
2692
+ 401:
2693
+ description: Unauthorized
2694
+ 404:
2695
+ description: Not found
2589
2696
  /admin/collection/{collectionId}/contacts/upsert:
2590
2697
  post:
2591
2698
  tags:
@@ -19580,6 +19687,70 @@ components:
19580
19687
  - items
19581
19688
  - limit
19582
19689
  - offset
19690
+ ContactSearchParams:
19691
+ type: object
19692
+ properties:
19693
+ collectionId:
19694
+ type: string
19695
+ q:
19696
+ type: string
19697
+ typeahead:
19698
+ type: boolean
19699
+ email:
19700
+ type: string
19701
+ phone:
19702
+ type: string
19703
+ id:
19704
+ type: string
19705
+ userId:
19706
+ type: string
19707
+ tags:
19708
+ type: array
19709
+ items:
19710
+ type: object
19711
+ additionalProperties: true
19712
+ tagsAll:
19713
+ type: array
19714
+ items:
19715
+ type: object
19716
+ additionalProperties: true
19717
+ source:
19718
+ type: string
19719
+ locale:
19720
+ type: string
19721
+ createdFrom:
19722
+ type: string
19723
+ createdTo:
19724
+ type: string
19725
+ externalIdKey:
19726
+ type: string
19727
+ externalIdValue:
19728
+ type: string
19729
+ customFieldKey:
19730
+ type: string
19731
+ customFieldValue:
19732
+ type: string
19733
+ limit:
19734
+ type: number
19735
+ offset:
19736
+ type: number
19737
+ required:
19738
+ - collectionId
19739
+ ContactSearchResponse:
19740
+ type: object
19741
+ properties:
19742
+ items:
19743
+ type: array
19744
+ items:
19745
+ $ref: "#/components/schemas/Contact"
19746
+ limit:
19747
+ type: number
19748
+ offset:
19749
+ type: number
19750
+ required:
19751
+ - items
19752
+ - limit
19753
+ - offset
19583
19754
  PublicContactUpsertResponse:
19584
19755
  type: object
19585
19756
  properties:
@@ -34,6 +34,32 @@ export interface ContactListResponse {
34
34
  limit: number;
35
35
  offset: number;
36
36
  }
37
+ export interface ContactSearchParams {
38
+ collectionId: string;
39
+ q?: string;
40
+ typeahead?: boolean;
41
+ email?: string;
42
+ phone?: string;
43
+ id?: string;
44
+ userId?: string;
45
+ tags?: string | string[];
46
+ tagsAll?: string | string[];
47
+ source?: string;
48
+ locale?: string;
49
+ createdFrom?: string;
50
+ createdTo?: string;
51
+ externalIdKey?: string;
52
+ externalIdValue?: string;
53
+ customFieldKey?: string;
54
+ customFieldValue?: string;
55
+ limit?: number;
56
+ offset?: number;
57
+ }
58
+ export interface ContactSearchResponse {
59
+ items: Contact[];
60
+ limit: number;
61
+ offset: number;
62
+ }
37
63
  export type PublicContactUpsertRequest = Partial<Pick<Contact, "email" | "phone" | "userId" | "firstName" | "lastName" | "displayName" | "company" | "tags" | "source" | "notes" | "avatarUrl" | "locale" | "timezone" | "externalIds">> & {
38
64
  customFields?: ContactCustomFields;
39
65
  };
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.14.10 | Generated: 2026-05-16T17:03:23.904Z
3
+ Version: 1.14.11 | Generated: 2026-05-19T10:35:51.071Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -4460,6 +4460,40 @@ interface ContactListResponse {
4460
4460
  }
4461
4461
  ```
4462
4462
 
4463
+ **ContactSearchParams** (interface)
4464
+ ```typescript
4465
+ interface ContactSearchParams {
4466
+ collectionId: string
4467
+ q?: string
4468
+ typeahead?: boolean
4469
+ email?: string
4470
+ phone?: string
4471
+ id?: string
4472
+ userId?: string
4473
+ tags?: string | string[]
4474
+ tagsAll?: string | string[]
4475
+ source?: string
4476
+ locale?: string
4477
+ createdFrom?: string
4478
+ createdTo?: string
4479
+ externalIdKey?: string
4480
+ externalIdValue?: string
4481
+ customFieldKey?: string
4482
+ customFieldValue?: string
4483
+ limit?: number
4484
+ offset?: number
4485
+ }
4486
+ ```
4487
+
4488
+ **ContactSearchResponse** (interface)
4489
+ ```typescript
4490
+ interface ContactSearchResponse {
4491
+ items: Contact[]
4492
+ limit: number
4493
+ offset: number
4494
+ }
4495
+ ```
4496
+
4463
4497
  **PublicContactUpsertResponse** (interface)
4464
4498
  ```typescript
4465
4499
  interface PublicContactUpsertResponse {
@@ -8629,6 +8663,28 @@ Returns all proof type definitions. Proof types are templates that specify which
8629
8663
  **list**(collectionId: string,
8630
8664
  params?: { limit?: number; offset?: number; includeDeleted?: boolean }) → `Promise<ContactListResponse>`
8631
8665
 
8666
+ **search**({
8667
+ collectionId,
8668
+ q,
8669
+ typeahead,
8670
+ email,
8671
+ phone,
8672
+ id,
8673
+ userId,
8674
+ tags,
8675
+ tagsAll,
8676
+ source,
8677
+ locale,
8678
+ createdFrom,
8679
+ createdTo,
8680
+ externalIdKey,
8681
+ externalIdValue,
8682
+ customFieldKey,
8683
+ customFieldValue,
8684
+ limit,
8685
+ offset,
8686
+ }: ContactSearchParams) → `Promise<ContactSearchResponse>`
8687
+
8632
8688
  **get**(collectionId: string,
8633
8689
  contactId: string,
8634
8690
  params?: { includeDeleted?: boolean }) → `Promise<ContactResponse>`
@@ -0,0 +1,162 @@
1
+ # Contact Search
2
+
3
+ Admin-scoped endpoint for querying contacts within a collection. Supports
4
+ free-text search, type-ahead, identity lookup, structured filters, and JSONB
5
+ field queries. All parameters are optional and composable.
6
+
7
+ **Base path:** `GET /api/admin/:collectionId/contacts/search`
8
+
9
+ ---
10
+
11
+ ## Quick examples
12
+
13
+ ```ts
14
+ // Type-ahead while user types "joh" into a search box
15
+ await contact.search({ collectionId, q: "joh", typeahead: true })
16
+
17
+ // Find contacts by partial email
18
+ await contact.search({ collectionId, email: "acme.com" })
19
+
20
+ // Exact contact by UUID
21
+ await contact.search({ collectionId, id: "uuid-here" })
22
+
23
+ // All contacts tagged "vip" created this year
24
+ await contact.search({
25
+ collectionId,
26
+ tags: ["vip"],
27
+ createdFrom: "2026-01-01",
28
+ })
29
+
30
+ // Find by external ID (e.g. Shopify customer ID)
31
+ await contact.search({
32
+ collectionId,
33
+ externalIdKey: "shopify_id",
34
+ externalIdValue: "123456",
35
+ })
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Parameters
41
+
42
+ ### Free-text
43
+
44
+ | Param | Type | Description |
45
+ |---|---|---|
46
+ | `q` | `string` | General search term. Searches `first_name`, `last_name`, `display_name`, `company`, and all identity values (`email`, `phone`). When the value contains `@` only email identities are searched; when it matches a phone pattern only phone identities are searched. |
47
+ | `typeahead` | `boolean` | When `true`, uses prefix (`startsWith`) operators instead of substring (`contains`). Significantly cheaper per keystroke. Default limit becomes `10`; minimum `q` length is 2 characters. Use this for live search-as-you-type UIs. |
48
+
49
+ ### Identity filters
50
+
51
+ | Param | Type | Description |
52
+ |---|---|---|
53
+ | `email` | `string` | Partial match against normalised email identity values. Case-insensitive. |
54
+ | `phone` | `string` | Partial match against normalised phone identity values. |
55
+
56
+ ### Exact lookups
57
+
58
+ These short-circuit all text/filter logic and return at most one contact.
59
+
60
+ | Param | Type | Description |
61
+ |---|---|---|
62
+ | `id` | `string` (UUID) | Contact primary key. |
63
+ | `userId` | `string` | Firebase auth UID linked to the contact. |
64
+
65
+ ### Structured filters
66
+
67
+ All filters are ANDed with each other and with any text search.
68
+
69
+ | Param | Type | Description |
70
+ |---|---|---|
71
+ | `tags` | `string \| string[]` | Comma-separated string or repeated param. Contacts must have **any** of these tags. |
72
+ | `tagsAll` | `string \| string[]` | Contacts must have **all** of these tags. |
73
+ | `source` | `string` | Exact match on the `source` field. |
74
+ | `locale` | `string` | Exact match on the `locale` field (e.g. `"en-US"`). |
75
+ | `createdFrom` | `string` (ISO-8601) | Lower bound on `created_at`. |
76
+ | `createdTo` | `string` (ISO-8601) | Upper bound on `created_at`. |
77
+
78
+ ### JSONB field filters
79
+
80
+ Searches inside the `external_ids` or `custom_fields` JSONB columns by
81
+ key/value pair. Both params in a pair must be supplied together.
82
+
83
+ | Param | Type | Description |
84
+ |---|---|---|
85
+ | `externalIdKey` | `string` | Top-level key in `externalIds` (e.g. `"shopify_id"`). |
86
+ | `externalIdValue` | `string` | Expected value at that key. |
87
+ | `customFieldKey` | `string` | Top-level key in `customFields`. |
88
+ | `customFieldValue` | `string` | Expected value at that key. |
89
+
90
+ ### Pagination
91
+
92
+ | Param | Type | Default | Max |
93
+ |---|---|---|---|
94
+ | `limit` | `number` | `20` (`10` in typeahead mode) | `100` (`10` in typeahead mode) |
95
+ | `offset` | `number` | `0` | — |
96
+
97
+ ---
98
+
99
+ ## Response
100
+
101
+ ```ts
102
+ {
103
+ items: Contact[],
104
+ limit: number,
105
+ offset: number,
106
+ }
107
+ ```
108
+
109
+ Each `Contact` item follows the shape defined in `src/types/contact.ts`.
110
+
111
+ ---
112
+
113
+ ## SDK usage
114
+
115
+ ```ts
116
+ import { contact } from '@proveanything/smartlinks'
117
+
118
+ const results = await contact.search({
119
+ collectionId: "my-collection",
120
+ q: "jane",
121
+ tags: ["vip"],
122
+ limit: 20,
123
+ })
124
+
125
+ for (const c of results.items) {
126
+ console.log(c.displayName, c.email)
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Performance notes
133
+
134
+ The search endpoint is backed by **PostgreSQL trigram GIN indexes** via the
135
+ `pg_trgm` extension (migration `20260519000001`). This makes `ILIKE '%query%'`
136
+ scans fast even with millions of contacts per org.
137
+
138
+ **What is a trigram?** PostgreSQL decomposes every string into overlapping
139
+ 3-character windows ("trigrams") and stores them in an inverted index. A query
140
+ for `"acme"` is converted into trigrams `" ac", "acm", "cme", "me "` and the
141
+ index returns only rows containing those windows — so Postgres never scans rows
142
+ that can't match, regardless of whether the query is a prefix, suffix, or
143
+ middle-of-word substring.
144
+
145
+ **Type-ahead mode** (`typeahead: true`) is the recommended pattern for
146
+ keystroke-by-keystroke UI. It emits `LIKE 'query%'` (prefix) operators instead
147
+ of `LIKE '%query%'` (substring). Prefix scans are cheaper because they require
148
+ fewer trigram lookups, making them safe to call on every keypress. Minimum query
149
+ length is enforced at 2 characters server-side to prevent over-broad scans.
150
+
151
+ **Future scale path:** At very high volumes, routing `search` through
152
+ Elasticsearch would add typo-tolerance and relevance ranking. The API surface is
153
+ intentionally implementation-agnostic so the backing engine can be swapped
154
+ without client changes.
155
+
156
+ ---
157
+
158
+ ## Error codes
159
+
160
+ | Code | HTTP | Meaning |
161
+ |---|---|---|
162
+ | `SEARCH_REQUIRED` | 400 | No searchable parameter was provided. |
package/openapi.yaml CHANGED
@@ -2586,6 +2586,113 @@ paths:
2586
2586
  description: Unauthorized
2587
2587
  404:
2588
2588
  description: Not found
2589
+ /admin/collection/{collectionId}/contacts/search:
2590
+ get:
2591
+ tags:
2592
+ - contact
2593
+ summary: contact.search
2594
+ operationId: contact_search
2595
+ security:
2596
+ - bearerAuth: []
2597
+ parameters:
2598
+ - name: collectionId
2599
+ in: path
2600
+ required: true
2601
+ schema:
2602
+ type: string
2603
+ - name: q
2604
+ in: query
2605
+ required: false
2606
+ schema:
2607
+ type: string
2608
+ - name: typeahead
2609
+ in: query
2610
+ required: false
2611
+ schema:
2612
+ type: string
2613
+ - name: email
2614
+ in: query
2615
+ required: false
2616
+ schema:
2617
+ type: string
2618
+ - name: phone
2619
+ in: query
2620
+ required: false
2621
+ schema:
2622
+ type: string
2623
+ - name: id
2624
+ in: query
2625
+ required: false
2626
+ schema:
2627
+ type: string
2628
+ - name: userId
2629
+ in: query
2630
+ required: false
2631
+ schema:
2632
+ type: string
2633
+ - name: source
2634
+ in: query
2635
+ required: false
2636
+ schema:
2637
+ type: string
2638
+ - name: locale
2639
+ in: query
2640
+ required: false
2641
+ schema:
2642
+ type: string
2643
+ - name: createdFrom
2644
+ in: query
2645
+ required: false
2646
+ schema:
2647
+ type: string
2648
+ - name: createdTo
2649
+ in: query
2650
+ required: false
2651
+ schema:
2652
+ type: string
2653
+ - name: externalIdKey
2654
+ in: query
2655
+ required: false
2656
+ schema:
2657
+ type: string
2658
+ - name: externalIdValue
2659
+ in: query
2660
+ required: false
2661
+ schema:
2662
+ type: string
2663
+ - name: customFieldKey
2664
+ in: query
2665
+ required: false
2666
+ schema:
2667
+ type: string
2668
+ - name: customFieldValue
2669
+ in: query
2670
+ required: false
2671
+ schema:
2672
+ type: string
2673
+ - name: limit
2674
+ in: query
2675
+ required: false
2676
+ schema:
2677
+ type: string
2678
+ - name: offset
2679
+ in: query
2680
+ required: false
2681
+ schema:
2682
+ type: string
2683
+ responses:
2684
+ 200:
2685
+ description: Success
2686
+ content:
2687
+ application/json:
2688
+ schema:
2689
+ $ref: "#/components/schemas/ContactSearchResponse"
2690
+ 400:
2691
+ description: Bad request
2692
+ 401:
2693
+ description: Unauthorized
2694
+ 404:
2695
+ description: Not found
2589
2696
  /admin/collection/{collectionId}/contacts/upsert:
2590
2697
  post:
2591
2698
  tags:
@@ -19580,6 +19687,70 @@ components:
19580
19687
  - items
19581
19688
  - limit
19582
19689
  - offset
19690
+ ContactSearchParams:
19691
+ type: object
19692
+ properties:
19693
+ collectionId:
19694
+ type: string
19695
+ q:
19696
+ type: string
19697
+ typeahead:
19698
+ type: boolean
19699
+ email:
19700
+ type: string
19701
+ phone:
19702
+ type: string
19703
+ id:
19704
+ type: string
19705
+ userId:
19706
+ type: string
19707
+ tags:
19708
+ type: array
19709
+ items:
19710
+ type: object
19711
+ additionalProperties: true
19712
+ tagsAll:
19713
+ type: array
19714
+ items:
19715
+ type: object
19716
+ additionalProperties: true
19717
+ source:
19718
+ type: string
19719
+ locale:
19720
+ type: string
19721
+ createdFrom:
19722
+ type: string
19723
+ createdTo:
19724
+ type: string
19725
+ externalIdKey:
19726
+ type: string
19727
+ externalIdValue:
19728
+ type: string
19729
+ customFieldKey:
19730
+ type: string
19731
+ customFieldValue:
19732
+ type: string
19733
+ limit:
19734
+ type: number
19735
+ offset:
19736
+ type: number
19737
+ required:
19738
+ - collectionId
19739
+ ContactSearchResponse:
19740
+ type: object
19741
+ properties:
19742
+ items:
19743
+ type: array
19744
+ items:
19745
+ $ref: "#/components/schemas/Contact"
19746
+ limit:
19747
+ type: number
19748
+ offset:
19749
+ type: number
19750
+ required:
19751
+ - items
19752
+ - limit
19753
+ - offset
19583
19754
  PublicContactUpsertResponse:
19584
19755
  type: object
19585
19756
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.14.10",
3
+ "version": "1.14.11",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",