Repository: amp-labs/connectors
Pull request #1082 (2024-10-16 22:53:06)

feat: Intercom Read via search
# Changes

* First of all static schema for conversation originally had the right fields then they were replaced with `newsfeed` object. It seems this issue was not noticed as it affects "selectable fields" for Read. The right conversation schema is selected from OpenAPI (coming from POST search conversations)
* Some objects were not properly handled. A better distinction of `Ampersand Object Name` and matching URL path is introduced for exceptions.
![image](https://github.com/user-attachments/assets/212efb13-69bc-42e9-a66b-30d4ccd776dc)
* Tickets was added as a supported object. It can be queried via POST only. For `builders` this means they must pass Since parameter. This is added into [documentation](https://github.com/amp-labs/docs/pull/78).
* Conversations, Contacts, Tickets fallback to search endpoint when Since parameter is provided.
* Activity Logs is now an only GET endpoint that uses `Since` param.

This should complete full support of searchable objects by `updated_at` property.

Cobalt0s: * **#1082** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1082?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈
* `main`

This stack of pull requests is managed by Graphite. <a href="https://stacking.dev/?utm_source=stack-comment">Learn more about stacking.</a>
<h2></h2>

 Join @Cobalt0s and the rest of your teammates on <a href="https://graphite.dev?utm-source=stack-comment"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="11px" height="11px"/> <b>Graphite</b></a>
  <!-- Current dependencies on/for this PR: -->
Cobalt0s: @RajatPawar addressed your comments in the second commit: https://github.com/amp-labs/connectors/pull/1082/commits/1e66d010e2c17f916646493ace3ee7c4ac824bda

Patch:
diff --git a/providers/intercom/connector.go b/providers/intercom/connector.go
index 3e5f86c2..69179fb5 100644
--- a/providers/intercom/connector.go
+++ b/providers/intercom/connector.go
@@ -59,9 +59,10 @@ func (c *Connector) String() string {
 	return c.Provider() + ".Connector"
 }
 
-// nolint:unused
-func (c *Connector) getURL(arg string) (*urlbuilder.URL, error) {
-	return constructURL(c.BaseURL, arg)
+func (c *Connector) getURL(objectName string) (*urlbuilder.URL, error) {
+	path := objectNameToURLPath.Get(objectName)
+
+	return constructURL(c.BaseURL, path)
 }
 
 func (c *Connector) setBaseURL(newURL string) {
diff --git a/providers/intercom/metadata/schemas.json b/providers/intercom/metadata/schemas.json
index d447b4d6..fd4676c1 100755
--- a/providers/intercom/metadata/schemas.json
+++ b/providers/intercom/metadata/schemas.json
@@ -144,21 +144,32 @@
     "conversations": {
       "displayName": "Conversations",
       "fields": {
-        "body": "body",
-        "cover_image_url": "cover_image_url",
+        "admin_assignee_id": "admin_assignee_id",
+        "ai_agent": "ai_agent",
+        "ai_agent_participated": "ai_agent_participated",
+        "contacts": "contacts",
+        "conversation_parts": "conversation_parts",
+        "conversation_rating": "conversation_rating",
         "created_at": "created_at",
-        "deliver_silently": "deliver_silently",
+        "custom_attributes": "custom_attributes",
+        "first_contact_reply": "first_contact_reply",
         "id": "id",
-        "labels": "labels",
-        "name": "name",
-        "newsfeed_assignments": "newsfeed_assignments",
-        "reactions": "reactions",
-        "sender_id": "sender_id",
+        "linked_objects": "linked_objects",
+        "open": "open",
+        "priority": "priority",
+        "read": "read",
+        "sla_applied": "sla_applied",
+        "snoozed_until": "snoozed_until",
+        "source": "source",
         "state": "state",
+        "statistics": "statistics",
+        "tags": "tags",
+        "team_assignee_id": "team_assignee_id",
+        "teammates": "teammates",
         "title": "title",
         "type": "type",
         "updated_at": "updated_at",
-        "workspace_id": "workspace_id"
+        "waiting_since": "waiting_since"
       }
     },
     "data_attributes": {
@@ -245,30 +256,6 @@
         "workspace_id": "workspace_id"
       }
     },
-    "scroll": {
-      "displayName": "Company Scroll",
-      "fields": {
-        "app_id": "app_id",
-        "company_id": "company_id",
-        "created_at": "created_at",
-        "custom_attributes": "custom_attributes",
-        "id": "id",
-        "industry": "industry",
-        "last_request_at": "last_request_at",
-        "monthly_spend": "monthly_spend",
-        "name": "name",
-        "plan": "plan",
-        "remote_created_at": "remote_created_at",
-        "segments": "segments",
-        "session_count": "session_count",
-        "size": "size",
-        "tags": "tags",
-        "type": "type",
-        "updated_at": "updated_at",
-        "user_count": "user_count",
-        "website": "website"
-      }
-    },
     "segments": {
       "displayName": "Segments",
       "fields": {
@@ -328,6 +315,30 @@
         "updated_at": "updated_at",
         "workspace_id": "workspace_id"
       }
+    },
+    "tickets": {
+      "displayName": "Tickets",
+      "fields": {
+        "admin_assignee_id": "admin_assignee_id",
+        "category": "category",
+        "contacts": "contacts",
+        "created_at": "created_at",
+        "id": "id",
+        "is_shared": "is_shared",
+        "linked_objects": "linked_objects",
+        "open": "open",
+        "snoozed_until": "snoozed_until",
+        "team_assignee_id": "team_assignee_id",
+        "ticket_attributes": "ticket_attributes",
+        "ticket_id": "ticket_id",
+        "ticket_parts": "ticket_parts",
+        "ticket_state": "ticket_state",
+        "ticket_state_external_label": "ticket_state_external_label",
+        "ticket_state_internal_label": "ticket_state_internal_label",
+        "ticket_type": "ticket_type",
+        "type": "type",
+        "updated_at": "updated_at"
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/providers/intercom/objectNames.go b/providers/intercom/objectNames.go
index 3ae5c7ce..fe93c079 100644
--- a/providers/intercom/objectNames.go
+++ b/providers/intercom/objectNames.go
@@ -5,6 +5,10 @@ import (
 	"github.com/amp-labs/connectors/providers/intercom/metadata"
 )
 
+// Tickets is a special object which cannot be READ using GET.
+// Full read and incremental read are done for tickets using POST search.
+const ticketsObjectName = "tickets"
+
 // Supported object names can be found under schemas.json.
 var supportedObjectsByRead = handy.NewSetFromList( //nolint:gochecknoglobals
 	metadata.Schemas.GetObjectNames(),
@@ -18,9 +22,33 @@ var ObjectNameToResponseField = handy.NewDefaultMap(map[string]string{ //nolint:
 	"events":        "events",
 	"segments":      "segments",
 	"activity_logs": "activity_logs",
+	"tickets":       "tickets",
+	"conversations": "conversations",
 },
 	func(key string) string {
 		// Other objects are mapped to `data`.
 		return "data"
 	},
 )
+
+var objectNameToURLPath = handy.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+	"activity_logs": "/admins/activity_logs",
+	"collections":   "/help_center/collections",
+	"help_centers":  "/help_center/help_centers",
+	"news_items":    "/news/news_items",
+	"newsfeeds":     "/news/newsfeeds",
+}, func(obj string) string {
+	return obj
+})
+
+// nolint:gomnd
+var incrementalSearchObjectPagination = handy.NewDefaultMap(map[string]int{ //nolint:gochecknoglobals
+	// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/conversations/searchconversations
+	"conversations": 150,
+	// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/searchcontacts
+	"contacts": 50,
+	// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/tickets/searchtickets
+	ticketsObjectName: 150,
+}, func(k string) int {
+	return DefaultPageSize
+})
diff --git a/providers/intercom/params.go b/providers/intercom/params.go
index 57b7ab5e..549ac10d 100644
--- a/providers/intercom/params.go
+++ b/providers/intercom/params.go
@@ -10,11 +10,8 @@ import (
 	"golang.org/x/oauth2"
 )
 
-const (
-	// DefaultPageSize is number of elements per page.
-	DefaultPageSize              = 60
-	DefaultConversationsPageSize = 150
-)
+// DefaultPageSize is the number of elements per page.
+const DefaultPageSize = 60
 
 // Option is a function which mutates the connector configuration.
 type Option = func(params *parameters)
diff --git a/providers/intercom/read.go b/providers/intercom/read.go
index 2250f8eb..cbae4b33 100644
--- a/providers/intercom/read.go
+++ b/providers/intercom/read.go
@@ -37,25 +37,8 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 func (c *Connector) performReadQuery(
 	ctx context.Context, config common.ReadParams,
 ) (*common.JSONHTTPResponse, *urlbuilder.URL, error) {
-	if config.ObjectName == "conversations" && !config.Since.IsZero() {
-		// Conversations with non-empty Since fallback to POST, searching for conversation by time.
-		// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/conversations/searchconversations
-		url, err := constructURL(c.BaseURL, config.ObjectName, "search")
-		if err != nil {
-			return nil, nil, err
-		}
-
-		conversation, err := c.createSearchPayload(config.NextPage, config.Since)
-		if err != nil {
-			return nil, nil, err
-		}
-
-		rsp, err := c.Client.Post(ctx, url.String(), &conversation, apiVersionHeader)
-		if err != nil {
-			return nil, nil, err
-		}
-
-		return rsp, url, nil
+	if rsp, url, err, searchable := c.readViaSearch(ctx, config); searchable {
+		return rsp, url, err
 	}
 
 	// Default.
@@ -65,6 +48,8 @@ func (c *Connector) performReadQuery(
 		return nil, nil, err
 	}
 
+	url = enhanceReadWithQueryParams(url, config.ObjectName, config.Since)
+
 	rsp, err := c.Client.Get(ctx, url.String(), apiVersionHeader)
 	if err != nil {
 		return nil, nil, err
@@ -90,54 +75,10 @@ func (c *Connector) buildReadURL(config common.ReadParams) (*urlbuilder.URL, err
 	return url, nil
 }
 
-func (c *Connector) createSearchPayload(
-	nextPageURL common.NextPageToken, since time.Time,
-) (*searchReqPayload, error) {
-	url, err := urlbuilder.New(nextPageURL.String())
-	if err != nil {
-		return nil, err
+func enhanceReadWithQueryParams(url *urlbuilder.URL, objectName string, since time.Time) *urlbuilder.URL {
+	if objectName == "activity_logs" && !since.IsZero() {
+		url.WithQueryParam("created_at_after", strconv.FormatInt(since.Unix(), 10))
 	}
 
-	// Unix time format is used.
-	updatedAfter := strconv.FormatInt(since.Unix(), 10)
-	// We no longer request by GET, so query parameter must be moved to the POST payload.
-	startingAfter, _ := url.GetFirstQueryParam("starting_after")
-
-	conversation := searchReqPayload{
-		Query: searchQuery{
-			Operator: "AND",
-			Value: []searchQueryValue{{
-				Field:    "updated_at",
-				Operator: ">=",
-				Value:    updatedAfter,
-			}},
-		},
-		Pagination: searchPagination{
-			PerPage:       DefaultConversationsPageSize,
-			StartingAfter: startingAfter,
-		},
-	}
-
-	return &conversation, nil
-}
-
-type searchReqPayload struct {
-	Query      searchQuery      `json:"query"`
-	Pagination searchPagination `json:"pagination"`
-}
-
-type searchQuery struct {
-	Operator string             `json:"operator"`
-	Value    []searchQueryValue `json:"value"`
-}
-
-type searchQueryValue struct {
-	Field    string `json:"field"`
-	Operator string `json:"operator"`
-	Value    string `json:"value"`
-}
-
-type searchPagination struct {
-	PerPage       int    `json:"per_page"`                 //nolint:tagliatelle
-	StartingAfter string `json:"starting_after,omitempty"` //nolint:tagliatelle
+	return url
 }
diff --git a/providers/intercom/search.go b/providers/intercom/search.go
new file mode 100644
index 00000000..81a094d5
--- /dev/null
+++ b/providers/intercom/search.go
@@ -0,0 +1,116 @@
+package intercom
+
+import (
+	"context"
+	"strconv"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+)
+
+// Search is used when `Since` parameter is provided for the select few objects.
+// Documentation below explains how search is done for "conversations" object. Others, use the same query language.
+// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/conversations/searchconversations
+// https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/searchcontacts
+func (c *Connector) readViaSearch(
+	ctx context.Context, config common.ReadParams,
+) (*common.JSONHTTPResponse, *urlbuilder.URL, error, bool) {
+	if !incrementalSearchObjectPagination.Has(config.ObjectName) {
+		return nil, nil, nil, false
+	}
+
+	if config.Since.IsZero() && config.ObjectName != ticketsObjectName {
+		// Search is only relevant when we do incremental reading.
+		// Tickets is an exception. We can do full read only via POST.
+		return nil, nil, nil, false
+	}
+
+	url, err := constructURL(c.BaseURL, config.ObjectName, "search")
+	if err != nil {
+		return nil, nil, err, true
+	}
+
+	conversation, err := c.createSearchPayload(config)
+	if err != nil {
+		return nil, nil, err, true
+	}
+
+	rsp, err := c.Client.Post(ctx, url.String(), &conversation, apiVersionHeader)
+	if err != nil {
+		return nil, nil, err, true
+	}
+
+	return rsp, url, nil, true
+}
+
+func (c *Connector) createSearchPayload(params common.ReadParams) (*searchReqPayload, error) {
+	if params.Since.IsZero() && params.ObjectName == ticketsObjectName {
+		// Perform full read for tickets using POST query.
+		// This is a hack, query is designed to return all objects.
+		return &searchReqPayload{
+			Query: searchQuery{
+				Operator: "OR",
+				Value: []searchQueryValue{{
+					Field:    "open",
+					Operator: "=",
+					Value:    "true",
+				}, {
+					Field:    "open",
+					Operator: "=",
+					Value:    "false",
+				}},
+			},
+			Pagination: searchPagination{
+				PerPage: incrementalSearchObjectPagination.Get(params.ObjectName),
+			},
+		}, nil
+	}
+
+	url, err := urlbuilder.New(params.NextPage.String())
+	if err != nil {
+		return nil, err
+	}
+
+	// Unix time format is used.
+	updatedAfter := strconv.FormatInt(params.Since.Unix(), 10)
+	// We no longer request by GET, so query parameter must be moved to the POST payload.
+	startingAfter, _ := url.GetFirstQueryParam("starting_after")
+
+	conversation := searchReqPayload{
+		Query: searchQuery{
+			Operator: "AND",
+			Value: []searchQueryValue{{
+				Field:    "updated_at",
+				Operator: ">",
+				Value:    updatedAfter,
+			}},
+		},
+		Pagination: searchPagination{
+			PerPage:       incrementalSearchObjectPagination.Get(params.ObjectName),
+			StartingAfter: startingAfter,
+		},
+	}
+
+	return &conversation, nil
+}
+
+type searchReqPayload struct {
+	Query      searchQuery      `json:"query"`
+	Pagination searchPagination `json:"pagination"`
+}
+
+type searchQuery struct {
+	Operator string             `json:"operator"`
+	Value    []searchQueryValue `json:"value"`
+}
+
+type searchQueryValue struct {
+	Field    string `json:"field"`
+	Operator string `json:"operator"`
+	Value    string `json:"value"`
+}
+
+type searchPagination struct {
+	PerPage       int    `json:"per_page"`                 //nolint:tagliatelle
+	StartingAfter string `json:"starting_after,omitempty"` //nolint:tagliatelle
+}
diff --git a/scripts/openapi/intercom/metadata/main.go b/scripts/openapi/intercom/metadata/main.go
index 3a995225..490565a7 100644
--- a/scripts/openapi/intercom/metadata/main.go
+++ b/scripts/openapi/intercom/metadata/main.go
@@ -21,6 +21,7 @@ var (
 		"/me",
 		"/tickets/search",
 		"/contacts/search",
+		"/companies/scroll", // covered by companies
 		"/conversations/search",
 		"/articles/search", // this one is similar to /articles
 	}
@@ -33,19 +34,38 @@ var (
 		"segments":        "Segments",
 		"news_items":      "News Items",
 		"newsfeeds":       "Newsfeeds",
+		"tickets":         "Tickets",
+	}
+	searchEndpoints = []string{ // nolint:gochecknoglobals
+		"*/search",
+	}
+	searchObjectEndpoints = map[string]string{ // nolint:gochecknoglobals
+		"/contacts/search":      "contacts",
+		"/conversations/search": "conversations",
+		"/articles/search":      "articles",
+		"/tickets/search":       "tickets",
 	}
 )
 
 func main() {
 	explorer, err := openapi.FileManager.GetExplorer()
-	must(err)
+	handy.Must(err)
 
-	objects, err := explorer.ReadObjectsGet(
+	readObjects, err := explorer.ReadObjectsGet(
 		api3.NewDenyPathStrategy(ignoreEndpoints),
 		nil, displayNameOverride,
 		api3.CustomMappingObjectCheck(intercom.ObjectNameToResponseField),
 	)
-	must(err)
+	handy.Must(err)
+
+	searchObjects, err := explorer.ReadObjectsPost(
+		api3.NewAllowPathStrategy(searchEndpoints),
+		searchObjectEndpoints, displayNameOverride,
+		api3.CustomMappingObjectCheck(intercom.ObjectNameToResponseField),
+	)
+	handy.Must(err)
+
+	objects := searchObjects.Combine(readObjects)
 
 	schemas := scrapper.NewObjectMetadataResult()
 	registry := handy.NamedLists[string]{}
@@ -67,14 +87,8 @@ func main() {
 		}
 	}
 
-	must(metadata.FileManager.SaveSchemas(schemas))
-	must(metadata.FileManager.SaveQueryParamStats(scrapper.CalculateQueryParamStats(registry)))
+	handy.Must(metadata.FileManager.SaveSchemas(schemas))
+	handy.Must(metadata.FileManager.SaveQueryParamStats(scrapper.CalculateQueryParamStats(registry)))
 
 	slog.Info("Completed.")
 }
-
-func must(err error) {
-	if err != nil {
-		panic(err)
-	}
-}


Test patch:
diff --git a/providers/intercom/read_test.go b/providers/intercom/read_test.go
index c8770d4a..ab9280f3 100644
--- a/providers/intercom/read_test.go
+++ b/providers/intercom/read_test.go
@@ -5,6 +5,7 @@ import (
 	"net/http"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
@@ -31,6 +32,8 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 	responseContactsSecondPage := testutils.DataFromFile(t, "read-contacts-2-second-page.json")
 	responseContactsThirdPage := testutils.DataFromFile(t, "read-contacts-3-last-page.json")
 	responseReadConversations := testutils.DataFromFile(t, "read-conversations.json")
+	requestSearchConversations := testutils.DataFromFile(t, "read-search-conversations-request.json")
+	responseSearchConversations := testutils.DataFromFile(t, "read-search-conversations.json")
 	responseNotesFirstPage := testutils.DataFromFile(t, "read-notes-1-first-page.json")
 	responseNotesSecondPage := testutils.DataFromFile(t, "read-notes-2-last-page.json")
 
@@ -259,6 +262,46 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 			},
 			ExpectedErrs: nil,
 		},
+		{
+			Name: "Incremental read of conversations via search",
+			Input: common.ReadParams{
+				ObjectName: "conversations",
+				Fields:     connectors.Fields("id", "state", "title"),
+				Since:      time.Unix(1726674883, 0),
+			},
+			// notes is not supported for now, but its payload is good for testing
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.BodyBytes(requestSearchConversations),
+				Then:  mockserver.Response(http.StatusOK, responseSearchConversations),
+			}.Server(),
+			Comparator: func(baseURL string, actual, expected *common.ReadResult) bool {
+				expectedNextPage := strings.ReplaceAll(expected.NextPage.String(), "{{testServerURL}}", baseURL)
+
+				return mockutils.ReadResultComparator.SubsetFields(actual, expected) &&
+					mockutils.ReadResultComparator.SubsetRaw(actual, expected) &&
+					actual.NextPage.String() == expectedNextPage &&
+					actual.Done == expected.Done
+			},
+			Expected: &common.ReadResult{
+				Rows: 1,
+				Data: []common.ReadResultRow{{
+					Fields: map[string]any{
+						"id":    "5",
+						"state": "open",
+						"title": "What is return policy?",
+					},
+					Raw: map[string]any{
+						"ai_agent_participated": false,
+						"created_at":            float64(1726752048),
+						"updated_at":            float64(1726752145),
+					},
+				}},
+				NextPage: "{{testServerURL}}/conversations/search?starting_after=WzE3MjY3NTIxNDUwMDAsNSwyXQ==",
+				Done:     false,
+			},
+			ExpectedErrs: nil,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/providers/intercom/test/read-search-conversations-request.json b/providers/intercom/test/read-search-conversations-request.json
new file mode 100644
index 00000000..f2a317d3
--- /dev/null
+++ b/providers/intercom/test/read-search-conversations-request.json
@@ -0,0 +1,15 @@
+{
+  "query": {
+    "operator": "AND",
+    "value": [
+      {
+        "field": "updated_at",
+        "operator": ">",
+        "value": "1726674883"
+      }
+    ]
+  },
+  "pagination": {
+    "per_page": 150
+  }
+}
diff --git a/providers/intercom/test/read-search-conversations.json b/providers/intercom/test/read-search-conversations.json
new file mode 100644
index 00000000..f0ae282e
--- /dev/null
+++ b/providers/intercom/test/read-search-conversations.json
@@ -0,0 +1,115 @@
+{
+  "type": "conversation.list",
+  "pages": {
+    "type": "pages",
+    "next": {
+      "page": 2,
+      "starting_after": "WzE3MjY3NTIxNDUwMDAsNSwyXQ=="
+    },
+    "page": 1,
+    "per_page": 1,
+    "total_pages": 2
+  },
+  "total_count": 2,
+  "conversations": [
+    {
+      "type": "conversation",
+      "id": "5",
+      "created_at": 1726752048,
+      "updated_at": 1726752145,
+      "waiting_since": null,
+      "snoozed_until": null,
+      "source": {
+        "type": "conversation",
+        "id": "2416030620",
+        "delivered_as": "customer_initiated",
+        "subject": "",
+        "body": "<p>wwww</p>",
+        "author": {
+          "type": "user",
+          "id": "66eaf772c864ff5de8d2e180",
+          "name": "CK",
+          "email": "ck@gmail.com"
+        },
+        "attachments": [],
+        "url": "http://localhost:5173/",
+        "redacted": false
+      },
+      "contacts": {
+        "type": "contact.list",
+        "contacts": [
+          {
+            "type": "contact",
+            "id": "66eaf772c864ff5de8d2e180",
+            "external_id": "951357"
+          }
+        ]
+      },
+      "first_contact_reply": {
+        "created_at": 1726752048,
+        "type": "conversation",
+        "url": "http://localhost:5173/"
+      },
+      "admin_assignee_id": null,
+      "team_assignee_id": null,
+      "open": true,
+      "state": "open",
+      "read": true,
+      "tags": {
+        "type": "tag.list",
+        "tags": []
+      },
+      "priority": "not_priority",
+      "sla_applied": null,
+      "statistics": {
+        "type": "conversation_statistics",
+        "time_to_assignment": null,
+        "time_to_admin_reply": 96,
+        "time_to_first_close": null,
+        "time_to_last_close": null,
+        "median_time_to_reply": 31,
+        "first_contact_reply_at": 1726752049,
+        "first_assignment_at": null,
+        "first_admin_reply_at": 1726752145,
+        "first_close_at": null,
+        "last_assignment_at": null,
+        "last_assignment_admin_reply_at": null,
+        "last_contact_reply_at": 1726752114,
+        "last_admin_reply_at": 1726752145,
+        "last_close_at": null,
+        "last_closed_by_id": null,
+        "count_reopens": 0,
+        "count_assignments": 0,
+        "count_conversation_parts": 7
+      },
+      "conversation_rating": null,
+      "teammates": {
+        "type": "admin.list",
+        "admins": [
+          {
+            "type": "admin",
+            "id": "7387622"
+          }
+        ]
+      },
+      "title": "What is return policy?",
+      "custom_attributes": {
+        "Copilot used": false
+      },
+      "topics": {
+        "type": "topic.list",
+        "topics": [],
+        "total_count": 0
+      },
+      "ticket": null,
+      "linked_objects": {
+        "type": "list",
+        "data": [],
+        "total_count": 0,
+        "has_more": false
+      },
+      "ai_agent": null,
+      "ai_agent_participated": false
+    }
+  ]
+}
diff --git a/test/intercom/search/main.go b/test/intercom/search/main.go
index 4f47a6e9..51ae0267 100644
--- a/test/intercom/search/main.go
+++ b/test/intercom/search/main.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/amp-labs/connectors"
 	"github.com/amp-labs/connectors/common"
-	"github.com/amp-labs/connectors/providers/intercom"
 	msTest "github.com/amp-labs/connectors/test/intercom"
 	"github.com/amp-labs/connectors/test/utils"
 )
@@ -42,10 +41,6 @@ func main() {
 	fmt.Println("Reading conversations..")
 	utils.DumpJSON(res, os.Stdout)
 
-	if res.Rows > intercom.DefaultConversationsPageSize {
-		utils.Fail(fmt.Sprintf("expected max %v rows", intercom.DefaultConversationsPageSize))
-	}
-
 	if len(res.NextPage) == 0 {
 		return
 	}