lettermint 0.1.0 → 0.2.1
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/CHANGELOG.md +42 -0
- data/README.md +141 -28
- data/docs/api.md +210 -0
- data/docs/sdk.md +510 -0
- data/lib/lettermint/client.rb +3 -29
- data/lib/lettermint/email_message.rb +11 -2
- data/lib/lettermint/errors.rb +75 -9
- data/lib/lettermint/http_client.rb +45 -24
- data/lib/lettermint/resources/base.rb +33 -0
- data/lib/lettermint/resources/domains.rb +66 -0
- data/lib/lettermint/resources/messages.rb +80 -0
- data/lib/lettermint/resources/projects.rb +103 -0
- data/lib/lettermint/resources/routes.rb +92 -0
- data/lib/lettermint/resources/stats.rb +23 -0
- data/lib/lettermint/resources/suppressions.rb +58 -0
- data/lib/lettermint/resources/team.rb +38 -0
- data/lib/lettermint/resources/webhooks.rb +122 -0
- data/lib/lettermint/sending_api.rb +75 -0
- data/lib/lettermint/team_api.rb +90 -0
- data/lib/lettermint/version.rb +1 -1
- data/lib/lettermint.rb +15 -0
- metadata +14 -2
- data/lettermint.gemspec +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8aad4ee2bba82edddc66ea5843cb3218394a4efca38bc82b9c2c7b784cd98f2f
|
|
4
|
+
data.tar.gz: 9bcdbaffbb553fe38d8e83afee6a1b8b2e6b231e7953fb9342e9c64d3b979455
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9b83c4a5fb73350768a2a49774e712ecd65d5b0cd5001475c3df4748ce5e8bca67e596a6105a809cf6d1512b13d394be8b40fe202f593f8579aa1a4efb71de2
|
|
7
|
+
data.tar.gz: 26a144391b3bf492267b82ef115f89c9e1527fd19dff8c604379cb35c871f52dfe340df2f20ee1950e55516aa25baf2b686d9433c0c3e1051afb476382f2af90
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-10-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `NotFoundError` for HTTP 404 responses (subclass of `HttpRequestError`)
|
|
13
|
+
- `code` and `details` readers on `HttpRequestError`, inherited by every subclass
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Error bodies are parsed from the three shapes used by the official Lettermint SDKs: `{"error":{"code","message","details"}}`, `{"error":"<string>"}`, and `{"message":"<string>"}`
|
|
18
|
+
- Error message resolves from `error.message`, then top-level `message`, then `"HTTP <status>"`; the per-status fallback literals (`Unknown client error`, `Validation error`, `Rate limit exceeded`) are removed
|
|
19
|
+
- `response_body` keeps non-JSON bodies as a String (for example an HTML 404 page from a proxy); empty bodies are nil
|
|
20
|
+
- A String `error` value is now exposed as `code` and no longer used as the message; a 400 with `{"error":"..."}` and no `message` reports `HTTP 400`
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 400 responses with an `{"error":{...}}` body produced a stringified Hash as the message
|
|
25
|
+
- 422 responses with an `{"error":{...}}` body set `error_type` to a Hash; it is now the `code` string
|
|
26
|
+
|
|
27
|
+
[0.2.1]: https://github.com/onetimesecret/lettermint-ruby/compare/v0.2.0...v0.2.1
|
|
28
|
+
|
|
29
|
+
## [0.2.0] - 2026-04-01
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `TeamAPI` for managing team resources (members, invitations)
|
|
34
|
+
- `SendingAPI` for domain and sender identity management
|
|
35
|
+
- Resource class infrastructure (`BaseResource`, `Resource`)
|
|
36
|
+
- Raw HTTP methods (`Client#get`, `#post`, `#put`, `#delete`) for arbitrary endpoint access
|
|
37
|
+
- `ConnectionError` for network-level failures
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- HTTP response body validation before JSON parsing
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- TypeError leak in webhook header parsing
|
|
46
|
+
- Blank recipient validation
|
|
47
|
+
|
|
48
|
+
[0.2.0]: https://github.com/onetimesecret/lettermint-ruby/compare/v0.1.0...v0.2.0
|
|
49
|
+
|
|
8
50
|
## [0.1.0] - 2026-02-18
|
|
9
51
|
|
|
10
52
|
### Added
|
data/README.md
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
[](https://www.ruby-lang.org)
|
|
5
5
|
[](https://github.com/onetimesecret/lettermint-ruby/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
An unofficial but orthodox Ruby SDK for the [Lettermint](https://lettermint.co) transactional email API. Based on the official [Python SDK](https://github.com/lettermint/lettermint-python).
|
|
8
|
+
|
|
9
|
+
The SDK provides two API clients:
|
|
10
|
+
|
|
11
|
+
- **SendingAPI** (aliased as `Client`) — Project-level email sending with `x-lettermint-token` authentication
|
|
12
|
+
- **TeamAPI** — Team-level management (domains, projects, webhooks, etc.) with `lm_team_*` token authentication
|
|
8
13
|
|
|
9
14
|
## Installation
|
|
10
15
|
|
|
@@ -27,7 +32,8 @@ gem install lettermint
|
|
|
27
32
|
```ruby
|
|
28
33
|
require 'lettermint'
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
# SendingAPI for email sending (aliased as Client for backward compatibility)
|
|
36
|
+
client = Lettermint::SendingAPI.new(api_token: 'your-project-token')
|
|
31
37
|
|
|
32
38
|
response = client.email
|
|
33
39
|
.from('sender@example.com')
|
|
@@ -40,6 +46,21 @@ response = client.email
|
|
|
40
46
|
puts response.message_id
|
|
41
47
|
```
|
|
42
48
|
|
|
49
|
+
### Team Management
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# TeamAPI for team-level operations (requires lm_team_* token)
|
|
53
|
+
team_api = Lettermint::TeamAPI.new(team_token: 'lm_team_your-token')
|
|
54
|
+
|
|
55
|
+
# List domains
|
|
56
|
+
domains = team_api.domains.list
|
|
57
|
+
puts domains['data'].map { |d| d['domain'] }
|
|
58
|
+
|
|
59
|
+
# Get team info
|
|
60
|
+
team = team_api.team.get
|
|
61
|
+
puts team['name']
|
|
62
|
+
```
|
|
63
|
+
|
|
43
64
|
## Email Options
|
|
44
65
|
|
|
45
66
|
### Multiple Recipients
|
|
@@ -223,12 +244,117 @@ webhooks using the `x-lettermint-delivery` header value as an idempotency key --
|
|
|
223
244
|
example, by recording processed delivery IDs in a database or cache and rejecting
|
|
224
245
|
duplicates.
|
|
225
246
|
|
|
247
|
+
## TeamAPI
|
|
248
|
+
|
|
249
|
+
The TeamAPI provides access to team-level management operations. It requires a team token (prefixed with `lm_team_`).
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
team_api = Lettermint::TeamAPI.new(team_token: 'lm_team_your-token')
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Available Resources
|
|
256
|
+
|
|
257
|
+
| Resource | Methods |
|
|
258
|
+
|----------|---------|
|
|
259
|
+
| `team` | `get`, `update`, `usage`, `members` |
|
|
260
|
+
| `domains` | `list`, `create`, `find`, `delete`, `verify_dns`, `verify_dns_record`, `update_projects` |
|
|
261
|
+
| `projects` | `list`, `create`, `find`, `update`, `delete`, `rotate_token`, `add_member`, `remove_member`, `update_members`, `routes` |
|
|
262
|
+
| `webhooks` | `list`, `create`, `find`, `update`, `delete`, `test`, `deliveries`, `delivery`, `regenerate_secret` |
|
|
263
|
+
| `messages` | `list`, `find`, `html`, `text`, `source`, `events` |
|
|
264
|
+
| `suppressions` | `list`, `create`, `delete` |
|
|
265
|
+
| `routes` | `list`, `create`, `find`, `update`, `delete`, `verify_inbound_domain` |
|
|
266
|
+
| `stats` | `get` |
|
|
267
|
+
|
|
268
|
+
### Examples
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# Domains
|
|
272
|
+
team_api.domains.create(domain: 'mail.example.com')
|
|
273
|
+
team_api.domains.verify_dns('domain-id')
|
|
274
|
+
|
|
275
|
+
# Projects
|
|
276
|
+
projects = team_api.projects.list(sort: '-created_at')
|
|
277
|
+
project = team_api.projects.create(name: 'My Project')
|
|
278
|
+
team_api.projects.rotate_token(project['id'])
|
|
279
|
+
|
|
280
|
+
# Webhooks
|
|
281
|
+
team_api.webhooks.create(
|
|
282
|
+
name: 'My Webhook',
|
|
283
|
+
url: 'https://example.com/webhook',
|
|
284
|
+
events: ['message.sent', 'message.delivered']
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Messages (search and retrieve)
|
|
288
|
+
messages = team_api.messages.list(status: 'delivered', tag: 'welcome')
|
|
289
|
+
html_body = team_api.messages.html('message-id')
|
|
290
|
+
|
|
291
|
+
# Suppressions
|
|
292
|
+
team_api.suppressions.create(
|
|
293
|
+
emails: ['bounce@example.com'],
|
|
294
|
+
reason: 'hard_bounce',
|
|
295
|
+
scope: 'project',
|
|
296
|
+
project_id: 'project-id'
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Stats
|
|
300
|
+
stats = team_api.stats.get(from: '2026-01-01', to: '2026-01-31')
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Pagination
|
|
304
|
+
|
|
305
|
+
All list methods support cursor-based pagination:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# First page
|
|
309
|
+
result = team_api.domains.list(page_size: 10)
|
|
310
|
+
|
|
311
|
+
# Next page
|
|
312
|
+
if result['meta']['next_cursor']
|
|
313
|
+
next_page = team_api.domains.list(
|
|
314
|
+
page_size: 10,
|
|
315
|
+
page_cursor: result['meta']['next_cursor']
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Sorting and Filtering
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# Sort by field (prefix with - for descending)
|
|
324
|
+
team_api.projects.list(sort: '-created_at')
|
|
325
|
+
team_api.domains.list(sort: 'domain')
|
|
326
|
+
|
|
327
|
+
# Filter by field
|
|
328
|
+
team_api.messages.list(status: 'delivered', from_email: 'noreply@example.com')
|
|
329
|
+
team_api.domains.list(status: 'verified')
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Raw HTTP Methods
|
|
333
|
+
|
|
334
|
+
The SendingAPI provides raw HTTP methods for accessing API endpoints not yet wrapped in typed methods:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
client = Lettermint::SendingAPI.new(api_token: 'your-api-token')
|
|
338
|
+
|
|
339
|
+
# GET with query params
|
|
340
|
+
response = client.get('/some-endpoint', params: { limit: 10 })
|
|
341
|
+
|
|
342
|
+
# POST with JSON body
|
|
343
|
+
response = client.post('/some-endpoint', data: { key: 'value' })
|
|
344
|
+
|
|
345
|
+
# PUT
|
|
346
|
+
response = client.put('/some-endpoint/123', data: { key: 'new-value' })
|
|
347
|
+
|
|
348
|
+
# DELETE
|
|
349
|
+
client.delete('/some-endpoint/123')
|
|
350
|
+
```
|
|
351
|
+
|
|
226
352
|
## Error Handling
|
|
227
353
|
|
|
228
354
|
```ruby
|
|
229
355
|
require 'lettermint'
|
|
230
356
|
|
|
231
|
-
client = Lettermint::
|
|
357
|
+
client = Lettermint::SendingAPI.new(api_token: 'your-api-token')
|
|
232
358
|
|
|
233
359
|
begin
|
|
234
360
|
response = client.email
|
|
@@ -249,12 +375,20 @@ rescue Lettermint::ValidationError => e
|
|
|
249
375
|
rescue Lettermint::ClientError => e
|
|
250
376
|
# 400 errors
|
|
251
377
|
puts "Client error: #{e.message}"
|
|
378
|
+
rescue Lettermint::NotFoundError => e
|
|
379
|
+
# 404 errors
|
|
380
|
+
puts "Not found: #{e.code}" # e.g. "RESOURCE_NOT_FOUND" (may be nil)
|
|
381
|
+
puts "Details: #{e.details}" # Hash, Array or nil
|
|
252
382
|
rescue Lettermint::TimeoutError => e
|
|
253
383
|
# Request timeout
|
|
254
384
|
puts "Timeout: #{e.message}"
|
|
385
|
+
rescue Lettermint::ConnectionError => e
|
|
386
|
+
# Network-level failures (DNS, connection refused, etc.)
|
|
387
|
+
puts "Connection error: #{e.message}"
|
|
388
|
+
puts "Original: #{e.original_exception}" if e.original_exception
|
|
255
389
|
rescue Lettermint::HttpRequestError => e
|
|
256
|
-
# Other HTTP errors
|
|
257
|
-
puts "HTTP error #{e.status_code}: #{e.message}"
|
|
390
|
+
# Other HTTP errors. `code` and `details` are available on every HTTP error.
|
|
391
|
+
puts "HTTP error #{e.status_code}: #{e.message} (#{e.code})"
|
|
258
392
|
end
|
|
259
393
|
```
|
|
260
394
|
|
|
@@ -282,37 +416,16 @@ Set defaults once at application boot (e.g., in a Rails initializer):
|
|
|
282
416
|
|
|
283
417
|
```ruby
|
|
284
418
|
Lettermint.configure do |config|
|
|
285
|
-
config.base_url = 'https://custom.api.com/v1'
|
|
286
419
|
config.timeout = 60
|
|
287
420
|
end
|
|
288
421
|
```
|
|
289
422
|
|
|
290
|
-
All clients created afterward inherit these defaults
|
|
291
|
-
|
|
292
|
-
```ruby
|
|
293
|
-
client = Lettermint::Client.new(api_token: 'your-api-token')
|
|
294
|
-
# Uses the global base_url and timeout
|
|
295
|
-
```
|
|
423
|
+
All clients created afterward inherit these defaults.
|
|
296
424
|
|
|
297
425
|
### Per-Client Overrides
|
|
298
426
|
|
|
299
|
-
Explicit keyword arguments take precedence over global configuration:
|
|
300
|
-
|
|
301
427
|
```ruby
|
|
302
|
-
client = Lettermint::
|
|
303
|
-
api_token: 'your-api-token',
|
|
304
|
-
base_url: 'https://other.api.com/v1',
|
|
305
|
-
timeout: 10
|
|
306
|
-
)
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Block Configuration
|
|
310
|
-
|
|
311
|
-
```ruby
|
|
312
|
-
client = Lettermint::Client.new(api_token: 'your-api-token') do |config|
|
|
313
|
-
config.base_url = 'https://custom.api.com/v1'
|
|
314
|
-
config.timeout = 60
|
|
315
|
-
end
|
|
428
|
+
client = Lettermint::SendingAPI.new(api_token: 'your-api-token', timeout: 10)
|
|
316
429
|
```
|
|
317
430
|
|
|
318
431
|
## Requirements
|
data/docs/api.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Lettermint Team API Reference
|
|
2
|
+
|
|
3
|
+
`https://api.lettermint.co/v1` · Auth: `Authorization: Bearer {team_token}`
|
|
4
|
+
|
|
5
|
+
Pagination: cursor-based via `page[size]` (default 30), `page[cursor]`. Sorting: comma-separated fields, `-` prefix for desc.
|
|
6
|
+
|
|
7
|
+
**Conventions:** `?` = nullable/optional, `*` = required, defaults in parens. List variants omit nested includes. `?include=` params noted per endpoint.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Enums
|
|
12
|
+
|
|
13
|
+
| Name | Values |
|
|
14
|
+
|------|--------|
|
|
15
|
+
| AttachmentDelivery | `inline` `url` |
|
|
16
|
+
| DnsRecordStatus | `active` `failed` `pending` |
|
|
17
|
+
| DomainStatus | `verified` `partially_verified` `pending_verification` `failed_verification` |
|
|
18
|
+
| InitialRoutes | `both` `transactional` `broadcast` |
|
|
19
|
+
| MessageEventType | `queued` `processed` `suppressed` `delivered` `soft_bounced` `hard_bounced` `spam_complaint` `failed` |
|
|
20
|
+
| MessageStatus | `pending` `queued` `suppressed` `processed` `delivered` `opened` `clicked` `soft_bounced` |
|
|
21
|
+
| MessageType | `inbound` `outbound` |
|
|
22
|
+
| Plan | `free` `starter` `growth` `pro` |
|
|
23
|
+
| RecordType | `TXT` `CNAME` `MX` |
|
|
24
|
+
| RouteType | `transactional` `broadcast` `inbound` |
|
|
25
|
+
| SuppressionReason | `spam_complaint` `hard_bounce` `unsubscribe` `manual` |
|
|
26
|
+
| SuppressionScope | `global` `team` `project` `route` |
|
|
27
|
+
| SuppressionType | `email` `domain` `extension` |
|
|
28
|
+
| VolumeTier | `300` `10k` `50k` `125k` `500k` `750k` `1M` `1.5M` |
|
|
29
|
+
| WebhookDeliveryStatus | `pending` `success` `failed` `client_error` `server_error` `timeout` |
|
|
30
|
+
| WebhookEvent | `message.{created,sent,delivered,hard_bounced,soft_bounced,spam_complaint,failed,suppressed}` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Generic
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `GET /ping` | Health check. Also accepts `X-Lettermint-Token: {project_token}` |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Team
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `GET /team` | `?include=features,featuresCount,featuresExists` |
|
|
47
|
+
| `PUT /team` | Body: `{name}` |
|
|
48
|
+
| `GET /team/usage` | Current period + up to 12 historical |
|
|
49
|
+
| `GET /team/members` | Paginated |
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
TeamData { id, name, plan: Plan, tier: VolumeTier, verified_at?, created_at,
|
|
53
|
+
features: string[], addons: [{type?, expires_at?}],
|
|
54
|
+
domains_count?, projects_count?, members_count? }
|
|
55
|
+
TeamMemberData { id, role?, joined_at?, user?: {id, name, email, avatar?} }
|
|
56
|
+
UsageDetail { current_period, historical_usage[]: {usage: int, last_incremented_at?, period_start, period_end} }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Domains
|
|
62
|
+
|
|
63
|
+
| | |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `GET /domains` | `filter[status]`, `filter[domain]` · `sort`: domain, created_at, status_changed_at |
|
|
66
|
+
| `POST /domains` | Body: `{domain*}` (max 255, regex validated) |
|
|
67
|
+
| `GET /domains/{id}` | `?include=dnsRecords,dnsRecordsCount,dnsRecordsExists` |
|
|
68
|
+
| `DELETE /domains/{id}` | |
|
|
69
|
+
| `POST /domains/{id}/dns-records/verify` | Verify all records |
|
|
70
|
+
| `POST /domains/{id}/dns-records/{recordId}/verify` | Verify single |
|
|
71
|
+
| `PUT /domains/{id}/projects` | Body: `{project_ids[]}` |
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
DomainData { id, domain, status_changed_at?, created_at, dns_records[], projects[] }
|
|
75
|
+
DomainListData { id, domain, status: DomainStatus, status_changed_at?, created_at }
|
|
76
|
+
DnsRecord { id, type: RecordType, hostname, fqdn, content,
|
|
77
|
+
status: DnsRecordStatus, verified_at?, last_checked_at? }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Projects
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `GET /projects` | `filter[search]` · `sort`: name, created_at |
|
|
87
|
+
| `POST /projects` | Body: `{name*, smtp_enabled? (false), initial_routes? (both)}`. Returns `api_token` |
|
|
88
|
+
| `GET /projects/{id}` | `?include=routes,domains,teamMembers,messageStats` (+ Count/Exists) |
|
|
89
|
+
| `PUT /projects/{id}` | Body: `{name?, smtp_enabled?, default_route_id?}` |
|
|
90
|
+
| `DELETE /projects/{id}` | |
|
|
91
|
+
| `POST /projects/{id}/rotate-token` | Returns `new_token` |
|
|
92
|
+
| `PUT /projects/{id}/members` | Body: `{team_member_ids[]}` |
|
|
93
|
+
| `POST /projects/{id}/members/{memberId}` | Add member |
|
|
94
|
+
| `DELETE /projects/{id}/members/{memberId}` | Remove member |
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
ProjectData { id, name, smtp_enabled: bool, default_route_id?,
|
|
98
|
+
token_generated_at?, token_last_used_at?, token_last_used_ip?,
|
|
99
|
+
created_at, updated_at,
|
|
100
|
+
routes[], domains[], team_members[],
|
|
101
|
+
last_28_days: {messages_transactional, messages_broadcast, messages_inbound: int, deliverability: float} }
|
|
102
|
+
ProjectListData { id, name, smtp_enabled, routes_count, domains_count,
|
|
103
|
+
team_members_count, last_28_days, created_at, updated_at }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Routes
|
|
109
|
+
|
|
110
|
+
| | |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `GET /projects/{id}/routes` | `filter[route_type]`, `filter[is_default]`, `filter[search]` · `sort`: name, slug, created_at |
|
|
113
|
+
| `POST /projects/{id}/routes` | Body: `{name*, route_type*, slug?}` |
|
|
114
|
+
| `GET /routes/{id}` | `?include=project,statistics` |
|
|
115
|
+
| `PUT /routes/{id}` | Body: `{name?, settings{track_opens, track_clicks, disable_hosted_unsubscribe}?, inbound_settings{inbound_domain, inbound_spam_threshold, attachment_delivery}?}` |
|
|
116
|
+
| `DELETE /routes/{id}` | |
|
|
117
|
+
| `POST /routes/{id}/verify-inbound-domain` | |
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
RouteData { id, project_id, slug, name, route_type: RouteType, is_default: bool,
|
|
121
|
+
created_at, updated_at,
|
|
122
|
+
inbound_address?, inbound_domain?, inbound_domain_verified_at?,
|
|
123
|
+
inbound_spam_threshold?, attachment_delivery?,
|
|
124
|
+
project?, webhooks_count?, suppressed_recipients_count?, statistics? }
|
|
125
|
+
RouteListData { id, slug, name, route_type, is_default,
|
|
126
|
+
webhooks_count, suppressed_recipients_count, created_at, updated_at }
|
|
127
|
+
RouteStatistic { date, sent_count, delivered_count, opened_count, clicked_count,
|
|
128
|
+
hard_bounce_count, spam_complaint_count, inbound_received_count }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Messages
|
|
134
|
+
|
|
135
|
+
| | |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `GET /messages` | `filter[type]`, `filter[status]`, `filter[route_id]`, `filter[domain_id]`, `filter[tag]`, `filter[from_email]`, `filter[subject]`, `filter[from_date]`, `filter[to_date]` · `sort`: type, status, from_email, subject, created_at, status_changed_at |
|
|
138
|
+
| `GET /messages/{id}` | Full detail |
|
|
139
|
+
| `GET /messages/{id}/events` | `sort`: timestamp, event |
|
|
140
|
+
| `GET /messages/{id}/source` | Returns `message/rfc822` |
|
|
141
|
+
| `GET /messages/{id}/html` | Returns `text/html` |
|
|
142
|
+
| `GET /messages/{id}/text` | Returns `text/plain` |
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
MessageData { id, type: MessageType, status: MessageStatus, status_changed_at?,
|
|
146
|
+
tag?, from_email, from_name?, reply_to?, subject?,
|
|
147
|
+
to?, cc?, bcc?: [{email, name?}],
|
|
148
|
+
attachments?: [{size (0), filename ("unknown"), content_id?, content_type ("application/octet-stream")}],
|
|
149
|
+
metadata?, route_id, created_at, spam_score?, spam_symbols?: [{name, score, options[], description?}] }
|
|
150
|
+
MessageListData { id, type, status, from_email, from_name?, subject?,
|
|
151
|
+
to?, cc?, bcc?, reply_to?, tag?, created_at }
|
|
152
|
+
MessageEvent { message_id, event: MessageEventType, metadata?, timestamp }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Webhooks
|
|
158
|
+
|
|
159
|
+
| | |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `GET /webhooks` | `filter[enabled]`, `filter[event]`, `filter[route_id]`, `filter[search]` · `sort`: name, url, created_at |
|
|
162
|
+
| `POST /webhooks` | Body: `{route_id*, name*, url*, events[]*, enabled? (true)}`. Returns `secret` (shown once) |
|
|
163
|
+
| `GET /webhooks/{id}` | |
|
|
164
|
+
| `PUT /webhooks/{id}` | Body: `{name?, url?, enabled?, events[]?}` |
|
|
165
|
+
| `DELETE /webhooks/{id}` | |
|
|
166
|
+
| `POST /webhooks/{id}/test` | Returns `delivery_id` |
|
|
167
|
+
| `POST /webhooks/{id}/regenerate-secret` | Returns new `secret` |
|
|
168
|
+
| `GET /webhooks/{id}/deliveries` | `filter[status]`, `filter[event_type]`, `filter[from_date]`, `filter[to_date]` |
|
|
169
|
+
| `GET /webhooks/{id}/deliveries/{deliveryId}` | Full payload/response |
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
WebhookData { id, route_id, name, url, events: WebhookEvent[], enabled: bool,
|
|
173
|
+
last_called_at?, created_at, updated_at, secret? }
|
|
174
|
+
WebhookDelivery { id, webhook_id, event_type: WebhookEvent, status: WebhookDeliveryStatus,
|
|
175
|
+
attempt_number, http_status_code?, duration_ms?,
|
|
176
|
+
payload[], response_body?, response_headers?, error_message?,
|
|
177
|
+
delivered_at?, timestamp }
|
|
178
|
+
WebhookDeliveryList { ...sans payload/response_body/response_headers/error_message, + created_at }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Suppressions
|
|
184
|
+
|
|
185
|
+
| | |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `GET /suppressions` | `filter[scope]`, `filter[route_id]`, `filter[project_id]`, `filter[value]`, `filter[reason]` · `sort`: value, created_at, reason |
|
|
188
|
+
| `POST /suppressions` | Body: `{reason*, scope*, email?, emails[]? (max 1000), route_id?, project_id?}` |
|
|
189
|
+
| `DELETE /suppressions/{id}` | |
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
SuppressedRecipient { id, type: SuppressionType, value, reason: SuppressionReason,
|
|
193
|
+
scope: SuppressionScope, project_id?, route_id?, created_at, updated_at }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Stats
|
|
199
|
+
|
|
200
|
+
| | |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `GET /stats` | `from*` (Y-m-d), `to*` (Y-m-d, max 90 days span), `project_id?` |
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
StatsData { from, to, totals: StatsTotals, daily: StatsDaily[] }
|
|
206
|
+
StatsTotals / StatsDaily { sent, delivered, hard_bounced, spam_complaints: int,
|
|
207
|
+
opened?, clicked?: int (null if tracking disabled),
|
|
208
|
+
inbound: {received: int},
|
|
209
|
+
transactional, broadcast: {sent, hard_bounced, spam_complaints: int} }
|
|
210
|
+
```
|