Skip to content

Commit 3b42e6a

Browse files
authored
Merge pull request #5 from loops-so/uploads-transactional
Uploads and transactional methods
2 parents 3c19b5a + e11aba5 commit 3b42e6a

11 files changed

Lines changed: 597 additions & 109 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## v2.3.0 - Jun 10, 2026
2+
3+
- Added `Uploads.upload()` to upload an image file in a single call (create, PUT to pre-signed URL, and complete).
4+
- Added transactional email content endpoints: `Transactional.create()`, `.get()`, `.update()`, `.ensure_draft()`, and `.publish()`. `Transactional.list()` returns a new data shape.
5+
16
## v2.2.0 - May 21, 2026
27

38
- Added support for campaigns, email messages, themes, components, and dedicated sending IP endpoints.

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
loops_sdk (2.2.0)
4+
loops_sdk (2.3.0)
55
faraday
66

77
GEM

README.md

Lines changed: 212 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ You can use custom contact properties in API calls. Please make sure to [add cus
114114
- [ContactProperties.list()](#contactpropertieslist)
115115
- [MailingLists.list()](#mailinglistslist)
116116
- [Events.send()](#eventssend)
117-
- [Transactional.send()](#transactionalsend)
118117
- [Transactional.list()](#transactionallist)
118+
- [Transactional.create()](#transactionalcreate)
119+
- [Transactional.get()](#transactionalget)
120+
- [Transactional.update()](#transactionalupdate)
121+
- [Transactional.ensure_draft()](#transactionalensure_draft)
122+
- [Transactional.publish()](#transactionalpublish)
123+
- [Transactional.send()](#transactionalsend)
119124
- [DedicatedSendingIps.list()](#dedicatedsendingipslist)
120125
- [Themes.list()](#themeslist)
121126
- [Themes.get()](#themesget)
@@ -127,6 +132,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus
127132
- [Campaigns.update()](#campaignsupdate)
128133
- [EmailMessages.get()](#emailmessagesget)
129134
- [EmailMessages.update()](#emailmessagesupdate)
135+
- [Uploads.upload()](#uploadsupload)
130136

131137
---
132138

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

727733
---
728734

735+
### Transactional.list()
736+
737+
List transactional emails, most recently created first.
738+
739+
[API Reference](https://loops.so/docs/api-reference/list-transactional-emails)
740+
741+
#### Parameters
742+
743+
| Name | Type | Required | Notes |
744+
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
745+
| `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
746+
| `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
747+
748+
#### Example
749+
750+
```ruby
751+
response = LoopsSdk::Transactional.list
752+
753+
response = LoopsSdk::Transactional.list(perPage: 15, cursor: "cursor_value")
754+
```
755+
756+
#### Response
757+
758+
```json
759+
{
760+
"pagination": {
761+
"totalResults": 23,
762+
"returnedResults": 20,
763+
"perPage": 20,
764+
"totalPages": 2,
765+
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
766+
"nextPage": "https://app.loops.so/api/v1/transactional-emails?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
767+
},
768+
"data": [
769+
{
770+
"id": "clfn0k1yg001imo0fdeqg30i8",
771+
"name": "Welcome email",
772+
"draftEmailMessageId": null,
773+
"publishedEmailMessageId": "msg_123",
774+
"createdAt": "2023-11-06T17:48:07.249Z",
775+
"updatedAt": "2023-11-06T17:48:07.249Z",
776+
"dataVariables": ["confirmationUrl"]
777+
}
778+
]
779+
}
780+
```
781+
782+
---
783+
784+
### Transactional.create()
785+
786+
Create a transactional email. An empty draft email message is created automatically.
787+
788+
[API Reference](https://loops.so/docs/api-reference/create-transactional-email)
789+
790+
#### Parameters
791+
792+
| Name | Type | Required | Notes |
793+
| ------ | ------ | -------- | ----- |
794+
| `name` | string | Yes | |
795+
796+
#### Example
797+
798+
```ruby
799+
response = LoopsSdk::Transactional.create(name: "Welcome email")
800+
```
801+
802+
#### Response
803+
804+
Returns the transactional email with `draftEmailMessageId` and `draftEmailMessageContentRevisionId`. Use these when updating the draft via `EmailMessages.update()`.
805+
806+
```json
807+
{
808+
"id": "txn_123",
809+
"name": "Welcome email",
810+
"draftEmailMessageId": "msg_123",
811+
"draftEmailMessageContentRevisionId": "revision_123",
812+
"publishedEmailMessageId": null,
813+
"createdAt": "2023-11-06T17:48:07.249Z",
814+
"updatedAt": "2023-11-06T17:48:07.249Z",
815+
"dataVariables": []
816+
}
817+
```
818+
819+
---
820+
821+
### Transactional.get()
822+
823+
Get a single transactional email by ID.
824+
825+
[API Reference](https://loops.so/docs/api-reference/get-transactional-email)
826+
827+
#### Parameters
828+
829+
| Name | Type | Required | Notes |
830+
| ------------------ | ------ | -------- | ----- |
831+
| `transactional_id` | string | Yes | |
832+
833+
#### Example
834+
835+
```ruby
836+
response = LoopsSdk::Transactional.get(transactional_id: "txn_123")
837+
```
838+
839+
---
840+
841+
### Transactional.update()
842+
843+
Update a transactional email's name.
844+
845+
[API Reference](https://loops.so/docs/api-reference/update-transactional-email)
846+
847+
#### Parameters
848+
849+
| Name | Type | Required | Notes |
850+
| ------------------ | ------ | -------- | ----- |
851+
| `transactional_id` | string | Yes | |
852+
| `name` | string | Yes | |
853+
854+
#### Example
855+
856+
```ruby
857+
response = LoopsSdk::Transactional.update(
858+
transactional_id: "txn_123",
859+
name: "Updated name"
860+
)
861+
```
862+
863+
---
864+
865+
### Transactional.ensure_draft()
866+
867+
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.
868+
869+
[API Reference](https://loops.so/docs/api-reference/ensure-transactional-email-draft)
870+
871+
#### Parameters
872+
873+
| Name | Type | Required | Notes |
874+
| ------------------ | ------ | -------- | ----- |
875+
| `transactional_id` | string | Yes | |
876+
877+
#### Example
878+
879+
```ruby
880+
response = LoopsSdk::Transactional.ensure_draft(transactional_id: "txn_123")
881+
```
882+
883+
---
884+
885+
### Transactional.publish()
886+
887+
Publish a transactional email's current draft. The draft becomes the published version and the draft is cleared.
888+
889+
[API Reference](https://loops.so/docs/api-reference/publish-transactional-email)
890+
891+
#### Parameters
892+
893+
| Name | Type | Required | Notes |
894+
| ------------------ | ------ | -------- | ----- |
895+
| `transactional_id` | string | Yes | |
896+
897+
#### Example
898+
899+
```ruby
900+
response = LoopsSdk::Transactional.publish(transactional_id: "txn_123")
901+
```
902+
903+
---
904+
729905
### Transactional.send()
730906

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

820996
---
821997

822-
### Transactional.list()
823-
824-
Get a list of published transactional emails.
825-
826-
[API Reference](https://loops.so/docs/api-reference/list-transactional-emails)
827-
828-
#### Parameters
829-
830-
| Name | Type | Required | Notes |
831-
| --------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
832-
| `perPage` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
833-
| `cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
834-
835-
#### Example
836-
837-
```ruby
838-
response = LoopsSdk::Transactional.list
839-
840-
response = LoopsSdk::Transactional.list(perPage: 15)
841-
```
842-
843-
#### Response
844-
845-
```json
846-
{
847-
"pagination": {
848-
"totalResults": 23,
849-
"returnedResults": 20,
850-
"perPage": 20,
851-
"totalPages": 2,
852-
"nextCursor": "clyo0q4wo01p59fsecyxqsh38",
853-
"nextPage": "https://app.loops.so/api/v1/transactional?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
854-
},
855-
"data": [
856-
{
857-
"id": "clfn0k1yg001imo0fdeqg30i8",
858-
"lastUpdated": "2023-11-06T17:48:07.249Z",
859-
"dataVariables": []
860-
},
861-
{
862-
"id": "cll42l54f20i1la0lfooe3z12",
863-
"lastUpdated": "2025-02-02T02:56:28.845Z",
864-
"dataVariables": [
865-
"confirmationUrl"
866-
]
867-
},
868-
{
869-
"id": "clw6rbuwp01rmeiyndm80155l",
870-
"lastUpdated": "2024-05-14T19:02:52.000Z",
871-
"dataVariables": [
872-
"firstName",
873-
"lastName",
874-
"inviteLink"
875-
]
876-
},
877-
...
878-
]
879-
}
880-
```
881-
882-
---
883-
884998
### DedicatedSendingIps.list()
885999

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

11321246
---
11331247

1248+
### Uploads.upload()
1249+
1250+
Upload an image file for use in LMX email content.
1251+
1252+
Supported image types: JPEG, PNG, GIF, and WebP (max 4 MB). MIME type is detected from file contents, or pass `content_type:` to override.
1253+
1254+
[API Reference](https://loops.so/docs/api-reference/create-upload)
1255+
1256+
#### Parameters
1257+
1258+
| Name | Type | Required | Notes |
1259+
| --------------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
1260+
| `path` | string | Yes | Path to the image file on disk. |
1261+
| `content_type` | string | No | MIME type override. Supported: `image/jpeg`, `image/png`, `image/gif`, `image/webp`. |
1262+
1263+
#### Example
1264+
1265+
```ruby
1266+
response = LoopsSdk::Uploads.upload(path: "./header.png")
1267+
1268+
# Use the returned URL in LMX
1269+
lmx = %(<Image src="#{response['finalUrl']}" alt="Header" />)
1270+
```
1271+
1272+
#### Response
1273+
1274+
```json
1275+
{
1276+
"emailAssetId": "asset_123",
1277+
"finalUrl": "https://cdn.loops.so/asset_123.png"
1278+
}
1279+
```
1280+
1281+
---
1282+
11341283
## Testing
11351284

11361285
Run tests with `bundle exec rspec`.

lib/loops_sdk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require_relative "loops_sdk/components"
1717
require_relative "loops_sdk/campaigns"
1818
require_relative "loops_sdk/email_messages"
19+
require_relative "loops_sdk/uploads"
1920

2021
module LoopsSdk
2122
class << self

lib/loops_sdk/transactional.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@ module LoopsSdk
44
class Transactional < Base
55
class << self
66
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 })
88
end
9+
10+
def create(name:)
11+
make_request(method: :post, path: "v1/transactional-emails", body: { name: name })
12+
end
13+
14+
def get(transactional_id:)
15+
make_request(method: :get, path: "v1/transactional-emails/#{transactional_id}")
16+
end
17+
18+
def update(transactional_id:, name:)
19+
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}", body: { name: name })
20+
end
21+
22+
def ensure_draft(transactional_id:)
23+
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/draft")
24+
end
25+
26+
def publish(transactional_id:)
27+
make_request(method: :post, path: "v1/transactional-emails/#{transactional_id}/publish")
28+
end
29+
930
def send(transactional_id:, email:, add_to_audience: false, data_variables: {}, attachments: [], headers: {})
1031
attachments = attachments.map do |attachment|
1132
attachment.transform_keys { |key| key == :content_type ? :contentType : key }

0 commit comments

Comments
 (0)