unitpost 0.0.0 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +160 -3
- data/lib/unitpost/client.rb +53 -0
- data/lib/unitpost/errors.rb +29 -0
- data/lib/unitpost/generated/operations.rb +1121 -0
- data/lib/unitpost/http_client.rb +160 -0
- data/lib/unitpost/resources.rb +464 -0
- data/lib/unitpost/version.rb +3 -1
- data/lib/unitpost/webhooks.rb +76 -0
- data/lib/unitpost.rb +10 -2
- metadata +72 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fffb836d80d04f7e9a1fc9491451022bdd6f6349aceb18751b5bb8963c02db76
|
|
4
|
+
data.tar.gz: 1265d40f0d6c6da34a464f402eb54d12f02261459cd286d8980cc9cbae4907f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f785497b63324e0398ea4d0c9e4c087b695c9c4bd59669722e2a8e340084837e480cbaf783b9b96b903f1ff44cf6cfcc689dd94da6ece8508233182e3c9decc
|
|
7
|
+
data.tar.gz: 4e4c44b15c5f38125a5c9206be4f55a6eac770ea1a594be4fd4f9bf80b1a665df09bda598101bed833c643179ab69acb9b2ae48f83054f9bd03b0e10df2cd384
|
data/README.md
CHANGED
|
@@ -1,4 +1,161 @@
|
|
|
1
|
-
# unitpost
|
|
1
|
+
# unitpost
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Official [Unitpost](https://www.unitpost.com) SDK for Ruby. Send email and manage
|
|
4
|
+
contacts, segments, campaigns, templates, domains, suppressions, and webhooks.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
gem install unitpost
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or add to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "unitpost"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Ruby 3.0+.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "unitpost"
|
|
24
|
+
|
|
25
|
+
unitpost = Unitpost::Client.new # reads UNITPOST_API_KEY (or pass api_key:)
|
|
26
|
+
|
|
27
|
+
result = unitpost.emails.send({
|
|
28
|
+
from: "Acme <you@yourdomain.com>", # bare address works too
|
|
29
|
+
to: "customer@example.com",
|
|
30
|
+
subject: "Hello",
|
|
31
|
+
html: "<p>It works!</p>"
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if result.error
|
|
35
|
+
warn "#{result.error.code}: #{result.error.message}"
|
|
36
|
+
else
|
|
37
|
+
puts result.data["id"]
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every method returns a `Unitpost::Result` with `#data` and `#error` (and a
|
|
42
|
+
`#success?` helper). An API-level failure populates `#error` (a
|
|
43
|
+
`Unitpost::Error`) instead of raising.
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Pass `api_key:` to `Unitpost::Client.new`, or set `UNITPOST_API_KEY`. Create a
|
|
48
|
+
key at [unitpost.com → Settings → API keys](https://www.unitpost.com).
|
|
49
|
+
|
|
50
|
+
## Client surface
|
|
51
|
+
|
|
52
|
+
| Accessor | Covers |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
|
|
55
|
+
| `contacts` | CRUD, import jobs |
|
|
56
|
+
| `contact_fields` | Custom field definitions |
|
|
57
|
+
| `segments` | Segments + membership |
|
|
58
|
+
| `topics` | Topics + contact topic prefs |
|
|
59
|
+
| `campaigns` | Draft, schedule, send, pause/resume |
|
|
60
|
+
| `templates` | Template CRUD |
|
|
61
|
+
| `brand_kits` | Brand kit |
|
|
62
|
+
| `domains` | Domains + verify |
|
|
63
|
+
| `webhooks` | Endpoints + `verify` |
|
|
64
|
+
| `api_keys` | API key management |
|
|
65
|
+
| `suppressions` | Suppression list |
|
|
66
|
+
| `usage` | Billing-period usage snapshot |
|
|
67
|
+
|
|
68
|
+
## Pagination
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
unitpost.contacts.list_all.each do |contact|
|
|
72
|
+
puts contact["email"]
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Suppressions
|
|
77
|
+
|
|
78
|
+
The suppression list holds addresses excluded from every send — single, batch,
|
|
79
|
+
and campaign, including cc/bcc — case-insensitively. Hard bounces and spam
|
|
80
|
+
complaints are added automatically; you can also add your own:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Suppress one address, or many in a single call (idempotent — re-adding is a no-op).
|
|
84
|
+
unitpost.suppressions.create({ email: "bounced@example.com", reason: "manual" })
|
|
85
|
+
unitpost.suppressions.create({ emails: ["a@example.com", "b@example.com"] })
|
|
86
|
+
|
|
87
|
+
# Look one up by id or email; list (optionally filter scope); un-suppress.
|
|
88
|
+
unitpost.suppressions.get("bounced@example.com")
|
|
89
|
+
unitpost.suppressions.list_all.each { |s| puts "#{s["email"]} #{s["reason"]}" }
|
|
90
|
+
unitpost.suppressions.delete("bounced@example.com")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`reason` accepts `manual`, `import`, or `api` — the engine-written `bounce` and
|
|
94
|
+
`complaint` reasons are read-only. Unitpost-wide (`platform`) entries are visible
|
|
95
|
+
but can't be removed via the API.
|
|
96
|
+
|
|
97
|
+
## Verifying webhooks
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
event = Unitpost.verify_webhook(
|
|
101
|
+
payload: raw_body, # the raw request body string
|
|
102
|
+
secret: ENV.fetch("UNITPOST_WEBHOOK_SECRET"),
|
|
103
|
+
headers: request.headers
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or from the client (Resend-style alias):
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
event = client.webhooks.verify(payload: raw_body, secret: secret, headers: request.headers)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Idempotency
|
|
114
|
+
|
|
115
|
+
`emails.send` and `emails.batch` are safe to retry by default — when you don't
|
|
116
|
+
pass `idempotency_key:`, the SDK generates one so an automatic retry can't
|
|
117
|
+
double-send. Pass your own to dedupe across separate calls:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
unitpost.emails.send({ ... }, idempotency_key: SecureRandom.uuid)
|
|
121
|
+
unitpost.emails.batch([ ... ], idempotency_key: SecureRandom.uuid)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> The server dedupes on `(workspace, idempotency key)` for 24h and replays the
|
|
125
|
+
> original result instead of re-sending (the email id for a send, the full list
|
|
126
|
+
> envelope for a batch).
|
|
127
|
+
|
|
128
|
+
## Retries
|
|
129
|
+
|
|
130
|
+
Transient failures are retried automatically: `429`, `5xx`, and transport errors
|
|
131
|
+
(timeouts / network) retry up to `max_retries` times (default `2`). A `429`/`503`
|
|
132
|
+
`Retry-After` is honored; otherwise exponential backoff with full jitter is used.
|
|
133
|
+
Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
|
|
134
|
+
safe to replay are retried: `GET`s and writes carrying an idempotency key (which
|
|
135
|
+
`emails.send` and `emails.batch` always do); any other keyless write is never
|
|
136
|
+
auto-retried.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Defaults shown; set max_retries: 0 to disable.
|
|
140
|
+
unitpost = Unitpost::Client.new(max_retries: 2, retry_base_delay: 0.5, max_retry_delay: 20.0)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Resources
|
|
144
|
+
|
|
145
|
+
| | |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| Docs + API reference | https://www.unitpost.com/docs |
|
|
148
|
+
| Product / SMTP / MCP guides | https://www.unitpost.com/guides |
|
|
149
|
+
| OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
|
|
150
|
+
| LLM index | https://www.unitpost.com/llms.txt |
|
|
151
|
+
| Dashboard / API keys | https://www.unitpost.com |
|
|
152
|
+
| Email components (`@unitpost/email`) | https://www.unitpost.com/components |
|
|
153
|
+
| Template gallery | https://www.unitpost.com/templates/gallery |
|
|
154
|
+
| Playground | https://www.unitpost.com/playground |
|
|
155
|
+
| Migration Assistant | https://www.unitpost.com/migration |
|
|
156
|
+
| MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
|
|
157
|
+
| Support | support@unitpost.com |
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "http_client"
|
|
4
|
+
require_relative "resources"
|
|
5
|
+
|
|
6
|
+
module Unitpost
|
|
7
|
+
# The Unitpost client. Construct once, reuse everywhere.
|
|
8
|
+
#
|
|
9
|
+
# require "unitpost"
|
|
10
|
+
# unitpost = Unitpost::Client.new # reads UNITPOST_API_KEY
|
|
11
|
+
# result = unitpost.emails.send(from: ..., to: ..., subject: ..., html: ...)
|
|
12
|
+
# if result.error
|
|
13
|
+
# warn "#{result.error.code}: #{result.error.message}"
|
|
14
|
+
# else
|
|
15
|
+
# puts result.data["id"]
|
|
16
|
+
# end
|
|
17
|
+
class Client
|
|
18
|
+
attr_reader :emails, :contacts, :contact_fields, :segments, :topics,
|
|
19
|
+
:campaigns, :templates, :brand_kits, :domains, :webhooks, :api_keys, :suppressions,
|
|
20
|
+
:usage
|
|
21
|
+
|
|
22
|
+
def initialize(api_key: nil, base_url: nil, timeout: 30, headers: {}, adapter: nil, stubs: nil,
|
|
23
|
+
max_retries: 2, retry_base_delay: 0.5, max_retry_delay: 20.0, sleeper: nil)
|
|
24
|
+
http = HttpClient.new(
|
|
25
|
+
api_key: api_key,
|
|
26
|
+
base_url: base_url,
|
|
27
|
+
timeout: timeout,
|
|
28
|
+
headers: headers,
|
|
29
|
+
adapter: adapter,
|
|
30
|
+
stubs: stubs,
|
|
31
|
+
max_retries: max_retries,
|
|
32
|
+
retry_base_delay: retry_base_delay,
|
|
33
|
+
max_retry_delay: max_retry_delay,
|
|
34
|
+
sleeper: sleeper
|
|
35
|
+
)
|
|
36
|
+
@emails = Resources::Emails.new(http)
|
|
37
|
+
@contacts = Resources::Contacts.new(http)
|
|
38
|
+
@contact_fields = Resources::ContactFields.new(http)
|
|
39
|
+
@segments = Resources::Segments.new(http)
|
|
40
|
+
@topics = Resources::Topics.new(http)
|
|
41
|
+
@campaigns = Resources::Campaigns.new(http)
|
|
42
|
+
@templates = Resources::Templates.new(http)
|
|
43
|
+
@brand_kits = Resources::BrandKits.new(http)
|
|
44
|
+
@domains = Resources::Domains.new(http)
|
|
45
|
+
@webhooks = Resources::Webhooks.new(http)
|
|
46
|
+
@api_keys = Resources::ApiKeys.new(http)
|
|
47
|
+
@suppressions = Resources::Suppressions.new(http)
|
|
48
|
+
# NOTE (pre-launch): `events` is intentionally absent while the
|
|
49
|
+
# custom-events surface is behind the launch gate.
|
|
50
|
+
@usage = Resources::Usage.new(http)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unitpost
|
|
4
|
+
# A structured API error. Returned on Result#error (not raised by default).
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :code, :status, :request_id, :details
|
|
7
|
+
|
|
8
|
+
# +details+ holds field-level validation errors (present on 422
|
|
9
|
+
# validation_error): an array of { "field" => "<dotted.path>",
|
|
10
|
+
# "message" => "<why>" } hashes. Empty array otherwise.
|
|
11
|
+
def initialize(code:, message:, status:, request_id: nil, details: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@code = code
|
|
14
|
+
@status = status
|
|
15
|
+
@request_id = request_id
|
|
16
|
+
@details = details || []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised by Unitpost.verify_webhook when a delivery can't be trusted.
|
|
21
|
+
class WebhookVerificationError < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Every SDK method returns one of these; exactly one of data/error is set.
|
|
24
|
+
Result = Struct.new(:data, :error, keyword_init: true) do
|
|
25
|
+
def success?
|
|
26
|
+
error.nil?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|