unione 1.1.3 → 1.2.3
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 +4 -4
- data/AGENTS.md +3 -3
- data/README.md +219 -126
- data/docs/Domain.md +8 -3
- data/lib/unione/api/domain/domain_api.rb +9 -3
- data/lib/unione/api/email/model/message_object_recipients_item.rb +25 -0
- data/lib/unione/client/api_client.rb +1 -1
- data/lib/unione/client/configuration.rb +1 -1
- data/lib/unione/common/model/body_object.rb +12 -0
- data/lib/unione/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 905c25d5be2c9eb7f499e528962a173bd5852d40ed4239d07d9484d830227900
|
|
4
|
+
data.tar.gz: 2b0bc8aa4a2149a3e663ed7da251174ceeb180671e3f90ee932814ad791cbfc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0916665cc508953defe333e3517a0b72b0000a6dde8ffce70753e1cf01a5b9772fd78a26a118c188481f55c41bc40601345c86574399afab16a296e96323c06
|
|
7
|
+
data.tar.gz: 92a7e623a9c261c4577cbeb3cbce612dfa21742bdad0a48c30628f844f2f4d0fb5bacf0755d9eaec37d5c6588da303398b60cfb07c412276416e04a32d20f00f
|
data/AGENTS.md
CHANGED
|
@@ -11,9 +11,9 @@ UniOne Ruby SDK — client for the UniOne transactional email Web API (Web API 1
|
|
|
11
11
|
gem build *.gemspec
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
- **Source label.** Every request carries the query parameter `platform=ruby-sdk.1.
|
|
14
|
+
- **Source label.** Every request carries the query parameter `platform=ruby-sdk.1.2.3`, added in one place, the shared request client (`ApiClient`); a package built on this SDK puts its own label in front: `<package>.<version>,ruby-sdk.1.2.3`.
|
|
15
15
|
- **Tests.** Before a release the generator builds and lints the package and runs every example of docs/ and of this file against a mock of the API: no real API key, no real sends.
|
|
16
|
-
- **Release.** The version (1.
|
|
16
|
+
- **Release.** The version (1.2.3 in the gemspec) and the tag `v1.2.3` come from the generator; README.md states the Web API version the release was generated from.
|
|
17
17
|
- **Secrets.** Never commit API keys; the examples read `UNIONE_API_KEY` from the environment.
|
|
18
18
|
|
|
19
19
|
## Integrating UniOne with this SDK
|
|
@@ -38,7 +38,7 @@ Rules that generated code must follow:
|
|
|
38
38
|
- Base URL `https://api.unione.io/en/transactional/api/v1` (an account registered on another host uses that one: `eu1.unione.io`, `us1.unione.io`); every method is POST with a JSON body; auth header `X-API-KEY`. The SDK does all of it.
|
|
39
39
|
- First email without DNS: `domain/get-sandbox-domain` → `confirmed-email/create` (the user clicks the link within 48 hours) → `email/send` from `noreply@<sandbox-domain>` to the confirmed address. Account API key only; up to 10 confirmed addresses, 100 emails a day; the domain expires after 31 days.
|
|
40
40
|
- Never ship a sandbox domain as the production sender (it is not in `domain/list`); read `from_email` from config. Production needs a verified domain: `domain/get-dns-records` → DNS records → `domain/validate-verification-record`, `domain/validate-dkim` → `domain/list`.
|
|
41
|
-
- Set `message.idempotence_key` on retryable sends; retry network errors, `429` and `5xx` with backoff, not other errors unchanged. Up to 500 recipients and 10 MB per request
|
|
41
|
+
- Set `message.idempotence_key` on retryable sends; retry network errors, `429` and `5xx` with backoff, not other errors unchanged. Up to 500 recipients and 10 MB per request with the attachments in base64 (a third larger than the files); always `body.html` or `body.plaintext`.
|
|
42
42
|
- Delivery events via webhook (`webhook/set`) with a fast, idempotent HTTPS handler, not polling; check the `auth` field of every notification: the MD5 of the request body with the `auth` value replaced by the API key.
|
|
43
43
|
- Keep the default unsubscribe and tracking behaviour; changes go through UniOne support.
|
|
44
44
|
- Do not invent methods or fields. Check [docs/](docs/) of this package, the [API reference](https://docs.unione.io/en/web-api-ref) or [llms.txt](https://docs.unione.io/llms.txt).
|
data/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# UniOne Ruby SDK
|
|
1
|
+
# UniOne Ruby SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://rubygems.org/gems/unione) [](LICENSE)
|
|
4
4
|
|
|
5
5
|
Official Ruby library for the [UniOne](https://unione.io) transactional email API.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[API reference](https://docs.unione.io/en/web-api-ref) · Docs for LLMs: [llms.txt](https://docs.unione.io/llms.txt)
|
|
8
|
+
|
|
9
|
+
> **On Ruby on Rails?** Install [`unione-rails`](https://github.com/unione-repo/unione-rails) instead: this SDK plus configuration, a mailer and a webhook route.
|
|
8
10
|
|
|
9
11
|
## Install
|
|
10
12
|
|
|
@@ -14,106 +16,214 @@ gem install unione
|
|
|
14
16
|
|
|
15
17
|
Requirements: Ruby 2.7+. No third-party dependencies.
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
1. Sign up at [cp.unione.io](https://cp.unione.io/en/user/registration/).
|
|
20
|
-
2. Copy your API key from [Settings → Security → API](https://cp.unione.io/en/settings/security/api).
|
|
21
|
-
3. Put it in an environment variable:
|
|
19
|
+
Get an API key in the [control panel](https://cp.unione.io/en/settings/security/api) and keep it in the environment, never in code:
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
22
|
export UNIONE_API_KEY="your-api-key"
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
## Send an email
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'unione'
|
|
29
|
+
|
|
30
|
+
Body = UniOne::Common::Model::BodyObject
|
|
31
|
+
Email = UniOne::Api::Email::Model
|
|
32
|
+
Recipient = UniOne::Api::Email::Model::MessageObjectRecipientsItem
|
|
33
|
+
|
|
34
|
+
client = UniOne.client(ENV.fetch('UNIONE_API_KEY'))
|
|
35
|
+
|
|
36
|
+
message = Email::MessageObject.new(
|
|
37
|
+
recipients: [Recipient.from_address('Alex <alex@example.com>')],
|
|
38
|
+
body: Body.html('<p>Hello, {{to_name}}! Your order has shipped.</p>'),
|
|
39
|
+
subject: 'Your order',
|
|
40
|
+
from_email: 'shop@example.com',
|
|
41
|
+
from_name: 'Shop'
|
|
42
|
+
)
|
|
43
|
+
response = client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
44
|
+
puts response.job_id
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The sender address must be on a domain verified in your account. No domain yet? Send from the [sandbox domain](#no-domain-yet-the-test-sandbox-domain) in three steps.
|
|
48
|
+
|
|
49
|
+
## Contents
|
|
29
50
|
|
|
30
|
-
|
|
51
|
+
- [Recipes](#recipes): personalization · attachments · CC and BCC · scheduling · safe retries · errors · webhooks
|
|
52
|
+
- [No domain yet: the test sandbox domain](#no-domain-yet-the-test-sandbox-domain)
|
|
53
|
+
- [Going to production](#going-to-production)
|
|
54
|
+
- [Reference](#reference)
|
|
55
|
+
- [Help](#help)
|
|
31
56
|
|
|
32
|
-
|
|
33
|
-
It delivers only to addresses you confirm.
|
|
57
|
+
## Recipes
|
|
34
58
|
|
|
35
|
-
|
|
59
|
+
Every recipe continues the example above: the client and its imports are there.
|
|
36
60
|
|
|
37
|
-
|
|
61
|
+
### Personalize for each recipient
|
|
62
|
+
|
|
63
|
+
Up to 500 recipients per call, each gets a separate copy with its own substitutions. Metadata of a recipient comes back in its webhook events.
|
|
38
64
|
|
|
39
65
|
```ruby
|
|
40
|
-
|
|
66
|
+
message = Email::MessageObject.new(
|
|
67
|
+
recipients: [
|
|
68
|
+
Recipient.from_address('Alex <alex@example.com>', { 'order' => '1001' }).with_metadata({ 'user_id' => '42' }),
|
|
69
|
+
Recipient.from_address('Maria <maria@example.com>', { 'order' => '1002' }).with_metadata({ 'user_id' => '43' })
|
|
70
|
+
],
|
|
71
|
+
body: Body.html('<p>{{to_name}}, order #{{order}} has shipped.</p>'),
|
|
72
|
+
subject: 'Order #{{order}}',
|
|
73
|
+
from_email: 'shop@example.com'
|
|
74
|
+
)
|
|
75
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
76
|
+
```
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
### Attach a file
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
Common = UniOne::Common::Model
|
|
43
82
|
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
message = Email::MessageObject.new(
|
|
84
|
+
recipients: [Recipient.from_address('alex@example.com')],
|
|
85
|
+
body: Body.html('<p>Your invoice is attached.</p>'),
|
|
86
|
+
subject: 'Invoice',
|
|
87
|
+
from_email: 'shop@example.com',
|
|
88
|
+
attachments: [
|
|
89
|
+
Common::AttachmentsArrayItem.new(
|
|
90
|
+
type: 'application/pdf',
|
|
91
|
+
name: 'invoice.pdf',
|
|
92
|
+
content: [File.binread('invoice.pdf')].pack('m0')
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
46
97
|
```
|
|
47
98
|
|
|
48
|
-
|
|
99
|
+
### CC and BCC
|
|
100
|
+
|
|
101
|
+
Every address goes to `recipients` (each gets a copy); the visible ones also go to the headers. `audit@example.com` is in neither header: a BCC. The rules and limits: [CC and BCC](https://docs.unione.io/en/cc-and-bcc).
|
|
49
102
|
|
|
50
103
|
```ruby
|
|
51
|
-
|
|
104
|
+
Common = UniOne::Common::Model
|
|
52
105
|
|
|
53
|
-
|
|
106
|
+
message = Email::MessageObject.new(
|
|
107
|
+
recipients: [
|
|
108
|
+
Recipient.from_address('alex@example.com'),
|
|
109
|
+
Recipient.from_address('boss@example.com'),
|
|
110
|
+
Recipient.from_address('audit@example.com')
|
|
111
|
+
],
|
|
112
|
+
body: Body.html('<p>The report is attached.</p>'),
|
|
113
|
+
subject: 'Weekly report',
|
|
114
|
+
from_email: 'shop@example.com',
|
|
115
|
+
headers: Common::HeadersObject.new(extra: { 'To' => 'alex@example.com', 'CC' => 'boss@example.com' })
|
|
116
|
+
)
|
|
117
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
118
|
+
```
|
|
54
119
|
|
|
55
|
-
|
|
120
|
+
### Schedule
|
|
121
|
+
|
|
122
|
+
`send_at` is UTC, up to 24 hours ahead.
|
|
56
123
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
124
|
+
```ruby
|
|
125
|
+
send_at = (Time.now.utc + 3600).strftime('%Y-%m-%d %H:%M:%S') # UTC, "YYYY-MM-DD hh:mm:ss"
|
|
126
|
+
message = Email::MessageObject.new(
|
|
127
|
+
recipients: [Recipient.from_address('alex@example.com')],
|
|
128
|
+
body: Body.text('See you tomorrow!'),
|
|
129
|
+
subject: 'Reminder',
|
|
130
|
+
from_email: 'shop@example.com',
|
|
131
|
+
options: Email::MessageObjectOptions.new(send_at: send_at)
|
|
132
|
+
)
|
|
133
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
61
134
|
```
|
|
62
135
|
|
|
63
|
-
|
|
136
|
+
### Retry safely
|
|
64
137
|
|
|
65
|
-
|
|
138
|
+
A repeat with the same `idempotence_key` within a minute is not sent twice: retry network errors, timeouts, 429 and 5xx with it.
|
|
66
139
|
|
|
67
140
|
```ruby
|
|
68
|
-
|
|
141
|
+
message = Email::MessageObject.new(
|
|
142
|
+
recipients: [Recipient.from_address('alex@example.com')],
|
|
143
|
+
body: Body.html('<p>Use this link to reset your password.</p>'),
|
|
144
|
+
subject: 'Reset your password',
|
|
145
|
+
from_email: 'shop@example.com',
|
|
146
|
+
idempotence_key: 'password-reset-8f3a'
|
|
147
|
+
)
|
|
148
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
149
|
+
```
|
|
69
150
|
|
|
70
|
-
|
|
71
|
-
Email = UniOne::Api::Email::Model
|
|
151
|
+
### Handle errors
|
|
72
152
|
|
|
73
|
-
|
|
153
|
+
Recipients the API drops (unsubscribed, invalid, suppressed) are not an error: they are in `failed_emails` of the response, the rest still get the message.
|
|
74
154
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
body: Common::BodyObject.new(
|
|
83
|
-
html: '<p>It works!</p>'
|
|
84
|
-
),
|
|
85
|
-
subject: 'Hello from UniOne',
|
|
86
|
-
from_email: 'noreply@<sandbox-domain>'
|
|
87
|
-
)
|
|
88
|
-
))
|
|
89
|
-
p response.job_id
|
|
155
|
+
```ruby
|
|
156
|
+
begin
|
|
157
|
+
client.email.send_email(request)
|
|
158
|
+
rescue UniOne::Client::ApiError => e
|
|
159
|
+
# HTTP status (0: network error or timeout), API error code, message
|
|
160
|
+
puts "#{e.http_status} #{e.error_code}: #{e.message}"
|
|
161
|
+
end
|
|
90
162
|
```
|
|
91
163
|
|
|
92
|
-
|
|
164
|
+
### Receive delivery events
|
|
93
165
|
|
|
94
|
-
|
|
166
|
+
Register your HTTPS endpoint once (`webhook/set`). Then check the signature of every notification (`auth`: MD5 of the raw body with the `auth` value replaced by your API key; a wrong one gets 401) and process the events.
|
|
95
167
|
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
```ruby
|
|
169
|
+
require 'digest'
|
|
170
|
+
|
|
171
|
+
# raw: the body of the POST request as received; returns the HTTP status to answer
|
|
172
|
+
def handle_webhook(raw)
|
|
173
|
+
callback = UniOne::Api::Webhook::WebhookApi.parse_callback(raw)
|
|
174
|
+
auth = callback.auth.to_s
|
|
175
|
+
# auth: MD5 of the raw body with the auth value replaced by your API key
|
|
176
|
+
return 401 unless Digest::MD5.hexdigest(raw.gsub(auth, ENV.fetch('UNIONE_API_KEY'))) == auth
|
|
177
|
+
|
|
178
|
+
(callback.events_by_user || []).each do |user|
|
|
179
|
+
(user.events || []).each do |event|
|
|
180
|
+
# email, status (delivered, opened, hard_bounced, spam, ...), job_id, metadata
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
200
|
|
184
|
+
end
|
|
104
185
|
```
|
|
105
186
|
|
|
106
|
-
|
|
187
|
+
> **Answer 200 within 3 seconds** and process the events in the background (a queue): otherwise UniOne repeats the request every 10 minutes for 24 hours (`retry_count` grows), and a handler that failed every request for at least 10 events of a day is turned inactive. The handler must take at least **5 parallel requests of up to 100 events each**; deduplicate, a repeat delivers the same events again.
|
|
188
|
+
|
|
189
|
+
## No domain yet: the test sandbox domain
|
|
190
|
+
|
|
191
|
+
Until your domain is verified, send from the **sandbox domain**: a test domain every account has, already verified, with DKIM and SPF set. It delivers only to addresses you confirm (the confirmation link comes by email, valid 48 hours), up to 100 emails a day. The sandbox domain is deleted 31 days after it was created, but you can get it again: the next `domain/get-sandbox-domain` call creates a new one (with a new name, so read it from the call, do not hardcode it).
|
|
107
192
|
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
193
|
+
```ruby
|
|
194
|
+
ConfirmedEmail = UniOne::Api::ConfirmedEmail::Model
|
|
195
|
+
|
|
196
|
+
# 1. Your sandbox domain: created on the first call, the same one afterwards
|
|
197
|
+
domain = client.domain.get_sandbox_domain.domain
|
|
198
|
+
|
|
199
|
+
# 2. Confirm your inbox: UniOne sends a link, click it within 48 hours
|
|
200
|
+
client.confirmed_email.create(ConfirmedEmail::ConfirmedEmailRequest.new(email: 'you@example.com'))
|
|
201
|
+
|
|
202
|
+
# 3. Send from any address on the sandbox domain to the confirmed one
|
|
203
|
+
message = Email::MessageObject.new(
|
|
204
|
+
recipients: [Recipient.from_address('you@example.com')],
|
|
205
|
+
body: Body.html('<p>It works!</p>'),
|
|
206
|
+
subject: 'Hello from UniOne',
|
|
207
|
+
from_email: "noreply@#{domain}"
|
|
208
|
+
)
|
|
209
|
+
client.email.send_email(Email::EmailSendRequest.new(message: message))
|
|
114
210
|
```
|
|
115
211
|
|
|
116
|
-
|
|
212
|
+
<details>
|
|
213
|
+
<summary>Sandbox limits</summary>
|
|
214
|
+
|
|
215
|
+
- Sends only to confirmed addresses: up to 10 per account.
|
|
216
|
+
- Up to 100 emails a day.
|
|
217
|
+
- The sandbox domain expires after 31 days; call `domain/get-sandbox-domain` again to get a new one.
|
|
218
|
+
- Works only with the account API key, not a project key.
|
|
219
|
+
- Not listed in `domain/list`; the other `domain/*` methods do not apply to it.
|
|
220
|
+
|
|
221
|
+
The sandbox is for development and testing. Do not use a sandbox domain as the sender in production.
|
|
222
|
+
|
|
223
|
+
</details>
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary>The same with curl</summary>
|
|
117
227
|
|
|
118
228
|
```bash
|
|
119
229
|
BASE=https://api.unione.io/en/transactional/api/v1
|
|
@@ -143,58 +253,51 @@ curl -s -X POST $BASE/email/send.json \
|
|
|
143
253
|
}'
|
|
144
254
|
```
|
|
145
255
|
|
|
146
|
-
|
|
256
|
+
</details>
|
|
147
257
|
|
|
148
|
-
|
|
149
|
-
- Up to 100 emails a day.
|
|
150
|
-
- The sandbox domain expires after 31 days; call `domain/get-sandbox-domain` again to get a new one.
|
|
151
|
-
- Works only with the account API key, not a project key.
|
|
152
|
-
- Not listed in `domain/list`; the other `domain/*` methods do not apply to it.
|
|
153
|
-
|
|
154
|
-
The sandbox is for development and testing. Do not use a sandbox domain as the sender in production.
|
|
155
|
-
|
|
156
|
-
## Before production: verify your domain
|
|
157
|
-
|
|
158
|
-
1. `domain/get-dns-records` — returns the values for your DNS records.
|
|
159
|
-
2. Add these records at your DNS provider (the domain page of the control panel shows them too):
|
|
160
|
-
|
|
161
|
-
| Type | Host | Value |
|
|
162
|
-
|---|---|---|
|
|
163
|
-
| TXT | `@` | the `verification-record` value (`unione-validate-hash=…`) |
|
|
164
|
-
| TXT | `<selector>._domainkey` (the selector is on the domain page of the control panel, e.g. `us`) | `v=DKIM1; k=rsa; p=<dkim>` |
|
|
165
|
-
| TXT | `@` | `v=spf1 include:spf.unione.io ~all` |
|
|
166
|
-
| CNAME | `_dmarc` | the `dmarc` value |
|
|
258
|
+
## Going to production
|
|
167
259
|
|
|
168
|
-
|
|
169
|
-
4. `domain/list` — wait until the verification record is confirmed and DKIM is active.
|
|
170
|
-
5. Read `from_email` from your config, so switching from the sandbox needs no code change.
|
|
260
|
+
**1. Verify your sending domain.** `domain/get-dns-records` returns the verification record, the DKIM key and the DMARC target; add the records at your DNS provider, then `domain/validate-verification-record` and `domain/validate-dkim`.
|
|
171
261
|
|
|
172
|
-
|
|
262
|
+
| Type | Host | Value | Where it comes from |
|
|
263
|
+
|---|---|---|---|
|
|
264
|
+
| TXT | `@` | `unione-validate-hash=…` | `verification-record` of the response, as is |
|
|
265
|
+
| TXT | `<selector>._domainkey` (the selector is on the domain page of the control panel, e.g. `us`) | `v=DKIM1; k=rsa; p=<dkim>` | `dkim` of the response after `v=DKIM1; k=rsa; p=` |
|
|
266
|
+
| TXT | `@` | `v=spf1 include:spf.unione.io ~all` | not in the response; add the include to your SPF record if you have one |
|
|
267
|
+
| CNAME | `_dmarc` | `<domain>.dmarc.…` | `dmarc` of the response (UniOne publishes the DMARC policy); keep your own DMARC TXT record instead if the domain has one |
|
|
173
268
|
|
|
174
|
-
|
|
175
|
-
- **Limits:** up to 500 recipients and 10 MB per request; each attachment up to 7 MB.
|
|
176
|
-
- **Body:** always include `body.html` or `body.plaintext`.
|
|
177
|
-
- **Delivery status:** register a webhook (`webhook/set`) with a fast, idempotent HTTPS handler instead of polling.
|
|
178
|
-
- **Defaults:** keep the default unsubscribe footer and tracking; changing them requires UniOne support.
|
|
179
|
-
- **Errors:** retry `429` and `5xx` with backoff; do not retry other errors unchanged.
|
|
269
|
+
**2. Make sending reliable.**
|
|
180
270
|
|
|
181
|
-
|
|
271
|
+
| | |
|
|
272
|
+
|---|---|
|
|
273
|
+
| Retries | `429` and `5xx` with backoff and the same `idempotence_key`; fix other errors instead of retrying |
|
|
274
|
+
| Limits | 500 recipients and 10 MB per request, attachments included: base64 makes a file about a third larger |
|
|
275
|
+
| Body | always `html` or `plaintext` (a template can supply it) |
|
|
276
|
+
| Delivery status | a webhook, not polling |
|
|
277
|
+
| Queue | send from a background job, not inside the web request |
|
|
278
|
+
| Config | `from_email` from config, so the sandbox → production switch needs no code change |
|
|
279
|
+
| Defaults | keep the unsubscribe footer and tracking; changing them needs UniOne support |
|
|
182
280
|
|
|
183
|
-
|
|
184
|
-
- [Web API reference](https://docs.unione.io/en/web-api-ref)
|
|
185
|
-
- [llms.txt](https://docs.unione.io/llms.txt) for AI coding agents
|
|
186
|
-
- [UniOne MCP server and agent setup](https://unione.io/en/agents)
|
|
187
|
-
- [Support](https://cp.unione.io/en/support)
|
|
281
|
+
## Reference
|
|
188
282
|
|
|
189
|
-
|
|
283
|
+
### Sections
|
|
190
284
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
285
|
+
| Section | Methods | Description |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| [Email](docs/Email.md) | `send_email`, `subscribe` | This group of methods is related with email sending. |
|
|
288
|
+
| [EmailValidation](docs/EmailValidation.md) | `single` | Email validation methods. As a result of the check, you’ll get detailed information on email validity. It will help eliminate nonexistent emails, decrease the number of complaints, clean up the subscribers list and eventually enhance your sender reputation score. For example, you may add validation to a subscription form on your site to let users know they’ve just entered an invalid address. |
|
|
289
|
+
| [ConfirmedEmail](docs/ConfirmedEmail.md) | `create`, `list`, `delete` | Manage email addresses confirmed for sandbox-domain sending. These methods are available only with a main account API key. |
|
|
290
|
+
| [Template](docs/Template.md) | `set`, `get`, `list`, `delete` | You have the option of storing a template on the UniOne side and then using it by id, which is highly recommended for sending repetitive messages or high volume traffic. UniOne supports two different [template engines](https://docs.unione.io/en/template-engines). |
|
|
291
|
+
| [Webhook](docs/Webhook.md) | `set`, `get`, `list`, `delete` | UniOne lets you recieve notifications about email status changes or spam blocks. You provide an URL and every time a watched event occurs we will notify you by submitting a request with JSON data to this URL. |
|
|
292
|
+
| [Suppression](docs/Suppression.md) | `set`, `get`, `list`, `delete` | Methods for managing suppression lists. An email can be added to suppression list because of unsubscription, bounce or complain. |
|
|
293
|
+
| [Domain](docs/Domain.md) | `get_dns_records`, `validate_verification_record`, `validate_dkim`, `list`, `get_sandbox_domain`, `delete` | You can use these methods to setup sender domains and check their statuses. They might be useful in case you want to automate adding domains for your own clients. |
|
|
294
|
+
| [EventDump](docs/EventDump.md) | `create`, `get`, `list`, `delete` | These methods are used to obtain a list of events for a given period of time. The data is stored for up to 45 days, depending on your tariff, i. e. you may fetch all event data starting from 00:00:00 31 days ago and up to current time. For custom tarifs with big sending volumes event dumps are available only for the last 7 days. |
|
|
295
|
+
| [Tag](docs/Tag.md) | `list`, `delete` | This family of methods is used to manage user-defined tags. Tags are used to categorize letters based on your own criteria, e. g. to create a distinction between marketing and transactional letters or group together all notifications for a particular order type. UniOne allows you to assign tags to all outgoing emails, and the tags you’ve set may then be used to filter and analyze delivery statistics. |
|
|
296
|
+
| [Project](docs/Project.md) | `create`, `update`, `list`, `delete` | Projects allow you to group several independent domains, webhooks, templates, and unsubscription lists together. It's very useful if you have your own clients and manage their data from a single UniOne account. Each project receives its own API key, and all API calls with that API key only deal with the data of that project. This way, you can assign a separate project for each client and be sure that they won't conflict; e.g. unsubscribing from one project won't affect another. |
|
|
297
|
+
| [System](docs/System.md) | `ping`, `info` | System operations. |
|
|
298
|
+
| [Obsolete](docs/Obsolete.md) | `unsubscribed_set`, `unsubscribed_check`, `unsubscribed_list` | Obsolete methods dealing with unsubscribes. We recommend using the more flexible [suppression](https://docs.unione.io/en/web-api-ref#suppression) methods instead. Note that you can't resubscribe an unsubscribed recipient using WebAPI, but you can send him an email with subscription link using email/subscribe method. |
|
|
196
299
|
|
|
197
|
-
|
|
300
|
+
### API host
|
|
198
301
|
|
|
199
302
|
The second argument selects the API host (datacenter) or a full base URL.
|
|
200
303
|
|
|
@@ -205,7 +308,7 @@ client = UniOne.client('YOUR_API_KEY', 'us1.unione.io')
|
|
|
205
308
|
client = UniOne.client('YOUR_API_KEY', UniOne::Client::Configuration::HOST_US1)
|
|
206
309
|
```
|
|
207
310
|
|
|
208
|
-
|
|
311
|
+
### Custom HTTP client / headers
|
|
209
312
|
|
|
210
313
|
`headers:` adds HTTP headers to every request. `transport:` replaces the HTTP layer: any object responding to `call(method, uri, headers, body, timeout)` that returns `[status, body]` (a lambda works too). Wrap the default `Client::NetHttpTransport` for retries or logging, or call Faraday / HTTParty in your own one; an exception of the transport becomes `ApiError`.
|
|
211
314
|
|
|
@@ -248,23 +351,13 @@ headers = UniOne::Common::Model::HeadersObject.new(extra: {
|
|
|
248
351
|
})
|
|
249
352
|
```
|
|
250
353
|
|
|
251
|
-
|
|
354
|
+
### Error handling
|
|
252
355
|
|
|
253
356
|
Every method raises `ApiError` when the API returns an error; it carries the [API error code](https://docs.unione.io/en/web-api-ref#api-errors) and the HTTP status.
|
|
254
357
|
|
|
255
|
-
##
|
|
358
|
+
## Help
|
|
256
359
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
| [ConfirmedEmail](docs/ConfirmedEmail.md) | `create`, `list`, `delete` | Manage email addresses confirmed for sandbox-domain sending. These methods are available only with a main account API key. |
|
|
262
|
-
| [Template](docs/Template.md) | `set`, `get`, `list`, `delete` | You have the option of storing a template on the UniOne side and then using it by id, which is highly recommended for sending repetitive messages or high volume traffic. UniOne supports two different [template engines](https://docs.unione.io/en/template-engines). |
|
|
263
|
-
| [Webhook](docs/Webhook.md) | `set`, `get`, `list`, `delete` | UniOne lets you recieve notifications about email status changes or spam blocks. You provide an URL and every time a watched event occurs we will notify you by submitting a request with JSON data to this URL. |
|
|
264
|
-
| [Suppression](docs/Suppression.md) | `set`, `get`, `list`, `delete` | Methods for managing suppression lists. An email can be added to suppression list because of unsubscription, bounce or complain. |
|
|
265
|
-
| [Domain](docs/Domain.md) | `get_dns_records`, `validate_verification_record`, `validate_dkim`, `list`, `get_sandbox_domain`, `delete` | You can use these methods to setup sender domains and check their statuses. They might be useful in case you want to automate adding domains for your own clients. |
|
|
266
|
-
| [EventDump](docs/EventDump.md) | `create`, `get`, `list`, `delete` | These methods are used to obtain a list of events for a given period of time. The data is stored for up to 45 days, depending on your tariff, i. e. you may fetch all event data starting from 00:00:00 31 days ago and up to current time. For custom tarifs with big sending volumes event dumps are available only for the last 7 days. |
|
|
267
|
-
| [Tag](docs/Tag.md) | `list`, `delete` | This family of methods is used to manage user-defined tags. Tags are used to categorize letters based on your own criteria, e. g. to create a distinction between marketing and transactional letters or group together all notifications for a particular order type. UniOne allows you to assign tags to all outgoing emails, and the tags you’ve set may then be used to filter and analyze delivery statistics. |
|
|
268
|
-
| [Project](docs/Project.md) | `create`, `update`, `list`, `delete` | Projects allow you to group several independent domains, webhooks, templates, and unsubscription lists together. It's very useful if you have your own clients and manage their data from a single UniOne account. Each project receives its own API key, and all API calls with that API key only deal with the data of that project. This way, you can assign a separate project for each client and be sure that they won't conflict; e.g. unsubscribing from one project won't affect another. |
|
|
269
|
-
| [System](docs/System.md) | `ping`, `info` | System operations. |
|
|
270
|
-
| [Obsolete](docs/Obsolete.md) | `unsubscribed_set`, `unsubscribed_check`, `unsubscribed_list` | Obsolete methods dealing with unsubscribes. We recommend using the more flexible [suppression](https://docs.unione.io/en/web-api-ref#suppression) methods instead. Note that you can't resubscribe an unsubscribed recipient using WebAPI, but you can send him an email with subscription link using email/subscribe method. |
|
|
360
|
+
- [Integration guides](https://docs.unione.io/en/integrations)
|
|
361
|
+
- AI coding agents: [llms.txt](https://docs.unione.io/llms.txt), the [MCP server](https://unione.io/en/agents), skills: `npx skills add https://github.com/unione-repo/unione-skills.git`
|
|
362
|
+
- [Support](https://cp.unione.io/en/support) · [Issues](https://github.com/unione-repo/unione-ruby-sdk/issues)
|
|
363
|
+
- Contributing: [AGENTS.md](AGENTS.md) — the SDK is generated from the OpenAPI spec.
|
data/docs/Domain.md
CHANGED
|
@@ -33,13 +33,16 @@ You should add then DNS records like this to all of the domains you want to conf
|
|
|
33
33
|
|
|
34
34
|
`@ IN TXT "unione-validate-hash=483bb362ebdbeedd755cfb1d4d661"`
|
|
35
35
|
|
|
36
|
-
`us._domainkey IN TXT "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo7"`
|
|
36
|
+
`us._domainkey IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo7"`
|
|
37
37
|
|
|
38
38
|
`@ IN TXT "v=spf1 include:spf.unione.io ~all"`
|
|
39
39
|
|
|
40
|
+
`_dmarc IN CNAME "example.com.dmarc.unione.io."`
|
|
41
|
+
|
|
40
42
|
- First record value should contain "verification-record" field returned by get-dns-records method "as is".
|
|
41
|
-
- Second record value should contain "dkim" field value after "p=". Mind the name of this record! "us._domainkey." prefix is important for DKIM selector of UniOne to work properly.
|
|
42
|
-
- Third record contains optional, but very recommended SPF entry. Probably you already have one, then just add append include:spf.unione.io right after v=spf1.
|
|
43
|
+
- Second record value should contain "dkim" field value after "v=DKIM1; k=rsa; p=". Mind the name of this record! "us._domainkey." prefix is important for DKIM selector of UniOne to work properly.
|
|
44
|
+
- Third record contains optional, but very recommended SPF entry. It is not returned by the method. Probably you already have one, then just add append include:spf.unione.io right after v=spf1.
|
|
45
|
+
- Fourth record is a CNAME of the "_dmarc" host to the "dmarc" field value: UniOne then publishes the DMARC policy of the domain. If the domain already has its own DMARC TXT record, keep it instead.
|
|
43
46
|
|
|
44
47
|
Syntax requirements of different DNS providers may differ:
|
|
45
48
|
- sometimes it's necesarry to put full domain name instead of @ sign.
|
|
@@ -196,6 +199,8 @@ Creates a sandbox domain for the account or returns the existing one.
|
|
|
196
199
|
|
|
197
200
|
Returns an account sandbox domain that is ready for use in the `from_email` field of [email/send](https://docs.unione.io/en/web-api-ref#email-send). If the account has no sandbox domain, UniOne creates one and returns it. Repeated calls return the same domain.
|
|
198
201
|
|
|
202
|
+
The sandbox domain is removed 31 days after it was created; the next call creates a new one. Up to 100 emails a day can be sent from it (the default limit of the account).
|
|
203
|
+
|
|
199
204
|
The method is available only with an account API key. Project API keys cannot create sandbox domains.
|
|
200
205
|
|
|
201
206
|
### Request
|
|
@@ -30,13 +30,16 @@ module UniOne
|
|
|
30
30
|
#
|
|
31
31
|
# `@ IN TXT "unione-validate-hash=483bb362ebdbeedd755cfb1d4d661"`
|
|
32
32
|
#
|
|
33
|
-
# `us._domainkey IN TXT "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo7"`
|
|
33
|
+
# `us._domainkey IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo7"`
|
|
34
34
|
#
|
|
35
35
|
# `@ IN TXT "v=spf1 include:spf.unione.io ~all"`
|
|
36
36
|
#
|
|
37
|
+
# `_dmarc IN CNAME "example.com.dmarc.unione.io."`
|
|
38
|
+
#
|
|
37
39
|
# - First record value should contain "verification-record" field returned by get-dns-records method "as is".
|
|
38
|
-
# - Second record value should contain "dkim" field value after "p=". Mind the name of this record! "us._domainkey." prefix is important for DKIM selector of UniOne to work properly.
|
|
39
|
-
# - Third record contains optional, but very recommended SPF entry. Probably you already have one, then just add append include:spf.unione.io right after v=spf1.
|
|
40
|
+
# - Second record value should contain "dkim" field value after "v=DKIM1; k=rsa; p=". Mind the name of this record! "us._domainkey." prefix is important for DKIM selector of UniOne to work properly.
|
|
41
|
+
# - Third record contains optional, but very recommended SPF entry. It is not returned by the method. Probably you already have one, then just add append include:spf.unione.io right after v=spf1.
|
|
42
|
+
# - Fourth record is a CNAME of the "_dmarc" host to the "dmarc" field value: UniOne then publishes the DMARC policy of the domain. If the domain already has its own DMARC TXT record, keep it instead.
|
|
40
43
|
#
|
|
41
44
|
# Syntax requirements of different DNS providers may differ:
|
|
42
45
|
# - sometimes it's necesarry to put full domain name instead of @ sign.
|
|
@@ -102,6 +105,9 @@ module UniOne
|
|
|
102
105
|
# [email/send](https://docs.unione.io/en/web-api-ref#email-send). If the account has no sandbox
|
|
103
106
|
# domain, UniOne creates one and returns it. Repeated calls return the same domain.
|
|
104
107
|
#
|
|
108
|
+
# The sandbox domain is removed 31 days after it was created; the next call creates a new one. Up to
|
|
109
|
+
# 100 emails a day can be sent from it (the default limit of the account).
|
|
110
|
+
#
|
|
105
111
|
# The method is available only with an account API key. Project API keys cannot create sandbox
|
|
106
112
|
# domains.
|
|
107
113
|
#
|
|
@@ -47,6 +47,31 @@ module UniOne
|
|
|
47
47
|
@metadata = metadata
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# A recipient from "email" or "Name <email>": the name goes to the to_name substitution (the
|
|
51
|
+
# name in the "To" header).
|
|
52
|
+
# @param address [String]
|
|
53
|
+
# @param substitutions [Hash{String => Object}, nil] more substitutions of the recipient
|
|
54
|
+
# @return [MessageObjectRecipientsItem]
|
|
55
|
+
def self.from_address(address, substitutions = nil)
|
|
56
|
+
email = address.to_s.strip
|
|
57
|
+
name = ''
|
|
58
|
+
match = email.match(/\A(.*)<([^>]+)>\z/m)
|
|
59
|
+
if match
|
|
60
|
+
email = match[2].strip
|
|
61
|
+
name = match[1].strip.gsub(/\A"+|"+\z/, '')
|
|
62
|
+
end
|
|
63
|
+
substitutions = { 'to_name' => name }.merge(substitutions || {}) unless name.empty?
|
|
64
|
+
new(email: email, substitutions: substitutions)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sets the metadata of the recipient (returned in the webhook events of its email).
|
|
68
|
+
# @param metadata [Hash{String => String}]
|
|
69
|
+
# @return [MessageObjectRecipientsItem] the recipient
|
|
70
|
+
def with_metadata(metadata)
|
|
71
|
+
self.metadata = metadata
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
50
75
|
# Builds the object from decoded JSON. Lenient: a missing field, nil or a value of another type
|
|
51
76
|
# gives nil (data that is not a Hash gives an empty object).
|
|
52
77
|
def self.from_h(data)
|
|
@@ -7,7 +7,7 @@ require 'uri'
|
|
|
7
7
|
module UniOne
|
|
8
8
|
module Client
|
|
9
9
|
# The SDK and its version in the "platform" query parameter of every request.
|
|
10
|
-
PLATFORM = 'ruby-sdk.1.
|
|
10
|
+
PLATFORM = 'ruby-sdk.1.2.3'
|
|
11
11
|
|
|
12
12
|
# Default transport (HTTP layer of ApiClient): Net::HTTP of the standard library.
|
|
13
13
|
#
|
|
@@ -36,7 +36,7 @@ module UniOne
|
|
|
36
36
|
# default ones of the same name
|
|
37
37
|
attr_accessor :headers
|
|
38
38
|
|
|
39
|
-
def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: 30, user_agent: 'unione-ruby-sdk/1.
|
|
39
|
+
def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: 30, user_agent: 'unione-ruby-sdk/1.2.3', platform: nil, headers: {})
|
|
40
40
|
@api_key = api_key
|
|
41
41
|
@base_url = base_url.sub(%r{/+\z}, '')
|
|
42
42
|
@timeout = timeout
|
|
@@ -28,6 +28,18 @@ module UniOne
|
|
|
28
28
|
@amp = amp
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# An HTML body, with an optional plain-text part.
|
|
32
|
+
# @return [BodyObject]
|
|
33
|
+
def self.html(html, plaintext = nil)
|
|
34
|
+
new(html: html, plaintext: plaintext)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A plain-text body.
|
|
38
|
+
# @return [BodyObject]
|
|
39
|
+
def self.text(plaintext)
|
|
40
|
+
new(plaintext: plaintext)
|
|
41
|
+
end
|
|
42
|
+
|
|
31
43
|
# Builds the object from decoded JSON. Lenient: a missing field, nil or a value of another type
|
|
32
44
|
# gives nil (data that is not a Hash gives an empty object).
|
|
33
45
|
def self.from_h(data)
|
data/lib/unione/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unione
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- UniOne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email:
|