Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## v2.2.0 - May 21, 2026

- Added support for campaigns, email messages, themes, components, and dedicated sending IP endpoints.
- THe SDK now treats `201` responses as success and raises `APIError` for new `401`, `413`, and `422` responses.

## v2.1.0 - Aug 8, 2026

- Added support for contact suppression endpoints with `Contacts.check_suppression()` and `Contacts.remove_suppression()`.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
loops_sdk (2.1.0)
loops_sdk (2.2.0)
faraday

GEM
Expand Down
261 changes: 261 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ You can use custom contact properties in API calls. Please make sure to [add cus
- [Events.send()](#eventssend)
- [Transactional.send()](#transactionalsend)
- [Transactional.list()](#transactionallist)
- [DedicatedSendingIps.list()](#dedicatedsendingipslist)
- [Themes.list()](#themeslist)
- [Themes.get()](#themesget)
- [Components.list()](#componentslist)
- [Components.get()](#componentsget)
- [Campaigns.list()](#campaignslist)
- [Campaigns.create()](#campaignscreate)
- [Campaigns.get()](#campaignsget)
- [Campaigns.update()](#campaignsupdate)
- [EmailMessages.get()](#emailmessagesget)
- [EmailMessages.update()](#emailmessagesupdate)

---

Expand Down Expand Up @@ -870,6 +881,256 @@ response = LoopsSdk::Transactional.list(perPage: 15)

---

### DedicatedSendingIps.list()

Get Loops' dedicated sending IP addresses.

[API Reference](https://loops.so/docs/api-reference/list-dedicated-sending-ips)

#### Parameters

None

#### Example

```ruby
response = LoopsSdk::DedicatedSendingIps.list
```

#### Response

Returns an array of IP address strings.

```json
["1.2.3.4", "5.6.7.8"]
```

---

### Themes.list()

List email themes.

[API Reference](https://loops.so/docs/api-reference/list-themes)

#### Parameters

| Name | Type | Required | Notes |
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
| `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |

#### Example

```ruby
response = LoopsSdk::Themes.list

response = LoopsSdk::Themes.list(perPage: 15, cursor: "cursor_value")
```

---

### Themes.get()

Get a single theme by ID.

[API Reference](https://loops.so/docs/api-reference/get-theme)

#### Parameters

| Name | Type | Required | Notes |
| ---------- | ------ | -------- | ----- |
| `theme_id` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::Themes.get(theme_id: "theme_123")
```

---

### Components.list()

List email components.

[API Reference](https://loops.so/docs/api-reference/list-components)

#### Parameters

| Name | Type | Required | Notes |
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
| `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |

#### Example

```ruby
response = LoopsSdk::Components.list
```

---

### Components.get()

Get a single component by ID.

[API Reference](https://loops.so/docs/api-reference/get-component)

#### Parameters

| Name | Type | Required | Notes |
| -------------- | ------ | -------- | ----- |
| `component_id` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::Components.get(component_id: "component_123")
```

---

### Campaigns.list()

List campaigns.

[API Reference](https://loops.so/docs/api-reference/list-campaigns)

#### Parameters

| Name | Type | Required | Notes |
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
| `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |

#### Example

```ruby
response = LoopsSdk::Campaigns.list
```

---

### Campaigns.create()

Create a draft campaign. An empty email message is created automatically.

[API Reference](https://loops.so/docs/api-reference/create-campaign)

#### Parameters

| Name | Type | Required | Notes |
| ------ | ------ | -------- | ----- |
| `name` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::Campaigns.create(name: "Spring announcement")
```

---

### Campaigns.get()

Get a single campaign by ID.

[API Reference](https://loops.so/docs/api-reference/get-campaign)

#### Parameters

| Name | Type | Required | Notes |
| ------------- | ------ | -------- | ----- |
| `campaign_id` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::Campaigns.get(campaign_id: "campaign_123")
```

---

### Campaigns.update()

Update a draft campaign's name.

[API Reference](https://loops.so/docs/api-reference/update-campaign)

#### Parameters

| Name | Type | Required | Notes |
| ------------- | ------ | -------- | ----- |
| `campaign_id` | string | Yes | |
| `name` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::Campaigns.update(
campaign_id: "campaign_123",
name: "Updated campaign name"
)
```

---

### EmailMessages.get()

Get an email message, including its LMX content.

[API Reference](https://loops.so/docs/api-reference/get-email-message)

#### Parameters

| Name | Type | Required | Notes |
| ------------------- | ------ | -------- | ----- |
| `email_message_id` | string | Yes | |

#### Example

```ruby
response = LoopsSdk::EmailMessages.get(email_message_id: "message_123")
```

---

### EmailMessages.update()

Update an email message for a draft campaign.

[API Reference](https://loops.so/docs/api-reference/update-email-message)

#### Parameters

| Name | Type | Required | Notes |
| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `email_message_id` | string | Yes | |
| `expected_revision_id` | string | No | The `contentRevisionId` from your last fetch. Required to avoid stale concurrent updates. |
| `subject` | string | No | |
| `preview_text` | string | No | |
| `from_name` | string | No | |
| `from_email` | string | No | Sender username without `@` or domain. The team's sending domain is appended automatically. |
| `reply_to_email` | string | No | Must be empty or a valid email address. |
| `lmx` | string | No | Email body serialized as LMX. Styles must be embedded in the LMX `<Style />` tag. |

#### Example

```ruby
response = LoopsSdk::EmailMessages.update(
email_message_id: "message_123",
expected_revision_id: "revision_123",
subject: "Spring announcement",
preview_text: "See what's new",
from_name: "Loops",
from_email: "hello",
lmx: "<Email><Style /></Email>"
)
```

---

## Testing

Run tests with `bundle exec rspec`.
Expand Down
5 changes: 5 additions & 0 deletions lib/loops_sdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
require_relative "loops_sdk/transactional"
require_relative "loops_sdk/contact_properties"
require_relative "loops_sdk/api_key"
require_relative "loops_sdk/dedicated_sending_ips"
require_relative "loops_sdk/themes"
require_relative "loops_sdk/components"
require_relative "loops_sdk/campaigns"
require_relative "loops_sdk/email_messages"

module LoopsSdk
class << self
Expand Down
4 changes: 2 additions & 2 deletions lib/loops_sdk/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ class << self

def handle_response(response)
case response.status
when 200
when 200, 201
JSON.parse(response.body)
when 429
limit = response.headers["x-ratelimit-limit"]
remaining = response.headers["x-ratelimit-remaining"]
raise RateLimitError.new(limit, remaining)
when 400, 404, 405, 409, 500
when 400, 401, 404, 405, 409, 413, 422, 500
raise APIError.new(response.status, response.body)
else
raise APIError.new(response.status, "Unexpected error occurred")
Expand Down
23 changes: 23 additions & 0 deletions lib/loops_sdk/campaigns.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module LoopsSdk
class Campaigns < Base
class << self
def list(perPage: 20, cursor: nil)
make_request(method: :get, path: "v1/campaigns", params: { perPage: perPage, cursor: cursor })
end

def create(name:)
make_request(method: :post, path: "v1/campaigns", body: { name: name })
end

def get(campaign_id:)
make_request(method: :get, path: "v1/campaigns/#{campaign_id}")
end

def update(campaign_id:, name:)
make_request(method: :post, path: "v1/campaigns/#{campaign_id}", body: { name: name })
end
end
end
end
15 changes: 15 additions & 0 deletions lib/loops_sdk/components.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module LoopsSdk
class Components < Base
class << self
def list(perPage: 20, cursor: nil)
make_request(method: :get, path: "v1/components", params: { perPage: perPage, cursor: cursor })
end

def get(component_id:)
make_request(method: :get, path: "v1/components/#{component_id}")
end
end
end
end
11 changes: 11 additions & 0 deletions lib/loops_sdk/dedicated_sending_ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module LoopsSdk
class DedicatedSendingIps < Base
class << self
def list
make_request(method: :get, path: "v1/dedicated-sending-ips")
end
end
end
end
24 changes: 24 additions & 0 deletions lib/loops_sdk/email_messages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module LoopsSdk
class EmailMessages < Base
class << self
def get(email_message_id:)
make_request(method: :get, path: "v1/email-messages/#{email_message_id}")
end

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)
body = {
expectedRevisionId: expected_revision_id,
subject: subject,
previewText: preview_text,
fromName: from_name,
fromEmail: from_email,
replyToEmail: reply_to_email,
lmx: lmx
}.compact
make_request(method: :post, path: "v1/email-messages/#{email_message_id}", body: body)
end
end
end
end
Loading
Loading