kit-rb 0.2.0 → 0.3.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 +4 -4
- data/.githooks/pre-commit +21 -0
- data/.githooks/pre-push +6 -0
- data/CHANGELOG.md +70 -0
- data/README.md +58 -16
- data/docs/DESIGN.md +38 -14
- data/docs/TASKS.md +47 -0
- data/lib/kit/auth/api_key.rb +7 -0
- data/lib/kit/auth/credential.rb +22 -0
- data/lib/kit/auth/oauth.rb +7 -8
- data/lib/kit/connection.rb +53 -20
- data/lib/kit/errors.rb +52 -15
- data/lib/kit/oauth/token.rb +9 -0
- data/lib/kit/objects/account.rb +44 -3
- data/lib/kit/objects/bulk_result.rb +39 -0
- data/lib/kit/objects/custom_field.rb +4 -2
- data/lib/kit/objects/post.rb +4 -2
- data/lib/kit/objects/sequence_email.rb +5 -2
- data/lib/kit/objects/subscriber.rb +16 -9
- data/lib/kit/objects/tag.rb +5 -3
- data/lib/kit/objects/webhook_endpoint.rb +7 -2
- data/lib/kit/pagination.rb +47 -3
- data/lib/kit/resources/account.rb +4 -4
- data/lib/kit/resources/base.rb +46 -4
- data/lib/kit/resources/broadcasts.rb +32 -13
- data/lib/kit/resources/bulk.rb +18 -20
- data/lib/kit/resources/custom_fields.rb +2 -2
- data/lib/kit/resources/forms.rb +3 -3
- data/lib/kit/resources/posts.rb +1 -1
- data/lib/kit/resources/purchases.rb +10 -6
- data/lib/kit/resources/sequences.rb +49 -25
- data/lib/kit/resources/snippets.rb +13 -8
- data/lib/kit/resources/subscribers.rb +23 -9
- data/lib/kit/resources/tags.rb +6 -6
- data/lib/kit/resources/webhook_endpoints.rb +12 -4
- data/lib/kit/resources/webhooks.rb +1 -1
- data/lib/kit/version.rb +1 -1
- data/lib/kit/webhooks/delivery.rb +61 -0
- data/lib/kit/webhooks/events.rb +113 -0
- data/lib/kit/webhooks/signature.rb +100 -0
- data/lib/kit-rb.rb +5 -0
- data/sig/kit-rb.rbs +170 -29
- metadata +11 -1
data/lib/kit/resources/bulk.rb
CHANGED
|
@@ -3,60 +3,58 @@
|
|
|
3
3
|
module Kit
|
|
4
4
|
module Resources
|
|
5
5
|
# The /v4/bulk endpoints — batch operations that all require OAuth. Each call
|
|
6
|
-
# takes an array of items and an optional callback_url
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# takes an array of items and an optional callback_url and returns a
|
|
7
|
+
# BulkResult: the affected records under `items`, the rejected inputs under
|
|
8
|
+
# `failures`, and `async?` when Kit queued the batch (202) and will report
|
|
9
|
+
# the outcome to callback_url instead.
|
|
10
10
|
class Bulk < Base
|
|
11
11
|
# POST /v4/bulk/subscribers
|
|
12
12
|
def create_subscribers(subscribers, callback_url: nil)
|
|
13
|
-
post
|
|
13
|
+
bulk(:post, "/v4/bulk/subscribers", "subscribers", subscribers: subscribers, callback_url: callback_url)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# POST /v4/bulk/custom_fields
|
|
17
17
|
def create_custom_fields(custom_fields, callback_url: nil)
|
|
18
|
-
post
|
|
18
|
+
bulk(:post, "/v4/bulk/custom_fields", "custom_fields",
|
|
19
|
+
custom_fields: custom_fields, callback_url: callback_url)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# POST /v4/bulk/custom_fields/subscribers
|
|
22
23
|
def update_custom_field_values(custom_field_values, callback_url: nil)
|
|
23
|
-
post
|
|
24
|
+
bulk(:post, "/v4/bulk/custom_fields/subscribers", "custom_field_values",
|
|
24
25
|
custom_field_values: custom_field_values, callback_url: callback_url)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# POST /v4/bulk/forms/subscribers
|
|
28
29
|
def add_subscribers_to_forms(additions, callback_url: nil)
|
|
29
|
-
post
|
|
30
|
+
bulk(:post, "/v4/bulk/forms/subscribers", "subscribers", additions: additions, callback_url: callback_url)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# POST /v4/bulk/tags
|
|
33
34
|
def create_tags(tags, callback_url: nil)
|
|
34
|
-
post
|
|
35
|
+
bulk(:post, "/v4/bulk/tags", "tags", tags: tags, callback_url: callback_url)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
# DELETE /v4/bulk/tags
|
|
38
|
+
# DELETE /v4/bulk/tags — returns failures only.
|
|
38
39
|
def delete_tags(tags, callback_url: nil)
|
|
39
|
-
delete
|
|
40
|
+
bulk(:delete, "/v4/bulk/tags", nil, tags: tags, callback_url: callback_url)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# POST /v4/bulk/tags/subscribers
|
|
43
44
|
def tag_subscribers(taggings, callback_url: nil)
|
|
44
|
-
post
|
|
45
|
+
bulk(:post, "/v4/bulk/tags/subscribers", "subscribers", taggings: taggings, callback_url: callback_url)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
# DELETE /v4/bulk/tags/subscribers
|
|
48
|
+
# DELETE /v4/bulk/tags/subscribers — returns failures only.
|
|
48
49
|
def remove_tag_subscribers(taggings, callback_url: nil)
|
|
49
|
-
delete
|
|
50
|
+
bulk(:delete, "/v4/bulk/tags/subscribers", nil, taggings: taggings, callback_url: callback_url)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
private
|
|
53
54
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def delete(path, **body)
|
|
59
|
-
http_delete(path, body: body.compact)
|
|
55
|
+
def bulk(verb, path, key, **body)
|
|
56
|
+
status, response = @connection.request_with_status(verb, path, body: body.compact)
|
|
57
|
+
Objects::BulkResult.from(status, response, key)
|
|
60
58
|
end
|
|
61
59
|
end
|
|
62
60
|
end
|
|
@@ -16,12 +16,12 @@ module Kit
|
|
|
16
16
|
|
|
17
17
|
# PUT /v4/custom_fields/:id
|
|
18
18
|
def update(id, label:)
|
|
19
|
-
one(:put, "/v4/custom_fields/#{id}", "custom_field", Objects::CustomField, body: { label: label })
|
|
19
|
+
one(:put, "/v4/custom_fields/#{path_id(id)}", "custom_field", Objects::CustomField, body: { label: label })
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# DELETE /v4/custom_fields/:id
|
|
23
23
|
def delete(id)
|
|
24
|
-
http_delete("/v4/custom_fields/#{id}")
|
|
24
|
+
http_delete("/v4/custom_fields/#{path_id(id)}")
|
|
25
25
|
nil
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/kit/resources/forms.rb
CHANGED
|
@@ -12,18 +12,18 @@ module Kit
|
|
|
12
12
|
|
|
13
13
|
# GET /v4/forms/:form_id/subscribers
|
|
14
14
|
def subscribers(form_id, **params)
|
|
15
|
-
collection("/v4/forms/#{form_id}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
15
|
+
collection("/v4/forms/#{path_id(form_id)}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# POST /v4/forms/:form_id/subscribers/:subscriber_id
|
|
19
19
|
def add_subscriber(form_id, subscriber_id, referrer: nil)
|
|
20
|
-
one(:post, "/v4/forms/#{form_id}/subscribers/#{subscriber_id}", "subscriber",
|
|
20
|
+
one(:post, "/v4/forms/#{path_id(form_id)}/subscribers/#{path_id(subscriber_id)}", "subscriber",
|
|
21
21
|
Objects::Subscriber, body: { referrer: referrer }.compact)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# POST /v4/forms/:form_id/subscribers
|
|
25
25
|
def add_subscriber_by_email(form_id, email_address:, referrer: nil)
|
|
26
|
-
one(:post, "/v4/forms/#{form_id}/subscribers", "subscriber", Objects::Subscriber,
|
|
26
|
+
one(:post, "/v4/forms/#{path_id(form_id)}/subscribers", "subscriber", Objects::Subscriber,
|
|
27
27
|
body: { email_address: email_address, referrer: referrer }.compact)
|
|
28
28
|
end
|
|
29
29
|
end
|
data/lib/kit/resources/posts.rb
CHANGED
|
@@ -12,14 +12,18 @@ module Kit
|
|
|
12
12
|
|
|
13
13
|
# GET /v4/purchases/:id
|
|
14
14
|
def get(id)
|
|
15
|
-
one(:get, "/v4/purchases/#{id}", "purchase", Objects::Purchase)
|
|
15
|
+
one(:get, "/v4/purchases/#{path_id(id)}", "purchase", Objects::Purchase)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# POST /v4/purchases — the request wraps the fields under a "purchase"
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
def create(
|
|
22
|
-
|
|
18
|
+
# POST /v4/purchases — the request wraps the fields under a "purchase"
|
|
19
|
+
# key. The spec marks every field required; `products` is an array of
|
|
20
|
+
# { name, pid, lid, quantity, unit_price, sku }.
|
|
21
|
+
def create(email_address:, transaction_id:, status:, currency:, transaction_time: OMIT, subtotal: OMIT,
|
|
22
|
+
tax: OMIT, shipping: OMIT, discount: OMIT, total: OMIT, products: OMIT)
|
|
23
|
+
purchase = given(email_address: email_address, transaction_id: transaction_id, status: status,
|
|
24
|
+
currency: currency, transaction_time: transaction_time, subtotal: subtotal, tax: tax,
|
|
25
|
+
shipping: shipping, discount: discount, total: total, products: products)
|
|
26
|
+
one(:post, "/v4/purchases", "purchase", Objects::Purchase, body: { purchase: purchase })
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
end
|
|
@@ -10,67 +10,91 @@ module Kit
|
|
|
10
10
|
collection("/v4/sequences", "sequences", Objects::Sequence, params)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# GET /v4/sequences/:id
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# GET /v4/sequences/:id — pass include: "stats" to embed the sequence's
|
|
14
|
+
# performance stats.
|
|
15
|
+
def get(id, include: nil)
|
|
16
|
+
one(:get, "/v4/sequences/#{path_id(id)}", "sequence", Objects::Sequence, params: { include: include }.compact)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# GET /v4/sequences/:sequence_id/subscribers
|
|
19
20
|
def subscribers(sequence_id, **params)
|
|
20
|
-
collection("/v4/sequences/#{sequence_id}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
21
|
+
collection("/v4/sequences/#{path_id(sequence_id)}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
# POST /v4/sequences
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# POST /v4/sequences. `send_days` is an array of weekday names,
|
|
25
|
+
# `send_hour` 0–23, `time_zone` an IANA name; `exclude_subscriber_sources`
|
|
26
|
+
# is [{ type: "tag"|"sequence"|"form"|"segment", ids: [...] }].
|
|
27
|
+
def create(name:, email_address: OMIT, email_template_id: OMIT, send_days: OMIT, send_hour: OMIT,
|
|
28
|
+
time_zone: OMIT, active: OMIT, repeat: OMIT, hold: OMIT, exclude_subscriber_sources: OMIT)
|
|
29
|
+
body = given(name: name, email_address: email_address, email_template_id: email_template_id,
|
|
30
|
+
send_days: send_days, send_hour: send_hour, time_zone: time_zone, active: active,
|
|
31
|
+
repeat: repeat, hold: hold, exclude_subscriber_sources: exclude_subscriber_sources)
|
|
32
|
+
one(:post, "/v4/sequences", "sequence", Objects::Sequence, body: body)
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
# PUT /v4/sequences/:id
|
|
29
|
-
def update(id,
|
|
30
|
-
|
|
35
|
+
# PUT /v4/sequences/:id — same fields as #create; only those passed change.
|
|
36
|
+
def update(id, name: OMIT, email_address: OMIT, email_template_id: OMIT, send_days: OMIT, send_hour: OMIT,
|
|
37
|
+
time_zone: OMIT, active: OMIT, repeat: OMIT, hold: OMIT, exclude_subscriber_sources: OMIT)
|
|
38
|
+
body = given(name: name, email_address: email_address, email_template_id: email_template_id,
|
|
39
|
+
send_days: send_days, send_hour: send_hour, time_zone: time_zone, active: active,
|
|
40
|
+
repeat: repeat, hold: hold, exclude_subscriber_sources: exclude_subscriber_sources)
|
|
41
|
+
one(:put, "/v4/sequences/#{path_id(id)}", "sequence", Objects::Sequence, body: body)
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
# DELETE /v4/sequences/:id
|
|
34
45
|
def delete(id)
|
|
35
|
-
http_delete("/v4/sequences/#{id}")
|
|
46
|
+
http_delete("/v4/sequences/#{path_id(id)}")
|
|
36
47
|
nil
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# POST /v4/sequences/:sequence_id/subscribers/:subscriber_id
|
|
40
51
|
def add_subscriber(sequence_id, subscriber_id)
|
|
41
|
-
one(:post, "/v4/sequences/#{sequence_id}/subscribers/#{subscriber_id}", "subscriber", Objects::Subscriber)
|
|
52
|
+
one(:post, "/v4/sequences/#{path_id(sequence_id)}/subscribers/#{path_id(subscriber_id)}", "subscriber", Objects::Subscriber)
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
# POST /v4/sequences/:sequence_id/subscribers
|
|
45
56
|
def add_subscriber_by_email(sequence_id, email_address:)
|
|
46
|
-
one(:post, "/v4/sequences/#{sequence_id}/subscribers", "subscriber", Objects::Subscriber,
|
|
57
|
+
one(:post, "/v4/sequences/#{path_id(sequence_id)}/subscribers", "subscriber", Objects::Subscriber,
|
|
47
58
|
body: { email_address: email_address })
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
# GET /v4/sequences/:sequence_id/emails
|
|
51
62
|
def emails(sequence_id, **params)
|
|
52
|
-
collection("/v4/sequences/#{sequence_id}/emails", "emails", Objects::SequenceEmail, params)
|
|
63
|
+
collection("/v4/sequences/#{path_id(sequence_id)}/emails", "emails", Objects::SequenceEmail, params)
|
|
53
64
|
end
|
|
54
65
|
|
|
55
|
-
# GET /v4/sequences/:sequence_id/emails/:id
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
# GET /v4/sequences/:sequence_id/emails/:id — include: "stats" embeds the
|
|
67
|
+
# email's performance stats.
|
|
68
|
+
def email(sequence_id, id, include: nil)
|
|
69
|
+
one(:get, "/v4/sequences/#{path_id(sequence_id)}/emails/#{path_id(id)}", "email", Objects::SequenceEmail,
|
|
70
|
+
params: { include: include }.compact)
|
|
58
71
|
end
|
|
59
72
|
|
|
60
|
-
# POST /v4/sequences/:sequence_id/emails
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
# POST /v4/sequences/:sequence_id/emails. `delay_unit` is "days" or
|
|
74
|
+
# "hours"; `send_days` nil resets to all seven days; `position` is
|
|
75
|
+
# zero-based and defaults to last.
|
|
76
|
+
def create_email(sequence_id, subject:, delay_value:, delay_unit:, preview_text: OMIT, content: OMIT,
|
|
77
|
+
email_template_id: OMIT, published: OMIT, send_days: OMIT, position: OMIT)
|
|
78
|
+
body = given(subject: subject, delay_value: delay_value, delay_unit: delay_unit, preview_text: preview_text,
|
|
79
|
+
content: content, email_template_id: email_template_id, published: published,
|
|
80
|
+
send_days: send_days, position: position)
|
|
81
|
+
one(:post, "/v4/sequences/#{path_id(sequence_id)}/emails", "email", Objects::SequenceEmail, body: body)
|
|
64
82
|
end
|
|
65
83
|
|
|
66
|
-
# PUT /v4/sequences/:sequence_id/emails/:id
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
# PUT /v4/sequences/:sequence_id/emails/:id — only the fields passed
|
|
85
|
+
# change; pass nil for email_template_id or send_days to clear them.
|
|
86
|
+
def update_email(sequence_id, id, subject: OMIT, delay_value: OMIT, delay_unit: OMIT, preview_text: OMIT,
|
|
87
|
+
content: OMIT, email_template_id: OMIT, published: OMIT, send_days: OMIT, position: OMIT)
|
|
88
|
+
body = given(subject: subject, delay_value: delay_value, delay_unit: delay_unit, preview_text: preview_text,
|
|
89
|
+
content: content, email_template_id: email_template_id, published: published,
|
|
90
|
+
send_days: send_days, position: position)
|
|
91
|
+
one(:put, "/v4/sequences/#{path_id(sequence_id)}/emails/#{path_id(id)}", "email", Objects::SequenceEmail,
|
|
92
|
+
body: body)
|
|
69
93
|
end
|
|
70
94
|
|
|
71
95
|
# DELETE /v4/sequences/:sequence_id/emails/:id
|
|
72
96
|
def delete_email(sequence_id, id)
|
|
73
|
-
http_delete("/v4/sequences/#{sequence_id}/emails/#{id}")
|
|
97
|
+
http_delete("/v4/sequences/#{path_id(sequence_id)}/emails/#{path_id(id)}")
|
|
74
98
|
nil
|
|
75
99
|
end
|
|
76
100
|
end
|
|
@@ -11,18 +11,23 @@ module Kit
|
|
|
11
11
|
|
|
12
12
|
# GET /v4/snippets/:id
|
|
13
13
|
def get(id)
|
|
14
|
-
one(:get, "/v4/snippets/#{id}", "snippet", Objects::Snippet)
|
|
14
|
+
one(:get, "/v4/snippets/#{path_id(id)}", "snippet", Objects::Snippet)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# POST /v4/snippets
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
# POST /v4/snippets. An "inline" snippet carries Liquid text in
|
|
18
|
+
# `content`; a "block" snippet carries HTML in
|
|
19
|
+
# `document_attributes: { value_html: ... }`.
|
|
20
|
+
def create(name:, snippet_type:, content: OMIT, document_attributes: OMIT)
|
|
21
|
+
body = given(name: name, snippet_type: snippet_type, content: content,
|
|
22
|
+
document_attributes: document_attributes)
|
|
23
|
+
one(:post, "/v4/snippets", "snippet", Objects::Snippet, body: body)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
# PUT /v4/snippets/:id
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
# PUT /v4/snippets/:id — rename, archive/restore, or replace the content
|
|
27
|
+
# (snippet_type cannot change).
|
|
28
|
+
def update(id, name: OMIT, archived: OMIT, content: OMIT, document_attributes: OMIT)
|
|
29
|
+
body = given(name: name, archived: archived, content: content, document_attributes: document_attributes)
|
|
30
|
+
one(:put, "/v4/snippets/#{path_id(id)}", "snippet", Objects::Snippet, body: body)
|
|
26
31
|
end
|
|
27
32
|
end
|
|
28
33
|
end
|
|
@@ -13,7 +13,7 @@ module Kit
|
|
|
13
13
|
|
|
14
14
|
# GET /v4/subscribers/:id
|
|
15
15
|
def get(id)
|
|
16
|
-
one(:get, "/v4/subscribers/#{id}", "subscriber", Objects::Subscriber)
|
|
16
|
+
one(:get, "/v4/subscribers/#{path_id(id)}", "subscriber", Objects::Subscriber)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# POST /v4/subscribers — email_address required; first_name, state, fields optional.
|
|
@@ -26,12 +26,17 @@ module Kit
|
|
|
26
26
|
# PUT /v4/subscribers/:id
|
|
27
27
|
def update(id, first_name: nil, email_address: nil, fields: nil)
|
|
28
28
|
body = { first_name: first_name, email_address: email_address, fields: fields }.compact
|
|
29
|
-
one(:put, "/v4/subscribers/#{id}", "subscriber", Objects::Subscriber, body: body)
|
|
29
|
+
one(:put, "/v4/subscribers/#{path_id(id)}", "subscriber", Objects::Subscriber, body: body)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# POST /v4/subscribers/:id/unsubscribe
|
|
32
|
+
# POST /v4/subscribers/:id/unsubscribe — the API answers 204 with no body,
|
|
33
|
+
# so this returns nil. Should Kit ever echo the subscriber back, it is
|
|
34
|
+
# returned as a Subscriber instead of being discarded.
|
|
33
35
|
def unsubscribe(id)
|
|
34
|
-
|
|
36
|
+
body = http_post("/v4/subscribers/#{path_id(id)}/unsubscribe")
|
|
37
|
+
return nil unless body.is_a?(Hash) && body.key?("subscriber")
|
|
38
|
+
|
|
39
|
+
Objects::Subscriber.from(body.fetch("subscriber"))
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
# POST /v4/subscribers/filter — the same filters as #list, sent in the
|
|
@@ -42,24 +47,33 @@ module Kit
|
|
|
42
47
|
|
|
43
48
|
# GET /v4/subscribers/:id/tags — the tags applied to a subscriber.
|
|
44
49
|
def tags(id, **params)
|
|
45
|
-
collection("/v4/subscribers/#{id}/tags", "tags", Objects::Tag, params)
|
|
50
|
+
collection("/v4/subscribers/#{path_id(id)}/tags", "tags", Objects::Tag, params)
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
# GET /v4/subscribers/:id/stats — the subscriber's engagement stats.
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
# Bound the window with email_sent_after / email_sent_before (yyyy-mm-dd).
|
|
55
|
+
def stats(id, email_sent_after: nil, email_sent_before: nil)
|
|
56
|
+
params = { email_sent_after: email_sent_after, email_sent_before: email_sent_before }.compact
|
|
57
|
+
one(:get, "/v4/subscribers/#{path_id(id)}/stats", "subscriber", Objects::SubscriberStats, params: params)
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
# POST /v4/subscribers/:id/location — set the subscriber's location
|
|
54
61
|
# (a hash of city/state_province/country_code/latitude/longitude/timezone).
|
|
55
62
|
def set_location(id, location:)
|
|
56
|
-
one(:post, "/v4/subscribers/#{id}/location", "subscriber", Objects::Subscriber,
|
|
63
|
+
one(:post, "/v4/subscribers/#{path_id(id)}/location", "subscriber", Objects::Subscriber,
|
|
64
|
+
body: { location: location })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# PATCH /v4/subscribers/:id/location — replace a pinned location. Kit
|
|
68
|
+
# requires the full location (all six keys) on update, not a partial.
|
|
69
|
+
def update_location(id, location:)
|
|
70
|
+
one(:patch, "/v4/subscribers/#{path_id(id)}/location", "subscriber", Objects::Subscriber,
|
|
57
71
|
body: { location: location })
|
|
58
72
|
end
|
|
59
73
|
|
|
60
74
|
# DELETE /v4/subscribers/:id/location
|
|
61
75
|
def remove_location(id)
|
|
62
|
-
http_delete("/v4/subscribers/#{id}/location")
|
|
76
|
+
http_delete("/v4/subscribers/#{path_id(id)}/location")
|
|
63
77
|
nil
|
|
64
78
|
end
|
|
65
79
|
end
|
data/lib/kit/resources/tags.rb
CHANGED
|
@@ -16,36 +16,36 @@ module Kit
|
|
|
16
16
|
|
|
17
17
|
# PUT /v4/tags/:id
|
|
18
18
|
def update(id, name:)
|
|
19
|
-
one(:put, "/v4/tags/#{id}", "tag", Objects::Tag, body: { name: name })
|
|
19
|
+
one(:put, "/v4/tags/#{path_id(id)}", "tag", Objects::Tag, body: { name: name })
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# POST /v4/tags/:tag_id/subscribers/:id
|
|
23
23
|
def tag_subscriber(tag_id, subscriber_id)
|
|
24
|
-
one(:post, "/v4/tags/#{tag_id}/subscribers/#{subscriber_id}", "subscriber", Objects::Subscriber)
|
|
24
|
+
one(:post, "/v4/tags/#{path_id(tag_id)}/subscribers/#{path_id(subscriber_id)}", "subscriber", Objects::Subscriber)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# DELETE /v4/tags/:tag_id/subscribers/:id
|
|
28
28
|
def remove_subscriber(tag_id, subscriber_id)
|
|
29
|
-
http_delete("/v4/tags/#{tag_id}/subscribers/#{subscriber_id}")
|
|
29
|
+
http_delete("/v4/tags/#{path_id(tag_id)}/subscribers/#{path_id(subscriber_id)}")
|
|
30
30
|
nil
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# POST /v4/tags/:tag_id/subscribers — tag a subscriber by email address.
|
|
34
34
|
def tag_subscriber_by_email(tag_id, email_address:)
|
|
35
|
-
one(:post, "/v4/tags/#{tag_id}/subscribers", "subscriber", Objects::Subscriber,
|
|
35
|
+
one(:post, "/v4/tags/#{path_id(tag_id)}/subscribers", "subscriber", Objects::Subscriber,
|
|
36
36
|
body: { email_address: email_address })
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# DELETE /v4/tags/:tag_id/subscribers — remove a tag from a subscriber by
|
|
40
40
|
# email address (passed as a query parameter).
|
|
41
41
|
def remove_subscriber_by_email(tag_id, email_address:)
|
|
42
|
-
http_delete("/v4/tags/#{tag_id}/subscribers", params: { email_address: email_address })
|
|
42
|
+
http_delete("/v4/tags/#{path_id(tag_id)}/subscribers", params: { email_address: email_address })
|
|
43
43
|
nil
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# GET /v4/tags/:tag_id/subscribers
|
|
47
47
|
def subscribers(tag_id, **params)
|
|
48
|
-
collection("/v4/tags/#{tag_id}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
48
|
+
collection("/v4/tags/#{path_id(tag_id)}/subscribers", "subscribers", Objects::Subscriber, params)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -16,7 +16,7 @@ module Kit
|
|
|
16
16
|
|
|
17
17
|
# GET /v4/webhook_endpoints/:id
|
|
18
18
|
def get(id)
|
|
19
|
-
one(:get, "/v4/webhook_endpoints/#{id}", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
19
|
+
one(:get, "/v4/webhook_endpoints/#{path_id(id)}", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# POST /v4/webhook_endpoints
|
|
@@ -25,21 +25,29 @@ module Kit
|
|
|
25
25
|
body: { url: url, events: events, name: name, description: description }.compact)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# PATCH /v4/webhook_endpoints/:id — change name/url/description, pause or
|
|
29
|
+
# resume delivery with status: "active" | "disabled", or replace the
|
|
30
|
+
# subscribed events (the list given here replaces the whole set).
|
|
31
|
+
def update(id, name: nil, url: nil, description: nil, status: nil, events: nil)
|
|
32
|
+
body = { name: name, url: url, description: description, status: status, events: events }.compact
|
|
33
|
+
one(:patch, "/v4/webhook_endpoints/#{path_id(id)}", "webhook_endpoint", Objects::WebhookEndpoint, body: body)
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
# DELETE /v4/webhook_endpoints/:id
|
|
29
37
|
def delete(id)
|
|
30
|
-
http_delete("/v4/webhook_endpoints/#{id}")
|
|
38
|
+
http_delete("/v4/webhook_endpoints/#{path_id(id)}")
|
|
31
39
|
nil
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
# POST /v4/webhook_endpoints/:id/rotate_secret
|
|
35
43
|
def rotate_secret(id, force: nil)
|
|
36
|
-
one(:post, "/v4/webhook_endpoints/#{id}/rotate_secret", "webhook_endpoint", Objects::WebhookEndpoint,
|
|
44
|
+
one(:post, "/v4/webhook_endpoints/#{path_id(id)}/rotate_secret", "webhook_endpoint", Objects::WebhookEndpoint,
|
|
37
45
|
body: { force: force }.compact)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
# POST /v4/webhook_endpoints/:id/revoke_previous_secret
|
|
41
49
|
def revoke_previous_secret(id)
|
|
42
|
-
one(:post, "/v4/webhook_endpoints/#{id}/revoke_previous_secret", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
50
|
+
one(:post, "/v4/webhook_endpoints/#{path_id(id)}/revoke_previous_secret", "webhook_endpoint", Objects::WebhookEndpoint)
|
|
43
51
|
end
|
|
44
52
|
end
|
|
45
53
|
end
|
data/lib/kit/version.rb
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kit
|
|
6
|
+
module Webhooks
|
|
7
|
+
# One event inside a delivery. `id` is the deduplication key (deliveries
|
|
8
|
+
# are retried whole, so the same event can arrive twice); `type` is the
|
|
9
|
+
# event name (e.g. "subscriber.created"); `created` an ISO 8601 UTC string;
|
|
10
|
+
# `data` the event-specific payload, keyed by resource ("subscriber",
|
|
11
|
+
# "tag", ...), left as a Hash because its shape varies per type.
|
|
12
|
+
Event = Data.define(:id, :type, :created, :data) do
|
|
13
|
+
def self.from(hash)
|
|
14
|
+
new(id: hash["id"], type: hash["type"], created: hash["created"], data: hash["data"] || {})
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The JSON envelope a webhook endpoint receives (developers.kit.com/
|
|
19
|
+
# webhooks/delivery-format, verified 2026-09-06): { delivery_id, events[] }
|
|
20
|
+
# with 1..100 events of one type, plus the headers X-Kit-Delivery (the same
|
|
21
|
+
# id), X-Kit-Signature, and User-Agent "Kit-Webhooks/2.0". Answer 2xx to
|
|
22
|
+
# acknowledge; anything else is retried (8 attempts over ~41 hours).
|
|
23
|
+
#
|
|
24
|
+
# delivery = Kit::Webhooks::Delivery.from_request(
|
|
25
|
+
# request.raw_post, request.headers["X-Kit-Signature"], secret: secret
|
|
26
|
+
# )
|
|
27
|
+
# delivery.events.each { |event| handle(event) unless seen?(event.id) }
|
|
28
|
+
# The header carrying the delivery id on each request.
|
|
29
|
+
DELIVERY_HEADER = "X-Kit-Delivery"
|
|
30
|
+
|
|
31
|
+
Delivery = Data.define(:delivery_id, :events) do
|
|
32
|
+
def self.from(hash)
|
|
33
|
+
new(delivery_id: hash["delivery_id"], events: Array(hash["events"]).map { |event| Event.from(event) })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parses a raw JSON body. Raises UnexpectedResponseError when it is not
|
|
37
|
+
# the documented envelope.
|
|
38
|
+
def self.parse(payload)
|
|
39
|
+
body = JSON.parse(payload)
|
|
40
|
+
raise UnexpectedResponseError.new("webhook delivery is not a JSON object", body: body) unless body.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
from(body)
|
|
43
|
+
rescue JSON::ParserError => e
|
|
44
|
+
raise UnexpectedResponseError.new("webhook delivery is not valid JSON: #{e.message}", body: payload)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Verifies the signature, then parses. The one call a webhook receiver
|
|
48
|
+
# needs; a failed check raises SignatureError before any JSON is read.
|
|
49
|
+
def self.from_request(payload, signature_header, secret:, tolerance: Signature::DEFAULT_TOLERANCE,
|
|
50
|
+
now: Time.now.to_i)
|
|
51
|
+
Signature.verify!(payload, signature_header, secret: secret, tolerance: tolerance, now: now)
|
|
52
|
+
parse(payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Every event in a delivery shares one type.
|
|
56
|
+
def type
|
|
57
|
+
events.first&.type
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|