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.3.0 - Jun 10, 2026

- Added `Uploads.upload()` to upload an image file in a single call (create, PUT to pre-signed URL, and complete).
- Added transactional email content endpoints: `Transactional.create()`, `.get()`, `.update()`, `.ensure_draft()`, and `.publish()`. `Transactional.list()` returns a new data shape.

## v2.2.0 - May 21, 2026

- Added support for campaigns, email messages, themes, components, and dedicated sending IP endpoints.
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.2.0)
loops_sdk (2.3.0)
faraday

GEM
Expand Down
275 changes: 212 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,13 @@ You can use custom contact properties in API calls. Please make sure to [add cus
- [ContactProperties.list()](#contactpropertieslist)
- [MailingLists.list()](#mailinglistslist)
- [Events.send()](#eventssend)
- [Transactional.send()](#transactionalsend)
- [Transactional.list()](#transactionallist)
- [Transactional.create()](#transactionalcreate)
- [Transactional.get()](#transactionalget)
- [Transactional.update()](#transactionalupdate)
- [Transactional.ensure_draft()](#transactionalensure_draft)
- [Transactional.publish()](#transactionalpublish)
- [Transactional.send()](#transactionalsend)
- [DedicatedSendingIps.list()](#dedicatedsendingipslist)
- [Themes.list()](#themeslist)
- [Themes.get()](#themesget)
Expand All @@ -127,6 +132,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus
- [Campaigns.update()](#campaignsupdate)
- [EmailMessages.get()](#emailmessagesget)
- [EmailMessages.update()](#emailmessagesupdate)
- [Uploads.upload()](#uploadsupload)

---

Expand Down Expand Up @@ -726,6 +732,176 @@ This method will return a success or error:

---

### Transactional.list()

List transactional emails, most recently created first.

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

#### 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::Transactional.list

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

#### Response

```json
{
"pagination": {
"totalResults": 23,
"returnedResults": 20,
"perPage": 20,
"totalPages": 2,
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
"nextPage": "https://app.loops.so/api/v1/transactional-emails?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
},
"data": [
{
"id": "clfn0k1yg001imo0fdeqg30i8",
"name": "Welcome email",
"draftEmailMessageId": null,
"publishedEmailMessageId": "msg_123",
"createdAt": "2023-11-06T17:48:07.249Z",
"updatedAt": "2023-11-06T17:48:07.249Z",
"dataVariables": ["confirmationUrl"]
}
]
}
```

---

### Transactional.create()

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

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

#### Parameters

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

#### Example

```ruby
response = LoopsSdk::Transactional.create(name: "Welcome email")
```

#### Response

Returns the transactional email with `draftEmailMessageId` and `draftEmailMessageContentRevisionId`. Use these when updating the draft via `EmailMessages.update()`.

```json
{
"id": "txn_123",
"name": "Welcome email",
"draftEmailMessageId": "msg_123",
"draftEmailMessageContentRevisionId": "revision_123",
"publishedEmailMessageId": null,
"createdAt": "2023-11-06T17:48:07.249Z",
"updatedAt": "2023-11-06T17:48:07.249Z",
"dataVariables": []
}
```

---

### Transactional.get()

Get a single transactional email by ID.

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

#### Parameters

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

#### Example

```ruby
response = LoopsSdk::Transactional.get(transactional_id: "txn_123")
```

---

### Transactional.update()

Update a transactional email's name.

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

#### Parameters

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

#### Example

```ruby
response = LoopsSdk::Transactional.update(
transactional_id: "txn_123",
name: "Updated name"
)
```

---

### Transactional.ensure_draft()

Ensure a transactional email has a draft email message. If a draft already exists it is returned unchanged; otherwise a new empty draft is created.

[API Reference](https://loops.so/docs/api-reference/ensure-transactional-email-draft)

#### Parameters

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

#### Example

```ruby
response = LoopsSdk::Transactional.ensure_draft(transactional_id: "txn_123")
```

---

### Transactional.publish()

Publish a transactional email's current draft. The draft becomes the published version and the draft is cleared.

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

#### Parameters

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

#### Example

```ruby
response = LoopsSdk::Transactional.publish(transactional_id: "txn_123")
```

---

### Transactional.send()

Send a transactional email to a contact. [Learn about sending transactional email](https://loops.so/docs/transactional/guide)
Expand Down Expand Up @@ -819,68 +995,6 @@ If there is a problem with the request, a descriptive error message will be retu

---

### Transactional.list()

Get a list of published transactional emails.

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

#### 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::Transactional.list

response = LoopsSdk::Transactional.list(perPage: 15)
```

#### Response

```json
{
"pagination": {
"totalResults": 23,
"returnedResults": 20,
"perPage": 20,
"totalPages": 2,
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
"nextPage": "https://app.loops.so/api/v1/transactional?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
},
"data": [
{
"id": "clfn0k1yg001imo0fdeqg30i8",
"lastUpdated": "2023-11-06T17:48:07.249Z",
"dataVariables": []
},
{
"id": "cll42l54f20i1la0lfooe3z12",
"lastUpdated": "2025-02-02T02:56:28.845Z",
"dataVariables": [
"confirmationUrl"
]
},
{
"id": "clw6rbuwp01rmeiyndm80155l",
"lastUpdated": "2024-05-14T19:02:52.000Z",
"dataVariables": [
"firstName",
"lastName",
"inviteLink"
]
},
...
]
}
```

---

### DedicatedSendingIps.list()

Get Loops' dedicated sending IP addresses.
Expand Down Expand Up @@ -1131,6 +1245,41 @@ response = LoopsSdk::EmailMessages.update(

---

### Uploads.upload()

Upload an image file for use in LMX email content.

Supported image types: JPEG, PNG, GIF, and WebP (max 4 MB). MIME type is detected from file contents, or pass `content_type:` to override.

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

#### Parameters

| Name | Type | Required | Notes |
| --------------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
| `path` | string | Yes | Path to the image file on disk. |
| `content_type` | string | No | MIME type override. Supported: `image/jpeg`, `image/png`, `image/gif`, `image/webp`. |

#### Example

```ruby
response = LoopsSdk::Uploads.upload(path: "./header.png")

# Use the returned URL in LMX
lmx = %(<Image src="#{response['finalUrl']}" alt="Header" />)
```

#### Response

```json
{
"emailAssetId": "asset_123",
"finalUrl": "https://cdn.loops.so/asset_123.png"
}
```

---

## Testing

Run tests with `bundle exec rspec`.
Expand Down
1 change: 1 addition & 0 deletions lib/loops_sdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require_relative "loops_sdk/components"
require_relative "loops_sdk/campaigns"
require_relative "loops_sdk/email_messages"
require_relative "loops_sdk/uploads"

module LoopsSdk
class << self
Expand Down
23 changes: 22 additions & 1 deletion lib/loops_sdk/transactional.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,29 @@ module LoopsSdk
class Transactional < Base
class << self
def list(perPage: 20, cursor: nil)
make_request(method: :get, path: "v1/transactional", params: { perPage: perPage, cursor: cursor })
make_request(method: :get, path: "v1/transactional-emails", params: { perPage: perPage, cursor: cursor })
end

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

def get(transactional_id:)
make_request(method: :get, path: "v1/transactional-emails/#{transactional_id}")
end

def update(transactional_id:, name:)
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}", body: { name: name })
end

def ensure_draft(transactional_id:)
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/draft")
end

def publish(transactional_id:)
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/publish")
end

def send(transactional_id:, email:, add_to_audience: false, data_variables: {}, attachments: [], headers: {})
attachments = attachments.map do |attachment|
attachment.transform_keys { |key| key == :content_type ? :contentType : key }
Expand Down
Loading
Loading