forest_admin_datasource_intercom 1.49.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/LICENSE +202 -0
- data/README.md +811 -0
- data/Rakefile +6 -0
- data/exe/forest_admin_intercom_probe +287 -0
- data/forest_admin_datasource_intercom.gemspec +38 -0
- data/lib/forest_admin_datasource_intercom/cache.rb +84 -0
- data/lib/forest_admin_datasource_intercom/client.rb +546 -0
- data/lib/forest_admin_datasource_intercom/collections/admin.rb +83 -0
- data/lib/forest_admin_datasource_intercom/collections/base_collection.rb +161 -0
- data/lib/forest_admin_datasource_intercom/collections/company/serializer.rb +44 -0
- data/lib/forest_admin_datasource_intercom/collections/company.rb +69 -0
- data/lib/forest_admin_datasource_intercom/collections/contact/serializer.rb +106 -0
- data/lib/forest_admin_datasource_intercom/collections/contact.rb +266 -0
- data/lib/forest_admin_datasource_intercom/collections/contact_identity.rb +77 -0
- data/lib/forest_admin_datasource_intercom/collections/conversation/serializer.rb +75 -0
- data/lib/forest_admin_datasource_intercom/collections/conversation/timeline.rb +40 -0
- data/lib/forest_admin_datasource_intercom/collections/conversation.rb +153 -0
- data/lib/forest_admin_datasource_intercom/collections/cursor_collection.rb +363 -0
- data/lib/forest_admin_datasource_intercom/collections/custom_attributes.rb +55 -0
- data/lib/forest_admin_datasource_intercom/collections/fetch_all_collection.rb +196 -0
- data/lib/forest_admin_datasource_intercom/collections/offset_collection.rb +311 -0
- data/lib/forest_admin_datasource_intercom/collections/records_by_id.rb +97 -0
- data/lib/forest_admin_datasource_intercom/collections/relations.rb +294 -0
- data/lib/forest_admin_datasource_intercom/collections/team.rb +70 -0
- data/lib/forest_admin_datasource_intercom/collections/team_membership.rb +51 -0
- data/lib/forest_admin_datasource_intercom/collections/ticket/derived_columns.rb +130 -0
- data/lib/forest_admin_datasource_intercom/collections/ticket/serializer.rb +70 -0
- data/lib/forest_admin_datasource_intercom/collections/ticket.rb +143 -0
- data/lib/forest_admin_datasource_intercom/collections/ticket_state.rb +40 -0
- data/lib/forest_admin_datasource_intercom/collections/ticket_type.rb +46 -0
- data/lib/forest_admin_datasource_intercom/collections/timeline.rb +40 -0
- data/lib/forest_admin_datasource_intercom/configuration.rb +226 -0
- data/lib/forest_admin_datasource_intercom/datasource.rb +78 -0
- data/lib/forest_admin_datasource_intercom/pagination/cursor_walker.rb +123 -0
- data/lib/forest_admin_datasource_intercom/query/caller_zone.rb +48 -0
- data/lib/forest_admin_datasource_intercom/query/condition_tree_translator.rb +169 -0
- data/lib/forest_admin_datasource_intercom/query/day_bounds.rb +75 -0
- data/lib/forest_admin_datasource_intercom/query/filter_value.rb +177 -0
- data/lib/forest_admin_datasource_intercom/query/operator_table.rb +54 -0
- data/lib/forest_admin_datasource_intercom/query/search_fields.rb +225 -0
- data/lib/forest_admin_datasource_intercom/query/search_fields.yml +663 -0
- data/lib/forest_admin_datasource_intercom/rate_limiter.rb +148 -0
- data/lib/forest_admin_datasource_intercom/retry_policy.rb +95 -0
- data/lib/forest_admin_datasource_intercom/schema/attribute_naming.rb +52 -0
- data/lib/forest_admin_datasource_intercom/schema/data_attributes_introspector.rb +88 -0
- data/lib/forest_admin_datasource_intercom/schema/ticket_attributes_introspector.rb +133 -0
- data/lib/forest_admin_datasource_intercom/throttle.rb +19 -0
- data/lib/forest_admin_datasource_intercom/version.rb +3 -0
- data/lib/forest_admin_datasource_intercom.rb +57 -0
- metadata +180 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# The table of (endpoint x field x operator): what Intercom's search endpoints
|
|
2
|
+
# really filter, and with which operators. It is the single source of truth of
|
|
3
|
+
# this datasource's filtering -- the schema derives every column's
|
|
4
|
+
# filter_operators from it, so no collection can advertise a filter the
|
|
5
|
+
# translator would then refuse.
|
|
6
|
+
#
|
|
7
|
+
# `source` is the provenance of a row, and it is not decoration:
|
|
8
|
+
#
|
|
9
|
+
# measured -- observed against a real workspace, by `forest_admin_intercom_probe`
|
|
10
|
+
# or during a spike, and recorded here with the date;
|
|
11
|
+
# spec -- read off Intercom's documentation and nothing else.
|
|
12
|
+
#
|
|
13
|
+
# The distinction exists because the two disagree. Measured during lot 1:
|
|
14
|
+
# `company_id` is documented on a ticket and refused by `/tickets/search` with
|
|
15
|
+
# `invalid_field`. A `spec` row is therefore a candidate, never a promise, and
|
|
16
|
+
# the probe is what turns it into one. Run it against the customer's workspace
|
|
17
|
+
# and merge the evidence it writes into this table by hand.
|
|
18
|
+
#
|
|
19
|
+
# `type` decides how a column's Forest operators are derived from the Intercom
|
|
20
|
+
# ones, and how a value reaches the wire:
|
|
21
|
+
#
|
|
22
|
+
# string -- an exact-match field
|
|
23
|
+
# text -- a field Intercom also matches per word (`~`)
|
|
24
|
+
# date -- epoch seconds on the wire, truncated to the UTC day on read
|
|
25
|
+
# boolean, number
|
|
26
|
+
version: 1
|
|
27
|
+
|
|
28
|
+
endpoints:
|
|
29
|
+
conversations:
|
|
30
|
+
path: conversations/search
|
|
31
|
+
# Nothing has been probed yet: fill this in with the probe's output.
|
|
32
|
+
measured_at: null
|
|
33
|
+
fields:
|
|
34
|
+
# The primary key, and the one column whose Forest operators are not
|
|
35
|
+
# derived from this table: every collection publishes `equal` and `in` on
|
|
36
|
+
# its key whatever is written here, the toolkit refusing a collection
|
|
37
|
+
# whose key carries neither. It has to be declared all the same, or the
|
|
38
|
+
# schema advertises a filter the translator has no row to write -- which
|
|
39
|
+
# is what a record detail becomes the moment a permission scope turns
|
|
40
|
+
# `id equals X` into an `and` the record endpoint cannot answer alone.
|
|
41
|
+
#
|
|
42
|
+
# A bare `id equals X` still reads the record endpoint, one request
|
|
43
|
+
# instead of a search. This row is for the compound case.
|
|
44
|
+
id:
|
|
45
|
+
field: id
|
|
46
|
+
type: string
|
|
47
|
+
operators: ['=', 'IN']
|
|
48
|
+
source: spec
|
|
49
|
+
state:
|
|
50
|
+
field: state
|
|
51
|
+
type: string
|
|
52
|
+
operators: ['=', '!=']
|
|
53
|
+
source: spec
|
|
54
|
+
open:
|
|
55
|
+
field: open
|
|
56
|
+
type: boolean
|
|
57
|
+
operators: ['=']
|
|
58
|
+
source: spec
|
|
59
|
+
read:
|
|
60
|
+
field: read
|
|
61
|
+
type: boolean
|
|
62
|
+
operators: ['=']
|
|
63
|
+
source: spec
|
|
64
|
+
priority:
|
|
65
|
+
field: priority
|
|
66
|
+
type: string
|
|
67
|
+
operators: ['=', '!=']
|
|
68
|
+
source: spec
|
|
69
|
+
title:
|
|
70
|
+
field: title
|
|
71
|
+
type: text
|
|
72
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
73
|
+
source: spec
|
|
74
|
+
admin_assignee_id:
|
|
75
|
+
field: admin_assignee_id
|
|
76
|
+
type: string
|
|
77
|
+
operators: ['=', '!=']
|
|
78
|
+
source: spec
|
|
79
|
+
team_assignee_id:
|
|
80
|
+
field: team_assignee_id
|
|
81
|
+
type: string
|
|
82
|
+
operators: ['=', '!=']
|
|
83
|
+
source: spec
|
|
84
|
+
source_type:
|
|
85
|
+
field: source.type
|
|
86
|
+
type: string
|
|
87
|
+
operators: ['=', '!=']
|
|
88
|
+
source: spec
|
|
89
|
+
source_delivered_as:
|
|
90
|
+
field: source.delivered_as
|
|
91
|
+
type: string
|
|
92
|
+
operators: ['=', '!=']
|
|
93
|
+
source: spec
|
|
94
|
+
source_subject:
|
|
95
|
+
field: source.subject
|
|
96
|
+
type: text
|
|
97
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
98
|
+
source: spec
|
|
99
|
+
# The full-text search of the collection: Intercom matches `~` per word,
|
|
100
|
+
# not as a substring, which the README states rather than the interface
|
|
101
|
+
# implying otherwise.
|
|
102
|
+
source_body:
|
|
103
|
+
field: source.body
|
|
104
|
+
type: text
|
|
105
|
+
operators: ['~', '!~']
|
|
106
|
+
source: spec
|
|
107
|
+
source_author_email:
|
|
108
|
+
field: source.author.email
|
|
109
|
+
type: text
|
|
110
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
111
|
+
source: spec
|
|
112
|
+
# The date operators are measured, and they are not the same on every
|
|
113
|
+
# endpoint: `/contacts/search` refuses `>=`, `<=` and `!=` where this one
|
|
114
|
+
# accepts them (25 August 2026, API 2.16). Whatever this table allows, a
|
|
115
|
+
# Date column publishes the two bounds alone -- see the translator.
|
|
116
|
+
created_at:
|
|
117
|
+
field: created_at
|
|
118
|
+
type: date
|
|
119
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
120
|
+
source: measured
|
|
121
|
+
updated_at:
|
|
122
|
+
field: updated_at
|
|
123
|
+
type: date
|
|
124
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
125
|
+
source: measured
|
|
126
|
+
waiting_since:
|
|
127
|
+
field: waiting_since
|
|
128
|
+
type: date
|
|
129
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
130
|
+
source: spec
|
|
131
|
+
snoozed_until:
|
|
132
|
+
field: snoozed_until
|
|
133
|
+
type: date
|
|
134
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
135
|
+
source: spec
|
|
136
|
+
closed_at:
|
|
137
|
+
field: statistics.last_close_at
|
|
138
|
+
type: date
|
|
139
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
140
|
+
source: spec
|
|
141
|
+
first_closed_at:
|
|
142
|
+
field: statistics.first_close_at
|
|
143
|
+
type: date
|
|
144
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
145
|
+
source: spec
|
|
146
|
+
closed_by_id:
|
|
147
|
+
field: statistics.last_closed_by_id
|
|
148
|
+
type: string
|
|
149
|
+
operators: ['=', '!=']
|
|
150
|
+
source: spec
|
|
151
|
+
first_contact_reply_at:
|
|
152
|
+
field: statistics.first_contact_reply_at
|
|
153
|
+
type: date
|
|
154
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
155
|
+
source: spec
|
|
156
|
+
last_contact_reply_at:
|
|
157
|
+
field: statistics.last_contact_reply_at
|
|
158
|
+
type: date
|
|
159
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
160
|
+
source: spec
|
|
161
|
+
last_admin_reply_at:
|
|
162
|
+
field: statistics.last_admin_reply_at
|
|
163
|
+
type: date
|
|
164
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
165
|
+
source: spec
|
|
166
|
+
reopen_count:
|
|
167
|
+
field: statistics.count_reopens
|
|
168
|
+
type: number
|
|
169
|
+
operators: ['=', '!=', '>', '<']
|
|
170
|
+
source: spec
|
|
171
|
+
part_count:
|
|
172
|
+
field: statistics.count_conversation_parts
|
|
173
|
+
type: number
|
|
174
|
+
operators: ['=', '!=', '>', '<']
|
|
175
|
+
source: spec
|
|
176
|
+
ai_agent_participated:
|
|
177
|
+
field: ai_agent_participated
|
|
178
|
+
type: boolean
|
|
179
|
+
operators: ['=']
|
|
180
|
+
source: spec
|
|
181
|
+
# The foreign key of the `contact` relation, and the reason lot 4 could
|
|
182
|
+
# turn the denormalized contact columns into one: Intercom matches a
|
|
183
|
+
# conversation against one of the contacts it carries. The column is
|
|
184
|
+
# singular and the wire field plural on purpose -- the row names its first
|
|
185
|
+
# contact, the endpoint asks "is this contact one of yours".
|
|
186
|
+
#
|
|
187
|
+
# `!=` is deliberately absent: on a field holding a list, "not that
|
|
188
|
+
# contact" is a question Intercom's DSL does not clearly answer, and the
|
|
189
|
+
# one thing this table may not do is publish a filter whose meaning is a
|
|
190
|
+
# guess.
|
|
191
|
+
contact_id:
|
|
192
|
+
field: contact_ids
|
|
193
|
+
type: string
|
|
194
|
+
operators: ['=']
|
|
195
|
+
source: measured
|
|
196
|
+
# Columns this collection publishes and will not filter, with the reason an
|
|
197
|
+
# operator reads when they try. A column absent from both tables is refused
|
|
198
|
+
# by name too -- these are the ones whose refusal is permanent and has an
|
|
199
|
+
# explanation worth giving.
|
|
200
|
+
refused:
|
|
201
|
+
tag_names:
|
|
202
|
+
reason: >-
|
|
203
|
+
Intercom filters conversations by tag id, and this column holds the
|
|
204
|
+
tag names. Filtering it would mean resolving a name to an id per
|
|
205
|
+
request, and a name the workspace renamed would silently match
|
|
206
|
+
nothing.
|
|
207
|
+
source: spec
|
|
208
|
+
company_name:
|
|
209
|
+
reason: >-
|
|
210
|
+
The conversation carries its company as an object read from the
|
|
211
|
+
payload; the search endpoint filters no company field.
|
|
212
|
+
source: spec
|
|
213
|
+
company_id:
|
|
214
|
+
reason: >-
|
|
215
|
+
The search endpoint filters no company field. Measured on
|
|
216
|
+
`/tickets/search`, which refuses `company_id` with `invalid_field`
|
|
217
|
+
although a ticket carries one; the same is assumed here until probed.
|
|
218
|
+
source: spec
|
|
219
|
+
timeline:
|
|
220
|
+
reason: >-
|
|
221
|
+
Built by the agent from the parts of a conversation, which the search
|
|
222
|
+
endpoint does not read.
|
|
223
|
+
source: measured
|
|
224
|
+
contact_name:
|
|
225
|
+
reason: >-
|
|
226
|
+
Read from the Contacts endpoint, not from the conversation. Filter
|
|
227
|
+
through the `contact` relation, which resolves against
|
|
228
|
+
`/contacts/search` and rewrites onto the contact id -- or on
|
|
229
|
+
`source_author_email`, which is on the conversation itself.
|
|
230
|
+
source: spec
|
|
231
|
+
contact_count:
|
|
232
|
+
reason: Counted by the agent from the contacts the payload carries.
|
|
233
|
+
source: measured
|
|
234
|
+
source_author_name:
|
|
235
|
+
reason: >-
|
|
236
|
+
The endpoint matches the author of the opening message by e-mail
|
|
237
|
+
alone -- `source.author.email` is a field, `source.author.name` is
|
|
238
|
+
not. Filter on `source_author_email`.
|
|
239
|
+
source: spec
|
|
240
|
+
# What `forest_admin_intercom_probe` enumerates on top of the fields above:
|
|
241
|
+
# names the documentation mentions, or that an ops team would plausibly
|
|
242
|
+
# search on. A candidate that turns out to be filterable becomes a field
|
|
243
|
+
# above -- with a column to expose it on, or with none, in which case it is
|
|
244
|
+
# a column the next lot may add.
|
|
245
|
+
candidates:
|
|
246
|
+
- source.id
|
|
247
|
+
- source.author.id
|
|
248
|
+
- source.author.type
|
|
249
|
+
- source.author.name
|
|
250
|
+
- statistics.time_to_assignment
|
|
251
|
+
- statistics.time_to_admin_reply
|
|
252
|
+
- statistics.time_to_first_close
|
|
253
|
+
- statistics.median_time_to_reply
|
|
254
|
+
- statistics.first_assignment_at
|
|
255
|
+
- statistics.first_admin_reply_at
|
|
256
|
+
- statistics.last_assignment_at
|
|
257
|
+
- statistics.count_assignments
|
|
258
|
+
- conversation_rating.score
|
|
259
|
+
- conversation_rating.remark
|
|
260
|
+
- conversation_rating.contact_id
|
|
261
|
+
- ai_agent.resolution_state
|
|
262
|
+
- ai_agent.last_answer_type
|
|
263
|
+
- ai_agent.rating
|
|
264
|
+
- channel_initiated
|
|
265
|
+
- tag_ids
|
|
266
|
+
- teammate_ids
|
|
267
|
+
- company_id
|
|
268
|
+
- topics
|
|
269
|
+
|
|
270
|
+
tickets:
|
|
271
|
+
path: tickets/search
|
|
272
|
+
measured_at: null
|
|
273
|
+
fields:
|
|
274
|
+
# The primary key, declared here for the same reason as on conversations:
|
|
275
|
+
# the schema publishes `equal` and `in` on it whatever this table says,
|
|
276
|
+
# so a row has to exist for the translator to write. `GET /tickets/{id}`
|
|
277
|
+
# answers the key on its own; the search answers it in an `and`.
|
|
278
|
+
id:
|
|
279
|
+
field: id
|
|
280
|
+
type: string
|
|
281
|
+
operators: ['=', 'IN']
|
|
282
|
+
source: spec
|
|
283
|
+
open:
|
|
284
|
+
field: open
|
|
285
|
+
type: boolean
|
|
286
|
+
operators: ['=']
|
|
287
|
+
source: spec
|
|
288
|
+
category:
|
|
289
|
+
field: category
|
|
290
|
+
type: string
|
|
291
|
+
operators: ['=', '!=']
|
|
292
|
+
source: spec
|
|
293
|
+
# A ticket reads its category as a label; the search takes another
|
|
294
|
+
# word for each (OpenAPI 2.16, "Searching by Category").
|
|
295
|
+
values:
|
|
296
|
+
Customer: request
|
|
297
|
+
Back-office: task
|
|
298
|
+
Tracker: tracker
|
|
299
|
+
ticket_type_id:
|
|
300
|
+
field: ticket_type_id
|
|
301
|
+
type: string
|
|
302
|
+
operators: ['=', '!=']
|
|
303
|
+
source: spec
|
|
304
|
+
admin_assignee_id:
|
|
305
|
+
field: admin_assignee_id
|
|
306
|
+
type: string
|
|
307
|
+
operators: ['=', '!=']
|
|
308
|
+
source: spec
|
|
309
|
+
team_assignee_id:
|
|
310
|
+
field: team_assignee_id
|
|
311
|
+
type: string
|
|
312
|
+
operators: ['=', '!=']
|
|
313
|
+
source: spec
|
|
314
|
+
created_at:
|
|
315
|
+
field: created_at
|
|
316
|
+
type: date
|
|
317
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
318
|
+
source: measured
|
|
319
|
+
updated_at:
|
|
320
|
+
field: updated_at
|
|
321
|
+
type: date
|
|
322
|
+
operators: ['>', '<', '>=', '<=', '=', '!=']
|
|
323
|
+
source: measured
|
|
324
|
+
# The foreign key of the `contact` relation. `spec` rather than
|
|
325
|
+
# `measured`, and the difference is visible to an operator: it is what
|
|
326
|
+
# decides whether the contacts of a ticket can be filtered at all, and the
|
|
327
|
+
# probe has not run on this endpoint. If Intercom refuses it, this row
|
|
328
|
+
# moves to the refused table and the relation stays navigable without
|
|
329
|
+
# being filterable -- the way `state` already is.
|
|
330
|
+
contact_id:
|
|
331
|
+
field: contact_ids
|
|
332
|
+
type: string
|
|
333
|
+
operators: ['=']
|
|
334
|
+
source: spec
|
|
335
|
+
refused:
|
|
336
|
+
company_id:
|
|
337
|
+
reason: >-
|
|
338
|
+
Measured during lot 1: `/tickets/search` refuses `company_id` with
|
|
339
|
+
`invalid_field`, although a ticket carries one. Filtering tickets by
|
|
340
|
+
account is not something this endpoint does.
|
|
341
|
+
source: measured
|
|
342
|
+
closed_at:
|
|
343
|
+
reason: >-
|
|
344
|
+
Derived by the agent from the parts of the ticket. The search endpoint
|
|
345
|
+
filters nothing of the sort, and ignores a sort on it without a word.
|
|
346
|
+
source: measured
|
|
347
|
+
closed_by_name:
|
|
348
|
+
reason: Derived by the agent from the parts of the ticket.
|
|
349
|
+
source: measured
|
|
350
|
+
last_reply_at:
|
|
351
|
+
reason: Derived by the agent from the parts of the ticket.
|
|
352
|
+
source: measured
|
|
353
|
+
last_responder_name:
|
|
354
|
+
reason: Derived by the agent from the parts of the ticket.
|
|
355
|
+
source: measured
|
|
356
|
+
last_responder_type:
|
|
357
|
+
reason: Derived by the agent from the parts of the ticket.
|
|
358
|
+
source: measured
|
|
359
|
+
state_label:
|
|
360
|
+
reason: >-
|
|
361
|
+
Read off the state object the ticket embeds. Whether the endpoint
|
|
362
|
+
filters a ticket state at all, and under which name, is one of the
|
|
363
|
+
probe's questions.
|
|
364
|
+
source: spec
|
|
365
|
+
ticket_type_name:
|
|
366
|
+
reason: >-
|
|
367
|
+
Read off the type object the ticket embeds. Filter on
|
|
368
|
+
`ticket_type_id`, which the endpoint does take.
|
|
369
|
+
source: spec
|
|
370
|
+
ticket_id:
|
|
371
|
+
reason: >-
|
|
372
|
+
The number the support team says out loud, which is not the id the
|
|
373
|
+
API answers by. The search endpoint carries no field for it. Filter
|
|
374
|
+
on `id`, which takes the value the url and the record page use.
|
|
375
|
+
source: spec
|
|
376
|
+
is_shared:
|
|
377
|
+
reason: >-
|
|
378
|
+
Whether the ticket is visible to the customer. On the payload and not
|
|
379
|
+
in the search DSL; the probe lists it as a candidate.
|
|
380
|
+
source: spec
|
|
381
|
+
state_id:
|
|
382
|
+
reason: >-
|
|
383
|
+
Read off the state object the ticket embeds. The endpoint takes no
|
|
384
|
+
filter on a ticket state under any name this table knows, which is
|
|
385
|
+
what keeps the `state` relation navigable without being filterable.
|
|
386
|
+
Whether it filters one at all is the probe's second question. Filter
|
|
387
|
+
on `open` or on `category` meanwhile.
|
|
388
|
+
source: spec
|
|
389
|
+
previous_state_id:
|
|
390
|
+
reason: >-
|
|
391
|
+
The state the ticket left, read off its payload. Filterable no more
|
|
392
|
+
than the state it is in, and for the same reason.
|
|
393
|
+
source: spec
|
|
394
|
+
contact_name:
|
|
395
|
+
reason: >-
|
|
396
|
+
Read from the Contacts endpoint, not from the ticket. Filter through
|
|
397
|
+
the `contact` relation, which resolves against `/contacts/search` and
|
|
398
|
+
rewrites onto `contact_ids`.
|
|
399
|
+
source: spec
|
|
400
|
+
contact_count:
|
|
401
|
+
reason: Counted by the agent from the contacts the payload carries.
|
|
402
|
+
source: spec
|
|
403
|
+
part_count:
|
|
404
|
+
reason: Counted by the agent from the parts the payload carries.
|
|
405
|
+
source: measured
|
|
406
|
+
timeline:
|
|
407
|
+
reason: >-
|
|
408
|
+
Built by the agent from the parts of the ticket, which the search
|
|
409
|
+
endpoint does not read. Filter on `last_reply_at` or on the state
|
|
410
|
+
instead.
|
|
411
|
+
source: measured
|
|
412
|
+
# R7, and it is a product decision rather than an implementation choice: a
|
|
413
|
+
# ticket attribute is filtered as `ticket_attribute.{id}`, and a same-named
|
|
414
|
+
# attribute carries a different id per ticket type (measured:
|
|
415
|
+
# `_default_title_` is 14162161 on one type and 14162165 on another). One
|
|
416
|
+
# union column cannot know which id a row's type uses, so filtering it
|
|
417
|
+
# server-side would take one collection per ticket type -- more collections
|
|
418
|
+
# in the interface, and a schema that changes shape when the customer adds a
|
|
419
|
+
# type. Until the customer says that trade is worth it, the attributes stay
|
|
420
|
+
# display-only, as lot 1 published them.
|
|
421
|
+
ticket_attributes:
|
|
422
|
+
filterable: false
|
|
423
|
+
reason: >-
|
|
424
|
+
An Intercom ticket attribute is filtered through an id that differs from
|
|
425
|
+
one ticket type to the next, so a column showing the attribute of every
|
|
426
|
+
type at once cannot say which id to filter on. Filter on the ticket type
|
|
427
|
+
and a native field instead.
|
|
428
|
+
source: measured
|
|
429
|
+
|
|
430
|
+
candidates:
|
|
431
|
+
- ticket_id
|
|
432
|
+
- state
|
|
433
|
+
- ticket_state.category
|
|
434
|
+
- ticket_state.id
|
|
435
|
+
- state_id
|
|
436
|
+
- is_shared
|
|
437
|
+
- previous_state_id
|
|
438
|
+
- company_ids
|
|
439
|
+
- source.author.email
|
|
440
|
+
- source.subject
|
|
441
|
+
- source.body
|
|
442
|
+
- title
|
|
443
|
+
- description
|
|
444
|
+
|
|
445
|
+
# The one endpoint of the whole API that sorts. `sortable: true` is what a
|
|
446
|
+
# column reads to publish `isSortable`, and it appears nowhere else in this
|
|
447
|
+
# file on purpose: `/conversations/search` accepts a `sort` and ignores it
|
|
448
|
+
# without a word (measured), so a row that stayed silent there would be a
|
|
449
|
+
# column promising an order Intercom never applies.
|
|
450
|
+
#
|
|
451
|
+
# The set is deliberately narrower than what the documentation implies. It
|
|
452
|
+
# says the results may be sorted by any attribute; nothing has been measured,
|
|
453
|
+
# and a sort Intercom refuses is a list view that fails rather than one that
|
|
454
|
+
# comes back unordered. These are the fields an ops team orders a contact list
|
|
455
|
+
# by, and the probe is what may widen the set.
|
|
456
|
+
contacts:
|
|
457
|
+
path: contacts/search
|
|
458
|
+
measured_at: null
|
|
459
|
+
fields:
|
|
460
|
+
# Measured rather than taken from the documentation: lot 1 reads the
|
|
461
|
+
# contacts of a page through `id IN [...]` on this endpoint, which is the
|
|
462
|
+
# membership operator every other field refuses.
|
|
463
|
+
id:
|
|
464
|
+
field: id
|
|
465
|
+
type: string
|
|
466
|
+
operators: ['=', 'IN']
|
|
467
|
+
source: measured
|
|
468
|
+
role:
|
|
469
|
+
field: role
|
|
470
|
+
type: string
|
|
471
|
+
operators: ['=', '!=']
|
|
472
|
+
source: spec
|
|
473
|
+
name:
|
|
474
|
+
field: name
|
|
475
|
+
type: text
|
|
476
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
477
|
+
source: spec
|
|
478
|
+
sortable: true
|
|
479
|
+
email:
|
|
480
|
+
field: email
|
|
481
|
+
type: text
|
|
482
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
483
|
+
source: spec
|
|
484
|
+
sortable: true
|
|
485
|
+
# Derived by the agent from the address, and filtered by Intercom all the
|
|
486
|
+
# same: the endpoint carries a field of its own for it, which is what
|
|
487
|
+
# makes "everyone at this customer" a filter rather than a wildcard.
|
|
488
|
+
email_domain:
|
|
489
|
+
field: email_domain
|
|
490
|
+
type: string
|
|
491
|
+
operators: ['=', '!=']
|
|
492
|
+
source: spec
|
|
493
|
+
phone:
|
|
494
|
+
field: phone
|
|
495
|
+
type: text
|
|
496
|
+
operators: ['=', '!=', '~', '!~', '^', '$']
|
|
497
|
+
source: spec
|
|
498
|
+
external_id:
|
|
499
|
+
field: external_id
|
|
500
|
+
type: string
|
|
501
|
+
operators: ['=', '!=']
|
|
502
|
+
source: spec
|
|
503
|
+
owner_id:
|
|
504
|
+
field: owner_id
|
|
505
|
+
type: string
|
|
506
|
+
operators: ['=', '!=']
|
|
507
|
+
source: spec
|
|
508
|
+
# Measured on 25 August 2026, API 2.16, and this is the asymmetry the
|
|
509
|
+
# per-endpoint table exists for: `/conversations/search` and
|
|
510
|
+
# `/tickets/search` accept `>=`, `<=` and `!=` on a date where this
|
|
511
|
+
# endpoint answers `data_invalid`. `IN` is refused on a date everywhere.
|
|
512
|
+
#
|
|
513
|
+
# Nothing is lost that an operator can see: a Date column publishes the
|
|
514
|
+
# two bounds alone whatever this table allows -- see the operator table --
|
|
515
|
+
# and the toolkit derives `before`, `after`, `today` and the whole
|
|
516
|
+
# `previous_*` family from them.
|
|
517
|
+
created_at:
|
|
518
|
+
field: created_at
|
|
519
|
+
type: date
|
|
520
|
+
operators: ['>', '<']
|
|
521
|
+
source: measured
|
|
522
|
+
sortable: true
|
|
523
|
+
updated_at:
|
|
524
|
+
field: updated_at
|
|
525
|
+
type: date
|
|
526
|
+
operators: ['>', '<']
|
|
527
|
+
source: measured
|
|
528
|
+
sortable: true
|
|
529
|
+
signed_up_at:
|
|
530
|
+
field: signed_up_at
|
|
531
|
+
type: date
|
|
532
|
+
operators: ['>', '<']
|
|
533
|
+
source: spec
|
|
534
|
+
sortable: true
|
|
535
|
+
last_seen_at:
|
|
536
|
+
field: last_seen_at
|
|
537
|
+
type: date
|
|
538
|
+
operators: ['>', '<']
|
|
539
|
+
source: spec
|
|
540
|
+
sortable: true
|
|
541
|
+
last_contacted_at:
|
|
542
|
+
field: last_contacted_at
|
|
543
|
+
type: date
|
|
544
|
+
operators: ['>', '<']
|
|
545
|
+
source: spec
|
|
546
|
+
sortable: true
|
|
547
|
+
last_replied_at:
|
|
548
|
+
field: last_replied_at
|
|
549
|
+
type: date
|
|
550
|
+
operators: ['>', '<']
|
|
551
|
+
source: spec
|
|
552
|
+
sortable: true
|
|
553
|
+
last_email_opened_at:
|
|
554
|
+
field: last_email_opened_at
|
|
555
|
+
type: date
|
|
556
|
+
operators: ['>', '<']
|
|
557
|
+
source: spec
|
|
558
|
+
last_email_clicked_at:
|
|
559
|
+
field: last_email_clicked_at
|
|
560
|
+
type: date
|
|
561
|
+
operators: ['>', '<']
|
|
562
|
+
source: spec
|
|
563
|
+
unsubscribed_from_emails:
|
|
564
|
+
field: unsubscribed_from_emails
|
|
565
|
+
type: boolean
|
|
566
|
+
operators: ['=']
|
|
567
|
+
source: spec
|
|
568
|
+
has_hard_bounced:
|
|
569
|
+
field: has_hard_bounced
|
|
570
|
+
type: boolean
|
|
571
|
+
operators: ['=']
|
|
572
|
+
source: spec
|
|
573
|
+
marked_email_as_spam:
|
|
574
|
+
field: marked_email_as_spam
|
|
575
|
+
type: boolean
|
|
576
|
+
operators: ['=']
|
|
577
|
+
source: spec
|
|
578
|
+
language_override:
|
|
579
|
+
field: language_override
|
|
580
|
+
type: string
|
|
581
|
+
operators: ['=', '!=']
|
|
582
|
+
source: spec
|
|
583
|
+
browser:
|
|
584
|
+
field: browser
|
|
585
|
+
type: string
|
|
586
|
+
operators: ['=', '!=']
|
|
587
|
+
source: spec
|
|
588
|
+
browser_language:
|
|
589
|
+
field: browser_language
|
|
590
|
+
type: string
|
|
591
|
+
operators: ['=', '!=']
|
|
592
|
+
source: spec
|
|
593
|
+
os:
|
|
594
|
+
field: os
|
|
595
|
+
type: string
|
|
596
|
+
operators: ['=', '!=']
|
|
597
|
+
source: spec
|
|
598
|
+
location_country:
|
|
599
|
+
field: location.country
|
|
600
|
+
type: string
|
|
601
|
+
operators: ['=', '!=']
|
|
602
|
+
source: spec
|
|
603
|
+
location_region:
|
|
604
|
+
field: location.region
|
|
605
|
+
type: string
|
|
606
|
+
operators: ['=', '!=']
|
|
607
|
+
source: spec
|
|
608
|
+
location_city:
|
|
609
|
+
field: location.city
|
|
610
|
+
type: string
|
|
611
|
+
operators: ['=', '!=']
|
|
612
|
+
source: spec
|
|
613
|
+
refused:
|
|
614
|
+
avatar:
|
|
615
|
+
reason: >-
|
|
616
|
+
The url of the contact's picture. Nothing filters an image, and
|
|
617
|
+
Intercom's search does not carry the field.
|
|
618
|
+
source: spec
|
|
619
|
+
company_id:
|
|
620
|
+
reason: >-
|
|
621
|
+
The first of the companies the contact belongs to, read off the
|
|
622
|
+
payload. `/contacts/search` filters no company field, so a company
|
|
623
|
+
list is reached from the company side -- open the company and read
|
|
624
|
+
its contacts.
|
|
625
|
+
source: spec
|
|
626
|
+
company_count:
|
|
627
|
+
reason: Counted by the agent from the companies the payload carries.
|
|
628
|
+
source: spec
|
|
629
|
+
session_count:
|
|
630
|
+
reason: >-
|
|
631
|
+
On the contact payload and not in the search DSL. Filter on
|
|
632
|
+
`last_seen_at` instead, which the endpoint does take.
|
|
633
|
+
source: spec
|
|
634
|
+
# Contact custom attributes are filtered as `custom_attributes.{name}` --
|
|
635
|
+
# by name, unlike a ticket attribute, which carries a different id per
|
|
636
|
+
# ticket type. So the obstacle is not R7 here, it is measurement: the
|
|
637
|
+
# operator set Intercom answers on a custom attribute of each data type has
|
|
638
|
+
# not been probed, and this package does not publish a filter it has not
|
|
639
|
+
# seen work. They ship typed and display-only, and the probe is what turns
|
|
640
|
+
# that around.
|
|
641
|
+
custom_attributes:
|
|
642
|
+
filterable: false
|
|
643
|
+
reason: >-
|
|
644
|
+
Contact custom attributes are published for reading only: which
|
|
645
|
+
operators Intercom answers on `custom_attributes.{name}` has not been
|
|
646
|
+
measured on a real workspace, and this datasource does not offer a
|
|
647
|
+
filter it has not seen work. Filter on a native field instead.
|
|
648
|
+
source: spec
|
|
649
|
+
candidates:
|
|
650
|
+
- tag_ids
|
|
651
|
+
- segment_id
|
|
652
|
+
- company_id
|
|
653
|
+
- companies.id
|
|
654
|
+
- companies.name
|
|
655
|
+
- android_app_name
|
|
656
|
+
- android_last_seen_at
|
|
657
|
+
- ios_app_name
|
|
658
|
+
- ios_last_seen_at
|
|
659
|
+
- utm_source
|
|
660
|
+
- utm_campaign
|
|
661
|
+
- utm_medium
|
|
662
|
+
- referrer
|
|
663
|
+
- session_count
|