loops_sdk 2.2.0 → 2.4.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.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class AudienceSegments < Base
5
+ class << self
6
+ def list(perPage: 20, cursor: nil)
7
+ make_request(method: :get, path: "v1/audience-segments", params: { perPage: perPage, cursor: cursor })
8
+ end
9
+
10
+ def get(audience_segment_id:)
11
+ make_request(method: :get, path: "v1/audience-segments/#{audience_segment_id}")
12
+ end
13
+
14
+ def create(name:, filter:, description: nil)
15
+ body = { name: name, filter: filter, description: description }.compact
16
+ make_request(method: :post, path: "v1/audience-segments", body: body)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -13,7 +13,7 @@ module LoopsSdk
13
13
  limit = response.headers["x-ratelimit-limit"]
14
14
  remaining = response.headers["x-ratelimit-remaining"]
15
15
  raise RateLimitError.new(limit, remaining)
16
- when 400, 401, 404, 405, 409, 413, 422, 500
16
+ when 400, 401, 404, 405, 409, 413, 422, 500, 501
17
17
  raise APIError.new(response.status, response.body)
18
18
  else
19
19
  raise APIError.new(response.status, "Unexpected error occurred")
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class CampaignGroups < Base
5
+ class << self
6
+ def list(perPage: 20, cursor: nil)
7
+ make_request(method: :get, path: "v1/campaign-groups", params: { perPage: perPage, cursor: cursor })
8
+ end
9
+
10
+ def create(name:, description: nil)
11
+ body = { name: name, description: description }.compact
12
+ make_request(method: :post, path: "v1/campaign-groups", body: body)
13
+ end
14
+
15
+ def get(campaign_group_id:)
16
+ make_request(method: :get, path: "v1/campaign-groups/#{campaign_group_id}")
17
+ end
18
+
19
+ def update(campaign_group_id:, name: nil, description: nil)
20
+ body = { name: name, description: description }.compact
21
+ make_request(method: :post, path: "v1/campaign-groups/#{campaign_group_id}", body: body)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,16 +7,32 @@ module LoopsSdk
7
7
  make_request(method: :get, path: "v1/campaigns", params: { perPage: perPage, cursor: cursor })
8
8
  end
9
9
 
10
- def create(name:)
11
- make_request(method: :post, path: "v1/campaigns", body: { name: name })
10
+ def create(name:, campaign_group_id: nil, mailing_list_id: nil, audience_segment_id: nil, audience_filter: nil, scheduling: nil)
11
+ body = {
12
+ name: name,
13
+ campaignGroupId: campaign_group_id,
14
+ mailingListId: mailing_list_id,
15
+ audienceSegmentId: audience_segment_id,
16
+ audienceFilter: audience_filter,
17
+ scheduling: scheduling
18
+ }.compact
19
+ make_request(method: :post, path: "v1/campaigns", body: body)
12
20
  end
13
21
 
14
22
  def get(campaign_id:)
15
23
  make_request(method: :get, path: "v1/campaigns/#{campaign_id}")
16
24
  end
17
25
 
18
- def update(campaign_id:, name:)
19
- make_request(method: :post, path: "v1/campaigns/#{campaign_id}", body: { name: name })
26
+ def update(campaign_id:, name: nil, campaign_group_id: nil, mailing_list_id: nil, audience_segment_id: nil, audience_filter: nil, scheduling: nil)
27
+ body = {
28
+ name: name,
29
+ campaignGroupId: campaign_group_id,
30
+ mailingListId: mailing_list_id,
31
+ audienceSegmentId: audience_segment_id,
32
+ audienceFilter: audience_filter,
33
+ scheduling: scheduling
34
+ }.compact
35
+ make_request(method: :post, path: "v1/campaigns/#{campaign_id}", body: body)
20
36
  end
21
37
  end
22
38
  end
@@ -10,6 +10,15 @@ module LoopsSdk
10
10
  def get(component_id:)
11
11
  make_request(method: :get, path: "v1/components/#{component_id}")
12
12
  end
13
+
14
+ def create(name:, lmx:)
15
+ make_request(method: :post, path: "v1/components", body: { name: name, lmx: lmx })
16
+ end
17
+
18
+ def update(component_id:, name: nil, lmx: nil)
19
+ body = { name: name, lmx: lmx }.compact
20
+ make_request(method: :post, path: "v1/components/#{component_id}", body: body)
21
+ end
13
22
  end
14
23
  end
15
24
  end
@@ -7,7 +7,23 @@ module LoopsSdk
7
7
  make_request(method: :get, path: "v1/email-messages/#{email_message_id}")
8
8
  end
9
9
 
10
- def update(email_message_id:, expected_revision_id: nil, subject: nil, preview_text: nil, from_name: nil, from_email: nil, reply_to_email: nil, lmx: nil)
10
+ def update(
11
+ email_message_id:,
12
+ expected_revision_id: nil,
13
+ subject: nil,
14
+ preview_text: nil,
15
+ from_name: nil,
16
+ from_email: nil,
17
+ reply_to_email: nil,
18
+ cc_email: nil,
19
+ bcc_email: nil,
20
+ language_code: nil,
21
+ email_format: nil,
22
+ lmx: nil,
23
+ contact_properties_fallbacks: nil,
24
+ event_properties_fallbacks: nil,
25
+ data_variables_fallbacks: nil
26
+ )
11
27
  body = {
12
28
  expectedRevisionId: expected_revision_id,
13
29
  subject: subject,
@@ -15,10 +31,32 @@ module LoopsSdk
15
31
  fromName: from_name,
16
32
  fromEmail: from_email,
17
33
  replyToEmail: reply_to_email,
18
- lmx: lmx
34
+ ccEmail: cc_email,
35
+ bccEmail: bcc_email,
36
+ languageCode: language_code,
37
+ emailFormat: email_format,
38
+ lmx: lmx,
39
+ contactPropertiesFallbacks: contact_properties_fallbacks,
40
+ eventPropertiesFallbacks: event_properties_fallbacks,
41
+ dataVariablesFallbacks: data_variables_fallbacks
19
42
  }.compact
20
43
  make_request(method: :post, path: "v1/email-messages/#{email_message_id}", body: body)
21
44
  end
45
+
46
+ def preview(email_message_id:, emails:, contact_properties: nil, event_properties: nil, data_variables: nil)
47
+ body = {
48
+ emails: emails,
49
+ contactProperties: contact_properties,
50
+ eventProperties: event_properties,
51
+ dataVariables: data_variables
52
+ }.compact
53
+ make_request(method: :post, path: "v1/email-messages/#{email_message_id}/preview", body: body)
54
+ end
55
+
56
+ def run_guardian(email_message_id:)
57
+ make_request(method: :get, path: "v1/email-messages/#{email_message_id}/guardian")
58
+ end
22
59
  end
23
60
  end
24
61
  end
62
+
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module LoopsSdk
6
+ class EventPatterns < Base
7
+ class << self
8
+ def list(perPage: 20, cursor: nil)
9
+ make_request(method: :get, path: "v1/event-patterns", params: { perPage: perPage, cursor: cursor })
10
+ end
11
+
12
+ def get(event_pattern_id:)
13
+ make_request(method: :get, path: "v1/event-patterns/#{event_pattern_id}")
14
+ end
15
+
16
+ def get_by_name(event_name:)
17
+ encoded_name = CGI.escapeURIComponent(event_name)
18
+ make_request(method: :get, path: "v1/event-patterns/by-name/#{encoded_name}")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,6 +10,16 @@ module LoopsSdk
10
10
  def get(theme_id:)
11
11
  make_request(method: :get, path: "v1/themes/#{theme_id}")
12
12
  end
13
+
14
+ def create(name:, styles: nil)
15
+ body = { name: name, styles: styles }.compact
16
+ make_request(method: :post, path: "v1/themes", body: body)
17
+ end
18
+
19
+ def update(theme_id:, name: nil, styles: nil)
20
+ body = { name: name, styles: styles }.compact
21
+ make_request(method: :post, path: "v1/themes/#{theme_id}", body: body)
22
+ end
13
23
  end
14
24
  end
15
25
  end
@@ -4,8 +4,31 @@ module LoopsSdk
4
4
  class Transactional < Base
5
5
  class << self
6
6
  def list(perPage: 20, cursor: nil)
7
- make_request(method: :get, path: "v1/transactional", params: { perPage: perPage, cursor: cursor })
7
+ make_request(method: :get, path: "v1/transactional-emails", params: { perPage: perPage, cursor: cursor })
8
8
  end
9
+
10
+ def create(name:, transactional_group_id: nil)
11
+ body = { name: name, transactionalGroupId: transactional_group_id }.compact
12
+ make_request(method: :post, path: "v1/transactional-emails", body: body)
13
+ end
14
+
15
+ def get(transactional_id:)
16
+ make_request(method: :get, path: "v1/transactional-emails/#{transactional_id}")
17
+ end
18
+
19
+ def update(transactional_id:, name: nil, transactional_group_id: nil)
20
+ body = { name: name, transactionalGroupId: transactional_group_id }.compact
21
+ make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}", body: body)
22
+ end
23
+
24
+ def ensure_draft(transactional_id:)
25
+ make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/draft")
26
+ end
27
+
28
+ def publish(transactional_id:)
29
+ make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/publish")
30
+ end
31
+
9
32
  def send(transactional_id:, email:, add_to_audience: false, data_variables: {}, attachments: [], headers: {})
10
33
  attachments = attachments.map do |attachment|
11
34
  attachment.transform_keys { |key| key == :content_type ? :contentType : key }
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class TransactionalGroups < Base
5
+ class << self
6
+ def list(perPage: 20, cursor: nil)
7
+ make_request(method: :get, path: "v1/transactional-groups", params: { perPage: perPage, cursor: cursor })
8
+ end
9
+
10
+ def create(name:, description: nil)
11
+ body = { name: name, description: description }.compact
12
+ make_request(method: :post, path: "v1/transactional-groups", body: body)
13
+ end
14
+
15
+ def get(transactional_group_id:)
16
+ make_request(method: :get, path: "v1/transactional-groups/#{transactional_group_id}")
17
+ end
18
+
19
+ def update(transactional_group_id:, name: nil, description: nil)
20
+ body = { name: name, description: description }.compact
21
+ make_request(method: :post, path: "v1/transactional-groups/#{transactional_group_id}", body: body)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class Uploads < Base
5
+ SUPPORTED_CONTENT_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
6
+
7
+ class << self
8
+ def upload(path:, content_type: nil)
9
+ data = File.binread(path)
10
+ content_type ||= detect_content_type(data)
11
+ validate_content_type!(content_type)
12
+
13
+ create_response = make_request(
14
+ method: :post,
15
+ path: "v1/uploads",
16
+ body: { contentType: content_type, contentLength: data.bytesize }
17
+ )
18
+
19
+ put_to_presigned_url(create_response["presignedUrl"], data, content_type)
20
+
21
+ make_request(
22
+ method: :post,
23
+ path: "v1/uploads/#{create_response["emailAssetId"]}/complete"
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def detect_content_type(data)
30
+ if data.start_with?("\xFF\xD8\xFF".b)
31
+ "image/jpeg"
32
+ elsif data.start_with?("\x89PNG\r\n\x1a\n".b)
33
+ "image/png"
34
+ elsif data.start_with?("GIF87a".b, "GIF89a".b)
35
+ "image/gif"
36
+ elsif data.bytesize >= 12 && data[0, 4] == "RIFF".b && data[8, 4] == "WEBP".b
37
+ "image/webp"
38
+ else
39
+ raise ArgumentError,
40
+ "Unable to detect image type. Pass content_type: explicitly. " \
41
+ "Supported types: #{SUPPORTED_CONTENT_TYPES.join(', ')}."
42
+ end
43
+ end
44
+
45
+ def validate_content_type!(content_type)
46
+ return if SUPPORTED_CONTENT_TYPES.include?(content_type)
47
+
48
+ raise ArgumentError,
49
+ "Unsupported content_type: #{content_type}. " \
50
+ "Supported types: #{SUPPORTED_CONTENT_TYPES.join(', ')}."
51
+ end
52
+
53
+ def put_to_presigned_url(url, data, content_type)
54
+ response = Faraday.put(url) do |req|
55
+ req.headers["Content-Type"] = content_type
56
+ req.headers["Content-Length"] = data.bytesize.to_s
57
+ req.body = data
58
+ end
59
+
60
+ return if response.success?
61
+
62
+ raise APIError.new(response.status, response.body)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LoopsSdk
4
- VERSION = "2.2.0"
4
+ VERSION = "2.4.0"
5
5
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoopsSdk
4
+ class Workflows < Base
5
+ class << self
6
+ def list(perPage: 20, cursor: nil)
7
+ make_request(method: :get, path: "v1/workflows", params: { perPage: perPage, cursor: cursor })
8
+ end
9
+
10
+ def create(name:, description: nil, mailing_list_id: nil)
11
+ body = {
12
+ name: name,
13
+ description: description,
14
+ mailingListId: mailing_list_id
15
+ }.compact
16
+ make_request(method: :post, path: "v1/workflows", body: body)
17
+ end
18
+
19
+ def get(workflow_id:)
20
+ make_request(method: :get, path: "v1/workflows/#{workflow_id}")
21
+ end
22
+
23
+ def update(workflow_id:, expected_revision_id:, name: nil, description: nil)
24
+ body = {
25
+ expectedRevisionId: expected_revision_id,
26
+ name: name,
27
+ description: description
28
+ }.compact
29
+ # expectedRevisionId may be null for older workflows; always include it
30
+ body[:expectedRevisionId] = expected_revision_id
31
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}", body: body)
32
+ end
33
+
34
+ def change_mailing_list(workflow_id:, expected_revision_id:, mailing_list_id:, dry_run: nil, queued_contact_policy: nil)
35
+ body = {
36
+ expectedRevisionId: expected_revision_id,
37
+ mailingListId: mailing_list_id,
38
+ dryRun: dry_run,
39
+ queuedContactPolicy: queued_contact_policy
40
+ }.compact
41
+ # expectedRevisionId and mailingListId may be null; always include them
42
+ body[:expectedRevisionId] = expected_revision_id
43
+ body[:mailingListId] = mailing_list_id
44
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}/mailing-list", body: body)
45
+ end
46
+
47
+ def get_node(workflow_id:, node_id:)
48
+ make_request(method: :get, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}")
49
+ end
50
+
51
+ def create_node(
52
+ workflow_id:,
53
+ expected_revision_id:,
54
+ insert_mode:,
55
+ node_type_name:,
56
+ from_node_id: nil,
57
+ to_node_id: nil,
58
+ before_node_id: nil
59
+ )
60
+ body = {
61
+ expectedRevisionId: expected_revision_id,
62
+ insertMode: insert_mode,
63
+ nodeTypeName: node_type_name,
64
+ fromNodeId: from_node_id,
65
+ toNodeId: to_node_id,
66
+ beforeNodeId: before_node_id
67
+ }.compact
68
+ body[:expectedRevisionId] = expected_revision_id
69
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes", body: body)
70
+ end
71
+
72
+ def update_node(workflow_id:, node_id:, expected_revision_id:, payload:)
73
+ body = {
74
+ expectedRevisionId: expected_revision_id,
75
+ payload: payload
76
+ }
77
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}", body: body)
78
+ end
79
+
80
+ def delete_node(workflow_id:, node_id:, expected_revision_id:, dry_run: nil, queued_contact_policy: nil)
81
+ body = {
82
+ expectedRevisionId: expected_revision_id,
83
+ dryRun: dry_run,
84
+ queuedContactPolicy: queued_contact_policy
85
+ }.compact
86
+ body[:expectedRevisionId] = expected_revision_id
87
+ make_request(method: :delete, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}", body: body)
88
+ end
89
+
90
+ def add_branch(workflow_id:, node_id:, expected_revision_id:)
91
+ body = { expectedRevisionId: expected_revision_id }
92
+ make_request(method: :post, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}/add-branch", body: body)
93
+ end
94
+
95
+ def delete_node_recursive(workflow_id:, node_id:, expected_revision_id:, dry_run: nil, queued_contact_policy: nil)
96
+ body = {
97
+ expectedRevisionId: expected_revision_id,
98
+ dryRun: dry_run,
99
+ queuedContactPolicy: queued_contact_policy
100
+ }.compact
101
+ body[:expectedRevisionId] = expected_revision_id
102
+ make_request(method: :delete, path: "v1/workflows/#{workflow_id}/nodes/#{node_id}/recursive", body: body)
103
+ end
104
+ end
105
+ end
106
+ end
data/lib/loops_sdk.rb CHANGED
@@ -15,7 +15,13 @@ require_relative "loops_sdk/dedicated_sending_ips"
15
15
  require_relative "loops_sdk/themes"
16
16
  require_relative "loops_sdk/components"
17
17
  require_relative "loops_sdk/campaigns"
18
+ require_relative "loops_sdk/campaign_groups"
18
19
  require_relative "loops_sdk/email_messages"
20
+ require_relative "loops_sdk/uploads"
21
+ require_relative "loops_sdk/audience_segments"
22
+ require_relative "loops_sdk/workflows"
23
+ require_relative "loops_sdk/event_patterns"
24
+ require_relative "loops_sdk/transactional_groups"
19
25
 
20
26
  module LoopsSdk
21
27
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loops_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Rowden
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-21 00:00:00.000000000 Z
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -35,7 +35,9 @@ files:
35
35
  - README.md
36
36
  - lib/loops_sdk.rb
37
37
  - lib/loops_sdk/api_key.rb
38
+ - lib/loops_sdk/audience_segments.rb
38
39
  - lib/loops_sdk/base.rb
40
+ - lib/loops_sdk/campaign_groups.rb
39
41
  - lib/loops_sdk/campaigns.rb
40
42
  - lib/loops_sdk/components.rb
41
43
  - lib/loops_sdk/configuration.rb
@@ -43,11 +45,15 @@ files:
43
45
  - lib/loops_sdk/contacts.rb
44
46
  - lib/loops_sdk/dedicated_sending_ips.rb
45
47
  - lib/loops_sdk/email_messages.rb
48
+ - lib/loops_sdk/event_patterns.rb
46
49
  - lib/loops_sdk/events.rb
47
50
  - lib/loops_sdk/mailing_lists.rb
48
51
  - lib/loops_sdk/themes.rb
49
52
  - lib/loops_sdk/transactional.rb
53
+ - lib/loops_sdk/transactional_groups.rb
54
+ - lib/loops_sdk/uploads.rb
50
55
  - lib/loops_sdk/version.rb
56
+ - lib/loops_sdk/workflows.rb
51
57
  homepage: https://loops.so/docs/api-reference
52
58
  licenses:
53
59
  - MIT