kit_api 0.1.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/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +120 -0
- data/lib/kit/client/accounts.rb +32 -0
- data/lib/kit/client/broadcasts.rb +41 -0
- data/lib/kit/client/custom_fields.rb +34 -0
- data/lib/kit/client/email_templates.rb +11 -0
- data/lib/kit/client/forms.rb +30 -0
- data/lib/kit/client/posts.rb +15 -0
- data/lib/kit/client/purchases.rb +20 -0
- data/lib/kit/client/segments.rb +11 -0
- data/lib/kit/client/sequences.rb +60 -0
- data/lib/kit/client/snippets.rb +24 -0
- data/lib/kit/client/subscribers.rb +54 -0
- data/lib/kit/client/tags.rb +56 -0
- data/lib/kit/client/webhooks.rb +36 -0
- data/lib/kit/client.rb +115 -0
- data/lib/kit/simulated.rb +232 -0
- data/lib/kit/version.rb +5 -0
- data/lib/kit.rb +68 -0
- data/lib/kit_api.rb +5 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4996d89da108c8784c235ec730e4d2b4137701d41c1942dfa3e6020d0f7c692e
|
|
4
|
+
data.tar.gz: 95b2e1edd7d2f15effd72df4d75804202a6685b82e08fcd35f485b51219d4f63
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '070426945ce384b443242bc1f72fe3546449fc5bfb05cde203b9bffe62177bdcf82992c5537a79e4881ad70cf0319429bae37f702921067bd35fc3eabb232364'
|
|
7
|
+
data.tar.gz: 77670b26e8ec2cfd5006e18560f93e69b27199ef2edcd4f2ec2fb5be0cd329218be354b26ae1fc1b47090793e2e8e987375cacd78daddf37ca8c96a520ca4d79
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — Unreleased
|
|
4
|
+
|
|
5
|
+
- Initial release: full Kit v4 API surface (subscribers, tags, custom fields,
|
|
6
|
+
forms, sequences, sequence emails, broadcasts, account, purchases, segments,
|
|
7
|
+
snippets, posts, email templates, webhooks, bulk operations).
|
|
8
|
+
- High-level `Kit.subscribe` / `Kit.tag` (by name, create-if-missing, cached) /
|
|
9
|
+
`Kit.unsubscribe` (by email, idempotent) conveniences.
|
|
10
|
+
- `Kit::Simulated` recording driver with an enforced surface-parity guarantee.
|
|
11
|
+
- Zero runtime dependencies.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mike D
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# kit_api
|
|
2
|
+
|
|
3
|
+
A plain-Ruby client for the [Kit](https://kit.com) (formerly ConvertKit) **v4 API**. Zero runtime dependencies — `Net::HTTP` and stdlib JSON, nothing else — plus a first-class simulated driver so your tests never touch the network.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/miked-bro/kit_api/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE.txt)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "kit_api"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
Kit.configure do |config|
|
|
18
|
+
config.driver = :v4
|
|
19
|
+
config.api_key = "kit_..." # from Kit → Settings → Developer
|
|
20
|
+
# config.access_token = "..." # or an OAuth token instead
|
|
21
|
+
# config.logger = Logger.new($stdout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# High-level conveniences for the common lifecycle:
|
|
25
|
+
Kit.subscribe(email: "sam@example.com", first_name: "Sam", fields: { plan: "pro" })
|
|
26
|
+
# => { subscriber_id: "123" }
|
|
27
|
+
|
|
28
|
+
Kit.tag(email: "sam@example.com", tag: "customer") # by tag NAME — created if missing
|
|
29
|
+
Kit.unsubscribe(email: "sam@example.com")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Everything else lives on the client, one method per documented endpoint:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Kit.client.subscribers(status: "active", per_page: 100)
|
|
36
|
+
Kit.client.create_broadcast(subject: "News", content: "<p>Hi!</p>", ...)
|
|
37
|
+
Kit.client.add_subscriber_to_sequence(4, email_address: "sam@example.com")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or build a client directly, no global config: `Kit::Client.new(api_key: "kit_...")`.
|
|
41
|
+
|
|
42
|
+
## The full surface
|
|
43
|
+
|
|
44
|
+
Methods return the parsed JSON body as-is (string keys); `204 No Content` returns `true`. Non-2xx responses raise `Kit::Error`, which carries `#status` and the raw `#body`.
|
|
45
|
+
|
|
46
|
+
| Resource | Methods |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Subscribers | `subscribers` `subscriber` `create_subscriber` `update_subscriber` `unsubscribe_subscriber` `bulk_create_subscribers` `subscriber_stats` `subscriber_tags` `pin_subscriber_location` `update_subscriber_location` `delete_subscriber_location` |
|
|
49
|
+
| Tags | `tags` `create_tag` `update_tag` `tag_subscribers` `tag_subscriber` `untag_subscriber` `bulk_create_tags` `bulk_delete_tags` `bulk_tag_subscribers` `bulk_untag_subscribers` |
|
|
50
|
+
| Custom fields | `custom_fields` `create_custom_field` `update_custom_field` `delete_custom_field` `bulk_create_custom_fields` `bulk_update_subscriber_fields` |
|
|
51
|
+
| Forms | `forms` `form_subscribers` `add_subscriber_to_form` `bulk_add_subscribers_to_forms` |
|
|
52
|
+
| Sequences | `sequences` `sequence` `create_sequence` `update_sequence` `delete_sequence` `sequence_subscribers` `add_subscriber_to_sequence` `sequence_emails` `sequence_email` `create_sequence_email` `update_sequence_email` `delete_sequence_email` |
|
|
53
|
+
| Broadcasts | `broadcasts` `broadcast` `create_broadcast` `update_broadcast` `delete_broadcast` `broadcast_stats` `broadcasts_stats` `broadcast_clicks` |
|
|
54
|
+
| Account | `account` `account_colors` `update_account_colors` `creator_profile` `email_stats` `growth_stats` |
|
|
55
|
+
| Purchases | `purchases` `purchase` `create_purchase` |
|
|
56
|
+
| Webhooks | `webhooks` `webhook` `create_webhook` `update_webhook` `delete_webhook` `rotate_webhook_secret` `revoke_previous_webhook_secret` |
|
|
57
|
+
| And | `segments` · `snippets` `snippet` `create_snippet` `update_snippet` · `posts` `post` · `email_templates` |
|
|
58
|
+
|
|
59
|
+
Methods that reach a subscriber take either `email_address:` or `subscriber_id:`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
Kit.client.tag_subscriber(5, email_address: "sam@example.com")
|
|
63
|
+
Kit.client.tag_subscriber(5, subscriber_id: 9)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Pagination
|
|
67
|
+
|
|
68
|
+
Every list endpoint is cursor-paginated. Pass the cursors straight through:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
cursor = nil
|
|
72
|
+
loop do
|
|
73
|
+
page = Kit.client.subscribers(per_page: 1000, after: cursor)
|
|
74
|
+
page["subscribers"].each { |subscriber| ... }
|
|
75
|
+
break unless page.dig("pagination", "has_next_page")
|
|
76
|
+
cursor = page.dig("pagination", "end_cursor")
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Testing with the simulated driver
|
|
81
|
+
|
|
82
|
+
The simulated driver mirrors the real client's surface exactly (a test in this gem enforces it), records every call, and talks to no one:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# The default driver is already :simulated — no configuration needed in tests.
|
|
86
|
+
class FunnelTest < ActiveSupport::TestCase
|
|
87
|
+
setup { Kit::Simulated.reset! }
|
|
88
|
+
|
|
89
|
+
test "signing up subscribes the lead" do
|
|
90
|
+
post signups_path, params: { email: "sam@example.com" }
|
|
91
|
+
|
|
92
|
+
subscribe = Kit::Simulated.calls.find { |call| call[:op] == :subscribe }
|
|
93
|
+
assert_equal "sam@example.com", subscribe[:email]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`Kit::Simulated.calls` is an array of `{ op:, **arguments }` hashes; `subscribe` returns deterministic ids (`"sim_<sha256 prefix>"`) so records that store a `subscriber_id` behave realistically.
|
|
99
|
+
|
|
100
|
+
## Rails
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# config/initializers/kit.rb
|
|
104
|
+
Kit.configure do |config|
|
|
105
|
+
config.driver = Rails.env.production? ? :v4 : :simulated
|
|
106
|
+
config.api_key = Rails.application.credentials.dig(:kit, :api_key)
|
|
107
|
+
config.logger = Rails.logger
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Good to know
|
|
112
|
+
|
|
113
|
+
- **No automatic retries.** Rate limits (120 requests/minute with an API key, 600 with OAuth) and transient failures raise `Kit::Error`; retry where you understand idempotency — e.g. `retry_on Kit::Error` in an Active Job.
|
|
114
|
+
- **OAuth-only endpoints.** Kit restricts the `bulk_*` methods and `create_purchase` to OAuth tokens; with an API key the API returns its own error.
|
|
115
|
+
- **Tag names are cached.** `Kit.tag`/`Kit.client.tag(email:, tag:)` resolves names to ids once per client. Renaming a tag in the Kit UI mid-process can leave a stale id; reconfigure (or build a fresh client) to flush.
|
|
116
|
+
- **Custom fields must exist.** Values in `fields:` only stick for custom fields already defined in your Kit account — create them in the UI or via `create_custom_field(label: "Plan")`.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
[MIT](LICENSE.txt)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Accounts
|
|
6
|
+
def account
|
|
7
|
+
request(:get, "/account")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def account_colors
|
|
11
|
+
request(:get, "/account/colors")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Replaces the whole palette (up to 10 hex codes) — include every color to keep.
|
|
15
|
+
def update_account_colors(colors)
|
|
16
|
+
request(:put, "/account/colors", body: { colors: colors })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def creator_profile
|
|
20
|
+
request(:get, "/account/creator_profile")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def email_stats
|
|
24
|
+
request(:get, "/account/email_stats")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def growth_stats(**params)
|
|
28
|
+
request(:get, "/account/growth_stats", params: params)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Broadcasts
|
|
6
|
+
def broadcasts(**params)
|
|
7
|
+
request(:get, "/broadcasts", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def broadcast(id)
|
|
11
|
+
request(:get, "/broadcasts/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# send_at: nil leaves it a draft; subscriber_filter targets tags/segments.
|
|
15
|
+
def create_broadcast(**attributes)
|
|
16
|
+
request(:post, "/broadcasts", body: attributes)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update_broadcast(id, **attributes)
|
|
20
|
+
request(:put, "/broadcasts/#{id}", body: attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete_broadcast(id)
|
|
24
|
+
request(:delete, "/broadcasts/#{id}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def broadcast_stats(id)
|
|
28
|
+
request(:get, "/broadcasts/#{id}/stats")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Filters: sent_after/before, status.
|
|
32
|
+
def broadcasts_stats(**params)
|
|
33
|
+
request(:get, "/broadcasts/stats", params: params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def broadcast_clicks(id, **params)
|
|
37
|
+
request(:get, "/broadcasts/#{id}/clicks", params: params)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module CustomFields
|
|
6
|
+
def custom_fields(**params)
|
|
7
|
+
request(:get, "/custom_fields", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create_custom_field(label:)
|
|
11
|
+
request(:post, "/custom_fields", body: { label: label })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update_custom_field(id, label:)
|
|
15
|
+
request(:put, "/custom_fields/#{id}", body: { label: label })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delete_custom_field(id)
|
|
19
|
+
request(:delete, "/custom_fields/#{id}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def bulk_create_custom_fields(custom_fields, callback_url: nil)
|
|
23
|
+
request(:post, "/bulk/custom_fields",
|
|
24
|
+
body: { custom_fields: custom_fields, callback_url: callback_url }.compact)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Values are { subscriber_id:, subscriber_custom_field_id:, value: } triples.
|
|
28
|
+
def bulk_update_subscriber_fields(custom_field_values, callback_url: nil)
|
|
29
|
+
request(:post, "/bulk/custom_fields/subscribers",
|
|
30
|
+
body: { custom_field_values: custom_field_values, callback_url: callback_url }.compact)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Forms
|
|
6
|
+
def forms(**params)
|
|
7
|
+
request(:get, "/forms", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def form_subscribers(id, **params)
|
|
11
|
+
request(:get, "/forms/#{id}/subscribers", params: params)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_subscriber_to_form(form_id, email_address: nil, subscriber_id: nil, referrer: nil)
|
|
15
|
+
if subscriber_id
|
|
16
|
+
request(:post, "/forms/#{form_id}/subscribers/#{subscriber_id}", body: { referrer: referrer }.compact)
|
|
17
|
+
else
|
|
18
|
+
request(:post, "/forms/#{form_id}/subscribers",
|
|
19
|
+
body: { email_address: email_address, referrer: referrer }.compact)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Additions are { form_id:, subscriber_id:, referrer: } objects.
|
|
24
|
+
def bulk_add_subscribers_to_forms(additions, callback_url: nil)
|
|
25
|
+
request(:post, "/bulk/forms/subscribers",
|
|
26
|
+
body: { additions: additions, callback_url: callback_url }.compact)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Purchases
|
|
6
|
+
def purchases(**params)
|
|
7
|
+
request(:get, "/purchases", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def purchase(id)
|
|
11
|
+
request(:get, "/purchases/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# OAuth only. An existing transaction_id appends products instead of duplicating.
|
|
15
|
+
def create_purchase(**attributes)
|
|
16
|
+
request(:post, "/purchases", body: { purchase: attributes })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Sequences
|
|
6
|
+
def sequences(**params)
|
|
7
|
+
request(:get, "/sequences", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sequence(id)
|
|
11
|
+
request(:get, "/sequences/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_sequence(name:, **attributes)
|
|
15
|
+
request(:post, "/sequences", body: { name: name, **attributes })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update_sequence(id, **attributes)
|
|
19
|
+
request(:put, "/sequences/#{id}", body: attributes)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete_sequence(id)
|
|
23
|
+
request(:delete, "/sequences/#{id}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sequence_subscribers(id, **params)
|
|
27
|
+
request(:get, "/sequences/#{id}/subscribers", params: params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_subscriber_to_sequence(sequence_id, email_address: nil, subscriber_id: nil)
|
|
31
|
+
if subscriber_id
|
|
32
|
+
request(:post, "/sequences/#{sequence_id}/subscribers/#{subscriber_id}")
|
|
33
|
+
else
|
|
34
|
+
request(:post, "/sequences/#{sequence_id}/subscribers", body: { email_address: email_address })
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sequence_emails(sequence_id, **params)
|
|
39
|
+
request(:get, "/sequences/#{sequence_id}/emails", params: params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def sequence_email(sequence_id, id)
|
|
43
|
+
request(:get, "/sequences/#{sequence_id}/emails/#{id}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_sequence_email(sequence_id, subject:, delay_value:, delay_unit:, **attributes)
|
|
47
|
+
request(:post, "/sequences/#{sequence_id}/emails",
|
|
48
|
+
body: { subject: subject, delay_value: delay_value, delay_unit: delay_unit, **attributes })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update_sequence_email(sequence_id, id, **attributes)
|
|
52
|
+
request(:put, "/sequences/#{sequence_id}/emails/#{id}", body: attributes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def delete_sequence_email(sequence_id, id)
|
|
56
|
+
request(:delete, "/sequences/#{sequence_id}/emails/#{id}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Snippets
|
|
6
|
+
def snippets(**params)
|
|
7
|
+
request(:get, "/snippets", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def snippet(id)
|
|
11
|
+
request(:get, "/snippets/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# snippet_type: "inline" takes content:; "block" takes document_attributes:.
|
|
15
|
+
def create_snippet(name:, **attributes)
|
|
16
|
+
request(:post, "/snippets", body: { name: name, **attributes })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update_snippet(id, **attributes)
|
|
20
|
+
request(:put, "/snippets/#{id}", body: attributes)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Subscribers
|
|
6
|
+
# Filters: email_address (exact), status, created_after/before,
|
|
7
|
+
# updated_after/before, sort_field, sort_order, include, slim.
|
|
8
|
+
def subscribers(**params)
|
|
9
|
+
request(:get, "/subscribers", params: params)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def subscriber(id)
|
|
13
|
+
request(:get, "/subscribers/#{id}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Upserts: an existing email_address gets its first_name updated.
|
|
17
|
+
def create_subscriber(email_address:, **attributes)
|
|
18
|
+
request(:post, "/subscribers", body: { email_address: email_address, **attributes })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def update_subscriber(id, **attributes)
|
|
22
|
+
request(:put, "/subscribers/#{id}", body: attributes)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unsubscribe_subscriber(id)
|
|
26
|
+
request(:post, "/subscribers/#{id}/unsubscribe")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def bulk_create_subscribers(subscribers, callback_url: nil)
|
|
30
|
+
request(:post, "/bulk/subscribers", body: { subscribers: subscribers, callback_url: callback_url }.compact)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subscriber_stats(id, **params)
|
|
34
|
+
request(:get, "/subscribers/#{id}/stats", params: params)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def subscriber_tags(id, **params)
|
|
38
|
+
request(:get, "/subscribers/#{id}/tags", params: params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pin_subscriber_location(id, **location)
|
|
42
|
+
request(:post, "/subscribers/#{id}/location", body: { location: location })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update_subscriber_location(id, **location)
|
|
46
|
+
request(:patch, "/subscribers/#{id}/location", body: { location: location })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete_subscriber_location(id)
|
|
50
|
+
request(:delete, "/subscribers/#{id}/location")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Tags
|
|
6
|
+
def tags(**params)
|
|
7
|
+
request(:get, "/tags", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create_tag(name)
|
|
11
|
+
request(:post, "/tags", body: { name: name })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update_tag(id, name:)
|
|
15
|
+
request(:put, "/tags/#{id}", body: { name: name })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def tag_subscribers(id, **params)
|
|
19
|
+
request(:get, "/tags/#{id}/subscribers", params: params)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tag_subscriber(tag_id, email_address: nil, subscriber_id: nil)
|
|
23
|
+
if subscriber_id
|
|
24
|
+
request(:post, "/tags/#{tag_id}/subscribers/#{subscriber_id}")
|
|
25
|
+
else
|
|
26
|
+
request(:post, "/tags/#{tag_id}/subscribers", body: { email_address: email_address })
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def untag_subscriber(tag_id, email_address: nil, subscriber_id: nil)
|
|
31
|
+
if subscriber_id
|
|
32
|
+
request(:delete, "/tags/#{tag_id}/subscribers/#{subscriber_id}")
|
|
33
|
+
else
|
|
34
|
+
request(:delete, "/tags/#{tag_id}/subscribers", params: { email_address: email_address })
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Bulk endpoints need OAuth; taggings are { tag_id:, subscriber_id: } pairs.
|
|
39
|
+
def bulk_create_tags(tags, callback_url: nil)
|
|
40
|
+
request(:post, "/bulk/tags", body: { tags: tags, callback_url: callback_url }.compact)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bulk_delete_tags(tags, callback_url: nil)
|
|
44
|
+
request(:delete, "/bulk/tags", body: { tags: tags, callback_url: callback_url }.compact)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bulk_tag_subscribers(taggings, callback_url: nil)
|
|
48
|
+
request(:post, "/bulk/tags/subscribers", body: { taggings: taggings, callback_url: callback_url }.compact)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bulk_untag_subscribers(taggings, callback_url: nil)
|
|
52
|
+
request(:delete, "/bulk/tags/subscribers", body: { taggings: taggings, callback_url: callback_url }.compact)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
class Client
|
|
5
|
+
module Webhooks
|
|
6
|
+
def webhooks(**params)
|
|
7
|
+
request(:get, "/webhook_endpoints", params: params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def webhook(id)
|
|
11
|
+
request(:get, "/webhook_endpoints/#{id}")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The signing secret comes back in plaintext only on this response.
|
|
15
|
+
def create_webhook(url:, events:, **attributes)
|
|
16
|
+
request(:post, "/webhook_endpoints", body: { url: url, events: events, **attributes })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update_webhook(id, **attributes)
|
|
20
|
+
request(:put, "/webhook_endpoints/#{id}", body: attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete_webhook(id)
|
|
24
|
+
request(:delete, "/webhook_endpoints/#{id}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rotate_webhook_secret(id, force: nil)
|
|
28
|
+
request(:post, "/webhook_endpoints/#{id}/rotate_secret", body: { force: force }.compact)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def revoke_previous_webhook_secret(id)
|
|
32
|
+
request(:post, "/webhook_endpoints/#{id}/revoke_previous_secret")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/kit/client.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
require_relative "client/accounts"
|
|
8
|
+
require_relative "client/broadcasts"
|
|
9
|
+
require_relative "client/custom_fields"
|
|
10
|
+
require_relative "client/email_templates"
|
|
11
|
+
require_relative "client/forms"
|
|
12
|
+
require_relative "client/posts"
|
|
13
|
+
require_relative "client/purchases"
|
|
14
|
+
require_relative "client/segments"
|
|
15
|
+
require_relative "client/sequences"
|
|
16
|
+
require_relative "client/snippets"
|
|
17
|
+
require_relative "client/subscribers"
|
|
18
|
+
require_relative "client/tags"
|
|
19
|
+
require_relative "client/webhooks"
|
|
20
|
+
|
|
21
|
+
module Kit
|
|
22
|
+
# The real Kit v4 API (https://developers.kit.com). One method per endpoint,
|
|
23
|
+
# named resources/resource(id)/create_resource/verb_resource; each returns
|
|
24
|
+
# the parsed JSON body as-is (204s return true). Pagination cursors pass
|
|
25
|
+
# straight through: subscribers(after: cursor, per_page: 100).
|
|
26
|
+
class Client
|
|
27
|
+
include Accounts, Broadcasts, CustomFields, EmailTemplates, Forms, Posts,
|
|
28
|
+
Purchases, Segments, Sequences, Snippets, Subscribers, Tags, Webhooks
|
|
29
|
+
|
|
30
|
+
def initialize(api_key: nil, access_token: nil, base_url: "https://api.kit.com/v4",
|
|
31
|
+
open_timeout: 10, read_timeout: 20)
|
|
32
|
+
raise Error, "Kit needs an api_key or an OAuth access_token" if api_key.nil? && access_token.nil?
|
|
33
|
+
|
|
34
|
+
@api_key = api_key
|
|
35
|
+
@access_token = access_token
|
|
36
|
+
@base_url = base_url
|
|
37
|
+
@open_timeout = open_timeout
|
|
38
|
+
@read_timeout = read_timeout
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# High-level conveniences, composed from the endpoint methods below.
|
|
42
|
+
|
|
43
|
+
def subscribe(email:, first_name:, fields: {})
|
|
44
|
+
body = create_subscriber(email_address: email, first_name: first_name, fields: fields)
|
|
45
|
+
{ subscriber_id: body.dig("subscriber", "id")&.to_s }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Tags by name: resolved to an id once per client (created if missing) and
|
|
49
|
+
# cached for the life of the process. Reconfiguring flushes the cache.
|
|
50
|
+
def tag(email:, tag:)
|
|
51
|
+
tag_subscriber(tag_id(tag), email_address: email)
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The v4 API unsubscribes by subscriber id only, so look the id up first.
|
|
56
|
+
# An address Kit doesn't know is already unsubscribed — succeed quietly.
|
|
57
|
+
def unsubscribe(email:)
|
|
58
|
+
subscriber = subscribers(email_address: email).fetch("subscribers", []).first
|
|
59
|
+
return true unless subscriber
|
|
60
|
+
|
|
61
|
+
unsubscribe_subscriber(subscriber["id"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
def tag_id(name)
|
|
66
|
+
@tag_ids ||= {}
|
|
67
|
+
@tag_ids[name] ||= begin
|
|
68
|
+
existing, cursor = nil, nil
|
|
69
|
+
loop do
|
|
70
|
+
page = tags(per_page: 1000, after: cursor)
|
|
71
|
+
existing = page.fetch("tags", []).find { |t| t["name"] == name }
|
|
72
|
+
cursor = page.dig("pagination", "end_cursor")
|
|
73
|
+
break if existing || !page.dig("pagination", "has_next_page")
|
|
74
|
+
end
|
|
75
|
+
(existing || create_tag(name).fetch("tag"))["id"]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
VERBS = {
|
|
80
|
+
get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put,
|
|
81
|
+
patch: Net::HTTP::Patch, delete: Net::HTTP::Delete
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
def request(method, path, params: nil, body: nil)
|
|
85
|
+
uri = URI("#{@base_url}#{path}")
|
|
86
|
+
query = params&.compact
|
|
87
|
+
uri.query = URI.encode_www_form(query) unless query.nil? || query.empty?
|
|
88
|
+
|
|
89
|
+
req = VERBS.fetch(method).new(uri.request_uri)
|
|
90
|
+
if @access_token
|
|
91
|
+
req["Authorization"] = "Bearer #{@access_token}"
|
|
92
|
+
else
|
|
93
|
+
req["X-Kit-Api-Key"] = @api_key
|
|
94
|
+
end
|
|
95
|
+
req["Accept"] = "application/json"
|
|
96
|
+
if body
|
|
97
|
+
req["Content-Type"] = "application/json"
|
|
98
|
+
req.body = JSON.generate(body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
102
|
+
http.use_ssl = uri.scheme == "https"
|
|
103
|
+
http.open_timeout = @open_timeout
|
|
104
|
+
http.read_timeout = @read_timeout
|
|
105
|
+
|
|
106
|
+
response = http.request(req)
|
|
107
|
+
unless response.code.start_with?("2")
|
|
108
|
+
raise Error.new("Kit API #{response.code}: #{response.body.to_s[0, 200]}",
|
|
109
|
+
status: response.code.to_i, body: response.body)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
response.body.to_s.empty? ? true : JSON.parse(response.body)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Kit
|
|
6
|
+
# Dev/test driver: records every call, invents ids, talks to no one. Same
|
|
7
|
+
# public surface as Client (a test enforces parity), so code under test can
|
|
8
|
+
# assert on Kit::Simulated.calls instead of stubbing HTTP:
|
|
9
|
+
#
|
|
10
|
+
# Kit::Simulated.reset!
|
|
11
|
+
# Kit.subscribe(email: "sam@example.com", first_name: "Sam")
|
|
12
|
+
# Kit::Simulated.calls # => [{ op: :subscribe, email: "sam@example.com", ... }]
|
|
13
|
+
class Simulated
|
|
14
|
+
def self.calls = @calls ||= []
|
|
15
|
+
def self.reset! = @calls = []
|
|
16
|
+
|
|
17
|
+
PAGE = { "has_previous_page" => false, "has_next_page" => false,
|
|
18
|
+
"start_cursor" => nil, "end_cursor" => nil, "per_page" => 500 }.freeze
|
|
19
|
+
|
|
20
|
+
# High-level conveniences
|
|
21
|
+
|
|
22
|
+
def subscribe(email:, first_name:, fields: {})
|
|
23
|
+
record(:subscribe, email: email, first_name: first_name, fields: fields)
|
|
24
|
+
{ subscriber_id: sim_id(email) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tag(email:, tag:)
|
|
28
|
+
record(:tag, email: email, tag: tag)
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unsubscribe(email:)
|
|
33
|
+
record(:unsubscribe, email: email)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Subscribers
|
|
38
|
+
|
|
39
|
+
def subscribers(**params) = record_page(:subscribers, "subscribers", **params)
|
|
40
|
+
def subscriber(id) = record_one(:subscriber, "subscriber", id, id: id)
|
|
41
|
+
|
|
42
|
+
def create_subscriber(email_address:, **attributes)
|
|
43
|
+
record(:create_subscriber, email_address: email_address, **attributes)
|
|
44
|
+
{ "subscriber" => { "id" => sim_id(email_address), "email_address" => email_address,
|
|
45
|
+
"state" => "active", **attributes.transform_keys(&:to_s) } }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def update_subscriber(id, **attributes) = record_one(:update_subscriber, "subscriber", id, id: id, **attributes)
|
|
49
|
+
def unsubscribe_subscriber(id) = record(:unsubscribe_subscriber, id: id)
|
|
50
|
+
def bulk_create_subscribers(subscribers, callback_url: nil) = record_bulk(:bulk_create_subscribers, subscribers: subscribers, callback_url: callback_url)
|
|
51
|
+
def subscriber_stats(id, **params) = record_one(:subscriber_stats, "subscriber", id, id: id, **params)
|
|
52
|
+
def subscriber_tags(id, **params) = record_page(:subscriber_tags, "tags", id: id, **params)
|
|
53
|
+
def pin_subscriber_location(id, **location) = record_one(:pin_subscriber_location, "subscriber", id, id: id, **location)
|
|
54
|
+
def update_subscriber_location(id, **location) = record_one(:update_subscriber_location, "subscriber", id, id: id, **location)
|
|
55
|
+
def delete_subscriber_location(id) = record(:delete_subscriber_location, id: id)
|
|
56
|
+
|
|
57
|
+
# Tags
|
|
58
|
+
|
|
59
|
+
def tags(**params) = record_page(:tags, "tags", **params)
|
|
60
|
+
|
|
61
|
+
def create_tag(name)
|
|
62
|
+
record(:create_tag, name: name)
|
|
63
|
+
{ "tag" => { "id" => sim_id(name), "name" => name } }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def update_tag(id, name:) = record_one(:update_tag, "tag", id, id: id, name: name)
|
|
67
|
+
def tag_subscribers(id, **params) = record_page(:tag_subscribers, "subscribers", id: id, **params)
|
|
68
|
+
|
|
69
|
+
def tag_subscriber(tag_id, email_address: nil, subscriber_id: nil)
|
|
70
|
+
record(:tag_subscriber, tag_id: tag_id, email_address: email_address, subscriber_id: subscriber_id)
|
|
71
|
+
{ "subscriber" => { "id" => subscriber_id || sim_id(email_address.to_s) } }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def untag_subscriber(tag_id, email_address: nil, subscriber_id: nil)
|
|
75
|
+
record(:untag_subscriber, tag_id: tag_id, email_address: email_address, subscriber_id: subscriber_id)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bulk_create_tags(tags, callback_url: nil) = record_bulk(:bulk_create_tags, tags: tags, callback_url: callback_url)
|
|
79
|
+
def bulk_delete_tags(tags, callback_url: nil) = record_bulk(:bulk_delete_tags, tags: tags, callback_url: callback_url)
|
|
80
|
+
def bulk_tag_subscribers(taggings, callback_url: nil) = record_bulk(:bulk_tag_subscribers, taggings: taggings, callback_url: callback_url)
|
|
81
|
+
def bulk_untag_subscribers(taggings, callback_url: nil) = record_bulk(:bulk_untag_subscribers, taggings: taggings, callback_url: callback_url)
|
|
82
|
+
|
|
83
|
+
# Custom fields
|
|
84
|
+
|
|
85
|
+
def custom_fields(**params) = record_page(:custom_fields, "custom_fields", **params)
|
|
86
|
+
|
|
87
|
+
def create_custom_field(label:)
|
|
88
|
+
record(:create_custom_field, label: label)
|
|
89
|
+
{ "custom_field" => { "id" => sim_id(label), "label" => label } }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def update_custom_field(id, label:) = record_one(:update_custom_field, "custom_field", id, id: id, label: label)
|
|
93
|
+
def delete_custom_field(id) = record(:delete_custom_field, id: id)
|
|
94
|
+
def bulk_create_custom_fields(custom_fields, callback_url: nil) = record_bulk(:bulk_create_custom_fields, custom_fields: custom_fields, callback_url: callback_url)
|
|
95
|
+
def bulk_update_subscriber_fields(custom_field_values, callback_url: nil) = record_bulk(:bulk_update_subscriber_fields, custom_field_values: custom_field_values, callback_url: callback_url)
|
|
96
|
+
|
|
97
|
+
# Forms
|
|
98
|
+
|
|
99
|
+
def forms(**params) = record_page(:forms, "forms", **params)
|
|
100
|
+
def form_subscribers(id, **params) = record_page(:form_subscribers, "subscribers", id: id, **params)
|
|
101
|
+
|
|
102
|
+
def add_subscriber_to_form(form_id, email_address: nil, subscriber_id: nil, referrer: nil)
|
|
103
|
+
record(:add_subscriber_to_form, form_id: form_id, email_address: email_address,
|
|
104
|
+
subscriber_id: subscriber_id, referrer: referrer)
|
|
105
|
+
{ "subscriber" => { "id" => subscriber_id || sim_id(email_address.to_s) } }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bulk_add_subscribers_to_forms(additions, callback_url: nil) = record_bulk(:bulk_add_subscribers_to_forms, additions: additions, callback_url: callback_url)
|
|
109
|
+
|
|
110
|
+
# Sequences
|
|
111
|
+
|
|
112
|
+
def sequences(**params) = record_page(:sequences, "sequences", **params)
|
|
113
|
+
def sequence(id) = record_one(:sequence, "sequence", id, id: id)
|
|
114
|
+
|
|
115
|
+
def create_sequence(name:, **attributes)
|
|
116
|
+
record(:create_sequence, name: name, **attributes)
|
|
117
|
+
{ "sequence" => { "id" => sim_id(name), "name" => name } }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def update_sequence(id, **attributes) = record_one(:update_sequence, "sequence", id, id: id, **attributes)
|
|
121
|
+
def delete_sequence(id) = record(:delete_sequence, id: id)
|
|
122
|
+
def sequence_subscribers(id, **params) = record_page(:sequence_subscribers, "subscribers", id: id, **params)
|
|
123
|
+
|
|
124
|
+
def add_subscriber_to_sequence(sequence_id, email_address: nil, subscriber_id: nil)
|
|
125
|
+
record(:add_subscriber_to_sequence, sequence_id: sequence_id, email_address: email_address,
|
|
126
|
+
subscriber_id: subscriber_id)
|
|
127
|
+
{ "subscriber" => { "id" => subscriber_id || sim_id(email_address.to_s) } }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def sequence_emails(sequence_id, **params) = record_page(:sequence_emails, "emails", sequence_id: sequence_id, **params)
|
|
131
|
+
def sequence_email(sequence_id, id) = record_one(:sequence_email, "email", id, sequence_id: sequence_id, id: id)
|
|
132
|
+
|
|
133
|
+
def create_sequence_email(sequence_id, subject:, delay_value:, delay_unit:, **attributes)
|
|
134
|
+
record(:create_sequence_email, sequence_id: sequence_id, subject: subject,
|
|
135
|
+
delay_value: delay_value, delay_unit: delay_unit, **attributes)
|
|
136
|
+
{ "email" => { "id" => sim_id(subject), "sequence_id" => sequence_id, "subject" => subject } }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def update_sequence_email(sequence_id, id, **attributes) = record_one(:update_sequence_email, "email", id, sequence_id: sequence_id, id: id, **attributes)
|
|
140
|
+
def delete_sequence_email(sequence_id, id) = record(:delete_sequence_email, sequence_id: sequence_id, id: id)
|
|
141
|
+
|
|
142
|
+
# Broadcasts
|
|
143
|
+
|
|
144
|
+
def broadcasts(**params) = record_page(:broadcasts, "broadcasts", **params)
|
|
145
|
+
def broadcast(id) = record_one(:broadcast, "broadcast", id, id: id)
|
|
146
|
+
|
|
147
|
+
def create_broadcast(**attributes)
|
|
148
|
+
record(:create_broadcast, **attributes)
|
|
149
|
+
{ "broadcast" => { "id" => sim_id(attributes.inspect), **attributes.transform_keys(&:to_s) } }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def update_broadcast(id, **attributes) = record_one(:update_broadcast, "broadcast", id, id: id, **attributes)
|
|
153
|
+
def delete_broadcast(id) = record(:delete_broadcast, id: id)
|
|
154
|
+
def broadcast_stats(id) = record_one(:broadcast_stats, "broadcast", id, id: id)
|
|
155
|
+
def broadcasts_stats(**params) = record_page(:broadcasts_stats, "broadcasts", **params)
|
|
156
|
+
def broadcast_clicks(id, **params) = record_one(:broadcast_clicks, "broadcast", id, id: id, **params)
|
|
157
|
+
|
|
158
|
+
# Account
|
|
159
|
+
|
|
160
|
+
def account = record_one(:account, "account", "sim_account")
|
|
161
|
+
def account_colors = (record(:account_colors); { "colors" => [] })
|
|
162
|
+
def update_account_colors(colors) = (record(:update_account_colors, colors: colors); { "colors" => colors })
|
|
163
|
+
def creator_profile = record_one(:creator_profile, "profile", "sim_profile")
|
|
164
|
+
def email_stats = record_one(:email_stats, "stats", "sim_stats")
|
|
165
|
+
def growth_stats(**params) = record_one(:growth_stats, "stats", "sim_stats", **params)
|
|
166
|
+
|
|
167
|
+
# Purchases
|
|
168
|
+
|
|
169
|
+
def purchases(**params) = record_page(:purchases, "purchases", **params)
|
|
170
|
+
def purchase(id) = record_one(:purchase, "purchase", id, id: id)
|
|
171
|
+
|
|
172
|
+
def create_purchase(**attributes)
|
|
173
|
+
record(:create_purchase, **attributes)
|
|
174
|
+
{ "purchase" => { "id" => sim_id(attributes.inspect), **attributes.transform_keys(&:to_s) } }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Segments, snippets, posts, email templates
|
|
178
|
+
|
|
179
|
+
def segments(**params) = record_page(:segments, "segments", **params)
|
|
180
|
+
def snippets(**params) = record_page(:snippets, "snippets", **params)
|
|
181
|
+
def snippet(id) = record_one(:snippet, "snippet", id, id: id)
|
|
182
|
+
|
|
183
|
+
def create_snippet(name:, **attributes)
|
|
184
|
+
record(:create_snippet, name: name, **attributes)
|
|
185
|
+
{ "snippet" => { "id" => sim_id(name), "name" => name } }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def update_snippet(id, **attributes) = record_one(:update_snippet, "snippet", id, id: id, **attributes)
|
|
189
|
+
def posts(**params) = record_page(:posts, "posts", **params)
|
|
190
|
+
def post(id) = record_one(:post, "post", id, id: id)
|
|
191
|
+
def email_templates(**params) = record_page(:email_templates, "email_templates", **params)
|
|
192
|
+
|
|
193
|
+
# Webhooks
|
|
194
|
+
|
|
195
|
+
def webhooks(**params) = record_page(:webhooks, "webhook_endpoints", **params)
|
|
196
|
+
def webhook(id) = record_one(:webhook, "webhook_endpoint", id, id: id)
|
|
197
|
+
|
|
198
|
+
def create_webhook(url:, events:, **attributes)
|
|
199
|
+
record(:create_webhook, url: url, events: events, **attributes)
|
|
200
|
+
{ "webhook_endpoint" => { "id" => sim_id(url), "url" => url, "events" => events, "secret" => "whsec_simulated" } }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def update_webhook(id, **attributes) = record_one(:update_webhook, "webhook_endpoint", id, id: id, **attributes)
|
|
204
|
+
def delete_webhook(id) = record(:delete_webhook, id: id)
|
|
205
|
+
def rotate_webhook_secret(id, force: nil) = record_one(:rotate_webhook_secret, "webhook_endpoint", id, id: id, force: force)
|
|
206
|
+
def revoke_previous_webhook_secret(id) = record_one(:revoke_previous_webhook_secret, "webhook_endpoint", id, id: id)
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
def record(op, **kwargs)
|
|
210
|
+
self.class.calls << { op: op, **kwargs }
|
|
211
|
+
Kit.config.logger&.info("[Kit simulated] #{op} #{kwargs.values.first}")
|
|
212
|
+
true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def record_page(op, key, **kwargs)
|
|
216
|
+
record(op, **kwargs)
|
|
217
|
+
{ key => [], "pagination" => PAGE.dup }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def record_one(op, key, id, **kwargs)
|
|
221
|
+
record(op, **kwargs)
|
|
222
|
+
{ key => { "id" => id } }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def record_bulk(op, **kwargs)
|
|
226
|
+
record(op, **kwargs.compact)
|
|
227
|
+
{ "failures" => [] }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def sim_id(seed) = "sim_#{Digest::SHA256.hexdigest(seed.to_s)[0, 12]}"
|
|
231
|
+
end
|
|
232
|
+
end
|
data/lib/kit/version.rb
ADDED
data/lib/kit.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kit/version"
|
|
4
|
+
require_relative "kit/client"
|
|
5
|
+
require_relative "kit/simulated"
|
|
6
|
+
|
|
7
|
+
# Kit (ConvertKit) v4 API client. Configure once, then call the high-level
|
|
8
|
+
# conveniences on the module or reach the full API surface via Kit.client.
|
|
9
|
+
#
|
|
10
|
+
# Kit.configure do |config|
|
|
11
|
+
# config.driver = :v4
|
|
12
|
+
# config.api_key = "kit_..."
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Kit.subscribe(email: "sam@example.com", first_name: "Sam")
|
|
16
|
+
# Kit.client.broadcasts(per_page: 10)
|
|
17
|
+
module Kit
|
|
18
|
+
class Error < StandardError
|
|
19
|
+
attr_reader :status, :body
|
|
20
|
+
|
|
21
|
+
def initialize(message, status: nil, body: nil)
|
|
22
|
+
@status = status
|
|
23
|
+
@body = body
|
|
24
|
+
super(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Config = Struct.new(:api_key, :access_token, :driver, :logger,
|
|
29
|
+
:open_timeout, :read_timeout, :base_url)
|
|
30
|
+
|
|
31
|
+
def self.config
|
|
32
|
+
@config ||= Config.new(nil, nil, :simulated, nil, 10, 20, "https://api.kit.com/v4")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.configure
|
|
36
|
+
yield config
|
|
37
|
+
@client = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Plain ||= — concurrent first-touch can build a redundant client, which is
|
|
41
|
+
# harmless (stateless but for the tag-name cache). Configure at boot.
|
|
42
|
+
def self.client
|
|
43
|
+
@client ||= case config.driver
|
|
44
|
+
when :v4
|
|
45
|
+
Client.new(api_key: config.api_key, access_token: config.access_token,
|
|
46
|
+
base_url: config.base_url, open_timeout: config.open_timeout,
|
|
47
|
+
read_timeout: config.read_timeout)
|
|
48
|
+
when :simulated
|
|
49
|
+
Simulated.new
|
|
50
|
+
else
|
|
51
|
+
raise Error, "Unknown Kit driver: #{config.driver.inspect} (use :v4 or :simulated)"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.reset! = (@config = nil; @client = nil)
|
|
56
|
+
|
|
57
|
+
def self.subscribe(email:, first_name:, fields: {})
|
|
58
|
+
client.subscribe(email: email, first_name: first_name, fields: fields)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.tag(email:, tag:)
|
|
62
|
+
client.tag(email: email, tag: tag)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.unsubscribe(email:)
|
|
66
|
+
client.unsubscribe(email: email)
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/kit_api.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kit_api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mike D
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'A plain-Ruby client for the Kit (formerly ConvertKit) v4 API: subscribers,
|
|
13
|
+
tags, custom fields, forms, sequences, broadcasts, purchases, webhooks and more.
|
|
14
|
+
Net::HTTP and stdlib JSON only, plus a first-class simulated driver for tests.'
|
|
15
|
+
email:
|
|
16
|
+
- mojonickle@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/kit.rb
|
|
25
|
+
- lib/kit/client.rb
|
|
26
|
+
- lib/kit/client/accounts.rb
|
|
27
|
+
- lib/kit/client/broadcasts.rb
|
|
28
|
+
- lib/kit/client/custom_fields.rb
|
|
29
|
+
- lib/kit/client/email_templates.rb
|
|
30
|
+
- lib/kit/client/forms.rb
|
|
31
|
+
- lib/kit/client/posts.rb
|
|
32
|
+
- lib/kit/client/purchases.rb
|
|
33
|
+
- lib/kit/client/segments.rb
|
|
34
|
+
- lib/kit/client/sequences.rb
|
|
35
|
+
- lib/kit/client/snippets.rb
|
|
36
|
+
- lib/kit/client/subscribers.rb
|
|
37
|
+
- lib/kit/client/tags.rb
|
|
38
|
+
- lib/kit/client/webhooks.rb
|
|
39
|
+
- lib/kit/simulated.rb
|
|
40
|
+
- lib/kit/version.rb
|
|
41
|
+
- lib/kit_api.rb
|
|
42
|
+
homepage: https://github.com/miked-bro/kit_api
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata:
|
|
46
|
+
source_code_uri: https://github.com/miked-bro/kit_api
|
|
47
|
+
changelog_uri: https://github.com/miked-bro/kit_api/blob/main/CHANGELOG.md
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3.2'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.6.9
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Kit (ConvertKit) v4 API client with zero dependencies.
|
|
66
|
+
test_files: []
|