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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 258d8d544bae08baa517a8699aa32fd9cdb3edf2d6625b8c3e2da0607e3dd3dc
4
- data.tar.gz: 0e6549783185c6b4ddba87aa75d607763538d2533dc4e441a808b34686af77fc
3
+ metadata.gz: fffb836d80d04f7e9a1fc9491451022bdd6f6349aceb18751b5bb8963c02db76
4
+ data.tar.gz: 1265d40f0d6c6da34a464f402eb54d12f02261459cd286d8980cc9cbae4907f4
5
5
  SHA512:
6
- metadata.gz: 9f0ed12282ee80f55a36327a56afebe22aa4cb49fe5fc61b8519fb90046c17f1a252d8bf2bfa6861452ea9f010930e471e2ddb292367ea7a7ab259ebc5d35237
7
- data.tar.gz: e74e10b17e8805170b810e16c3a2e1d158b6371898735cc20ee6844628d34e60069c1ec48fce401718445cf70a9428d48db5541bf90d6e6de85617cb711574c2
6
+ metadata.gz: 6f785497b63324e0398ea4d0c9e4c087b695c9c4bd59669722e2a8e340084837e480cbaf783b9b96b903f1ff44cf6cfcc689dd94da6ece8508233182e3c9decc
7
+ data.tar.gz: 4e4c44b15c5f38125a5c9206be4f55a6eac770ea1a594be4fd4f9bf80b1a665df09bda598101bed833c643179ab69acb9b2ae48f83054f9bd03b0e10df2cd384
data/README.md CHANGED
@@ -1,4 +1,161 @@
1
- # unitpost (RubyGems)
1
+ # unitpost
2
2
 
3
- Name reserved for **[Unitpost](https://unitpost.com)**. Placeholder gem; the
4
- official Unitpost Ruby SDK will be published under this name.
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