sentdm 0.32.0 → 0.33.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +1 -1
  4. data/lib/sentdm/client.rb +6 -0
  5. data/lib/sentdm/models/channel_event_payload.rb +139 -2
  6. data/lib/sentdm/models/contact_event.rb +15 -7
  7. data/lib/sentdm/models/contact_event_payload.rb +77 -16
  8. data/lib/sentdm/models/conversation_messages_list.rb +125 -4
  9. data/lib/sentdm/models/message_event_payload.rb +20 -1
  10. data/lib/sentdm/models/message_retrieve_activities_response.rb +20 -5
  11. data/lib/sentdm/models/message_retrieve_status_response.rb +133 -6
  12. data/lib/sentdm/models/message_send_params.rb +67 -3
  13. data/lib/sentdm/models/message_send_response.rb +13 -4
  14. data/lib/sentdm/models/template_body.rb +82 -1
  15. data/lib/sentdm/models/template_header.rb +91 -3
  16. data/lib/sentdm/models/template_variable.rb +15 -1
  17. data/lib/sentdm/models/webhook_list_events_response.rb +324 -9
  18. data/lib/sentdm/resources/messages.rb +33 -5
  19. data/lib/sentdm/version.rb +1 -1
  20. data/lib/sentdm.rb +1 -1
  21. data/rbi/sentdm/client.rbi +6 -0
  22. data/rbi/sentdm/models/channel_event_payload.rbi +253 -2
  23. data/rbi/sentdm/models/contact_event.rbi +28 -12
  24. data/rbi/sentdm/models/contact_event_payload.rbi +118 -28
  25. data/rbi/sentdm/models/conversation_messages_list.rbi +202 -7
  26. data/rbi/sentdm/models/message_event_payload.rbi +22 -0
  27. data/rbi/sentdm/models/message_retrieve_activities_response.rbi +23 -5
  28. data/rbi/sentdm/models/message_retrieve_status_response.rbi +214 -9
  29. data/rbi/sentdm/models/message_send_params.rbi +100 -2
  30. data/rbi/sentdm/models/message_send_response.rbi +15 -5
  31. data/rbi/sentdm/models/template_body.rbi +133 -0
  32. data/rbi/sentdm/models/template_header.rbi +143 -2
  33. data/rbi/sentdm/models/template_variable.rbi +10 -0
  34. data/rbi/sentdm/models/webhook_list_events_response.rbi +478 -12
  35. data/rbi/sentdm/resources/messages.rbi +58 -3
  36. data/sig/sentdm/models/channel_event_payload.rbs +57 -0
  37. data/sig/sentdm/models/contact_event_payload.rbs +24 -9
  38. data/sig/sentdm/models/conversation_messages_list.rbs +48 -3
  39. data/sig/sentdm/models/message_event_payload.rbs +10 -0
  40. data/sig/sentdm/models/message_retrieve_activities_response.rbs +5 -0
  41. data/sig/sentdm/models/message_retrieve_status_response.rbs +48 -3
  42. data/sig/sentdm/models/message_send_params.rbs +15 -0
  43. data/sig/sentdm/models/template_body.rbs +44 -0
  44. data/sig/sentdm/models/template_header.rbs +44 -0
  45. data/sig/sentdm/models/webhook_list_events_response.rbs +145 -0
  46. data/sig/sentdm/resources/messages.rbs +3 -0
  47. metadata +2 -2
@@ -18,7 +18,9 @@ module Sentdm
18
18
  # The account whose market this is, named as on every other family. When an
19
19
  # organization receives an event for one of its sender profiles this is the
20
20
  # profile, so a reseller compares it with its own id and anything different is one
21
- # of its profiles.
21
+ # of its profiles. Matches customer_id on GET /v3/channels and the sender
22
+ # profile's id. Together with channel, country, and number_type, it identifies the
23
+ # market.
22
24
  sig { returns(T.nilable(String)) }
23
25
  attr_reader :account_id
24
26
 
@@ -34,6 +36,33 @@ module Sentdm
34
36
  sig { params(channel: String).void }
35
37
  attr_writer :channel
36
38
 
39
+ # What a market has been given: the identity it registers under, its programme,
40
+ # and any documents attached.
41
+ #
42
+ # What it does not carry is what the market asks for. That is the subject of GET
43
+ # /v3/compliance/requirements, and it is the same answer for every caller — a
44
+ # description of what a compliance regime wants, not a record of one customer's
45
+ # progress through it. It was reported here as well for a while, which put the
46
+ # same array in six response shapes and left a caller deciding which of two
47
+ # sources to believe.
48
+ #
49
+ # Present on a list read for markets that register (carrying brand and campaign),
50
+ # but with documents absent — documents are not fetched for a list, because a
51
+ # catalog lookup and a document read per market would multiply across a page.
52
+ # Absent documents is distinct from an empty list: absent says they were not
53
+ # fetched; empty says the market has been given none. The parent object is null
54
+ # only when the market registers with nobody and compliance was not computed —
55
+ # nothing to show at all.
56
+ sig { returns(T.nilable(Sentdm::ChannelEventPayload::Compliance)) }
57
+ attr_reader :compliance
58
+
59
+ sig do
60
+ params(
61
+ compliance: T.nilable(Sentdm::ChannelEventPayload::Compliance::OrHash)
62
+ ).void
63
+ end
64
+ attr_writer :compliance
65
+
37
66
  # The kind of sender the market uses, for example TEN_DLC, LOCAL, or ALPHANUMERIC.
38
67
  # Omitted when the subject has no sender type of its own.
39
68
  sig { returns(T.nilable(String)) }
@@ -109,6 +138,8 @@ module Sentdm
109
138
  country: String,
110
139
  account_id: String,
111
140
  channel: String,
141
+ compliance:
142
+ T.nilable(Sentdm::ChannelEventPayload::Compliance::OrHash),
112
143
  number_type: T.nilable(String),
113
144
  reason: T.nilable(String),
114
145
  sender_value: T.nilable(String),
@@ -125,12 +156,32 @@ module Sentdm
125
156
  # The account whose market this is, named as on every other family. When an
126
157
  # organization receives an event for one of its sender profiles this is the
127
158
  # profile, so a reseller compares it with its own id and anything different is one
128
- # of its profiles.
159
+ # of its profiles. Matches customer_id on GET /v3/channels and the sender
160
+ # profile's id. Together with channel, country, and number_type, it identifies the
161
+ # market.
129
162
  account_id: nil,
130
163
  # The channel this market belongs to: sms, whatsapp, or rcs. Never sent — that
131
164
  # value belongs to message events, where it names the smart-routing brand rather
132
165
  # than a channel that can be provisioned.
133
166
  channel: nil,
167
+ # What a market has been given: the identity it registers under, its programme,
168
+ # and any documents attached.
169
+ #
170
+ # What it does not carry is what the market asks for. That is the subject of GET
171
+ # /v3/compliance/requirements, and it is the same answer for every caller — a
172
+ # description of what a compliance regime wants, not a record of one customer's
173
+ # progress through it. It was reported here as well for a while, which put the
174
+ # same array in six response shapes and left a caller deciding which of two
175
+ # sources to believe.
176
+ #
177
+ # Present on a list read for markets that register (carrying brand and campaign),
178
+ # but with documents absent — documents are not fetched for a list, because a
179
+ # catalog lookup and a document read per market would multiply across a page.
180
+ # Absent documents is distinct from an empty list: absent says they were not
181
+ # fetched; empty says the market has been given none. The parent object is null
182
+ # only when the market registers with nobody and compliance was not computed —
183
+ # nothing to show at all.
184
+ compliance: nil,
134
185
  # The kind of sender the market uses, for example TEN_DLC, LOCAL, or ALPHANUMERIC.
135
186
  # Omitted when the subject has no sender type of its own.
136
187
  number_type: nil,
@@ -174,6 +225,7 @@ module Sentdm
174
225
  country: String,
175
226
  account_id: String,
176
227
  channel: String,
228
+ compliance: T.nilable(Sentdm::ChannelEventPayload::Compliance),
177
229
  number_type: T.nilable(String),
178
230
  reason: T.nilable(String),
179
231
  sender_value: T.nilable(String),
@@ -184,6 +236,205 @@ module Sentdm
184
236
  end
185
237
  def to_hash
186
238
  end
239
+
240
+ class Compliance < Sentdm::Internal::Type::BaseModel
241
+ OrHash =
242
+ T.type_alias do
243
+ T.any(
244
+ Sentdm::ChannelEventPayload::Compliance,
245
+ Sentdm::Internal::AnyHash
246
+ )
247
+ end
248
+
249
+ # The identity this market registers under, with inherit saying whose it is.
250
+ #
251
+ # Reported here rather than on the profile because it belongs to the registration
252
+ # this market files, and only one market files one. It was a top-level block for a
253
+ # while, which put a per-registration value beside a list of markets and left a
254
+ # caller to work out which market it belonged to.
255
+ #
256
+ # Absent for a market that registers with nobody — such a market asks for no
257
+ # identity, so there is none to report. Absent and null mean different things:
258
+ # absent says this market does not ask, null would say it asks and nothing was
259
+ # supplied.
260
+ #
261
+ # Untyped, like the request side, because its members are declared by the market's
262
+ # own schema rather than by a C# class. A typed pair here would be a second
263
+ # definition of what a market wants, free to drift from the one that validates.
264
+ sig { returns(T.nilable(T::Hash[Symbol, T.anything])) }
265
+ attr_accessor :brand
266
+
267
+ # The programme this market registers, with inherit saying whose it is.
268
+ #
269
+ # One, not a list. TcrCampaigns permits several and an account built on the admin
270
+ # side may hold them, but this surface offers one — which is what lets the
271
+ # market's PATCH be an upsert rather than a collection with an addressable create
272
+ # behind it. An account holding several is reported as its first and refused on
273
+ # write, rather than half-edited.
274
+ #
275
+ # Carries no id. Nothing addresses a campaign, and an undeclared key would be
276
+ # refused if the caller sent this object back — which it is meant to be able to
277
+ # do.
278
+ sig { returns(T.nilable(T::Hash[Symbol, T.anything])) }
279
+ attr_accessor :campaign
280
+
281
+ # What has been supplied for this market.
282
+ #
283
+ # Files, not values — the declared halves above carry the values. A document
284
+ # cannot be a JSON value, so it is sent as multipart on the channel call and
285
+ # reported here as a reference.
286
+ #
287
+ # Absent on a list read, which fetches identity but does not compute compliance
288
+ # documents per market. Absent and empty mean different things: absent says the
289
+ # documents were not fetched; empty says the market has been given none.
290
+ sig do
291
+ returns(
292
+ T.nilable(
293
+ T::Array[Sentdm::ChannelEventPayload::Compliance::Document]
294
+ )
295
+ )
296
+ end
297
+ attr_accessor :documents
298
+
299
+ # What a market has been given: the identity it registers under, its programme,
300
+ # and any documents attached.
301
+ #
302
+ # What it does not carry is what the market asks for. That is the subject of GET
303
+ # /v3/compliance/requirements, and it is the same answer for every caller — a
304
+ # description of what a compliance regime wants, not a record of one customer's
305
+ # progress through it. It was reported here as well for a while, which put the
306
+ # same array in six response shapes and left a caller deciding which of two
307
+ # sources to believe.
308
+ #
309
+ # Present on a list read for markets that register (carrying brand and campaign),
310
+ # but with documents absent — documents are not fetched for a list, because a
311
+ # catalog lookup and a document read per market would multiply across a page.
312
+ # Absent documents is distinct from an empty list: absent says they were not
313
+ # fetched; empty says the market has been given none. The parent object is null
314
+ # only when the market registers with nobody and compliance was not computed —
315
+ # nothing to show at all.
316
+ sig do
317
+ params(
318
+ brand: T.nilable(T::Hash[Symbol, T.anything]),
319
+ campaign: T.nilable(T::Hash[Symbol, T.anything]),
320
+ documents:
321
+ T.nilable(
322
+ T::Array[
323
+ Sentdm::ChannelEventPayload::Compliance::Document::OrHash
324
+ ]
325
+ )
326
+ ).returns(T.attached_class)
327
+ end
328
+ def self.new(
329
+ # The identity this market registers under, with inherit saying whose it is.
330
+ #
331
+ # Reported here rather than on the profile because it belongs to the registration
332
+ # this market files, and only one market files one. It was a top-level block for a
333
+ # while, which put a per-registration value beside a list of markets and left a
334
+ # caller to work out which market it belonged to.
335
+ #
336
+ # Absent for a market that registers with nobody — such a market asks for no
337
+ # identity, so there is none to report. Absent and null mean different things:
338
+ # absent says this market does not ask, null would say it asks and nothing was
339
+ # supplied.
340
+ #
341
+ # Untyped, like the request side, because its members are declared by the market's
342
+ # own schema rather than by a C# class. A typed pair here would be a second
343
+ # definition of what a market wants, free to drift from the one that validates.
344
+ brand: nil,
345
+ # The programme this market registers, with inherit saying whose it is.
346
+ #
347
+ # One, not a list. TcrCampaigns permits several and an account built on the admin
348
+ # side may hold them, but this surface offers one — which is what lets the
349
+ # market's PATCH be an upsert rather than a collection with an addressable create
350
+ # behind it. An account holding several is reported as its first and refused on
351
+ # write, rather than half-edited.
352
+ #
353
+ # Carries no id. Nothing addresses a campaign, and an undeclared key would be
354
+ # refused if the caller sent this object back — which it is meant to be able to
355
+ # do.
356
+ campaign: nil,
357
+ # What has been supplied for this market.
358
+ #
359
+ # Files, not values — the declared halves above carry the values. A document
360
+ # cannot be a JSON value, so it is sent as multipart on the channel call and
361
+ # reported here as a reference.
362
+ #
363
+ # Absent on a list read, which fetches identity but does not compute compliance
364
+ # documents per market. Absent and empty mean different things: absent says the
365
+ # documents were not fetched; empty says the market has been given none.
366
+ documents: nil
367
+ )
368
+ end
369
+
370
+ sig do
371
+ override.returns(
372
+ {
373
+ brand: T.nilable(T::Hash[Symbol, T.anything]),
374
+ campaign: T.nilable(T::Hash[Symbol, T.anything]),
375
+ documents:
376
+ T.nilable(
377
+ T::Array[Sentdm::ChannelEventPayload::Compliance::Document]
378
+ )
379
+ }
380
+ )
381
+ end
382
+ def to_hash
383
+ end
384
+
385
+ class Document < Sentdm::Internal::Type::BaseModel
386
+ OrHash =
387
+ T.type_alias do
388
+ T.any(
389
+ Sentdm::ChannelEventPayload::Compliance::Document,
390
+ Sentdm::Internal::AnyHash
391
+ )
392
+ end
393
+
394
+ # Identifier of the upload, for fetching it back through the documents endpoints.
395
+ sig { returns(T.nilable(String)) }
396
+ attr_accessor :document_id
397
+
398
+ sig { returns(T.nilable(String)) }
399
+ attr_accessor :file_name
400
+
401
+ # The catalog's name for this document, matching the requirement it satisfies.
402
+ sig { returns(T.nilable(String)) }
403
+ attr_reader :key
404
+
405
+ sig { params(key: String).void }
406
+ attr_writer :key
407
+
408
+ # A document a market asked for and has been given.
409
+ sig do
410
+ params(
411
+ document_id: T.nilable(String),
412
+ file_name: T.nilable(String),
413
+ key: String
414
+ ).returns(T.attached_class)
415
+ end
416
+ def self.new(
417
+ # Identifier of the upload, for fetching it back through the documents endpoints.
418
+ document_id: nil,
419
+ file_name: nil,
420
+ # The catalog's name for this document, matching the requirement it satisfies.
421
+ key: nil
422
+ )
423
+ end
424
+
425
+ sig do
426
+ override.returns(
427
+ {
428
+ document_id: T.nilable(String),
429
+ file_name: T.nilable(String),
430
+ key: String
431
+ }
432
+ )
433
+ end
434
+ def to_hash
435
+ end
436
+ end
437
+ end
187
438
  end
188
439
  end
189
440
  end
@@ -20,17 +20,25 @@ module Sentdm
20
20
  sig { params(field: String).void }
21
21
  attr_writer :field
22
22
 
23
- # Body of a contact.opt_in, contact.opt_out or contact.help event. Delivered when
24
- # a contact signals a consent change or asks for help.
23
+ # Body of a contact.opt_in, contact.opt_out, contact.help or
24
+ # contact.custom_keyword event. Delivered when a contact signals a consent change,
25
+ # asks for help, or sends one of your own auto-reply keywords.
25
26
  #
26
27
  # These events state the signal outright, so you do not have to recognise keywords
27
28
  # in the text of a message.received event. They also cover cases that produce no
28
29
  # inbound message at all, such as a network handling an opt-out on your behalf.
29
30
  #
30
- # Fields are ordered identity → resulting state → provenance → join key. Nothing
31
- # here restates the envelope: which of the three signals occurred is the
32
- # envelope's event, and when it was emitted is its timestamp. Retries carry the
33
- # same X-Webhook-Event-ID header, which is what to deduplicate on.
31
+ # Two of the four change consent and two do not: contact.help and
32
+ # contact.custom_keyword report the state the contact already had. Read opt_out
33
+ # for the state and the envelope's event for what happened, rather than inferring
34
+ # one from the other.
35
+ #
36
+ # Fields are ordered identity → resulting state → provenance → join keys. The two
37
+ # parties are from and to. Note that the message family has not moved to those
38
+ # names yet — message.received still calls the same two parties inbound_number and
39
+ # outbound_number. Nothing here restates the envelope: which signal occurred is
40
+ # the envelope's event, and when it was emitted is its timestamp. Retries carry
41
+ # the same X-Webhook-Event-ID header, which is what to deduplicate on.
34
42
  sig { returns(T.nilable(Sentdm::ContactEventPayload)) }
35
43
  attr_reader :payload
36
44
 
@@ -71,17 +79,25 @@ module Sentdm
71
79
  # The event family, for example message, templates or contact. Route on this
72
80
  # first, then on event for the specific change.
73
81
  field: nil,
74
- # Body of a contact.opt_in, contact.opt_out or contact.help event. Delivered when
75
- # a contact signals a consent change or asks for help.
82
+ # Body of a contact.opt_in, contact.opt_out, contact.help or
83
+ # contact.custom_keyword event. Delivered when a contact signals a consent change,
84
+ # asks for help, or sends one of your own auto-reply keywords.
76
85
  #
77
86
  # These events state the signal outright, so you do not have to recognise keywords
78
87
  # in the text of a message.received event. They also cover cases that produce no
79
88
  # inbound message at all, such as a network handling an opt-out on your behalf.
80
89
  #
81
- # Fields are ordered identity → resulting state → provenance → join key. Nothing
82
- # here restates the envelope: which of the three signals occurred is the
83
- # envelope's event, and when it was emitted is its timestamp. Retries carry the
84
- # same X-Webhook-Event-ID header, which is what to deduplicate on.
90
+ # Two of the four change consent and two do not: contact.help and
91
+ # contact.custom_keyword report the state the contact already had. Read opt_out
92
+ # for the state and the envelope's event for what happened, rather than inferring
93
+ # one from the other.
94
+ #
95
+ # Fields are ordered identity → resulting state → provenance → join keys. The two
96
+ # parties are from and to. Note that the message family has not moved to those
97
+ # names yet — message.received still calls the same two parties inbound_number and
98
+ # outbound_number. Nothing here restates the envelope: which signal occurred is
99
+ # the envelope's event, and when it was emitted is its timestamp. Retries carry
100
+ # the same X-Webhook-Event-ID header, which is what to deduplicate on.
85
101
  payload: nil,
86
102
  # The event-specific body.
87
103
  request_id: nil,
@@ -9,8 +9,9 @@ module Sentdm
9
9
  end
10
10
 
11
11
  # Whether the contact is opted out after this signal — the state to write to your
12
- # own record. Same meaning as opt_out on the contact resource. On contact.help
13
- # this reports the contact's existing state, which help does not change.
12
+ # own record. Same meaning as opt_out on the contact resource. On contact.help and
13
+ # contact.custom_keyword this reports the contact's existing state, which neither
14
+ # changes.
14
15
  #
15
16
  # Two signals from the same contact can arrive out of order, because each one is
16
17
  # queued on its own rather than against the contact. Compare the envelope's
@@ -35,6 +36,16 @@ module Sentdm
35
36
  sig { params(account_id: String).void }
36
37
  attr_writer :account_id
37
38
 
39
+ # The RCS agent the signal reached, when it reached one.
40
+ #
41
+ # Omitted entirely on channels that have no agent, rather than sent as null — an
42
+ # SMS or WhatsApp payload does not carry this key at all. On RCS it is the
43
+ # counterpart to To: a contact reaches an agent rather than a number, so exactly
44
+ # one of the two is populated and never both. If you run more than one agent, this
45
+ # is what tells you which of them the contact acted on.
46
+ sig { returns(T.nilable(String)) }
47
+ attr_accessor :agent_id
48
+
38
49
  # The channel the signal arrived on, for example sms or whatsapp.
39
50
  sig { returns(T.nilable(String)) }
40
51
  attr_reader :channel
@@ -43,14 +54,23 @@ module Sentdm
43
54
  attr_writer :channel
44
55
 
45
56
  # The contact who raised the signal. Always populated, including for contact.help
46
- # from a number you have not messaged before — the contact is created if it does
47
- # not exist yet, so this identifier is always resolvable against the contacts API.
57
+ # or contact.custom_keyword from a number you have not messaged before — the
58
+ # contact is created if it does not exist yet, so this identifier is always
59
+ # resolvable against the contacts API.
48
60
  sig { returns(T.nilable(String)) }
49
61
  attr_reader :contact_id
50
62
 
51
63
  sig { params(contact_id: String).void }
52
64
  attr_writer :contact_id
53
65
 
66
+ # The contact's number, in E.164 format with the leading + — who raised the
67
+ # signal. The same party message.received publishes as inbound_number.
68
+ sig { returns(T.nilable(String)) }
69
+ attr_reader :from
70
+
71
+ sig { params(from: String).void }
72
+ attr_writer :from
73
+
54
74
  # The inbound message that carried the signal, matching message_id on the
55
75
  # corresponding message.received event so the two can be joined.
56
76
  #
@@ -62,13 +82,19 @@ module Sentdm
62
82
  sig { returns(T.nilable(String)) }
63
83
  attr_accessor :message_id
64
84
 
65
- # The contact's number in E.164 format. Same value as phone_number on the contact
66
- # resource.
85
+ # The auto-reply template whose keyword the contact matched, joinable against the
86
+ # templates API.
87
+ #
88
+ # This is what identifies which signal arrived on contact.custom_keyword: every
89
+ # custom template reports the same event name, so the event alone cannot tell your
90
+ # booking keyword from your opening-hours one. One template holds as many keywords
91
+ # as you configured, so this is steadier to switch on than text.
92
+ #
93
+ # Populated on the compliance sub-types too, where it names the template that
94
+ # replied. Sent as null when no template was involved — a network-reported opt-out
95
+ # matches no keyword. The field is always present, so read it and check for null.
67
96
  sig { returns(T.nilable(String)) }
68
- attr_reader :phone_number
69
-
70
- sig { params(phone_number: String).void }
71
- attr_writer :phone_number
97
+ attr_accessor :template_id
72
98
 
73
99
  # The text the contact sent, for example STOP or UNSUBSCRIBE. Sent as null when
74
100
  # the signal did not arrive as text. The field is always present, so read it and
@@ -76,33 +102,60 @@ module Sentdm
76
102
  sig { returns(T.nilable(String)) }
77
103
  attr_accessor :text
78
104
 
79
- # Body of a contact.opt_in, contact.opt_out or contact.help event. Delivered when
80
- # a contact signals a consent change or asks for help.
105
+ # The number of yours that received the signal, in E.164 format with the leading
106
+ # +. Tells a multi-number account which of its senders the contact acted on, which
107
+ # nothing else on this payload answers.
108
+ #
109
+ # This is your number, not the contact's. That is the opposite of what to means on
110
+ # POST /v3/messages, where it is the list of recipients you are sending to. Reply
111
+ # to From, not to this field, or the message goes back to yourself.
112
+ #
113
+ # Sent as null when the signal did not arrive at a number of yours — an RCS signal
114
+ # terminates at an agent rather than a number, and a provider-reported opt-out may
115
+ # name no receiving number at all. The field is always present, so read it and
116
+ # check for null rather than checking whether the key exists.
117
+ sig { returns(T.nilable(String)) }
118
+ attr_accessor :to
119
+
120
+ # Body of a contact.opt_in, contact.opt_out, contact.help or
121
+ # contact.custom_keyword event. Delivered when a contact signals a consent change,
122
+ # asks for help, or sends one of your own auto-reply keywords.
81
123
  #
82
124
  # These events state the signal outright, so you do not have to recognise keywords
83
125
  # in the text of a message.received event. They also cover cases that produce no
84
126
  # inbound message at all, such as a network handling an opt-out on your behalf.
85
127
  #
86
- # Fields are ordered identity → resulting state → provenance → join key. Nothing
87
- # here restates the envelope: which of the three signals occurred is the
88
- # envelope's event, and when it was emitted is its timestamp. Retries carry the
89
- # same X-Webhook-Event-ID header, which is what to deduplicate on.
128
+ # Two of the four change consent and two do not: contact.help and
129
+ # contact.custom_keyword report the state the contact already had. Read opt_out
130
+ # for the state and the envelope's event for what happened, rather than inferring
131
+ # one from the other.
132
+ #
133
+ # Fields are ordered identity → resulting state → provenance → join keys. The two
134
+ # parties are from and to. Note that the message family has not moved to those
135
+ # names yet — message.received still calls the same two parties inbound_number and
136
+ # outbound_number. Nothing here restates the envelope: which signal occurred is
137
+ # the envelope's event, and when it was emitted is its timestamp. Retries carry
138
+ # the same X-Webhook-Event-ID header, which is what to deduplicate on.
90
139
  sig do
91
140
  params(
92
141
  opt_out: T::Boolean,
93
142
  source: String,
94
143
  account_id: String,
144
+ agent_id: T.nilable(String),
95
145
  channel: String,
96
146
  contact_id: String,
147
+ from: String,
97
148
  message_id: T.nilable(String),
98
- phone_number: String,
99
- text: T.nilable(String)
149
+ template_id: T.nilable(String),
150
+ text: T.nilable(String),
151
+ to: T.nilable(String)
100
152
  ).returns(T.attached_class)
101
153
  end
102
154
  def self.new(
103
155
  # Whether the contact is opted out after this signal — the state to write to your
104
- # own record. Same meaning as opt_out on the contact resource. On contact.help
105
- # this reports the contact's existing state, which help does not change.
156
+ # own record. Same meaning as opt_out on the contact resource. On contact.help and
157
+ # contact.custom_keyword this reports the contact's existing state, which neither
158
+ # changes.
106
159
  #
107
160
  # Two signals from the same contact can arrive out of order, because each one is
108
161
  # queued on its own rather than against the contact. Compare the envelope's
@@ -118,12 +171,24 @@ module Sentdm
118
171
  # The account the contact belongs to. Present so one endpoint can serve several
119
172
  # accounts.
120
173
  account_id: nil,
174
+ # The RCS agent the signal reached, when it reached one.
175
+ #
176
+ # Omitted entirely on channels that have no agent, rather than sent as null — an
177
+ # SMS or WhatsApp payload does not carry this key at all. On RCS it is the
178
+ # counterpart to To: a contact reaches an agent rather than a number, so exactly
179
+ # one of the two is populated and never both. If you run more than one agent, this
180
+ # is what tells you which of them the contact acted on.
181
+ agent_id: nil,
121
182
  # The channel the signal arrived on, for example sms or whatsapp.
122
183
  channel: nil,
123
184
  # The contact who raised the signal. Always populated, including for contact.help
124
- # from a number you have not messaged before — the contact is created if it does
125
- # not exist yet, so this identifier is always resolvable against the contacts API.
185
+ # or contact.custom_keyword from a number you have not messaged before — the
186
+ # contact is created if it does not exist yet, so this identifier is always
187
+ # resolvable against the contacts API.
126
188
  contact_id: nil,
189
+ # The contact's number, in E.164 format with the leading + — who raised the
190
+ # signal. The same party message.received publishes as inbound_number.
191
+ from: nil,
127
192
  # The inbound message that carried the signal, matching message_id on the
128
193
  # corresponding message.received event so the two can be joined.
129
194
  #
@@ -133,13 +198,35 @@ module Sentdm
133
198
  # number. The field is always present, so read it and check for null rather than
134
199
  # checking whether the key exists.
135
200
  message_id: nil,
136
- # The contact's number in E.164 format. Same value as phone_number on the contact
137
- # resource.
138
- phone_number: nil,
201
+ # The auto-reply template whose keyword the contact matched, joinable against the
202
+ # templates API.
203
+ #
204
+ # This is what identifies which signal arrived on contact.custom_keyword: every
205
+ # custom template reports the same event name, so the event alone cannot tell your
206
+ # booking keyword from your opening-hours one. One template holds as many keywords
207
+ # as you configured, so this is steadier to switch on than text.
208
+ #
209
+ # Populated on the compliance sub-types too, where it names the template that
210
+ # replied. Sent as null when no template was involved — a network-reported opt-out
211
+ # matches no keyword. The field is always present, so read it and check for null.
212
+ template_id: nil,
139
213
  # The text the contact sent, for example STOP or UNSUBSCRIBE. Sent as null when
140
214
  # the signal did not arrive as text. The field is always present, so read it and
141
215
  # check for null rather than checking whether the key exists.
142
- text: nil
216
+ text: nil,
217
+ # The number of yours that received the signal, in E.164 format with the leading
218
+ # +. Tells a multi-number account which of its senders the contact acted on, which
219
+ # nothing else on this payload answers.
220
+ #
221
+ # This is your number, not the contact's. That is the opposite of what to means on
222
+ # POST /v3/messages, where it is the list of recipients you are sending to. Reply
223
+ # to From, not to this field, or the message goes back to yourself.
224
+ #
225
+ # Sent as null when the signal did not arrive at a number of yours — an RCS signal
226
+ # terminates at an agent rather than a number, and a provider-reported opt-out may
227
+ # name no receiving number at all. The field is always present, so read it and
228
+ # check for null rather than checking whether the key exists.
229
+ to: nil
143
230
  )
144
231
  end
145
232
 
@@ -149,11 +236,14 @@ module Sentdm
149
236
  opt_out: T::Boolean,
150
237
  source: String,
151
238
  account_id: String,
239
+ agent_id: T.nilable(String),
152
240
  channel: String,
153
241
  contact_id: String,
242
+ from: String,
154
243
  message_id: T.nilable(String),
155
- phone_number: String,
156
- text: T.nilable(String)
244
+ template_id: T.nilable(String),
245
+ text: T.nilable(String),
246
+ to: T.nilable(String)
157
247
  }
158
248
  )
159
249
  end