@productcraft/mail 0.2.4 → 1.0.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.
package/api.md ADDED
@@ -0,0 +1,3716 @@
1
+ # @productcraft/mail API reference
2
+
3
+ <!-- Generated by scripts/gen-sdk.mjs from Specs/mail.json. Do not edit. -->
4
+ <!-- snippet-preamble: import { Mail } from "@productcraft/mail"; const mail = new Mail(); -->
5
+
6
+ Every method of `Mail`, grouped by resource. Types are under the `Mail` namespace (`import type { Mail } from "@productcraft/mail"`). Every method takes per-request options last (`{ idempotencyKey, timeout, maxRetries, headers, signal, workspaceId }`).
7
+
8
+ ## brand
9
+
10
+ ### `mail.brand.reset(options?)`
11
+
12
+ `DELETE /v1/workspaces/{workspace_id}/brand` → `void`
13
+
14
+ Remove all brand customizations and revert to platform defaults.
15
+
16
+ | Status | Error class | Codes | When |
17
+ |---|---|---|---|
18
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
19
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
20
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
21
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
22
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
23
+
24
+ ```ts
25
+ await mail.brand.reset();
26
+ ```
27
+
28
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-brand)
29
+
30
+ ### `mail.brand.retrieve(options?)`
31
+
32
+ `GET /v1/workspaces/{workspace_id}/brand` → `Mail.Brand`
33
+
34
+ Get the resolved brand tokens for the workspace. Always returns a complete object, with platform defaults filling in any unset fields.
35
+
36
+ | Status | Error class | Codes | When |
37
+ |---|---|---|---|
38
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
39
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
40
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
41
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
42
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
43
+
44
+ ```ts
45
+ const brand = await mail.brand.retrieve();
46
+ ```
47
+
48
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-brand)
49
+
50
+ ### `mail.brand.update(params?, options?)`
51
+
52
+ `PUT /v1/workspaces/{workspace_id}/brand` → `Mail.Brand`
53
+
54
+ Replace the workspace brand. Any field omitted from the body is reset to the platform default for that field. To partially-update, send back what GET returned with the desired diff applied.
55
+
56
+ - Params: `Mail.BrandUpdateParams`
57
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
58
+
59
+ | Status | Error class | Codes | When |
60
+ |---|---|---|---|
61
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure on the brand payload. |
62
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
63
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
64
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
65
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
66
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
67
+
68
+ ```ts
69
+ const brand = await mail.brand.update({ background_color: "string" });
70
+ ```
71
+
72
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-brand)
73
+
74
+ ## broadcasts
75
+
76
+ ### `mail.broadcasts.audience(params?, options?)`
77
+
78
+ `GET /v1/workspaces/{workspace_id}/broadcasts/audience` → `Mail.BroadcastAudience`
79
+
80
+ Count a broadcast audience.
81
+
82
+ How many contacts a broadcast to this segment and/or topic would reach if it were sent now: members of the segment, subscribed to the topic, minus anyone unsubscribed or on the suppression list.
83
+
84
+ - Params: `Mail.BroadcastAudienceParams`
85
+
86
+ | Status | Error class | Codes | When |
87
+ |---|---|---|---|
88
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
89
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
90
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
91
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
92
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `AUDIENCE_REQUIRED`, `SEGMENT_NOT_FOUND`, `TOPIC_NOT_FOUND` | Neither given (`AUDIENCE_REQUIRED`), or one does not exist in this workspace (`SEGMENT_NOT_FOUND` / `TOPIC_NOT_FOUND`). |
93
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
94
+
95
+ ```ts
96
+ const broadcastAudience = await mail.broadcasts.audience();
97
+ ```
98
+
99
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcasts-audience)
100
+
101
+ ### `mail.broadcasts.cancel(broadcastId, options?)`
102
+
103
+ `POST /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}/cancel` → `Mail.Broadcast`
104
+
105
+ Cancel a scheduled or sending broadcast.
106
+
107
+ Recipients not sent yet are marked `canceled`; messages already handed off are not recalled.
108
+
109
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
110
+
111
+ | Status | Error class | Codes | When |
112
+ |---|---|---|---|
113
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
114
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
115
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
116
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
117
+ | 409 | `ConflictError` | `CONFLICT`, `BROADCAST_NOT_CANCELABLE`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Draft, sent or already canceled (`BROADCAST_NOT_CANCELABLE`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
118
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
119
+
120
+ ```ts
121
+ const broadcast = await mail.broadcasts.cancel("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
122
+ ```
123
+
124
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-broadcasts-broadcast-id-cancel)
125
+
126
+ ### `mail.broadcasts.create(params, options?)`
127
+
128
+ `POST /v1/workspaces/{workspace_id}/broadcasts` → `Mail.Broadcast`
129
+
130
+ Create a broadcast (draft).
131
+
132
+ Content is a saved template (`template_name`) or inline Handlebars (`html`, `subject`, optional `text`). The audience is a segment, a topic, or both. Nothing is sent until `POST .../send`.
133
+
134
+ - Params: `Mail.BroadcastCreateParams`
135
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
136
+
137
+ | Status | Error class | Codes | When |
138
+ |---|---|---|---|
139
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
140
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
141
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
142
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
143
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
144
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `SEGMENT_NOT_FOUND`, `TOPIC_NOT_FOUND`, `BROADCAST_CONTENT_CONFLICT` | `segment_id` or `topic_id` does not name one of this workspace's (`SEGMENT_NOT_FOUND` / `TOPIC_NOT_FOUND`), or both `template_name` and `html` were given (`BROADCAST_CONTENT_CONFLICT`). |
145
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
146
+
147
+ ```ts
148
+ const broadcast = await mail.broadcasts.create({ name: "October product update" });
149
+ ```
150
+
151
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-broadcasts)
152
+
153
+ ### `mail.broadcasts.delete(broadcastId, options?)`
154
+
155
+ `DELETE /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}` → `void`
156
+
157
+ Delete a broadcast.
158
+
159
+ Drafts, sent and canceled broadcasts, with their recipient list. The messages themselves stay in the message log.
160
+
161
+ | Status | Error class | Codes | When |
162
+ |---|---|---|---|
163
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
164
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
165
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
166
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
167
+ | 409 | `ConflictError` | `CONFLICT`, `BROADCAST_NOT_DELETABLE` | Scheduled or sending: cancel it first (`BROADCAST_NOT_DELETABLE`). |
168
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
169
+
170
+ ```ts
171
+ await mail.broadcasts.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
172
+ ```
173
+
174
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-broadcasts-broadcast-id)
175
+
176
+ ### `mail.broadcasts.duplicate(broadcastId, options?)`
177
+
178
+ `POST /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}/duplicate` → `Mail.Broadcast`
179
+
180
+ Duplicate a broadcast.
181
+
182
+ A new draft with the same content and audience.
183
+
184
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
185
+
186
+ | Status | Error class | Codes | When |
187
+ |---|---|---|---|
188
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
189
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
190
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
191
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
192
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
193
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
194
+
195
+ ```ts
196
+ const broadcast = await mail.broadcasts.duplicate("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
197
+ ```
198
+
199
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-broadcasts-broadcast-id-duplicate)
200
+
201
+ ### `mail.broadcasts.list(params?, options?)`
202
+
203
+ `GET /v1/workspaces/{workspace_id}/broadcasts` → `PagePromise<Mail.Broadcast>`
204
+
205
+ List broadcasts, newest first.
206
+
207
+ - Params: `Mail.BroadcastListParams`
208
+ - Paginated: `for await` iterates every item; `await` gives one page
209
+
210
+ | Status | Error class | Codes | When |
211
+ |---|---|---|---|
212
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
213
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
214
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
215
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
216
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
217
+
218
+ ```ts
219
+ for await (const broadcast of mail.broadcasts.list()) {
220
+ console.log(broadcast);
221
+ }
222
+ const page = await mail.broadcasts.list({}, { timeout: 5_000 });
223
+ ```
224
+
225
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcasts)
226
+
227
+ ### `mail.broadcasts.retrieve(broadcastId, options?)`
228
+
229
+ `GET /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}` → `Mail.Broadcast`
230
+
231
+ Get a broadcast.
232
+
233
+ Includes `counts` once sending has started: recipients, sent, delivered, bounced, complained, opened, clicked, unsubscribed.
234
+
235
+ | Status | Error class | Codes | When |
236
+ |---|---|---|---|
237
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
238
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
239
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
240
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
241
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
242
+
243
+ ```ts
244
+ const broadcast = await mail.broadcasts.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
245
+ ```
246
+
247
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcasts-broadcast-id)
248
+
249
+ ### `mail.broadcasts.send(broadcastId, params?, options?)`
250
+
251
+ `POST /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}/send` → `Mail.Broadcast`
252
+
253
+ Send or schedule a broadcast.
254
+
255
+ Now, or at `scheduled_at`. When sending starts the audience is frozen (segment ∩ topic subscribers − unsubscribed − suppression list) and the messages go out at the broadcast rate (default 120 per minute; separate from the transactional limit), on the marketing stream, each with a one-click unsubscribe link and `List-Unsubscribe` headers. The send survives restarts: it continues from the recipients not yet sent.
256
+
257
+ - Params: `Mail.BroadcastSendParams`
258
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
259
+
260
+ | Status | Error class | Codes | When |
261
+ |---|---|---|---|
262
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
263
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
264
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
265
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
266
+ | 409 | `ConflictError` | `CONFLICT`, `BROADCAST_NOT_DRAFT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Not a draft (`BROADCAST_NOT_DRAFT`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
267
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `UNSUBSCRIBE_LINK_REQUIRED`, `BROADCAST_NOT_READY`, `BROADCAST_AUDIENCE_EMPTY`, `INVALID_FROM_ADDRESS`, `from_domain_not_verified` | Not ready: `UNSUBSCRIBE_LINK_REQUIRED` (the body does not contain `{{{unsubscribe_url}}}`) or `BROADCAST_NOT_READY` (`details.problems`: no audience, no content, no subject, missing template, no From); `BROADCAST_AUDIENCE_EMPTY` (nobody to send to); `INVALID_FROM_ADDRESS`; or `from_domain_not_verified` (`details: { fqdn, reason }`). |
268
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
269
+
270
+ ```ts
271
+ const broadcast = await mail.broadcasts.send("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { scheduled_at: "2026-11-02T09:00:00Z" });
272
+ ```
273
+
274
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-broadcasts-broadcast-id-send)
275
+
276
+ ### `mail.broadcasts.update(broadcastId, params?, options?)`
277
+
278
+ `PATCH /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}` → `Mail.Broadcast`
279
+
280
+ Update a draft broadcast.
281
+
282
+ Only drafts can change; null clears an optional field.
283
+
284
+ - Params: `Mail.BroadcastUpdateParams`
285
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
286
+
287
+ | Status | Error class | Codes | When |
288
+ |---|---|---|---|
289
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
290
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
291
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
292
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
293
+ | 409 | `ConflictError` | `CONFLICT`, `BROADCAST_NOT_DRAFT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Not a draft any more (`BROADCAST_NOT_DRAFT`, `details.status`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
294
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `SEGMENT_NOT_FOUND`, `TOPIC_NOT_FOUND`, `BROADCAST_CONTENT_CONFLICT` | `segment_id` or `topic_id` does not name one of this workspace's (`SEGMENT_NOT_FOUND` / `TOPIC_NOT_FOUND`), or both `template_name` and `html` were given (`BROADCAST_CONTENT_CONFLICT`). |
295
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
296
+
297
+ ```ts
298
+ const broadcast = await mail.broadcasts.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { from: "string" });
299
+ ```
300
+
301
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-broadcasts-broadcast-id)
302
+
303
+ ## broadcasts.clickedLinks
304
+
305
+ ### `mail.broadcasts.clickedLinks.list(broadcastId, options?)`
306
+
307
+ `GET /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}/clicked-links` → `Mail.ClickedLinks`
308
+
309
+ Links clicked in a broadcast.
310
+
311
+ Most clicked first. Empty until click tracking is enabled.
312
+
313
+ | Status | Error class | Codes | When |
314
+ |---|---|---|---|
315
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
316
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
317
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
318
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
319
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
320
+
321
+ ```ts
322
+ const clickedLinks = await mail.broadcasts.clickedLinks.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
323
+ ```
324
+
325
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcasts-broadcast-id-clicked-links)
326
+
327
+ ## broadcasts.recipients
328
+
329
+ ### `mail.broadcasts.recipients.list(broadcastId, params?, options?)`
330
+
331
+ `GET /v1/workspaces/{workspace_id}/broadcasts/{broadcast_id}/recipients` → `PagePromise<Mail.BroadcastRecipient>`
332
+
333
+ List a broadcast's recipients.
334
+
335
+ In send order, with each message status.
336
+
337
+ - Params: `Mail.BroadcastRecipientListParams`
338
+ - Paginated: `for await` iterates every item; `await` gives one page
339
+
340
+ | Status | Error class | Codes | When |
341
+ |---|---|---|---|
342
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
343
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
344
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
345
+ | 404 | `NotFoundError` | `NOT_FOUND`, `BROADCAST_NOT_FOUND` | No such broadcast (`BROADCAST_NOT_FOUND`). |
346
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
347
+
348
+ ```ts
349
+ for await (const recipient of mail.broadcasts.recipients.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b")) {
350
+ console.log(recipient);
351
+ }
352
+ const page = await mail.broadcasts.recipients.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {}, { timeout: 5_000 });
353
+ ```
354
+
355
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcasts-broadcast-id-recipients)
356
+
357
+ ## broadcasts.settings
358
+
359
+ ### `mail.broadcasts.settings.retrieve(options?)`
360
+
361
+ `GET /v1/workspaces/{workspace_id}/broadcast-settings` → `Mail.BroadcastSettings`
362
+
363
+ Get the broadcast settings.
364
+
365
+ The messages-per-minute pace broadcasts go out at.
366
+
367
+ | Status | Error class | Codes | When |
368
+ |---|---|---|---|
369
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
370
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
371
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
372
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
373
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
374
+
375
+ ```ts
376
+ const broadcastSettings = await mail.broadcasts.settings.retrieve();
377
+ ```
378
+
379
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-broadcast-settings)
380
+
381
+ ### `mail.broadcasts.settings.update(params, options?)`
382
+
383
+ `PUT /v1/workspaces/{workspace_id}/broadcast-settings` → `Mail.BroadcastSettings`
384
+
385
+ Update the broadcast settings.
386
+
387
+ Set the workspace campaign pace (1 to 300 messages per minute; null for the default, 120). It never affects transactional sends, which have their own limit.
388
+
389
+ - Params: `Mail.BroadcastSettingsUpdateParams`
390
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
391
+
392
+ | Status | Error class | Codes | When |
393
+ |---|---|---|---|
394
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
395
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
396
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
397
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
398
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
399
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
400
+
401
+ ```ts
402
+ const broadcastSettings = await mail.broadcasts.settings.update({ rate_per_minute: 1 });
403
+ ```
404
+
405
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-broadcast-settings)
406
+
407
+ ## contacts
408
+
409
+ ### `mail.contacts.create(params, options?)`
410
+
411
+ `POST /v1/workspaces/{workspace_id}/contacts` → `Mail.Contact`
412
+
413
+ Create a contact.
414
+
415
+ Emits `contact.created`. Without `consent`, the contact receives only topics it is explicitly opted in to.
416
+
417
+ - Params: `Mail.ContactCreateParams`
418
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
419
+
420
+ | Status | Error class | Codes | When |
421
+ |---|---|---|---|
422
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
423
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
424
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
425
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TOPIC_NOT_FOUND` | A `topics[].id` is not a topic of this workspace (`TOPIC_NOT_FOUND`). |
426
+ | 409 | `ConflictError` | `CONFLICT`, `CONTACT_EXISTS`, `CONTACT_LIMIT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | A contact with this email exists (`CONTACT_EXISTS`), the workspace has 100,000 contacts (`CONTACT_LIMIT`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
427
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `UNKNOWN_CONTACT_PROPERTY`, `INVALID_CONTACT_PROPERTY_VALUE` | A `properties` key is not a defined property (`UNKNOWN_CONTACT_PROPERTY`) or its value has the wrong type (`INVALID_CONTACT_PROPERTY_VALUE`). |
428
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
429
+
430
+ ```ts
431
+ const contact = await mail.contacts.create({ email: "ada@example.com" });
432
+ ```
433
+
434
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-contacts)
435
+
436
+ ### `mail.contacts.delete(contact, options?)`
437
+
438
+ `DELETE /v1/workspaces/{workspace_id}/contacts/{contact}` → `void`
439
+
440
+ Delete a contact.
441
+
442
+ Removes the contact, its properties and its topic choices. Emits `contact.deleted`. To stop mailing someone, unsubscribe them instead: a deleted contact that later receives a topic send is refused (`no_consent`), but its opt-out is forgotten.
443
+
444
+ | Status | Error class | Codes | When |
445
+ |---|---|---|---|
446
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
447
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
448
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
449
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`). |
450
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
451
+
452
+ ```ts
453
+ await mail.contacts.delete("ada@example.com");
454
+ ```
455
+
456
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-contacts-contact)
457
+
458
+ ### `mail.contacts.list(params?, options?)`
459
+
460
+ `GET /v1/workspaces/{workspace_id}/contacts` → `PagePromise<Mail.Contact>`
461
+
462
+ List contacts, newest first.
463
+
464
+ - Params: `Mail.ContactListParams`
465
+ - Paginated: `for await` iterates every item; `await` gives one page
466
+
467
+ | Status | Error class | Codes | When |
468
+ |---|---|---|---|
469
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
470
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
471
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
472
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
473
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
474
+
475
+ ```ts
476
+ for await (const contact of mail.contacts.list()) {
477
+ console.log(contact);
478
+ }
479
+ const page = await mail.contacts.list({}, { timeout: 5_000 });
480
+ ```
481
+
482
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts)
483
+
484
+ ### `mail.contacts.retrieve(contact, options?)`
485
+
486
+ `GET /v1/workspaces/{workspace_id}/contacts/{contact}` → `Mail.Contact`
487
+
488
+ Get a contact by id or email.
489
+
490
+ | Status | Error class | Codes | When |
491
+ |---|---|---|---|
492
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
493
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
494
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
495
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`). |
496
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
497
+
498
+ ```ts
499
+ const contact = await mail.contacts.retrieve("ada@example.com");
500
+ ```
501
+
502
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts-contact)
503
+
504
+ ### `mail.contacts.update(contact, params?, options?)`
505
+
506
+ `PATCH /v1/workspaces/{workspace_id}/contacts/{contact}` → `Mail.Contact`
507
+
508
+ Update a contact.
509
+
510
+ The email cannot change (delete and re-create). Emits `contact.updated` when something changed.
511
+
512
+ - Params: `Mail.ContactUpdateParams`
513
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
514
+
515
+ | Status | Error class | Codes | When |
516
+ |---|---|---|---|
517
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
518
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
519
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
520
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`). |
521
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
522
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `UNKNOWN_CONTACT_PROPERTY`, `INVALID_CONTACT_PROPERTY_VALUE` | `UNKNOWN_CONTACT_PROPERTY` or `INVALID_CONTACT_PROPERTY_VALUE`. |
523
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
524
+
525
+ ```ts
526
+ const contact = await mail.contacts.update("ada@example.com", { consent: { consented_at: "2026-10-24T09:00:00Z", source: "signup-form-2026" } });
527
+ ```
528
+
529
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-contacts-contact)
530
+
531
+ ## contacts.imports
532
+
533
+ ### `mail.contacts.imports.create(params, options?)`
534
+
535
+ `POST /v1/workspaces/{workspace_id}/contacts/imports` → `Mail.ContactImport`
536
+
537
+ Import contacts from a CSV file.
538
+
539
+ Queues the file and returns the import (`status: queued`); poll `GET .../contacts/imports/{id}` for progress. At most 10 MB and 100,000 rows, two imports running per workspace. Rows that assert consent (`consent` true or a `consented_at` date) are recorded with it and opted in to `topic_ids`; every other row starts without consent. An import emits no per-contact webhook events.
540
+
541
+ - Params: `Mail.ContactImportCreateParams`
542
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
543
+
544
+ | Status | Error class | Codes | When |
545
+ |---|---|---|---|
546
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
547
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
548
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
549
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TOPIC_NOT_FOUND` | A `topic_ids` entry is not a topic of this workspace (`TOPIC_NOT_FOUND`). |
550
+ | 409 | `ConflictError` | `CONFLICT`, `IMPORT_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Two imports are already running (`IMPORT_IN_PROGRESS`), or `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different body or route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
551
+ | 413 | `APIError` | `PAYLOAD_TOO_LARGE`, `IMPORT_TOO_LARGE` | The CSV is larger than 10 MB (`IMPORT_TOO_LARGE`). |
552
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `INVALID_CSV` | Empty file, or no header line with an `email` column (`INVALID_CSV`). |
553
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
554
+
555
+ ```ts
556
+ const contactImport = await mail.contacts.imports.create({
557
+ csv: "email,first_name,consent,consented_at\nada@example.com,Ada,true,2026-10-01T09:00:00Z\n",
558
+ on_conflict: "upsert",
559
+ });
560
+ ```
561
+
562
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-contacts-imports)
563
+
564
+ ### `mail.contacts.imports.list(params?, options?)`
565
+
566
+ `GET /v1/workspaces/{workspace_id}/contacts/imports` → `PagePromise<Mail.ContactImport>`
567
+
568
+ List contact imports, newest first.
569
+
570
+ - Params: `Mail.ContactImportListParams`
571
+ - Paginated: `for await` iterates every item; `await` gives one page
572
+
573
+ | Status | Error class | Codes | When |
574
+ |---|---|---|---|
575
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
576
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
577
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
578
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
579
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
580
+
581
+ ```ts
582
+ for await (const importItem of mail.contacts.imports.list()) {
583
+ console.log(importItem);
584
+ }
585
+ const page = await mail.contacts.imports.list({}, { timeout: 5_000 });
586
+ ```
587
+
588
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts-imports)
589
+
590
+ ### `mail.contacts.imports.retrieve(importId, options?)`
591
+
592
+ `GET /v1/workspaces/{workspace_id}/contacts/imports/{import_id}` → `Mail.ContactImport`
593
+
594
+ Get a contact import and its progress.
595
+
596
+ | Status | Error class | Codes | When |
597
+ |---|---|---|---|
598
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
599
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
600
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
601
+ | 404 | `NotFoundError` | `NOT_FOUND`, `IMPORT_NOT_FOUND` | No such import in this workspace (`IMPORT_NOT_FOUND`). |
602
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
603
+
604
+ ```ts
605
+ const contactImport = await mail.contacts.imports.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
606
+ ```
607
+
608
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts-imports-import-id)
609
+
610
+ ## contacts.properties
611
+
612
+ ### `mail.contacts.properties.create(params, options?)`
613
+
614
+ `POST /v1/workspaces/{workspace_id}/contact-properties` → `Mail.ContactProperty`
615
+
616
+ Define a contact property.
617
+
618
+ At most 50 per workspace.
619
+
620
+ - Params: `Mail.ContactPropertyCreateParams`
621
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
622
+
623
+ | Status | Error class | Codes | When |
624
+ |---|---|---|---|
625
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
626
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
627
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
628
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
629
+ | 409 | `ConflictError` | `CONFLICT`, `CONTACT_PROPERTY_EXISTS`, `CONTACT_PROPERTY_LIMIT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | The key exists (`CONTACT_PROPERTY_EXISTS`), the workspace has 50 properties (`CONTACT_PROPERTY_LIMIT`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
630
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `RESERVED_PROPERTY_KEY`, `INVALID_CONTACT_PROPERTY_VALUE` | The key is a contact field (`RESERVED_PROPERTY_KEY`) or the fallback does not match the type (`INVALID_CONTACT_PROPERTY_VALUE`). |
631
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
632
+
633
+ ```ts
634
+ const contactProperty = await mail.contacts.properties.create({ key: "company", type: "string" });
635
+ ```
636
+
637
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-contact-properties)
638
+
639
+ ### `mail.contacts.properties.delete(propertyId, options?)`
640
+
641
+ `DELETE /v1/workspaces/{workspace_id}/contact-properties/{property_id}` → `void`
642
+
643
+ Delete a contact property.
644
+
645
+ Values already stored on contacts stay, but are no longer validated; define the key again to use it.
646
+
647
+ | Status | Error class | Codes | When |
648
+ |---|---|---|---|
649
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
650
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
651
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
652
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_PROPERTY_NOT_FOUND` | No such property (`CONTACT_PROPERTY_NOT_FOUND`). |
653
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
654
+
655
+ ```ts
656
+ await mail.contacts.properties.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
657
+ ```
658
+
659
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-contact-properties-property-id)
660
+
661
+ ### `mail.contacts.properties.list(params?, options?)`
662
+
663
+ `GET /v1/workspaces/{workspace_id}/contact-properties` → `PagePromise<Mail.ContactProperty>`
664
+
665
+ List contact properties, newest first.
666
+
667
+ - Params: `Mail.ContactPropertyListParams`
668
+ - Paginated: `for await` iterates every item; `await` gives one page
669
+
670
+ | Status | Error class | Codes | When |
671
+ |---|---|---|---|
672
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
673
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
674
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
675
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
676
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
677
+
678
+ ```ts
679
+ for await (const property of mail.contacts.properties.list()) {
680
+ console.log(property);
681
+ }
682
+ const page = await mail.contacts.properties.list({}, { timeout: 5_000 });
683
+ ```
684
+
685
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contact-properties)
686
+
687
+ ### `mail.contacts.properties.retrieve(propertyId, options?)`
688
+
689
+ `GET /v1/workspaces/{workspace_id}/contact-properties/{property_id}` → `Mail.ContactProperty`
690
+
691
+ Get a contact property.
692
+
693
+ | Status | Error class | Codes | When |
694
+ |---|---|---|---|
695
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
696
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
697
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
698
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_PROPERTY_NOT_FOUND` | No such property (`CONTACT_PROPERTY_NOT_FOUND`). |
699
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
700
+
701
+ ```ts
702
+ const contactProperty = await mail.contacts.properties.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
703
+ ```
704
+
705
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contact-properties-property-id)
706
+
707
+ ### `mail.contacts.properties.update(propertyId, params?, options?)`
708
+
709
+ `PATCH /v1/workspaces/{workspace_id}/contact-properties/{property_id}` → `Mail.ContactProperty`
710
+
711
+ Change a contact property's fallback value.
712
+
713
+ The key and type are fixed once created.
714
+
715
+ - Params: `Mail.ContactPropertyUpdateParams`
716
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
717
+
718
+ | Status | Error class | Codes | When |
719
+ |---|---|---|---|
720
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
721
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
722
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
723
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_PROPERTY_NOT_FOUND` | No such property (`CONTACT_PROPERTY_NOT_FOUND`). |
724
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
725
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `INVALID_CONTACT_PROPERTY_VALUE` | The fallback does not match the type (`INVALID_CONTACT_PROPERTY_VALUE`). |
726
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
727
+
728
+ ```ts
729
+ const contactProperty = await mail.contacts.properties.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { fallback_value: "string" });
730
+ ```
731
+
732
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-contact-properties-property-id)
733
+
734
+ ## contacts.segments
735
+
736
+ ### `mail.contacts.segments.add(contact, segmentId, options?)`
737
+
738
+ `POST /v1/workspaces/{workspace_id}/contacts/{contact}/segments/{segment_id}` → `Mail.SegmentMembership`
739
+
740
+ Add a contact to a static segment.
741
+
742
+ Adds the contact. Adding a contact that is already a member changes nothing and answers the same way, with `added_at` from the first time.
743
+
744
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
745
+
746
+ | Status | Error class | Codes | When |
747
+ |---|---|---|---|
748
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
749
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
750
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
751
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`) or segment (`SEGMENT_NOT_FOUND`). |
752
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
753
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `SEGMENT_RULE_BASED` | The segment is rule-based: its members follow the rule (`SEGMENT_RULE_BASED`). |
754
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
755
+
756
+ ```ts
757
+ const segmentMembership = await mail.contacts.segments.add("ada@example.com", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
758
+ ```
759
+
760
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-contacts-contact-segments-segment-id)
761
+
762
+ ### `mail.contacts.segments.delete(contact, segmentId, options?)`
763
+
764
+ `DELETE /v1/workspaces/{workspace_id}/contacts/{contact}/segments/{segment_id}` → `void`
765
+
766
+ Remove a contact from a static segment.
767
+
768
+ 204 whether or not it was a member.
769
+
770
+ | Status | Error class | Codes | When |
771
+ |---|---|---|---|
772
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
773
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
774
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
775
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`) or segment (`SEGMENT_NOT_FOUND`). |
776
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `SEGMENT_RULE_BASED` | The segment is rule-based (`SEGMENT_RULE_BASED`). |
777
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
778
+
779
+ ```ts
780
+ await mail.contacts.segments.delete("ada@example.com", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
781
+ ```
782
+
783
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-contacts-contact-segments-segment-id)
784
+
785
+ ### `mail.contacts.segments.list(contact, options?)`
786
+
787
+ `GET /v1/workspaces/{workspace_id}/contacts/{contact}/segments` → `Mail.ContactSegments`
788
+
789
+ List a contact's segments.
790
+
791
+ Static segments it was added to and rule segments it matches now.
792
+
793
+ | Status | Error class | Codes | When |
794
+ |---|---|---|---|
795
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
796
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
797
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
798
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`). |
799
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
800
+
801
+ ```ts
802
+ const contactSegments = await mail.contacts.segments.list("ada@example.com");
803
+ ```
804
+
805
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts-contact-segments)
806
+
807
+ ## contacts.topics
808
+
809
+ ### `mail.contacts.topics.list(contact, options?)`
810
+
811
+ `GET /v1/workspaces/{workspace_id}/contacts/{contact}/topics` → `Mail.ContactTopics`
812
+
813
+ List a contact's topic subscriptions.
814
+
815
+ Every topic of the workspace, with the contact's own choice and whether a topic send would reach it now (and why).
816
+
817
+ | Status | Error class | Codes | When |
818
+ |---|---|---|---|
819
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
820
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
821
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
822
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`). |
823
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
824
+
825
+ ```ts
826
+ const contactTopics = await mail.contacts.topics.list("ada@example.com");
827
+ ```
828
+
829
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-contacts-contact-topics)
830
+
831
+ ### `mail.contacts.topics.update(contact, params, options?)`
832
+
833
+ `PATCH /v1/workspaces/{workspace_id}/contacts/{contact}/topics` → `Mail.ContactTopics`
834
+
835
+ Opt a contact in to or out of topics.
836
+
837
+ Records the contact's choice per topic (an explicit `opt_in` is consent for that topic). A global unsubscribe still wins: clear it with `PATCH .../contacts/{contact}` `unsubscribed: false`. Emits `contact.updated`.
838
+
839
+ - Params: `Mail.ContactTopicUpdateParams`
840
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
841
+
842
+ | Status | Error class | Codes | When |
843
+ |---|---|---|---|
844
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
845
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
846
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
847
+ | 404 | `NotFoundError` | `NOT_FOUND`, `CONTACT_NOT_FOUND`, `TOPIC_NOT_FOUND` | No such contact (`CONTACT_NOT_FOUND`) or topic (`TOPIC_NOT_FOUND`). |
848
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
849
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
850
+
851
+ ```ts
852
+ const contactTopics = await mail.contacts.topics.update("ada@example.com", { topics: [{ id: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b", subscription: "opt_in" }] });
853
+ ```
854
+
855
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-contacts-contact-topics)
856
+
857
+ ## deliverability
858
+
859
+ ### `mail.deliverability.retrieve(params?, options?)`
860
+
861
+ `GET /v1/workspaces/{workspace_id}/deliverability` → `Mail.Deliverability`
862
+
863
+ Deliverability verdict for the workspace over a lookback window. `unconfirmed` means mail was handed off and nothing came back either way — it is not a synonym for healthy.
864
+
865
+ - Params: `Mail.DeliverabilityRetrieveParams`
866
+
867
+ | Status | Error class | Codes | When |
868
+ |---|---|---|---|
869
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | window_days out of range. |
870
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Not authenticated. |
871
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Missing mail.message.read, or the mail service is not enabled. |
872
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
873
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
874
+
875
+ ```ts
876
+ const deliverability = await mail.deliverability.retrieve();
877
+ ```
878
+
879
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-deliverability)
880
+
881
+ ## domains
882
+
883
+ ### `mail.domains.create(params?, options?)`
884
+
885
+ `POST /v1/workspaces/{workspace_id}/domains` → `Mail.Domain`
886
+
887
+ Register a domain for a workspace.
888
+
889
+ Accepts Resend's create-domain body as well as ours: `name` for `fqdn`, `capabilities` for `intent`, `custom_return_path` for the MAIL FROM label, and `region`, `tls`, `open_tracking`, `click_tracking`, `tracking_subdomain`. Defaults to sending only (`intent: send`), as Resend does. Options we do not offer yet are refused with 422 rather than ignored.
890
+
891
+ - Params: `Mail.DomainCreateParams`
892
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
893
+
894
+ | Status | Error class | Codes | When |
895
+ |---|---|---|---|
896
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `INVALID_CAPABILITIES`, `MAIL_FROM_NOT_SENDING` | Validation failure on the request body: no `fqdn`/`name`, `fqdn` and `name` disagree, `intent` and `capabilities` disagree or enable nothing (`INVALID_CAPABILITIES`), or `custom_return_path` on a receive-only domain (`MAIL_FROM_NOT_SENDING`). |
897
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
898
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
899
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
900
+ | 409 | `ConflictError` | `CONFLICT`, `MAIL_FROM_HOST_IN_USE`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Domain already registered (globally unique); `MAIL_FROM_HOST_IN_USE`: the `custom_return_path` host already has records that are not ours (listed in `details.found`); or `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different body or route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
901
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `REGION_NOT_AVAILABLE`, `TLS_ENFORCED_NOT_AVAILABLE`, `TRACKING_NOT_AVAILABLE` | `REGION_NOT_AVAILABLE`: `region` is not the region this deployment sends from (`details.available`); `TLS_ENFORCED_NOT_AVAILABLE`: `tls: enforced`; `TRACKING_NOT_AVAILABLE`: open/click tracking or a tracking subdomain. |
902
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
903
+
904
+ ```ts
905
+ const domain = await mail.domains.create({ capabilities: { receiving: "enabled", sending: "enabled" } });
906
+ ```
907
+
908
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains)
909
+
910
+ ### `mail.domains.delete(domainId, options?)`
911
+
912
+ `DELETE /v1/workspaces/{workspace_id}/domains/{domain_id}` → `void`
913
+
914
+ Delete a domain (must have no active mailboxes left).
915
+
916
+ Permanently deletes the domain, its DNS record set and its Amazon SES sending identity. Mailboxes you already deleted on it are erased with it, together with the mail they still held and its stored files; this cannot be undone. Refused with 409 `DOMAIN_HAS_MAILBOXES` while any mailbox on it is active.
917
+
918
+ | Status | Error class | Codes | When |
919
+ |---|---|---|---|
920
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
921
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
922
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
923
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
924
+ | 409 | `ConflictError` | `CONFLICT`, `DOMAIN_HAS_MAILBOXES` | `DOMAIN_HAS_MAILBOXES`: the domain still has active mailboxes. `details.count` is how many; `details.mailboxes` lists them (`id`, `address`, first 100). Delete them, then try again. |
925
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
926
+ | 503 | `InternalServerError` | `SERVICE_UNAVAILABLE`, `SES_IDENTITY_DELETE_FAILED` | `SES_IDENTITY_DELETE_FAILED`: the domain's Amazon SES identity could not be removed. Nothing was deleted; retry. |
927
+
928
+ ```ts
929
+ await mail.domains.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
930
+ ```
931
+
932
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-domains-domain-id)
933
+
934
+ ### `mail.domains.list(params?, options?)`
935
+
936
+ `GET /v1/workspaces/{workspace_id}/domains` → `PagePromise<Mail.Domain>`
937
+
938
+ List domains registered to this workspace (newest first), cursor-paginated.
939
+
940
+ - Params: `Mail.DomainListParams`
941
+ - Paginated: `for await` iterates every item; `await` gives one page
942
+
943
+ | Status | Error class | Codes | When |
944
+ |---|---|---|---|
945
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor, search term, or status filter. |
946
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
947
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
948
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
949
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
950
+
951
+ ```ts
952
+ for await (const domain of mail.domains.list()) {
953
+ console.log(domain);
954
+ }
955
+ const page = await mail.domains.list({}, { timeout: 5_000 });
956
+ ```
957
+
958
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains)
959
+
960
+ ### `mail.domains.lookup(params, options?)`
961
+
962
+ `GET /v1/workspaces/{workspace_id}/domains/lookup` → `Mail.DomainLookup`
963
+
964
+ Look up a domain before adding it: zone, DNS provider, Domain Connect support, a better subdomain, existing records.
965
+
966
+ Creates nothing. Resolves only names at or under `fqdn` and its zone apex, plus the Domain Connect endpoints the zone advertises (fetched through the SSRF guard; cached per zone for an hour). Limited to 30 lookups per workspace per minute.
967
+
968
+ - Params: `Mail.DomainLookupParams`
969
+
970
+ | Status | Error class | Codes | When |
971
+ |---|---|---|---|
972
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `fqdn` missing or not a domain name. |
973
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
974
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
975
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
976
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 30 lookups this minute (`RATE_LIMITED`). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
977
+
978
+ ```ts
979
+ const domainLookup = await mail.domains.lookup({ fqdn: "acme.com" });
980
+ ```
981
+
982
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-lookup)
983
+
984
+ ### `mail.domains.retrieve(domainId, options?)`
985
+
986
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}` → `Mail.DomainDetail`
987
+
988
+ Get a single domain (with DNS instructions and its latest events).
989
+
990
+ The domain with `verification_status`, `region`, the effective `dns_provider`, `recommended_pending`, its DNS instructions, and `events`: the latest 20 domain events, newest first.
991
+
992
+ - Polling: `domains.poll(…)` waits for `status` ∈ `active`, `disabled`
993
+
994
+ | Status | Error class | Codes | When |
995
+ |---|---|---|---|
996
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `domain_id` is not a UUID. |
997
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
998
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
999
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1000
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1001
+
1002
+ ```ts
1003
+ const domainDetail = await mail.domains.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1004
+ ```
1005
+
1006
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id)
1007
+
1008
+ ### `mail.domains.testSend(domainId, params, options?)`
1009
+
1010
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/test-send` → `Mail.SendAccepted`
1011
+
1012
+ Send a built-in test email from this domain, no template needed.
1013
+
1014
+ Sends a fixed message (subject `[TEST] Sending from <domain> works`, a short body naming the domain and the From address) so a freshly verified domain can be tried before any template exists. The From address defaults to `test@<domain>` and must be at this exact domain. Like a template test send it skips the suppression list and the ordinary send limits, counts against the workspace test-send limit (30 per hour), and is recorded with `source: test_send`.
1015
+
1016
+ - Params: `Mail.DomainTestSendParams`
1017
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1018
+
1019
+ | Status | Error class | Codes | When |
1020
+ |---|---|---|---|
1021
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `domain_id` is not a UUID, or the body is malformed (bad `to` or `from`). |
1022
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1023
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1024
+ | 404 | `NotFoundError` | `NOT_FOUND`, `DOMAIN_NOT_FOUND` | `DOMAIN_NOT_FOUND`: no such domain in this workspace. |
1025
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different body or route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1026
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `FROM_DOMAIN_MISMATCH`, `from_domain_not_verified`, `domain_not_verified_by_provider` | `FROM_DOMAIN_MISMATCH`: the From address is not at this domain; or `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain is receive-only (`reason: not_registered`) or not verified yet (`reason: not_verified`); or `domain_not_verified_by_provider`: its DNS records have been broken for over 72 h (`health: failing`). Refused before anything is recorded or queued. |
1027
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 30 test sends this hour in this workspace: `RATE_LIMITED` with `details: { window, limit, retry_after }`. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1028
+
1029
+ ```ts
1030
+ const sendAccepted = await mail.domains.testSend("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { to: "you@acme.com" });
1031
+ ```
1032
+
1033
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-test-send)
1034
+
1035
+ ### `mail.domains.update(domainId, params?, options?)`
1036
+
1037
+ `PATCH /v1/workspaces/{workspace_id}/domains/{domain_id}` → `Mail.Domain`
1038
+
1039
+ Update domain settings: capabilities, DNS provider hint, custom MAIL FROM subdomain, DMARC recommendation.
1040
+
1041
+ `intent` (or Resend's `capabilities`) changes what the domain is used for. A pending domain just changes; a verified domain gaining sending or receiving has the new records checked live and is refused with 409 `CAPABILITY_NOT_VERIFIED` (the records to publish in `details.records`) until they pass. Turning receiving off is refused while active mailboxes exist (409 `DOMAIN_HAS_MAILBOXES`, the mailboxes in `details.mailboxes`; deleted mailboxes do not count); turning sending off also turns the custom MAIL FROM off. `tls`, `open_tracking`, `click_tracking` and `tracking_subdomain` are accepted for Resend compatibility; only the values in effect today pass (422 otherwise). `dns_provider` is the provider the setup wizard renders host names for. `mail_from_subdomain` sets the custom MAIL FROM label (`bounce` → `bounce.<fqdn>`; `null` turns it off): the domain's record set gains the MAIL FROM MX + SPF records, and once the domain is verified its MAIL FROM is pointed there (the default return address stays in use until the MX record is found, so mail never stops). Refused with 409 when that host already carries records that are not ours. `dmarc_managed: false` hides the `_dmarc` recommendation for domains whose DMARC is managed elsewhere (the policy is still detected and graded).
1042
+
1043
+ - Params: `Mail.DomainUpdateParams`
1044
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1045
+
1046
+ | Status | Error class | Codes | When |
1047
+ |---|---|---|---|
1048
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `INVALID_CAPABILITIES`, `MAIL_FROM_NOT_SENDING` | Validation failure on the request body; `intent` and `capabilities` disagree or enable nothing (`INVALID_CAPABILITIES`); or `mail_from_subdomain` on a receive-only domain (`MAIL_FROM_NOT_SENDING`). |
1049
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1050
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1051
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1052
+ | 409 | `ConflictError` | `CONFLICT`, `CAPABILITY_NOT_VERIFIED`, `DOMAIN_HAS_MAILBOXES`, `MAIL_FROM_HOST_IN_USE`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `CAPABILITY_NOT_VERIFIED`: a verified domain cannot gain sending or receiving until the new records verify (`details.failing`, `details.records`); `DOMAIN_HAS_MAILBOXES`: receiving cannot be turned off while active mailboxes exist (`details.count`, `details.mailboxes[].id\|address`); `MAIL_FROM_HOST_IN_USE`: the MAIL FROM host already has a CNAME, a foreign MX or a foreign SPF record (listed in `details.found`); or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1053
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `TLS_ENFORCED_NOT_AVAILABLE`, `TRACKING_NOT_AVAILABLE` | `TLS_ENFORCED_NOT_AVAILABLE`: `tls: enforced`; `TRACKING_NOT_AVAILABLE`: open/click tracking or a tracking subdomain. |
1054
+ | 429 | `RateLimitError` | `RATE_LIMITED` | `mail_from_subdomain` or `intent` changed less than 10 seconds ago (`RATE_LIMITED`). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1055
+
1056
+ ```ts
1057
+ const domain = await mail.domains.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { capabilities: { receiving: "enabled", sending: "enabled" } });
1058
+ ```
1059
+
1060
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-domains-domain-id)
1061
+
1062
+ ### `mail.domains.verify(domainId, options?)`
1063
+
1064
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/verify` → `Mail.Domain`
1065
+
1066
+ Run a synchronous DNS check against this domain; flips status to active when the required record set passes.
1067
+
1068
+ Legacy shape, kept as an alias of `POST .../dns-records/check`: returns the domain on success and a 400 with `dns_check_results` when a required record fails. New integrations should use `POST .../dns-records/check`, which returns per-record states.
1069
+
1070
+ | Status | Error class | Codes | When |
1071
+ |---|---|---|---|
1072
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Verification failed — one or more required records are still missing or wrong. |
1073
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1074
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1075
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1076
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Verified less than 10 seconds ago (`RATE_LIMITED`). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1077
+
1078
+ ```ts
1079
+ const domain = await mail.domains.verify("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1080
+ ```
1081
+
1082
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-verify)
1083
+
1084
+ ## domains.dnsRecords
1085
+
1086
+ ### `mail.domains.dnsRecords.apply(domainId, params, options?)`
1087
+
1088
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/dns-records/apply` → `Mail.DnsApplyResult`
1089
+
1090
+ Set up the domain's DNS records in one step with a short-lived Cloudflare or DigitalOcean API token.
1091
+
1092
+ Reads the zone through the provider's API and plans every record the domain needs: `create`, `update` (only ever an SPF merge into an existing SPF record, shown with `before`/`after`) or `skip` (already there). `dry_run: true` returns the plan and changes nothing; `dry_run: false` applies exactly that plan, runs a DNS check straight away and records `setup_method = provider_api`. Only names at or under the domain are written, nothing is ever deleted, and Cloudflare records are DNS-only (not proxied). The recommended DMARC record (`p=none`) is included when `include_dmarc` is true (the default) and no DMARC policy applies to the domain yet; an existing policy is never touched. Any blocking conflict refuses the whole apply with 409. The token is used for this request only: it is never stored, logged, queued or returned; revoke it afterwards (`warnings` says so when it could see more than this zone). Applies are limited to one per domain every 10 seconds; dry runs are not limited.
1093
+
1094
+ - Params: `Mail.DomainDnsRecordApplyParams`
1095
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1096
+
1097
+ | Status | Error class | Codes | When |
1098
+ |---|---|---|---|
1099
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `IDEMPOTENCY_KEY_REQUIRED`, `INVALID_ZONE`, `PROVIDER_TOKEN_REJECTED`, `PROVIDER_PERMISSION_DENIED`, `PROVIDER_ZONE_NOT_FOUND` | Validation failure; `Idempotency-Key` missing (`IDEMPOTENCY_KEY_REQUIRED`); `zone` not the domain or a parent (`INVALID_ZONE`); the provider rejected the token (`PROVIDER_TOKEN_REJECTED`), the token lacks DNS permissions (`PROVIDER_PERMISSION_DENIED`), or it cannot see a zone for the domain (`PROVIDER_ZONE_NOT_FOUND`). |
1100
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1101
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Missing `mail.update`, or the mail service is not enabled for the workspace. |
1102
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1103
+ | 409 | `ConflictError` | `CONFLICT`, `DNS_CONFLICTS`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `DNS_CONFLICTS`: a blocking conflict (a second SPF record, a CNAME at the name, another DKIM key at our selector, other MX records at the MAIL FROM host or the domain, a merge over 10 SPF lookups). Nothing was written; the body carries the full `plan` and `conflicts`. Or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1104
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Applied less than 10 seconds ago (`RATE_LIMITED`). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1105
+ | 502 | `InternalServerError` | `BAD_GATEWAY`, `PROVIDER_UNAVAILABLE`, `PROVIDER_ERROR` | The provider failed or did not answer (`PROVIDER_UNAVAILABLE`, `PROVIDER_ERROR`). Nothing was written unless the plan items say so. |
1106
+
1107
+ ```ts
1108
+ const dnsApplyResult = await mail.domains.dnsRecords.apply("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { dry_run: true, provider: "cloudflare", token: "dop_v1_EXAMPLE_TOKEN_DO_NOT_USE" });
1109
+ ```
1110
+
1111
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-dns-records-apply)
1112
+
1113
+ ### `mail.domains.dnsRecords.check(domainId, params?, options?)`
1114
+
1115
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/dns-records/check` → `Mail.DomainDnsRecords`
1116
+
1117
+ Check the domain's DNS records now.
1118
+
1119
+ Queries the zone's authoritative nameservers and a public resolver for every record, stores the result, flips a pending domain whose required records pass to `active`, and returns the same body as `GET .../dns-records`. Never fails because records are missing: their states say so. Limited to one call per domain every 10 seconds. Safe to retry: it changes nothing but the stored check result, so it takes no Idempotency-Key.
1120
+
1121
+ - Params: `Mail.DomainDnsRecordCheckParams`
1122
+
1123
+ | Status | Error class | Codes | When |
1124
+ |---|---|---|---|
1125
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `INVALID_ZONE` | `zone` is not the domain or one of its parents (`INVALID_ZONE`). |
1126
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1127
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1128
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1129
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Checked less than 10 seconds ago (`RATE_LIMITED`). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1130
+
1131
+ ```ts
1132
+ const domainDnsRecords = await mail.domains.dnsRecords.check("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1133
+ ```
1134
+
1135
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-dns-records-check)
1136
+
1137
+ ### `mail.domains.dnsRecords.export(domainId, params, options?)`
1138
+
1139
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}/dns-records/export` → `Response`
1140
+
1141
+ Download the domain's DNS records as a BIND zone fragment or a CSV.
1142
+
1143
+ `format=bind`: a zone-file fragment (`$ORIGIN <zone>.`, `$TTL 3600`, names relative to the zone, MX with priority, TXT split into ≤255-byte quoted strings, a comment per record with its purpose, required/recommended and any conflict) that Cloudflare and Route 53 import directly. `format=csv`: `type,name,value,priority,ttl,purpose,required` (the common DNS-import columns plus two), RFC 4180 quoting, fully qualified names. By default only records not yet verified; `all=true` for every record. Served as an attachment.
1144
+
1145
+ - Params: `Mail.DomainDnsRecordExportParams`
1146
+
1147
+ | Status | Error class | Codes | When |
1148
+ |---|---|---|---|
1149
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `format` missing or not `bind`/`csv`, or `zone` is not the domain or one of its parents. |
1150
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1151
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1152
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1153
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1154
+
1155
+ ```ts
1156
+ const response = await mail.domains.dnsRecords.export("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { format: "bind" });
1157
+ const text = await response.text();
1158
+ ```
1159
+
1160
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id-dns-records-export)
1161
+
1162
+ ### `mail.domains.dnsRecords.list(domainId, params?, options?)`
1163
+
1164
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}/dns-records` → `Mail.DomainDnsRecords`
1165
+
1166
+ Per-record DNS status for a domain.
1167
+
1168
+ Every record the domain needs for its intent, with its state (`missing`, `mismatch`, `conflict`, `propagating`, `verified`), what was found, a propagation hint and timestamps, as of the last check (the verifier re-checks pending domains every 5 minutes and active ones hourly). Does not run a DNS lookup; use `POST .../dns-records/check` for that.
1169
+
1170
+ - Params: `Mail.DomainDnsRecordListParams`
1171
+
1172
+ | Status | Error class | Codes | When |
1173
+ |---|---|---|---|
1174
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `INVALID_ZONE` | `zone` is not the domain or one of its parents (`INVALID_ZONE`). |
1175
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1176
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1177
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1178
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1179
+
1180
+ ```ts
1181
+ const domainDnsRecords = await mail.domains.dnsRecords.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1182
+ ```
1183
+
1184
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id-dns-records)
1185
+
1186
+ ## domains.domainConnect
1187
+
1188
+ ### `mail.domains.domainConnect.start(domainId, params?, options?)`
1189
+
1190
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/domain-connect/start` → `Mail.DomainConnectStart`
1191
+
1192
+ Start one-click DNS setup at the domain's DNS provider (Domain Connect).
1193
+
1194
+ Returns a signed Domain Connect URL for the provider that hosts the domain's zone (Cloudflare, GoDaddy, IONOS, Vercel and the others that have onboarded ProductCraft's template). Open it in the browser: the user signs in at their provider and confirms, the provider writes the DKIM key at `productcraft._domainkey.<domain>` and the MAIL FROM records at `bounce.<domain>` (MX to the regional feedback host, SPF merged into any existing SPF there), and then redirects to `GET /v1/domain-connect/callback`, which records `setup_method = domain_connect`, checks the records and returns the user to the domain in the console. With `include_dmarc` and no DMARC policy in place yet, a second confirmation adds `v=DMARC1; p=none;` at `_dmarc.<domain>`. The URL is valid for 10 minutes and works once. ProductCraft never receives a DNS credential. For sending domains whose MAIL FROM label is `bounce` (the default). Not available while ProductCraft has Domain Connect switched off (`domain_connect.available` in `GET .../domains/lookup` says whether it is).
1195
+
1196
+ - Params: `Mail.DomainDomainConnectStartParams`
1197
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1198
+
1199
+ | Status | Error class | Codes | When |
1200
+ |---|---|---|---|
1201
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure, or an id is not a UUID. |
1202
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1203
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Missing `mail.update`, or the mail service is not enabled for the workspace. |
1204
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1205
+ | 409 | `ConflictError` | `CONFLICT`, `DOMAIN_CONNECT_DISABLED`, `DOMAIN_CONNECT_UNAVAILABLE`, `DOMAIN_CONNECT_NOT_SENDING`, `DOMAIN_CONNECT_MAIL_FROM_LABEL`, `DKIM_KEY_MISSING`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | One-click setup is not possible for this domain; nothing was started. `DOMAIN_CONNECT_DISABLED`: ProductCraft has not switched Domain Connect on. `DOMAIN_CONNECT_UNAVAILABLE`: the DNS provider does not run Domain Connect or has not onboarded our template (`details.reason`: `no_record`, `settings_unreachable`, `template_not_onboarded` or `lookup_failed`; `details.provider_name`). `DOMAIN_CONNECT_NOT_SENDING`: a receive-only domain. `DOMAIN_CONNECT_MAIL_FROM_LABEL`: the MAIL FROM label is not `bounce` or MAIL FROM is off. `DKIM_KEY_MISSING`: no DKIM key to publish. Or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1206
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1207
+
1208
+ ```ts
1209
+ const domainConnectStart = await mail.domains.domainConnect.start("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { include_dmarc: true });
1210
+ ```
1211
+
1212
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-domain-connect-start)
1213
+
1214
+ ## domains.events
1215
+
1216
+ ### `mail.domains.events.list(domainId, params?, options?)`
1217
+
1218
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}/events` → `PagePromise<Mail.DomainEvent>`
1219
+
1220
+ List a domain's events (its timeline), newest first, cursor-paginated.
1221
+
1222
+ Added, records found, verified, degraded, failing, recovered, MAIL FROM verified, DMARC detected, check failed. An event is written only when something changed, so a domain checked every few minutes does not grow a row per check.
1223
+
1224
+ - Params: `Mail.DomainEventListParams`
1225
+ - Paginated: `for await` iterates every item; `await` gives one page
1226
+
1227
+ | Status | Error class | Codes | When |
1228
+ |---|---|---|---|
1229
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor, or `domain_id` is not a UUID. |
1230
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1231
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1232
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1233
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1234
+
1235
+ ```ts
1236
+ for await (const event of mail.domains.events.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b")) {
1237
+ console.log(event);
1238
+ }
1239
+ const page = await mail.domains.events.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {}, { timeout: 5_000 });
1240
+ ```
1241
+
1242
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id-events)
1243
+
1244
+ ## domains.shareLinks
1245
+
1246
+ ### `mail.domains.shareLinks.create(domainId, params?, options?)`
1247
+
1248
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/share-links` → `Mail.CreatedShareLink`
1249
+
1250
+ Create a share link: a read-only DNS setup page for whoever manages the domain's DNS.
1251
+
1252
+ Returns a link (valid 1–7 days, default 7) to a public page that shows the domain name, its DNS records with copy-ready values, their live status and zone-file / CSV downloads, without an account. The page shows nothing else about the workspace. The link is shown only in this response (only its hash is stored); revoke it with `DELETE .../share-links/{link_id}`. Optionally emails it from ProductCraft to up to 5 recipients per domain per day (20 per workspace). Requires `mail.update` on the domain.
1253
+
1254
+ - Params: `Mail.DomainShareLinkCreateParams`
1255
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1256
+
1257
+ | Status | Error class | Codes | When |
1258
+ |---|---|---|---|
1259
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure: `expires_in_days` outside 1–7, more than 5 `email_to` entries, or an invalid address. |
1260
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1261
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1262
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1263
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the key was used with a different body. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1264
+ | 429 | `RateLimitError` | `RATE_LIMITED` | The daily email cap for the domain (5 recipients) or the workspace (20) would be exceeded (`RATE_LIMITED`). No link is created. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1265
+ | 503 | `InternalServerError` | `SERVICE_UNAVAILABLE`, `EMAIL_UNAVAILABLE` | `EMAIL_UNAVAILABLE`: `email_to` was given but the email cap cannot be checked right now. No link is created; retry, or create it without `email_to`. |
1266
+
1267
+ ```ts
1268
+ const createdShareLink = await mail.domains.shareLinks.create("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { email_to: ["it@example.com"] });
1269
+ ```
1270
+
1271
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-share-links)
1272
+
1273
+ ### `mail.domains.shareLinks.delete(domainId, linkId, options?)`
1274
+
1275
+ `DELETE /v1/workspaces/{workspace_id}/domains/{domain_id}/share-links/{link_id}` → `void`
1276
+
1277
+ Revoke a share link.
1278
+
1279
+ The public page and its downloads answer 404 from now on. Revoking an already revoked or expired link succeeds and changes nothing.
1280
+
1281
+ | Status | Error class | Codes | When |
1282
+ |---|---|---|---|
1283
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | An id is not a UUID. |
1284
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1285
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1286
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain or share link not found in this workspace. |
1287
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1288
+
1289
+ ```ts
1290
+ await mail.domains.shareLinks.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
1291
+ ```
1292
+
1293
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-domains-domain-id-share-links-link-id)
1294
+
1295
+ ### `mail.domains.shareLinks.list(domainId, params?, options?)`
1296
+
1297
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}/share-links` → `PagePromise<Mail.ShareLink>`
1298
+
1299
+ List the domain's share links (newest first, including expired and revoked), cursor-paginated.
1300
+
1301
+ The links themselves are not returned: they are shown once, at creation.
1302
+
1303
+ - Params: `Mail.DomainShareLinkListParams`
1304
+ - Paginated: `for await` iterates every item; `await` gives one page
1305
+
1306
+ | Status | Error class | Codes | When |
1307
+ |---|---|---|---|
1308
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor, or an id is not a UUID. |
1309
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1310
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1311
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1312
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1313
+
1314
+ ```ts
1315
+ for await (const shareLink of mail.domains.shareLinks.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b")) {
1316
+ console.log(shareLink);
1317
+ }
1318
+ const page = await mail.domains.shareLinks.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {}, { timeout: 5_000 });
1319
+ ```
1320
+
1321
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id-share-links)
1322
+
1323
+ ## domains.smtpConfig
1324
+
1325
+ ### `mail.domains.smtpConfig.reset(domainId, options?)`
1326
+
1327
+ `DELETE /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config` → `void`
1328
+
1329
+ Drop the per-domain override; sends from this domain fall back to the workspace default (or ProductCraft Mail's own sending).
1330
+
1331
+ | Status | Error class | Codes | When |
1332
+ |---|---|---|---|
1333
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1334
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1335
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1336
+ | 404 | `NotFoundError` | `NOT_FOUND` | No per-domain override to delete |
1337
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1338
+
1339
+ ```ts
1340
+ await mail.domains.smtpConfig.reset("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1341
+ ```
1342
+
1343
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-domains-domain-id-smtp-config)
1344
+
1345
+ ### `mail.domains.smtpConfig.retrieve(domainId, options?)`
1346
+
1347
+ `GET /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config` → `Mail.GetSmtpConfig`
1348
+
1349
+ Get the per-domain SMTP override (returns null if unset; the workspace default applies in that case).
1350
+
1351
+ | Status | Error class | Codes | When |
1352
+ |---|---|---|---|
1353
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1354
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1355
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1356
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1357
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1358
+
1359
+ ```ts
1360
+ const getSmtpConfig = await mail.domains.smtpConfig.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1361
+ ```
1362
+
1363
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-domains-domain-id-smtp-config)
1364
+
1365
+ ### `mail.domains.smtpConfig.update(domainId, params, options?)`
1366
+
1367
+ `PUT /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config` → `Mail.SmtpConfig`
1368
+
1369
+ Create or update the per-domain SMTP override (resets verification).
1370
+
1371
+ - Params: `Mail.DomainSmtpConfigUpdateParams`
1372
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1373
+
1374
+ | Status | Error class | Codes | When |
1375
+ |---|---|---|---|
1376
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure or username/password mismatch. |
1377
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1378
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1379
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1380
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1381
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1382
+
1383
+ ```ts
1384
+ const smtpConfig = await mail.domains.smtpConfig.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {
1385
+ from_email: "noreply@acme.com",
1386
+ from_name: "Acme Notifications",
1387
+ host: "smtp.sendgrid.net",
1388
+ port: 587,
1389
+ secure: false,
1390
+ });
1391
+ ```
1392
+
1393
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-domains-domain-id-smtp-config)
1394
+
1395
+ ### `mail.domains.smtpConfig.verifySaved(domainId, options?)`
1396
+
1397
+ `POST /v1/workspaces/{workspace_id}/domains/{domain_id}/smtp-config/verify-saved` → `Mail.SmtpVerifyResult`
1398
+
1399
+ Test the per-domain SMTP override; updates status.
1400
+
1401
+ | Status | Error class | Codes | When |
1402
+ |---|---|---|---|
1403
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1404
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1405
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1406
+ | 404 | `NotFoundError` | `NOT_FOUND` | No per-domain override saved |
1407
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1408
+
1409
+ ```ts
1410
+ const smtpVerifyResult = await mail.domains.smtpConfig.verifySaved("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1411
+ ```
1412
+
1413
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-domains-domain-id-smtp-config-verify-saved)
1414
+
1415
+ ## emails
1416
+
1417
+ ### `mail.emails.batch(params, options?)`
1418
+
1419
+ `POST /v1/workspaces/{workspace_id}/emails/batch` → `Mail.SendBatchOpenApi`
1420
+
1421
+ Send up to 100 emails in one request.
1422
+
1423
+ Each row is a `POST /emails` body plus an optional `idempotency_key`, processed independently: the response reports `accepted` (with the email `id`) or `rejected` (with `error.code`) per row. An `Idempotency-Key` header covers the whole request; a row `idempotency_key` covers one row, in the same dedupe namespace (so the header must differ from every row key).
1424
+
1425
+ - Params: `Mail.EmailBatchParams`
1426
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1427
+
1428
+ | Status | Error class | Codes | When |
1429
+ |---|---|---|---|
1430
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `IDEMPOTENCY_KEY_CONFLICTS_WITH_ROW` | Validation failure (malformed row, more than 100 rows), or `IDEMPOTENCY_KEY_CONFLICTS_WITH_ROW`: the `Idempotency-Key` header equals a row `idempotency_key`. |
1431
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1432
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1433
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1434
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1435
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `from_domain_not_verified`, `domain_not_verified_by_provider`, `INVALID_FROM_ADDRESS` | For any row: `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain of the From address is not itself an active sending domain of this workspace (`reason: not_registered` or `not_verified`); refused before anything is recorded or queued. Or `domain_not_verified_by_provider`: the from-domain's DNS records have been broken for over 72 h, so the email provider no longer sends for it. Or `INVALID_FROM_ADDRESS`. The whole batch is refused; no row is sent. |
1436
+ | 429 | `RateLimitError` | `RATE_LIMITED` | The workspace send limits never 429 a batch: a row over one is rejected in the 200 body with `RATE_LIMITED`, and the other rows are unaffected. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1437
+
1438
+ ```ts
1439
+ const sendBatchOpenApi = await mail.emails.batch({
1440
+ emails: [{ from: "Acme <hello@mail.acme.com>", to: ["success@simulator.amazonses.com"] }],
1441
+ });
1442
+ ```
1443
+
1444
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-emails-batch)
1445
+
1446
+ ### `mail.emails.list(params?, options?)`
1447
+
1448
+ `GET /v1/workspaces/{workspace_id}/emails` → `PagePromise<Mail.Email>`
1449
+
1450
+ List emails.
1451
+
1452
+ One entry per email (send call), newest first, with its recipients and `last_event`, the worst status across its messages. Filter by tags with `tag[name]=value` (repeat for several; all must match). Emails sent before 2026-11-06 appear as one-recipient emails.
1453
+
1454
+ - Params: `Mail.EmailListParams`
1455
+ - Paginated: `for await` iterates every item; `await` gives one page
1456
+
1457
+ | Status | Error class | Codes | When |
1458
+ |---|---|---|---|
1459
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor or tag filter. |
1460
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1461
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1462
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1463
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1464
+
1465
+ ```ts
1466
+ for await (const email of mail.emails.list()) {
1467
+ console.log(email);
1468
+ }
1469
+ const page = await mail.emails.list({}, { timeout: 5_000 });
1470
+ ```
1471
+
1472
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-emails)
1473
+
1474
+ ### `mail.emails.metrics(params?, options?)`
1475
+
1476
+ `GET /v1/workspaces/{workspace_id}/emails/metrics` → `Mail.EmailMetrics`
1477
+
1478
+ Email metrics for a workspace.
1479
+
1480
+ Outbound email counts and rates over a range of up to 30 days, in hourly or daily buckets in your time zone, optionally per sending domain, filtered by domain, API key or source. Read-only.
1481
+
1482
+ - **Emails** (`sent`): messages that left the queue.
1483
+ - **Deliverability rate**: delivered / emails sent with delivery tracking. Emails sent before delivery tracking started, or through a custom SMTP relay, are confirmed at hand-off only, so for them `delivered` is not measured and the rate is `null` (`measured.delivery = false`), never 0.
1484
+ - **Bounce rate**: bounced / emails. Suppressed recipients never count.
1485
+ - **Complaint rate**: complained / delivered, from mailbox providers' feedback loops.
1486
+
1487
+ Rates are numbers from 0 to 1, or `null` when unmeasured or when the denominator is zero. Template test sends are excluded. Cached for up to 60 seconds.
1488
+
1489
+ - Params: `Mail.EmailMetricsParams`
1490
+
1491
+ | Status | Error class | Codes | When |
1492
+ |---|---|---|---|
1493
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A parameter is invalid: `domain_id` or `api_key_id` is not a UUID; more than 100 `domain_id`; an unknown `timezone`, `granularity`, `source` or dimension; a date that does not parse; `end_date` not after `start_date`; a range longer than 30 days or starting before the 30-day retention; or `granularity=hourly` over more than 7 days. The reason is in `message`. |
1494
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | No valid credential. |
1495
+ | 403 | `PermissionDeniedError` | `FORBIDDEN`, `SERVICE_NOT_ENABLED` | Caller lacks `mail.message.read` on the workspace (including non-members), or Mail is not enabled on it (`SERVICE_NOT_ENABLED`). |
1496
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. |
1497
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1498
+
1499
+ ```ts
1500
+ const emailMetrics = await mail.emails.metrics();
1501
+ ```
1502
+
1503
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-emails-metrics)
1504
+
1505
+ ### `mail.emails.retrieve(id, options?)`
1506
+
1507
+ `GET /v1/workspaces/{workspace_id}/emails/{id}` → `Mail.EmailDetail`
1508
+
1509
+ Retrieve an email.
1510
+
1511
+ The email as sent (from, to, cc, bcc, reply_to, subject, tags, custom headers) with one entry per recipient message and `last_event`, the worst status across them. Bodies are on `GET /messages/{id}/body`.
1512
+
1513
+ | Status | Error class | Codes | When |
1514
+ |---|---|---|---|
1515
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1516
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1517
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1518
+ | 404 | `NotFoundError` | `NOT_FOUND`, `EMAIL_NOT_FOUND` | `EMAIL_NOT_FOUND`: no email with this id in this workspace. |
1519
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1520
+
1521
+ ```ts
1522
+ const emailDetail = await mail.emails.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1523
+ ```
1524
+
1525
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-emails-id)
1526
+
1527
+ ### `mail.emails.send(params, options?)`
1528
+
1529
+ `POST /v1/workspaces/{workspace_id}/emails` → `Mail.SendEmail`
1530
+
1531
+ Send an email.
1532
+
1533
+ Send your own `html` and/or `text`, or a saved `template`, to up to 50 recipients across `to`, `cc` and `bcc`, with `reply_to`, custom `headers` and `tags`. Every recipient gets its own copy (its own message in `GET /messages`, suppression check and delivery status) showing the same `To` and `Cc` headers; `bcc` recipients appear in no header. A recipient on the suppression list (or unsubscribed, for a template with an unsubscribe link) is left out with a `suppressed` message, and the email is still accepted. Counts one send per recipient against the workspace limits. Pass an `Idempotency-Key` header to make retries safe.
1534
+
1535
+ - Params: `Mail.EmailSendParams`
1536
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1537
+
1538
+ | Status | Error class | Codes | When |
1539
+ |---|---|---|---|
1540
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `EMAIL_CONTENT_CONFLICT`, `RECIPIENT_LIMIT`, `MAIL_HEADER_NOT_ALLOWED` | Validation failure; `EMAIL_CONTENT_CONFLICT` (both or neither of `html`/`text` and `template`); `RECIPIENT_LIMIT` (more than 50 recipients); `MAIL_HEADER_NOT_ALLOWED` (`details.headers` names the refused headers). |
1541
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1542
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1543
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TEMPLATE_NOT_FOUND` | `TEMPLATE_NOT_FOUND`: the `template.name` is not a template of this workspace. |
1544
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1545
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `from_domain_not_verified`, `domain_not_verified_by_provider`, `INVALID_FROM_ADDRESS`, `TEMPLATE_RENDER_FAILED`, `TEMPLATE_DATA_INVALID` | `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain of the From address is not itself an active sending domain of this workspace (`reason: not_registered` or `not_verified`); refused before anything is recorded or queued. Or `domain_not_verified_by_provider`: the from-domain's DNS records have been broken for over 72 h, so the email provider no longer sends for it. Or `INVALID_FROM_ADDRESS`. Or `TEMPLATE_RENDER_FAILED` / `TEMPLATE_DATA_INVALID` for a template send. |
1546
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Over a workspace send limit (60 per minute, 600 per hour, 10,000 per day; each recipient counts): `RATE_LIMITED` with `details: { window, limit, retry_after }`. Nothing is recorded or queued. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1547
+
1548
+ ```ts
1549
+ const sendEmail = await mail.emails.send({ from: "Acme <hello@mail.acme.com>", to: ["success@simulator.amazonses.com"] });
1550
+ ```
1551
+
1552
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-emails)
1553
+
1554
+ ## integrations
1555
+
1556
+ ### `mail.integrations.delete(provider, options?)`
1557
+
1558
+ `DELETE /v1/workspaces/{workspace_id}/integrations/{provider}` → `void`
1559
+
1560
+ Disconnect an integration. Deletes the credential row.
1561
+
1562
+ Hard delete — the encrypted blob is removed. The customer must reconnect via the provider-specific OAuth flow to restore access.
1563
+
1564
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1565
+
1566
+ | Status | Error class | Codes | When |
1567
+ |---|---|---|---|
1568
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1569
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1570
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1571
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1572
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1573
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1574
+
1575
+ ```ts
1576
+ await mail.integrations.delete("provider");
1577
+ ```
1578
+
1579
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-integrations-provider)
1580
+
1581
+ ### `mail.integrations.list(options?)`
1582
+
1583
+ `GET /v1/workspaces/{workspace_id}/integrations` → `Mail.ListIntegrations`
1584
+
1585
+ List connected workspace integrations.
1586
+
1587
+ Returns one entry per connected provider (GitHub, …). No secrets in the response — only public identifiers + status + sync stamps.
1588
+
1589
+ | Status | Error class | Codes | When |
1590
+ |---|---|---|---|
1591
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1592
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1593
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1594
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1595
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1596
+
1597
+ ```ts
1598
+ const listIntegrations = await mail.integrations.list();
1599
+ ```
1600
+
1601
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-integrations)
1602
+
1603
+ ### `mail.integrations.retrieve(provider, options?)`
1604
+
1605
+ `GET /v1/workspaces/{workspace_id}/integrations/{provider}` → `Mail.IntegrationDetail`
1606
+
1607
+ Get a single integration detail with redacted credentials.
1608
+
1609
+ Includes the provider's `credentials_redacted` view. Secret fields (access tokens, signing keys) are never included.
1610
+
1611
+ | Status | Error class | Codes | When |
1612
+ |---|---|---|---|
1613
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1614
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1615
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1616
+ | 404 | `NotFoundError` | `NOT_FOUND` | No integration of the requested provider for this workspace. |
1617
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1618
+
1619
+ ```ts
1620
+ const integrationDetail = await mail.integrations.retrieve("provider");
1621
+ ```
1622
+
1623
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-integrations-provider)
1624
+
1625
+ ## integrations.github
1626
+
1627
+ ### `mail.integrations.github.connect(params, options?)`
1628
+
1629
+ `POST /v1/workspaces/{workspace_id}/integrations/github/connect` → `Mail.ConnectGitHub`
1630
+
1631
+ Mint a GitHub OAuth state token and return the authorize URL.
1632
+
1633
+ The frontend redirects the user to `authorize_url`; GitHub bounces back to the `/callback` endpoint with a `code` + `state` pair. state is single-use, 5-minute TTL.
1634
+
1635
+ - Params: `Mail.IntegrationGithubConnectParams`
1636
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1637
+
1638
+ | Status | Error class | Codes | When |
1639
+ |---|---|---|---|
1640
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1641
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1642
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1643
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1644
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1645
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1646
+
1647
+ ```ts
1648
+ const connectGitHub = await mail.integrations.github.connect({ return_to: "https://console.productcraft.co/mail/workspaces/acme/integrations" });
1649
+ ```
1650
+
1651
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-integrations-github-connect)
1652
+
1653
+ ## integrations.github.repos
1654
+
1655
+ ### `mail.integrations.github.repos.list(options?)`
1656
+
1657
+ `GET /v1/workspaces/{workspace_id}/integrations/github/repos` → `Mail.IntegrationGithubRepoListResponse`
1658
+
1659
+ List repos visible to the connected GitHub account.
1660
+
1661
+ | Status | Error class | Codes | When |
1662
+ |---|---|---|---|
1663
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1664
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1665
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1666
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1667
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1668
+
1669
+ ```ts
1670
+ const result = await mail.integrations.github.repos.list();
1671
+ ```
1672
+
1673
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-integrations-github-repos)
1674
+
1675
+ ## integrations.github.repos.branches
1676
+
1677
+ ### `mail.integrations.github.repos.branches.list(owner, repo, options?)`
1678
+
1679
+ `GET /v1/workspaces/{workspace_id}/integrations/github/repos/{owner}/{repo}/branches` → `Mail.IntegrationGithubRepoBranchListResponse`
1680
+
1681
+ List branches for the given repo.
1682
+
1683
+ | Status | Error class | Codes | When |
1684
+ |---|---|---|---|
1685
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1686
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1687
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1688
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1689
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1690
+
1691
+ ```ts
1692
+ const result = await mail.integrations.github.repos.branches.list("owner", "repo");
1693
+ ```
1694
+
1695
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-integrations-github-repos-owner-repo-branches)
1696
+
1697
+ ## integrations.github.settings
1698
+
1699
+ ### `mail.integrations.github.settings.update(params, options?)`
1700
+
1701
+ `PUT /v1/workspaces/{workspace_id}/integrations/github/settings` → `void`
1702
+
1703
+ Set the repo + branch + templates path to sync from.
1704
+
1705
+ - Params: `Mail.IntegrationGithubSettingsUpdateParams`
1706
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1707
+
1708
+ | Status | Error class | Codes | When |
1709
+ |---|---|---|---|
1710
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1711
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1712
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1713
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1714
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1715
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1716
+
1717
+ ```ts
1718
+ await mail.integrations.github.settings.update({ branch: "main", repo_full_name: "octocat/templates", templates_path: "/" });
1719
+ ```
1720
+
1721
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-integrations-github-settings)
1722
+
1723
+ ## integrations.github.sync
1724
+
1725
+ ### `mail.integrations.github.sync.create(options?)`
1726
+
1727
+ `POST /v1/workspaces/{workspace_id}/integrations/github/sync` → `Mail.StartSync`
1728
+
1729
+ Trigger an immediate sync from GitHub. Returns a sync id to poll.
1730
+
1731
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1732
+
1733
+ | Status | Error class | Codes | When |
1734
+ |---|---|---|---|
1735
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1736
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1737
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1738
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1739
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1740
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1741
+
1742
+ ```ts
1743
+ const startSync = await mail.integrations.github.sync.create();
1744
+ ```
1745
+
1746
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-integrations-github-sync)
1747
+
1748
+ ### `mail.integrations.github.sync.retrieve(syncId, options?)`
1749
+
1750
+ `GET /v1/workspaces/{workspace_id}/integrations/github/sync/{sync_id}` → `Mail.SyncStatus`
1751
+
1752
+ Poll a sync run's status.
1753
+
1754
+ | Status | Error class | Codes | When |
1755
+ |---|---|---|---|
1756
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1757
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1758
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1759
+ | 404 | `NotFoundError` | `NOT_FOUND` | Run not found or expired. |
1760
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1761
+
1762
+ ```ts
1763
+ const syncStatus = await mail.integrations.github.sync.retrieve("sync");
1764
+ ```
1765
+
1766
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-integrations-github-sync-sync-id)
1767
+
1768
+ ## mailboxes
1769
+
1770
+ ### `mail.mailboxes.create(params, options?)`
1771
+
1772
+ `POST /v1/workspaces/{workspace_id}/mailboxes` → `Mail.Mailbox`
1773
+
1774
+ Create a mailbox under a verified domain. Local-part + domain combination must be globally unique.
1775
+
1776
+ - Params: `Mail.MailboxCreateParams`
1777
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1778
+
1779
+ | Status | Error class | Codes | When |
1780
+ |---|---|---|---|
1781
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure (invalid local part, retention out of range). |
1782
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1783
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Caller lacks `mail.create` permission, or workspace has not enabled the `mail` service. |
1784
+ | 404 | `NotFoundError` | `NOT_FOUND` | Domain not found in this workspace. |
1785
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Mailbox local-part already exists on this domain, or the domain is not yet verified, or `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different body or route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1786
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Local part or retention rejected by service-layer validation. |
1787
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1788
+
1789
+ ```ts
1790
+ const mailbox = await mail.mailboxes.create({ domain_id: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b", local_part: "claude-qa-alpha" });
1791
+ ```
1792
+
1793
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-mailboxes)
1794
+
1795
+ ### `mail.mailboxes.delete(id, options?)`
1796
+
1797
+ `DELETE /v1/workspaces/{workspace_id}/mailboxes/{id}` → `void`
1798
+
1799
+ Soft-delete a mailbox: its status becomes `disabled` and it stops accepting new mail; existing messages are kept until retention purges them, and `PATCH` with `status: active` restores it. A deleted mailbox no longer blocks deleting its domain, and deleting the domain erases it and its remaining mail.
1800
+
1801
+ | Status | Error class | Codes | When |
1802
+ |---|---|---|---|
1803
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1804
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1805
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1806
+ | 404 | `NotFoundError` | `NOT_FOUND` | Mailbox not found in this workspace. |
1807
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1808
+
1809
+ ```ts
1810
+ await mail.mailboxes.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1811
+ ```
1812
+
1813
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-mailboxes-id)
1814
+
1815
+ ### `mail.mailboxes.list(params?, options?)`
1816
+
1817
+ `GET /v1/workspaces/{workspace_id}/mailboxes` → `PagePromise<Mail.Mailbox>`
1818
+
1819
+ List mailboxes in this workspace. 20 per page by default (max 200) — paginate with the cursor returned in `pagination.next_cursor` while `pagination.has_more` is true.
1820
+
1821
+ - Params: `Mail.MailboxListParams`
1822
+ - Paginated: `for await` iterates every item; `await` gives one page
1823
+
1824
+ | Status | Error class | Codes | When |
1825
+ |---|---|---|---|
1826
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1827
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1828
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Caller lacks `mail.read` permission, or workspace has not enabled the `mail` service. |
1829
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
1830
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1831
+
1832
+ ```ts
1833
+ for await (const mailbox of mail.mailboxes.list()) {
1834
+ console.log(mailbox);
1835
+ }
1836
+ const page = await mail.mailboxes.list({}, { timeout: 5_000 });
1837
+ ```
1838
+
1839
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes)
1840
+
1841
+ ### `mail.mailboxes.retrieve(id, options?)`
1842
+
1843
+ `GET /v1/workspaces/{workspace_id}/mailboxes/{id}` → `Mail.Mailbox`
1844
+
1845
+ Get one mailbox by id.
1846
+
1847
+ | Status | Error class | Codes | When |
1848
+ |---|---|---|---|
1849
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1850
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1851
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1852
+ | 404 | `NotFoundError` | `NOT_FOUND` | Mailbox not found in this workspace. |
1853
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1854
+
1855
+ ```ts
1856
+ const mailbox = await mail.mailboxes.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
1857
+ ```
1858
+
1859
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes-id)
1860
+
1861
+ ### `mail.mailboxes.update(id, params?, options?)`
1862
+
1863
+ `PATCH /v1/workspaces/{workspace_id}/mailboxes/{id}` → `Mail.Mailbox`
1864
+
1865
+ Update mailbox display name, retention, or status. Status transitions are restricted (active ↔ suspended; archived is terminal).
1866
+
1867
+ - Params: `Mail.MailboxUpdateParams`
1868
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
1869
+
1870
+ | Status | Error class | Codes | When |
1871
+ |---|---|---|---|
1872
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure or invalid status transition. |
1873
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1874
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1875
+ | 404 | `NotFoundError` | `NOT_FOUND` | Mailbox not found in this workspace. |
1876
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
1877
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Retention or status rejected by service-layer validation. |
1878
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1879
+
1880
+ ```ts
1881
+ const mailbox = await mail.mailboxes.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { display_name: "string" });
1882
+ ```
1883
+
1884
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-mailboxes-id)
1885
+
1886
+ ## mailboxes.messages
1887
+
1888
+ ### `mail.mailboxes.messages.delete(id, mid, options?)`
1889
+
1890
+ `DELETE /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}` → `void`
1891
+
1892
+ Soft-delete a message.
1893
+
1894
+ | Status | Error class | Codes | When |
1895
+ |---|---|---|---|
1896
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1897
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1898
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1899
+ | 404 | `NotFoundError` | `NOT_FOUND` | Message not found |
1900
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1901
+
1902
+ ```ts
1903
+ await mail.mailboxes.messages.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
1904
+ ```
1905
+
1906
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-mailboxes-id-messages-mid)
1907
+
1908
+ ### `mail.mailboxes.messages.list(id, params?, options?)`
1909
+
1910
+ `GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages` → `PagePromise<Mail.Message>`
1911
+
1912
+ List messages in a mailbox.
1913
+
1914
+ - Params: `Mail.MailboxMessageListParams`
1915
+ - Paginated: `for await` iterates every item; `await` gives one page
1916
+
1917
+ | Status | Error class | Codes | When |
1918
+ |---|---|---|---|
1919
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter or cursor. |
1920
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1921
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1922
+ | 404 | `NotFoundError` | `NOT_FOUND` | Mailbox not found in this workspace. |
1923
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1924
+
1925
+ ```ts
1926
+ for await (const message of mail.mailboxes.messages.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b")) {
1927
+ console.log(message);
1928
+ }
1929
+ const page = await mail.mailboxes.messages.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {}, { timeout: 5_000 });
1930
+ ```
1931
+
1932
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes-id-messages)
1933
+
1934
+ ### `mail.mailboxes.messages.raw(id, mid, options?)`
1935
+
1936
+ `GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/raw` → `Response`
1937
+
1938
+ Stream the raw RFC5322 .eml for a message. Gated by `mail.message.body.read` (admin+ by default; not granted to plain members) — the outbound lane's /messages/:id/body action.
1939
+
1940
+ | Status | Error class | Codes | When |
1941
+ |---|---|---|---|
1942
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1943
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1944
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Policy denies `mail.message.body.read`. |
1945
+ | 404 | `NotFoundError` | `NOT_FOUND` | Message not found |
1946
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1947
+
1948
+ ```ts
1949
+ const response = await mail.mailboxes.messages.raw("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
1950
+ const text = await response.text();
1951
+ ```
1952
+
1953
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes-id-messages-mid-raw)
1954
+
1955
+ ### `mail.mailboxes.messages.read(id, mid, options?)`
1956
+
1957
+ `POST /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/read` → `void`
1958
+
1959
+ Mark a message as read.
1960
+
1961
+ | Status | Error class | Codes | When |
1962
+ |---|---|---|---|
1963
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1964
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1965
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1966
+ | 404 | `NotFoundError` | `NOT_FOUND` | Message not found |
1967
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1968
+
1969
+ ```ts
1970
+ await mail.mailboxes.messages.read("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
1971
+ ```
1972
+
1973
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-mailboxes-id-messages-mid-read)
1974
+
1975
+ ### `mail.mailboxes.messages.retrieve(id, mid, options?)`
1976
+
1977
+ `GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}` → `Mail.MessageDetail`
1978
+
1979
+ Get a single message (metadata + attachment manifest; `body_preview` is null without `mail.message.body.read`). Use /:mid/raw for content.
1980
+
1981
+ | Status | Error class | Codes | When |
1982
+ |---|---|---|---|
1983
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
1984
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
1985
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
1986
+ | 404 | `NotFoundError` | `NOT_FOUND` | Message not found |
1987
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
1988
+
1989
+ ```ts
1990
+ const messageDetail = await mail.mailboxes.messages.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6");
1991
+ ```
1992
+
1993
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes-id-messages-mid)
1994
+
1995
+ ## mailboxes.messages.attachments
1996
+
1997
+ ### `mail.mailboxes.messages.attachments.retrieve(id, mid, position, options?)`
1998
+
1999
+ `GET /v1/workspaces/{workspace_id}/mailboxes/{id}/messages/{mid}/attachments/{position}` → `Response`
2000
+
2001
+ Stream a single attachment by its position. Gated by `mail.message.body.read`: an attachment is message content, and /:mid already names it without handing over the bytes.
2002
+
2003
+ | Status | Error class | Codes | When |
2004
+ |---|---|---|---|
2005
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2006
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2007
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | Policy denies `mail.message.body.read`. |
2008
+ | 404 | `NotFoundError` | `NOT_FOUND` | Attachment not found |
2009
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2010
+
2011
+ ```ts
2012
+ const response = await mail.mailboxes.messages.attachments.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "5d1e2f3a-4b5c-4d6e-8f70-8192a3b4c5d6", "position");
2013
+ const text = await response.text();
2014
+ ```
2015
+
2016
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-mailboxes-id-messages-mid-attachments-position)
2017
+
2018
+ ## messages
2019
+
2020
+ ### `mail.messages.body(id, options?)`
2021
+
2022
+ `GET /v1/workspaces/{workspace_id}/messages/{id}/body` → `Mail.OutboundMessageWithBody`
2023
+
2024
+ Get the cleartext body of an outbound message. Gated by `mail.message.body.read` (admin+ by default; not granted to plain members).
2025
+
2026
+ | Status | Error class | Codes | When |
2027
+ |---|---|---|---|
2028
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2029
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2030
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2031
+ | 404 | `NotFoundError` | `NOT_FOUND` | Not found. |
2032
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2033
+
2034
+ ```ts
2035
+ const outboundMessageWithBody = await mail.messages.body("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2036
+ ```
2037
+
2038
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages-id-body)
2039
+
2040
+ ### `mail.messages.erase(params, options?)`
2041
+
2042
+ `DELETE /v1/workspaces/{workspace_id}/messages` → `Mail.DeleteByRecipient`
2043
+
2044
+ RTBF: delete every message log row for `recipient` (case-insensitive). Returns the deleted-row count.
2045
+
2046
+ The bulk GDPR erase (`messages.erase` in the SDKs): removes every delivery-log row sent to one recipient address in this workspace (with its stored body and events), and strips the address from the cc, bcc and reply-to lists of other rows, in one transaction. Repeating it is harmless (the second call deletes 0 rows).
2047
+
2048
+ - Params: `Mail.MessageEraseParams`
2049
+
2050
+ | Status | Error class | Codes | When |
2051
+ |---|---|---|---|
2052
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Missing or invalid recipient. |
2053
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2054
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2055
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2056
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2057
+
2058
+ ```ts
2059
+ const deleteByRecipient = await mail.messages.erase({ recipient: "ada@example.com" });
2060
+ ```
2061
+
2062
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-messages)
2063
+
2064
+ ### `mail.messages.export(params?, options?)`
2065
+
2066
+ `GET /v1/workspaces/{workspace_id}/messages/export` → `Response`
2067
+
2068
+ Stream the filtered message log as CSV. Same filters as GET /messages. Body columns are intentionally omitted; bodies remain gated by `mail.message.body.read`. Rate-limited 1 concurrent + 5/hour per workspace.
2069
+
2070
+ Columns: id, created_at, sent_at, delivered_at, deferred_at, deferral_count, from_address, to_address, subject, template_name, status, spf_result, dkim_result, dmarc_result, bounce_code, bounce_detail, api_key_id, source, domain. The filters are resolved by the same code as `GET /messages`, so the same query string exports exactly the rows the list pages through. A rejected filter (400) does not count against the hourly cap.
2071
+
2072
+ - Params: `Mail.MessageExportParams`
2073
+
2074
+ | Status | Error class | Codes | When |
2075
+ |---|---|---|---|
2076
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter: an unknown status or source, a malformed uuid or date-time, `q` over 200 characters, `created_after` not earlier than `created_before`, or a window wider than 30 days. The reason is in `message`. |
2077
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2078
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2079
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2080
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Another export is already running, or this workspace has hit the hourly export cap. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2081
+
2082
+ ```ts
2083
+ const response = await mail.messages.export();
2084
+ const text = await response.text();
2085
+ ```
2086
+
2087
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages-export)
2088
+
2089
+ ### `mail.messages.list(params?, options?)`
2090
+
2091
+ `GET /v1/workspaces/{workspace_id}/messages` → `PagePromise<Mail.OutboundMessage>`
2092
+
2093
+ List outbound messages with cursor pagination.
2094
+
2095
+ One row per recipient delivery, newest first. Filters combine with AND: `q` (free text over recipient, from, subject, message id and Message-ID), `status` (any of, repeatable), `domain_id`, `api_key_id`, `source`, `template`, `created_after` / `created_before` (at most 30 days apart), plus the legacy `recipient` and `subject_contains`. `GET /messages/export` takes exactly the same filters and returns the same rows.
2096
+
2097
+ - Params: `Mail.MessageListParams`
2098
+ - Paginated: `for await` iterates every item; `await` gives one page
2099
+
2100
+ | Status | Error class | Codes | When |
2101
+ |---|---|---|---|
2102
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter or cursor: an unknown status or source, a malformed uuid or date-time, `q` over 200 characters, `created_after` not earlier than `created_before`, or a window wider than 30 days. The reason is in `message`. |
2103
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2104
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2105
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2106
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2107
+
2108
+ ```ts
2109
+ for await (const message of mail.messages.list()) {
2110
+ console.log(message);
2111
+ }
2112
+ const page = await mail.messages.list({}, { timeout: 5_000 });
2113
+ ```
2114
+
2115
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages)
2116
+
2117
+ ### `mail.messages.retrieve(id, options?)`
2118
+
2119
+ `GET /v1/workspaces/{workspace_id}/messages/{id}` → `Mail.OutboundMessageDetail`
2120
+
2121
+ Get a single outbound message (metadata + delivery verdicts; body not included) with its event timeline: `events[]` oldest first, `last_event`, `transport`, `bounce_type`, `bounce_subtype`. Use /:id/body for cleartext content.
2122
+
2123
+ | Status | Error class | Codes | When |
2124
+ |---|---|---|---|
2125
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2126
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2127
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2128
+ | 404 | `NotFoundError` | `NOT_FOUND` | Not found. |
2129
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2130
+
2131
+ ```ts
2132
+ const outboundMessageDetail = await mail.messages.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2133
+ ```
2134
+
2135
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages-id)
2136
+
2137
+ ### `mail.messages.timeline(params, options?)`
2138
+
2139
+ `GET /v1/workspaces/{workspace_id}/messages/timeline` → `Mail.MessageTimeline`
2140
+
2141
+ Day-bucketed inbound message volume for a workspace.
2142
+
2143
+ Counts INBOUND mail only: messages received by the workspace's mailboxes, in dense day buckets in [since, until). `metric=sent` (default, a historical name) counts received messages, not outbound sends; `metric=bounced` counts MX-time rejections matched to this workspace by envelope-to. For outbound counts and rates use `GET /emails/metrics`.
2144
+
2145
+ - Params: `Mail.MessageTimelineParams`
2146
+
2147
+ | Status | Error class | Codes | When |
2148
+ |---|---|---|---|
2149
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2150
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2151
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2152
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2153
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2154
+
2155
+ ```ts
2156
+ const messageTimeline = await mail.messages.timeline({ granularity: "day", since: "2026-04-01T00:00:00Z", until: "2026-05-01T00:00:00Z" });
2157
+ ```
2158
+
2159
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages-timeline)
2160
+
2161
+ ## messages.settings
2162
+
2163
+ ### `mail.messages.settings.retrieve(options?)`
2164
+
2165
+ `GET /v1/workspaces/{workspace_id}/messages/settings` → `Mail.OutboundMessageSettings`
2166
+
2167
+ Read this workspace's outbound-message settings. Absent → defaults (redact_bodies: false).
2168
+
2169
+ | Status | Error class | Codes | When |
2170
+ |---|---|---|---|
2171
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2172
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2173
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2174
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2175
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2176
+
2177
+ ```ts
2178
+ const outboundMessageSettings = await mail.messages.settings.retrieve();
2179
+ ```
2180
+
2181
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-messages-settings)
2182
+
2183
+ ### `mail.messages.settings.update(params, options?)`
2184
+
2185
+ `PATCH /v1/workspaces/{workspace_id}/messages/settings` → `Mail.OutboundMessageSettings`
2186
+
2187
+ Update this workspace's outbound-message settings. Gated on `mail.message.delete` — same privilege bar as RTBF, since both touch the PII boundary.
2188
+
2189
+ - Params: `Mail.MessageSettingsUpdateParams`
2190
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2191
+
2192
+ | Status | Error class | Codes | When |
2193
+ |---|---|---|---|
2194
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid payload. |
2195
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2196
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2197
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2198
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2199
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2200
+
2201
+ ```ts
2202
+ const outboundMessageSettings = await mail.messages.settings.update({ redact_bodies: true });
2203
+ ```
2204
+
2205
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-messages-settings)
2206
+
2207
+ ## received
2208
+
2209
+ ### `mail.received.delete(id, options?)`
2210
+
2211
+ `DELETE /v1/workspaces/{workspace_id}/received/{id}` → `void`
2212
+
2213
+ Delete a received email.
2214
+
2215
+ Soft delete: it disappears from every list at once; retention purges the stored message later.
2216
+
2217
+ | Status | Error class | Codes | When |
2218
+ |---|---|---|---|
2219
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `id` is not a uuid. |
2220
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2221
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.delete` on every mailbox. |
2222
+ | 404 | `NotFoundError` | `NOT_FOUND` | No such email in this workspace (or it was already deleted). |
2223
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2224
+
2225
+ ```ts
2226
+ await mail.received.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2227
+ ```
2228
+
2229
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-received-id)
2230
+
2231
+ ### `mail.received.export(params?, options?)`
2232
+
2233
+ `GET /v1/workspaces/{workspace_id}/received/export` → `Response`
2234
+
2235
+ Stream the filtered received email as CSV. Same filters as GET /received. Rate-limited 1 concurrent + 5/hour per workspace.
2236
+
2237
+ Columns: id, received_at, mailbox, from, to (`; `-separated), subject, spam_verdict, spf, dkim, dmarc, size_bytes, attachments (count). No body content. A cell starting with `=`, `+`, `-` or `@` is prefixed with `'` so a spreadsheet does not run it as a formula. A rejected filter (400) does not count against the hourly cap, which is separate from the sent-email export's.
2238
+
2239
+ - Params: `Mail.ReceivedExportParams`
2240
+
2241
+ | Status | Error class | Codes | When |
2242
+ |---|---|---|---|
2243
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter or cursor: a malformed uuid, boolean or date-time, `q` over 200 characters, `received_after` not earlier than `received_before`, or a window wider than 30 days. The reason is in `message`. |
2244
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2245
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.list` on every mailbox, or Mail is not enabled for this workspace. |
2246
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2247
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Another received-email export is already running, or this workspace has hit the hourly cap (5). Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2248
+
2249
+ ```ts
2250
+ const response = await mail.received.export();
2251
+ const text = await response.text();
2252
+ ```
2253
+
2254
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-received-export)
2255
+
2256
+ ### `mail.received.list(params?, options?)`
2257
+
2258
+ `GET /v1/workspaces/{workspace_id}/received` → `PagePromise<Mail.ReceivedEmail>`
2259
+
2260
+ List received email across every mailbox in the workspace.
2261
+
2262
+ Newest first, cursor-paged, deleted email excluded. Filters combine with AND: `q` (sender, subject, envelope recipient or To address as a substring; or an exact id or Message-ID), `mailbox_id`, `domain_id`, `received_after` / `received_before` (at most 30 days apart), `spam`, `unread`. `GET /received/export` takes exactly the same filters and returns the same rows. No body content.
2263
+
2264
+ - Params: `Mail.ReceivedListParams`
2265
+ - Paginated: `for await` iterates every item; `await` gives one page
2266
+
2267
+ | Status | Error class | Codes | When |
2268
+ |---|---|---|---|
2269
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter or cursor: a malformed uuid, boolean or date-time, `q` over 200 characters, `received_after` not earlier than `received_before`, or a window wider than 30 days. The reason is in `message`. |
2270
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2271
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.list` on every mailbox, or Mail is not enabled for this workspace. |
2272
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2273
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2274
+
2275
+ ```ts
2276
+ for await (const received of mail.received.list()) {
2277
+ console.log(received);
2278
+ }
2279
+ const page = await mail.received.list({}, { timeout: 5_000 });
2280
+ ```
2281
+
2282
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-received)
2283
+
2284
+ ### `mail.received.raw(id, options?)`
2285
+
2286
+ `GET /v1/workspaces/{workspace_id}/received/{id}/raw` → `Response`
2287
+
2288
+ Download the raw RFC 5322 message (.eml).
2289
+
2290
+ Every header, the full body and every attachment inline. Gated by `mail.message.body.read` (admin and up by default), like `/mailboxes/{id}/messages/{mid}/raw`.
2291
+
2292
+ | Status | Error class | Codes | When |
2293
+ |---|---|---|---|
2294
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `id` is not a uuid. |
2295
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2296
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.message.body.read`. |
2297
+ | 404 | `NotFoundError` | `NOT_FOUND` | No such email in this workspace (or it was deleted). |
2298
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2299
+
2300
+ ```ts
2301
+ const response = await mail.received.raw("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2302
+ const text = await response.text();
2303
+ ```
2304
+
2305
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-received-id-raw)
2306
+
2307
+ ### `mail.received.read(id, options?)`
2308
+
2309
+ `POST /v1/workspaces/{workspace_id}/received/{id}/read` → `void`
2310
+
2311
+ Mark a received email as read.
2312
+
2313
+ | Status | Error class | Codes | When |
2314
+ |---|---|---|---|
2315
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `id` is not a uuid. |
2316
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2317
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.read` on every mailbox. |
2318
+ | 404 | `NotFoundError` | `NOT_FOUND` | No such email in this workspace (or it was deleted). |
2319
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2320
+
2321
+ ```ts
2322
+ await mail.received.read("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2323
+ ```
2324
+
2325
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-received-id-read)
2326
+
2327
+ ### `mail.received.retrieve(id, options?)`
2328
+
2329
+ `GET /v1/workspaces/{workspace_id}/received/{id}` → `Mail.ReceivedEmailDetail`
2330
+
2331
+ Get one received email.
2332
+
2333
+ Metadata, the authentication results, the envelope recipient and the attachment manifest. `body_preview` is null without `mail.message.body.read`; the full message is at `/received/{id}/raw`.
2334
+
2335
+ | Status | Error class | Codes | When |
2336
+ |---|---|---|---|
2337
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `id` is not a uuid. |
2338
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2339
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.read` on every mailbox. |
2340
+ | 404 | `NotFoundError` | `NOT_FOUND` | No such email in this workspace (or it was deleted). |
2341
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2342
+
2343
+ ```ts
2344
+ const receivedEmailDetail = await mail.received.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2345
+ ```
2346
+
2347
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-received-id)
2348
+
2349
+ ## received.attachments
2350
+
2351
+ ### `mail.received.attachments.retrieve(id, position, options?)`
2352
+
2353
+ `GET /v1/workspaces/{workspace_id}/received/{id}/attachments/{position}` → `Response`
2354
+
2355
+ Download one attachment of a received email.
2356
+
2357
+ By its `position` from the detail's attachment manifest. Unscanned, untrusted bytes from a third party. Gated by `mail.message.body.read`.
2358
+
2359
+ | Status | Error class | Codes | When |
2360
+ |---|---|---|---|
2361
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `id` is not a uuid or `position` is not an integer. |
2362
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2363
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The policy does not grant `mail.message.body.read`. |
2364
+ | 404 | `NotFoundError` | `NOT_FOUND` | No such email in this workspace, or no attachment at that position. |
2365
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2366
+
2367
+ ```ts
2368
+ const response = await mail.received.attachments.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "position");
2369
+ const text = await response.text();
2370
+ ```
2371
+
2372
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-received-id-attachments-position)
2373
+
2374
+ ## segments
2375
+
2376
+ ### `mail.segments.create(params, options?)`
2377
+
2378
+ `POST /v1/workspaces/{workspace_id}/segments` → `Mail.Segment`
2379
+
2380
+ Create a segment.
2381
+
2382
+ Without `rule`, a static segment: add and remove contacts one at a time. With `rule`, a rule segment: its members are the contacts whose fields and properties match, evaluated every time it is read. At most 100 segments per workspace.
2383
+
2384
+ - Params: `Mail.SegmentCreateParams`
2385
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2386
+
2387
+ | Status | Error class | Codes | When |
2388
+ |---|---|---|---|
2389
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2390
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2391
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2392
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2393
+ | 409 | `ConflictError` | `CONFLICT`, `SEGMENT_EXISTS`, `SEGMENT_LIMIT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | A segment with this name exists (`SEGMENT_EXISTS`), the workspace has 100 segments (`SEGMENT_LIMIT`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2394
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `INVALID_SEGMENT_RULE` | The rule is invalid (`INVALID_SEGMENT_RULE`; `details.problems` lists each one with its condition index): an unknown field, `contains` on a number property, a missing or mistyped `value`, a `value` on `exists`. |
2395
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2396
+
2397
+ ```ts
2398
+ const segment = await mail.segments.create({ name: "Pro customers" });
2399
+ ```
2400
+
2401
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-segments)
2402
+
2403
+ ### `mail.segments.delete(segmentId, options?)`
2404
+
2405
+ `DELETE /v1/workspaces/{workspace_id}/segments/{segment_id}` → `void`
2406
+
2407
+ Delete a segment.
2408
+
2409
+ Contacts are not touched; a static segment's member list goes with it.
2410
+
2411
+ | Status | Error class | Codes | When |
2412
+ |---|---|---|---|
2413
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2414
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2415
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2416
+ | 404 | `NotFoundError` | `NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such segment (`SEGMENT_NOT_FOUND`). |
2417
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2418
+
2419
+ ```ts
2420
+ await mail.segments.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2421
+ ```
2422
+
2423
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-segments-segment-id)
2424
+
2425
+ ### `mail.segments.list(params?, options?)`
2426
+
2427
+ `GET /v1/workspaces/{workspace_id}/segments` → `PagePromise<Mail.Segment>`
2428
+
2429
+ List segments, newest first.
2430
+
2431
+ - Params: `Mail.SegmentListParams`
2432
+ - Paginated: `for await` iterates every item; `await` gives one page
2433
+
2434
+ | Status | Error class | Codes | When |
2435
+ |---|---|---|---|
2436
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2437
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2438
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2439
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2440
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2441
+
2442
+ ```ts
2443
+ for await (const segment of mail.segments.list()) {
2444
+ console.log(segment);
2445
+ }
2446
+ const page = await mail.segments.list({}, { timeout: 5_000 });
2447
+ ```
2448
+
2449
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-segments)
2450
+
2451
+ ### `mail.segments.metrics(segmentId, options?)`
2452
+
2453
+ `GET /v1/workspaces/{workspace_id}/segments/{segment_id}/metrics` → `Mail.SegmentMetrics`
2454
+
2455
+ A segment's size and unsubscribed share.
2456
+
2457
+ Counted on request.
2458
+
2459
+ | Status | Error class | Codes | When |
2460
+ |---|---|---|---|
2461
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2462
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2463
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2464
+ | 404 | `NotFoundError` | `NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such segment (`SEGMENT_NOT_FOUND`). |
2465
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2466
+
2467
+ ```ts
2468
+ const segmentMetrics = await mail.segments.metrics("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2469
+ ```
2470
+
2471
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-segments-segment-id-metrics)
2472
+
2473
+ ### `mail.segments.preview(params, options?)`
2474
+
2475
+ `POST /v1/workspaces/{workspace_id}/segments/preview` → `Mail.SegmentPreview`
2476
+
2477
+ Preview a segment rule.
2478
+
2479
+ Counts the contacts a rule selects and returns the newest few, without saving anything.
2480
+
2481
+ - Params: `Mail.SegmentPreviewParams`
2482
+
2483
+ | Status | Error class | Codes | When |
2484
+ |---|---|---|---|
2485
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2486
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2487
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2488
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2489
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `INVALID_SEGMENT_RULE` | The rule is invalid (`INVALID_SEGMENT_RULE`; `details.problems` lists each one with its condition index): an unknown field, `contains` on a number property, a missing or mistyped `value`, a `value` on `exists`. |
2490
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2491
+
2492
+ ```ts
2493
+ const segmentPreview = await mail.segments.preview({ rule: { conditions: [{ field: "plan", operator: "equals" }], match: "all" } });
2494
+ ```
2495
+
2496
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-segments-preview)
2497
+
2498
+ ### `mail.segments.retrieve(segmentId, options?)`
2499
+
2500
+ `GET /v1/workspaces/{workspace_id}/segments/{segment_id}` → `Mail.Segment`
2501
+
2502
+ Get a segment.
2503
+
2504
+ | Status | Error class | Codes | When |
2505
+ |---|---|---|---|
2506
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2507
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2508
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2509
+ | 404 | `NotFoundError` | `NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such segment (`SEGMENT_NOT_FOUND`). |
2510
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2511
+
2512
+ ```ts
2513
+ const segment = await mail.segments.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
2514
+ ```
2515
+
2516
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-segments-segment-id)
2517
+
2518
+ ### `mail.segments.update(segmentId, params?, options?)`
2519
+
2520
+ `PATCH /v1/workspaces/{workspace_id}/segments/{segment_id}` → `Mail.Segment`
2521
+
2522
+ Update a segment.
2523
+
2524
+ Rename it, or change a rule segment's rule. A segment's type (static or rule) is fixed.
2525
+
2526
+ - Params: `Mail.SegmentUpdateParams`
2527
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2528
+
2529
+ | Status | Error class | Codes | When |
2530
+ |---|---|---|---|
2531
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2532
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2533
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2534
+ | 404 | `NotFoundError` | `NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such segment (`SEGMENT_NOT_FOUND`). |
2535
+ | 409 | `ConflictError` | `CONFLICT`, `SEGMENT_EXISTS`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Another segment has this name (`SEGMENT_EXISTS`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2536
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `INVALID_SEGMENT_RULE`, `SEGMENT_TYPE_FIXED` | The rule is invalid (`INVALID_SEGMENT_RULE`; `details.problems` lists each one with its condition index): an unknown field, `contains` on a number property, a missing or mistyped `value`, a `value` on `exists`. Or a rule on a static segment (`SEGMENT_TYPE_FIXED`). |
2537
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2538
+
2539
+ ```ts
2540
+ const segment = await mail.segments.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { description: "string" });
2541
+ ```
2542
+
2543
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-segments-segment-id)
2544
+
2545
+ ## segments.contacts
2546
+
2547
+ ### `mail.segments.contacts.list(segmentId, params?, options?)`
2548
+
2549
+ `GET /v1/workspaces/{workspace_id}/segments/{segment_id}/contacts` → `PagePromise<Mail.Contact>`
2550
+
2551
+ List a segment's contacts.
2552
+
2553
+ Newest contact first. For a rule segment, the contacts matching its rule right now.
2554
+
2555
+ - Params: `Mail.SegmentContactListParams`
2556
+ - Paginated: `for await` iterates every item; `await` gives one page
2557
+
2558
+ | Status | Error class | Codes | When |
2559
+ |---|---|---|---|
2560
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2561
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2562
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2563
+ | 404 | `NotFoundError` | `NOT_FOUND`, `SEGMENT_NOT_FOUND` | No such segment (`SEGMENT_NOT_FOUND`). |
2564
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2565
+
2566
+ ```ts
2567
+ for await (const contact of mail.segments.contacts.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b")) {
2568
+ console.log(contact);
2569
+ }
2570
+ const page = await mail.segments.contacts.list("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", {}, { timeout: 5_000 });
2571
+ ```
2572
+
2573
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-segments-segment-id-contacts)
2574
+
2575
+ ## sendingDefaults
2576
+
2577
+ ### `mail.sendingDefaults.retrieve(options?)`
2578
+
2579
+ `GET /v1/workspaces/{workspace_id}/sending-defaults` → `Mail.SendingDefaults`
2580
+
2581
+ Get the workspace default sender.
2582
+
2583
+ The From used by template sends that name none, with the live verification status of its domain. `default_sender` is null when none is set.
2584
+
2585
+ | Status | Error class | Codes | When |
2586
+ |---|---|---|---|
2587
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2588
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2589
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2590
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2591
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2592
+
2593
+ ```ts
2594
+ const sendingDefaults = await mail.sendingDefaults.retrieve();
2595
+ ```
2596
+
2597
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-sending-defaults)
2598
+
2599
+ ### `mail.sendingDefaults.update(params, options?)`
2600
+
2601
+ `PUT /v1/workspaces/{workspace_id}/sending-defaults` → `Mail.SendingDefaults`
2602
+
2603
+ Set or clear the workspace default sender.
2604
+
2605
+ Replaces the default sender (`address: null` clears it). The change applies to the next send of every product at once, and emits a `sending_defaults.updated` webhook event. Repeating the same request changes nothing, so it takes no `Idempotency-Key`.
2606
+
2607
+ - Params: `Mail.SendingDefaultUpdateParams`
2608
+
2609
+ | Status | Error class | Codes | When |
2610
+ |---|---|---|---|
2611
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2612
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2613
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2614
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2615
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `sender_domain_not_in_workspace`, `INVALID_FROM_ADDRESS` | `sender_domain_not_in_workspace` (`details: { fqdn }`): the address is not on a sending domain of this workspace (unknown, another workspace's, or receive-only). `INVALID_FROM_ADDRESS`: the address could not be parsed. |
2616
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2617
+
2618
+ ```ts
2619
+ const sendingDefaults = await mail.sendingDefaults.update({ address: "Acme <hello@acme.com>" });
2620
+ ```
2621
+
2622
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-sending-defaults)
2623
+
2624
+ ## smtpConfig
2625
+
2626
+ ### `mail.smtpConfig.list(options?)`
2627
+
2628
+ `GET /v1/workspaces/{workspace_id}/smtp-configs` → `Mail.ListSmtpConfigs`
2629
+
2630
+ List every SMTP config in the workspace (default + per-domain overrides).
2631
+
2632
+ | Status | Error class | Codes | When |
2633
+ |---|---|---|---|
2634
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2635
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2636
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2637
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2638
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2639
+
2640
+ ```ts
2641
+ const listSmtpConfigs = await mail.smtpConfig.list();
2642
+ ```
2643
+
2644
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-smtp-configs)
2645
+
2646
+ ### `mail.smtpConfig.reset(options?)`
2647
+
2648
+ `DELETE /v1/workspaces/{workspace_id}/smtp-config` → `void`
2649
+
2650
+ Revert the workspace to ProductCraft Mail's own sending.
2651
+
2652
+ | Status | Error class | Codes | When |
2653
+ |---|---|---|---|
2654
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2655
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2656
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2657
+ | 404 | `NotFoundError` | `NOT_FOUND` | No SMTP config to delete |
2658
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2659
+
2660
+ ```ts
2661
+ await mail.smtpConfig.reset();
2662
+ ```
2663
+
2664
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-smtp-config)
2665
+
2666
+ ### `mail.smtpConfig.retrieve(options?)`
2667
+
2668
+ `GET /v1/workspaces/{workspace_id}/smtp-config` → `Mail.GetSmtpConfig`
2669
+
2670
+ Get the workspace-default SMTP config (returns null if unset). Per-domain overrides live under /workspaces/:id/domains/:domainId/smtp-config.
2671
+
2672
+ | Status | Error class | Codes | When |
2673
+ |---|---|---|---|
2674
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2675
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2676
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2677
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2678
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2679
+
2680
+ ```ts
2681
+ const getSmtpConfig = await mail.smtpConfig.retrieve();
2682
+ ```
2683
+
2684
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-smtp-config)
2685
+
2686
+ ### `mail.smtpConfig.update(params, options?)`
2687
+
2688
+ `PUT /v1/workspaces/{workspace_id}/smtp-config` → `Mail.SmtpConfig`
2689
+
2690
+ Create or update the workspace-default SMTP config (resets verification).
2691
+
2692
+ - Params: `Mail.SmtpConfigUpdateParams`
2693
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2694
+
2695
+ | Status | Error class | Codes | When |
2696
+ |---|---|---|---|
2697
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure or username/password mismatch. |
2698
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2699
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2700
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2701
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2702
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2703
+
2704
+ ```ts
2705
+ const smtpConfig = await mail.smtpConfig.update({
2706
+ from_email: "noreply@acme.com",
2707
+ from_name: "Acme Notifications",
2708
+ host: "smtp.sendgrid.net",
2709
+ port: 587,
2710
+ secure: false,
2711
+ });
2712
+ ```
2713
+
2714
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#put-v1-workspaces-workspace-id-smtp-config)
2715
+
2716
+ ### `mail.smtpConfig.verify(params, options?)`
2717
+
2718
+ `POST /v1/workspaces/{workspace_id}/smtp-config/verify` → `Mail.SmtpVerifyResult`
2719
+
2720
+ Test arbitrary SMTP creds without persisting.
2721
+
2722
+ - Params: `Mail.SmtpConfigVerifyParams`
2723
+
2724
+ | Status | Error class | Codes | When |
2725
+ |---|---|---|---|
2726
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure on the request body. |
2727
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2728
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2729
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2730
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2731
+
2732
+ ```ts
2733
+ const smtpVerifyResult = await mail.smtpConfig.verify({
2734
+ from_email: "noreply@acme.com",
2735
+ from_name: "Acme Notifications",
2736
+ host: "smtp.sendgrid.net",
2737
+ port: 587,
2738
+ secure: false,
2739
+ });
2740
+ ```
2741
+
2742
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-smtp-config-verify)
2743
+
2744
+ ### `mail.smtpConfig.verifySaved(options?)`
2745
+
2746
+ `POST /v1/workspaces/{workspace_id}/smtp-config/verify-saved` → `Mail.SmtpVerifyResult`
2747
+
2748
+ Test the persisted workspace-default SMTP config; updates status.
2749
+
2750
+ | Status | Error class | Codes | When |
2751
+ |---|---|---|---|
2752
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2753
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2754
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2755
+ | 404 | `NotFoundError` | `NOT_FOUND` | No SMTP config saved |
2756
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2757
+
2758
+ ```ts
2759
+ const smtpVerifyResult = await mail.smtpConfig.verifySaved();
2760
+ ```
2761
+
2762
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-smtp-config-verify-saved)
2763
+
2764
+ ## stats
2765
+
2766
+ ### `mail.stats.retrieve(params?, options?)`
2767
+
2768
+ `GET /v1/workspaces/{workspace_id}/stats` → `Mail.MailWorkspaceStats`
2769
+
2770
+ Mail stats for a workspace.
2771
+
2772
+ Outbound sent, delivered, bounced, complained, deferred, suppressed and unconfirmed counts in the window, the deliverability, bounce and complaint rates, domain totals, the last send time and a daily sent series. For ranges, buckets, filters and a per-domain breakdown use `GET /emails/metrics`; the rates here use its definitions.
2773
+
2774
+ `delivered` means the recipient's mail server confirmed the message; `unconfirmed` means the message was handed off and nothing came back either way, which is not the same as delivered. Days are UTC. Rates are 0 to 1, or null when not measured: delivery and complaints are only reported for mail with delivery tracking. Outbound messages are kept for 30 days, so 30d is the longest window. Cached for up to 60 seconds (`generated_at`).
2775
+
2776
+ - Params: `Mail.StatsRetrieveParams`
2777
+
2778
+ | Status | Error class | Codes | When |
2779
+ |---|---|---|---|
2780
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `window` is not one of 1d, 7d, 30d. |
2781
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2782
+ | 403 | `PermissionDeniedError` | `FORBIDDEN`, `SERVICE_NOT_ENABLED` | Caller lacks `mail.message.read` on the workspace (including non-members), or Mail is not enabled on it (`SERVICE_NOT_ENABLED`). |
2783
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. |
2784
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2785
+
2786
+ ```ts
2787
+ const mailWorkspaceStats = await mail.stats.retrieve();
2788
+ ```
2789
+
2790
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-stats)
2791
+
2792
+ ## suppression
2793
+
2794
+ ### `mail.suppression.create(params, options?)`
2795
+
2796
+ `POST /v1/workspaces/{workspace_id}/suppression` → `Mail.SuppressionEntry`
2797
+
2798
+ Add an email to the workspace suppression list.
2799
+
2800
+ - Params: `Mail.SuppressionCreateParams`
2801
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2802
+
2803
+ | Status | Error class | Codes | When |
2804
+ |---|---|---|---|
2805
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid payload (missing or malformed email). |
2806
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2807
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2808
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2809
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2810
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Email address rejected as invalid by the service layer. |
2811
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2812
+
2813
+ ```ts
2814
+ const suppressionEntry = await mail.suppression.create({ email: "bounced@example.com" });
2815
+ ```
2816
+
2817
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-suppression)
2818
+
2819
+ ### `mail.suppression.delete(email, options?)`
2820
+
2821
+ `DELETE /v1/workspaces/{workspace_id}/suppression/{email}` → `void`
2822
+
2823
+ Remove an address from the workspace suppression list.
2824
+
2825
+ | Status | Error class | Codes | When |
2826
+ |---|---|---|---|
2827
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2828
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2829
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2830
+ | 404 | `NotFoundError` | `NOT_FOUND` | Email is not on this workspace suppression list. |
2831
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2832
+
2833
+ ```ts
2834
+ await mail.suppression.delete("email");
2835
+ ```
2836
+
2837
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-suppression-email)
2838
+
2839
+ ### `mail.suppression.list(params?, options?)`
2840
+
2841
+ `GET /v1/workspaces/{workspace_id}/suppression` → `PagePromise<Mail.SuppressionEntry>`
2842
+
2843
+ List suppressed recipients in the workspace (most-recent first), cursor-paginated.
2844
+
2845
+ - Params: `Mail.SuppressionListParams`
2846
+ - Paginated: `for await` iterates every item; `await` gives one page
2847
+
2848
+ | Status | Error class | Codes | When |
2849
+ |---|---|---|---|
2850
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor, search term, or source filter. |
2851
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2852
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2853
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2854
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2855
+
2856
+ ```ts
2857
+ for await (const suppression of mail.suppression.list()) {
2858
+ console.log(suppression);
2859
+ }
2860
+ const page = await mail.suppression.list({}, { timeout: 5_000 });
2861
+ ```
2862
+
2863
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-suppression)
2864
+
2865
+ ## templates
2866
+
2867
+ ### `mail.templates.create(params, options?)`
2868
+
2869
+ `POST /v1/workspaces/{workspace_id}/templates` → `Mail.Template`
2870
+
2871
+ Create or update a template, layout, or partial. Idempotent on (workspace_id, name); `kind` cannot change after the first upsert.
2872
+
2873
+ - Params: `Mail.TemplateCreateParams`
2874
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
2875
+
2876
+ | Status | Error class | Codes | When |
2877
+ |---|---|---|---|
2878
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure (bad name / payload). |
2879
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2880
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2881
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2882
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
2883
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2884
+
2885
+ ```ts
2886
+ const template = await mail.templates.create({ body_html: "string", name: "order-confirmation", subject: "string" });
2887
+ ```
2888
+
2889
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates)
2890
+
2891
+ ### `mail.templates.delete(name, options?)`
2892
+
2893
+ `DELETE /v1/workspaces/{workspace_id}/templates/{name}` → `void`
2894
+
2895
+ Delete a template, layout, or partial by name.
2896
+
2897
+ | Status | Error class | Codes | When |
2898
+ |---|---|---|---|
2899
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2900
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2901
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2902
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
2903
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2904
+
2905
+ ```ts
2906
+ await mail.templates.delete("name");
2907
+ ```
2908
+
2909
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-templates-name)
2910
+
2911
+ ### `mail.templates.lint(params, options?)`
2912
+
2913
+ `POST /v1/workspaces/{workspace_id}/templates/lint` → `Mail.LintResult`
2914
+
2915
+ Lint a draft for deliverability red flags. Same render pipeline as /preview; returns a 0-100 score (higher is better) and a sorted list of findings (errors first). Self-developed heuristic linter, no external classifier — runs in-process.
2916
+
2917
+ - Params: `Mail.TemplateLintParams`
2918
+
2919
+ | Status | Error class | Codes | When |
2920
+ |---|---|---|---|
2921
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2922
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2923
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2924
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2925
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Underlying render failed. |
2926
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2927
+
2928
+ ```ts
2929
+ const lintResult = await mail.templates.lint({ body_html: "string" });
2930
+ ```
2931
+
2932
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-lint)
2933
+
2934
+ ### `mail.templates.list(params?, options?)`
2935
+
2936
+ `GET /v1/workspaces/{workspace_id}/templates` → `PagePromise<Mail.Template>`
2937
+
2938
+ List templates / layouts / partials in a workspace, name-ascending and cursor-paginated. Filter via ?kind=template|layout|partial (default: all), ?tags=foo,bar (comma-separated), ?search=<substring of the name>.
2939
+
2940
+ - Params: `Mail.TemplateListParams`
2941
+ - Paginated: `for await` iterates every item; `await` gives one page
2942
+
2943
+ | Status | Error class | Codes | When |
2944
+ |---|---|---|---|
2945
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid cursor or search term. |
2946
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2947
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2948
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2949
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2950
+
2951
+ ```ts
2952
+ for await (const template of mail.templates.list()) {
2953
+ console.log(template);
2954
+ }
2955
+ const page = await mail.templates.list({}, { timeout: 5_000 });
2956
+ ```
2957
+
2958
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-templates)
2959
+
2960
+ ### `mail.templates.preview(params, options?)`
2961
+
2962
+ `POST /v1/workspaces/{workspace_id}/templates/preview` → `Mail.RenderedMessage`
2963
+
2964
+ Preview an unsaved draft. Renders subject + bodyHtml against the workspace brand tokens + supplied data, with all workspace layouts/partials registered. Used by the editor for live preview.
2965
+
2966
+ - Params: `Mail.TemplatePreviewParams`
2967
+
2968
+ | Status | Error class | Codes | When |
2969
+ |---|---|---|---|
2970
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2971
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2972
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2973
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
2974
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Render failed (compile or runtime error). |
2975
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
2976
+
2977
+ ```ts
2978
+ const renderedMessage = await mail.templates.preview({ body_html: "string" });
2979
+ ```
2980
+
2981
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-preview)
2982
+
2983
+ ### `mail.templates.render(name, params, options?)`
2984
+
2985
+ `POST /v1/workspaces/{workspace_id}/templates/{name}/render` → `Mail.RenderedMessage`
2986
+
2987
+ Render a saved template against a data payload. Returns subject + html + text without sending.
2988
+
2989
+ - Params: `Mail.TemplateRenderParams`
2990
+
2991
+ | Status | Error class | Codes | When |
2992
+ |---|---|---|---|
2993
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
2994
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
2995
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
2996
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
2997
+ | 409 | `ConflictError` | `CONFLICT` | Cannot render a layout/partial — only sendable templates. |
2998
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | Render or schema validation failed. |
2999
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3000
+
3001
+ ```ts
3002
+ const renderedMessage = await mail.templates.render("name", { data: { name: "Alice", order_id: "ord_123" } });
3003
+ ```
3004
+
3005
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-name-render)
3006
+
3007
+ ### `mail.templates.retrieve(name, options?)`
3008
+
3009
+ `GET /v1/workspaces/{workspace_id}/templates/{name}` → `Mail.Template`
3010
+
3011
+ Get a template, layout, or partial by name.
3012
+
3013
+ | Status | Error class | Codes | When |
3014
+ |---|---|---|---|
3015
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3016
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3017
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3018
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
3019
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3020
+
3021
+ ```ts
3022
+ const template = await mail.templates.retrieve("name");
3023
+ ```
3024
+
3025
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-templates-name)
3026
+
3027
+ ### `mail.templates.send(name, params, options?)`
3028
+
3029
+ `POST /v1/workspaces/{workspace_id}/templates/{name}/send` → `Mail.SendAccepted`
3030
+
3031
+ Send a template-rendered message. Pass an `Idempotency-Key` header (1..256 chars, [A-Za-z0-9_\-:]) to make retries safe — replays return the original response with `Idempotent-Replay: true` for 24h. Same key + different request body returns 409 IDEMPOTENCY_KEY_REUSE.
3032
+
3033
+ - Params: `Mail.TemplateSendParams`
3034
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3035
+
3036
+ | Status | Error class | Codes | When |
3037
+ |---|---|---|---|
3038
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure (malformed body or recipient). |
3039
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3040
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3041
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
3042
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | IDEMPOTENCY_KEY_REUSE — same key, different body. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3043
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `RECIPIENT_SUPPRESSED`, `TOPIC_NOT_FOUND`, `from_domain_not_verified`, `domain_not_verified_by_provider`, `no_default_sender` | Recipient suppressed (`RECIPIENT_SUPPRESSED`; with a `topic_id`, or a template rendering `{{{unsubscribe_url}}}`, also when the recipient unsubscribed or never opted in: `details.reason` is `unsubscribe` or `no_consent`), `TOPIC_NOT_FOUND`, render failure, or invalid from address; or `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain of the From address is not itself an active sending domain of this workspace — `reason: not_registered` (unknown, registered to another workspace, or only a subdomain such as `mail.example.com` is verified; a verified subdomain does not cover its parent) or `reason: not_verified` (registered, DNS not verified yet). Refused before anything is recorded or queued; or `domain_not_verified_by_provider` (body `code`): the from-domain's DNS records have been broken for over 72 h (`health: failing`), so the email provider no longer sends for it; or `no_default_sender` (body `code`): the body has no `from` and the workspace has no default sender set (nothing is queued; an `email.skipped` webhook event is logged). |
3044
+ | 429 | `RateLimitError` | `RATE_LIMITED` | Over a workspace send limit (60 per minute, 600 per hour, 10,000 per day): `RATE_LIMITED` with `details: { window, limit, retry_after }`. Nothing is recorded or queued. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3045
+
3046
+ ```ts
3047
+ const sendAccepted = await mail.templates.send("name", { data: { name: "Alice", order_id: "ord_123" }, to: "user@example.com" });
3048
+ ```
3049
+
3050
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-name-send)
3051
+
3052
+ ### `mail.templates.sendBatch(name, params, options?)`
3053
+
3054
+ `POST /v1/workspaces/{workspace_id}/templates/{name}/send-batch` → `Mail.SendBatchOpenApi`
3055
+
3056
+ Send up to 100 messages with the same template in one request. Per-row partial-success: each row carries its own status (`accepted` | `rejected`).
3057
+
3058
+ Two ways to make retries safe. An `Idempotency-Key` header covers the whole request: a retry with the same key and body within 24 h returns the first response (with `Idempotent-Replay: true`) and sends nothing. A per-row `idempotency_key` covers one message: a later batch, or a `/send`, that repeats a row key replays that row instead of sending it again. Both share one dedupe namespace, so the header key must differ from every row key (400 otherwise).
3059
+
3060
+ - Params: `Mail.TemplateSendBatchParams`
3061
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3062
+
3063
+ | Status | Error class | Codes | When |
3064
+ |---|---|---|---|
3065
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `IDEMPOTENCY_KEY_CONFLICTS_WITH_ROW` | Validation failure (malformed body, batch too large), or the `Idempotency-Key` header equals a row `idempotency_key` (`IDEMPOTENCY_KEY_CONFLICTS_WITH_ROW`). |
3066
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3067
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3068
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
3069
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different request body or on a different route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3070
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `from_domain_not_verified`, `domain_not_verified_by_provider` | For any row: `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain of the From address is not itself an active sending domain of this workspace — `reason: not_registered` (unknown, registered to another workspace, or only a subdomain such as `mail.example.com` is verified; a verified subdomain does not cover its parent) or `reason: not_verified` (registered, DNS not verified yet). Refused before anything is recorded or queued (the whole batch is refused); or `domain_not_verified_by_provider` (body `code`): the from-domain's DNS records have been broken for over 72 h (`health: failing`), so the email provider no longer sends for it. The whole batch is refused; no row is sent. |
3071
+ | 429 | `RateLimitError` | `RATE_LIMITED` | The workspace send limits (60 per minute, 600 per hour, 10,000 per day) never 429 a batch: a row over one is rejected in the 200 body with `RATE_LIMITED`, and the other rows are unaffected. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3072
+
3073
+ ```ts
3074
+ const sendBatchOpenApi = await mail.templates.sendBatch("name", {
3075
+ messages: [
3076
+ {
3077
+ data: { code: "A1B2C3", name: "Alice" },
3078
+ from: "Acme <noreply@acme.com>",
3079
+ to: "alice@example.com",
3080
+ },
3081
+ ],
3082
+ });
3083
+ ```
3084
+
3085
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-name-send-batch)
3086
+
3087
+ ### `mail.templates.testSend(name, params, options?)`
3088
+
3089
+ `POST /v1/workspaces/{workspace_id}/templates/{name}/test-send` → `Mail.SendAccepted`
3090
+
3091
+ Send a test rendering of the template to an arbitrary recipient. Bypasses the suppression list and the ordinary send limits (test sends have their own: 30 per hour per workspace); subject is auto-prefixed with [TEST]; missing variables render as [varname] placeholders.
3092
+
3093
+ - Params: `Mail.TemplateTestSendParams`
3094
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3095
+
3096
+ | Status | Error class | Codes | When |
3097
+ |---|---|---|---|
3098
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3099
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3100
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | From-address domain is not registered to this workspace. |
3101
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
3102
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Cannot test-send a layout/partial, or `IDEMPOTENCY_KEY_REUSE`: the `Idempotency-Key` was already used with a different body or route. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3103
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY`, `from_domain_not_verified`, `domain_not_verified_by_provider` | Invalid from address or render failure; or `from_domain_not_verified` (body `code`, `details: { fqdn, reason }`): the domain of the From address is not itself an active sending domain of this workspace — `reason: not_registered` (unknown, registered to another workspace, or only a subdomain such as `mail.example.com` is verified; a verified subdomain does not cover its parent) or `reason: not_verified` (registered, DNS not verified yet). Refused before anything is recorded or queued; or `domain_not_verified_by_provider` (body `code`): the from-domain's DNS records have been broken for over 72 h (`health: failing`), so the email provider no longer sends for it. |
3104
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 30 test sends this hour in this workspace: `RATE_LIMITED` with `details: { window, limit, retry_after }`. Per-IP limit: more than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3105
+
3106
+ ```ts
3107
+ const sendAccepted = await mail.templates.testSend("name", { from: "Acme <hello@acme.com>", to: "ada@example.com" });
3108
+ ```
3109
+
3110
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-templates-name-test-send)
3111
+
3112
+ ## templates.tags
3113
+
3114
+ ### `mail.templates.tags.list(options?)`
3115
+
3116
+ `GET /v1/workspaces/{workspace_id}/templates/tags` → `Mail.ListTemplateTags`
3117
+
3118
+ Distinct tag list across all templates in this workspace (for autocomplete).
3119
+
3120
+ | Status | Error class | Codes | When |
3121
+ |---|---|---|---|
3122
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3123
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3124
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3125
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3126
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3127
+
3128
+ ```ts
3129
+ const listTemplateTags = await mail.templates.tags.list();
3130
+ ```
3131
+
3132
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-templates-tags)
3133
+
3134
+ ## templates.variables
3135
+
3136
+ ### `mail.templates.variables.list(name, options?)`
3137
+
3138
+ `GET /v1/workspaces/{workspace_id}/templates/{name}/variables` → `Mail.TemplateVariables`
3139
+
3140
+ Variables auto-detected from the saved template body + subject (Handlebars expressions, dotted paths preserved, dedup'd in first-occurrence order).
3141
+
3142
+ | Status | Error class | Codes | When |
3143
+ |---|---|---|---|
3144
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3145
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3146
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3147
+ | 404 | `NotFoundError` | `NOT_FOUND` | Template not found in this workspace. |
3148
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3149
+
3150
+ ```ts
3151
+ const templateVariables = await mail.templates.variables.list("name");
3152
+ ```
3153
+
3154
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-templates-name-variables)
3155
+
3156
+ ## topics
3157
+
3158
+ ### `mail.topics.create(params, options?)`
3159
+
3160
+ `POST /v1/workspaces/{workspace_id}/topics` → `Mail.Topic`
3161
+
3162
+ Create a topic.
3163
+
3164
+ Send on a topic with `topic_id` on a template send. Recipients manage their topics on the hosted unsubscribe page. At most 50 topics per workspace.
3165
+
3166
+ - Params: `Mail.TopicCreateParams`
3167
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3168
+
3169
+ | Status | Error class | Codes | When |
3170
+ |---|---|---|---|
3171
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3172
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3173
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3174
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3175
+ | 409 | `ConflictError` | `CONFLICT`, `TOPIC_EXISTS`, `TOPIC_LIMIT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | A topic with this name exists (`TOPIC_EXISTS`), the workspace has 50 topics (`TOPIC_LIMIT`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3176
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3177
+
3178
+ ```ts
3179
+ const topic = await mail.topics.create({ default_subscription: "opt_in", name: "Product updates" });
3180
+ ```
3181
+
3182
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-topics)
3183
+
3184
+ ### `mail.topics.delete(topicId, options?)`
3185
+
3186
+ `DELETE /v1/workspaces/{workspace_id}/topics/{topic_id}` → `void`
3187
+
3188
+ Delete a topic.
3189
+
3190
+ Also deletes every contact's choice for it. Sends naming it are refused afterwards (`TOPIC_NOT_FOUND`).
3191
+
3192
+ | Status | Error class | Codes | When |
3193
+ |---|---|---|---|
3194
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3195
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3196
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3197
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TOPIC_NOT_FOUND` | No such topic (`TOPIC_NOT_FOUND`). |
3198
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3199
+
3200
+ ```ts
3201
+ await mail.topics.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
3202
+ ```
3203
+
3204
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-topics-topic-id)
3205
+
3206
+ ### `mail.topics.list(params?, options?)`
3207
+
3208
+ `GET /v1/workspaces/{workspace_id}/topics` → `PagePromise<Mail.Topic>`
3209
+
3210
+ List topics, newest first.
3211
+
3212
+ - Params: `Mail.TopicListParams`
3213
+ - Paginated: `for await` iterates every item; `await` gives one page
3214
+
3215
+ | Status | Error class | Codes | When |
3216
+ |---|---|---|---|
3217
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3218
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3219
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3220
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3221
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3222
+
3223
+ ```ts
3224
+ for await (const topic of mail.topics.list()) {
3225
+ console.log(topic);
3226
+ }
3227
+ const page = await mail.topics.list({}, { timeout: 5_000 });
3228
+ ```
3229
+
3230
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-topics)
3231
+
3232
+ ### `mail.topics.retrieve(topicId, options?)`
3233
+
3234
+ `GET /v1/workspaces/{workspace_id}/topics/{topic_id}` → `Mail.Topic`
3235
+
3236
+ Get a topic.
3237
+
3238
+ | Status | Error class | Codes | When |
3239
+ |---|---|---|---|
3240
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3241
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3242
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3243
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TOPIC_NOT_FOUND` | No such topic (`TOPIC_NOT_FOUND`). |
3244
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3245
+
3246
+ ```ts
3247
+ const topic = await mail.topics.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
3248
+ ```
3249
+
3250
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-topics-topic-id)
3251
+
3252
+ ### `mail.topics.update(topicId, params?, options?)`
3253
+
3254
+ `PATCH /v1/workspaces/{workspace_id}/topics/{topic_id}` → `Mail.Topic`
3255
+
3256
+ Update a topic.
3257
+
3258
+ `default_subscription` cannot change: flipping it would silently subscribe or unsubscribe everyone who never chose.
3259
+
3260
+ - Params: `Mail.TopicUpdateParams`
3261
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3262
+
3263
+ | Status | Error class | Codes | When |
3264
+ |---|---|---|---|
3265
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3266
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3267
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3268
+ | 404 | `NotFoundError` | `NOT_FOUND`, `TOPIC_NOT_FOUND` | No such topic (`TOPIC_NOT_FOUND`). |
3269
+ | 409 | `ConflictError` | `CONFLICT`, `TOPIC_EXISTS`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Another topic has this name (`TOPIC_EXISTS`), or `IDEMPOTENCY_KEY_REUSE`. Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3270
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3271
+
3272
+ ```ts
3273
+ const topic = await mail.topics.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { description: "string" });
3274
+ ```
3275
+
3276
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-topics-topic-id)
3277
+
3278
+ ## webhooks
3279
+
3280
+ ### `mail.webhooks.unwrap({ payload, headers, secret? })`
3281
+
3282
+ Verifies the signature and returns `Mail.UnwrapWebhookEvent`. Events: `contact.created`, `contact.deleted`, `contact.updated`, `domain.created`, `domain.deleted`, `domain.updated`, `domain.verified`, `email.bounced`, `email.canceled`, `email.clicked`, `email.complained`, `email.delivered`, `email.delivery_delayed`, `email.failed`, `email.opened`, `email.received`, `email.scheduled`, `email.sent`, `email.skipped`, `email.suppressed`, `sending_defaults.updated`, `suppression.added`, `suppression.removed`, plus `UnknownWebhookEvent` for types added later.
3283
+
3284
+ ## webhooks.attempts
3285
+
3286
+ ### `mail.webhooks.attempts.list(params?, options?)`
3287
+
3288
+ `GET /v1/workspaces/{workspace_id}/webhooks/attempts` → `PagePromise<Mail.OutboundWebhookAttempt>`
3289
+
3290
+ Delivery attempts across all of this workspace's webhook endpoints, newest first, cursor-paginated, optionally for one event or one endpoint. Includes status code, latency, response body (truncated to 1KB), and error if any.
3291
+
3292
+ - Params: `Mail.WebhookAttemptListParams`
3293
+ - Paginated: `for await` iterates every item; `await` gives one page
3294
+
3295
+ | Status | Error class | Codes | When |
3296
+ |---|---|---|---|
3297
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid `limit`, `cursor`, `event_id` or `endpoint_id`. |
3298
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3299
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3300
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3301
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3302
+
3303
+ ```ts
3304
+ for await (const attempt of mail.webhooks.attempts.list()) {
3305
+ console.log(attempt);
3306
+ }
3307
+ const page = await mail.webhooks.attempts.list({}, { timeout: 5_000 });
3308
+ ```
3309
+
3310
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-attempts)
3311
+
3312
+ ## webhooks.endpoints
3313
+
3314
+ ### `mail.webhooks.endpoints.create(params, options?)`
3315
+
3316
+ `POST /v1/workspaces/{workspace_id}/webhooks/endpoints` → `Mail.WebhookEndpointWithSecret`
3317
+
3318
+ Add a webhook endpoint. Returns its signing secret once.
3319
+
3320
+ Deliveries are signed with the Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature` (`v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">`); the returned `whsec_…` secret works unchanged with any Standard Webhooks library. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h.
3321
+
3322
+ - Params: `Mail.WebhookEndpointCreateParams`
3323
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3324
+
3325
+ | Status | Error class | Codes | When |
3326
+ |---|---|---|---|
3327
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR`, `WEBHOOK_EVENT_TYPE_UNKNOWN` | Validation failure: URL not https with a public host, or an unknown event type (`WEBHOOK_EVENT_TYPE_UNKNOWN`). |
3328
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3329
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3330
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3331
+ | 409 | `ConflictError` | `CONFLICT`, `WEBHOOK_ENDPOINT_LIMIT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Already 10 endpoints (`WEBHOOK_ENDPOINT_LIMIT`), or Idempotency-Key reused with a different body (`IDEMPOTENCY_KEY_REUSE`). Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3332
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | The URL points at a private, loopback or internal address. |
3333
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3334
+
3335
+ ```ts
3336
+ const webhookEndpointWithSecret = await mail.webhooks.endpoints.create({ url: "https://api.acme.com/webhooks" });
3337
+ ```
3338
+
3339
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-webhooks-endpoints)
3340
+
3341
+ ### `mail.webhooks.endpoints.delete(endpointId, options?)`
3342
+
3343
+ `DELETE /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}` → `void`
3344
+
3345
+ Delete a webhook endpoint.
3346
+
3347
+ Queued retries are dropped. Its past attempts stay in the event log until the events expire.
3348
+
3349
+ | Status | Error class | Codes | When |
3350
+ |---|---|---|---|
3351
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `endpoint_id` is not a UUID. |
3352
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3353
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3354
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3355
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3356
+
3357
+ ```ts
3358
+ await mail.webhooks.endpoints.delete("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
3359
+ ```
3360
+
3361
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#delete-v1-workspaces-workspace-id-webhooks-endpoints-endpoint-id)
3362
+
3363
+ ### `mail.webhooks.endpoints.list(options?)`
3364
+
3365
+ `GET /v1/workspaces/{workspace_id}/webhooks/endpoints` → `PagePromise<Mail.WebhookEndpoint>`
3366
+
3367
+ List webhook endpoints, oldest first.
3368
+
3369
+ At most 10, so this is always one page. Signing secrets are never returned here, only their last 4 characters.
3370
+
3371
+ - Paginated: `for await` iterates every item; `await` gives one page
3372
+
3373
+ | Status | Error class | Codes | When |
3374
+ |---|---|---|---|
3375
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | The scope id in the path is malformed. |
3376
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3377
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3378
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3379
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3380
+
3381
+ ```ts
3382
+ for await (const endpoint of mail.webhooks.endpoints.list()) {
3383
+ console.log(endpoint);
3384
+ }
3385
+ ```
3386
+
3387
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-endpoints)
3388
+
3389
+ ### `mail.webhooks.endpoints.retrieve(endpointId, options?)`
3390
+
3391
+ `GET /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}` → `Mail.WebhookEndpoint`
3392
+
3393
+ Get one webhook endpoint (without its secret).
3394
+
3395
+ The endpoint's URL, subscription, on/off state, failure streak and the last 4 characters of its signing secret. The full secret is only returned when the endpoint is created or its secret is rotated.
3396
+
3397
+ | Status | Error class | Codes | When |
3398
+ |---|---|---|---|
3399
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `endpoint_id` is not a UUID. |
3400
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3401
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3402
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3403
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3404
+
3405
+ ```ts
3406
+ const webhookEndpoint = await mail.webhooks.endpoints.retrieve("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b");
3407
+ ```
3408
+
3409
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-endpoints-endpoint-id)
3410
+
3411
+ ### `mail.webhooks.endpoints.rotateSecret(endpointId, params?, options?)`
3412
+
3413
+ `POST /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/rotate-secret` → `Mail.WebhookEndpointWithSecret`
3414
+
3415
+ Rotate the signing secret. The new one is returned once.
3416
+
3417
+ For `grace_hours` (default 24) the previous secret keeps signing too: `webhook-signature` then lists one `v1,` signature per secret, so receivers can switch over without dropping a delivery.
3418
+
3419
+ - Params: `Mail.WebhookEndpointRotateSecretParams`
3420
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3421
+
3422
+ | Status | Error class | Codes | When |
3423
+ |---|---|---|---|
3424
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `grace_hours` out of range, or `endpoint_id` is not a UUID. |
3425
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3426
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3427
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3428
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Idempotency-Key reused with a different body (`IDEMPOTENCY_KEY_REUSE`). Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3429
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3430
+
3431
+ ```ts
3432
+ const webhookEndpointWithSecret = await mail.webhooks.endpoints.rotateSecret("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { grace_hours: 24 });
3433
+ ```
3434
+
3435
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-webhooks-endpoints-endpoint-id-rotate-secret)
3436
+
3437
+ ### `mail.webhooks.endpoints.test(endpointId, params, options?)`
3438
+
3439
+ `POST /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}/test` → `Mail.WebhookDeliveryAttempt`
3440
+
3441
+ Send a test event to this endpoint now and return the result.
3442
+
3443
+ Delivers the catalogue example of `event_type`, signed like any delivery, with `"test": true` in the body. Works while the endpoint is switched off or not subscribed to that type, is logged in the event log, and is never retried or counted toward an automatic disable. Waits up to 15 s for your receiver.
3444
+
3445
+ - Params: `Mail.WebhookEndpointTestParams`
3446
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3447
+
3448
+ | Status | Error class | Codes | When |
3449
+ |---|---|---|---|
3450
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Unknown `event_type`, or `endpoint_id` is not a UUID. |
3451
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3452
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3453
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3454
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Idempotency-Key reused with a different body (`IDEMPOTENCY_KEY_REUSE`). Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3455
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3456
+
3457
+ ```ts
3458
+ const webhookDeliveryAttempt = await mail.webhooks.endpoints.test("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { event_type: "email.sent" });
3459
+ ```
3460
+
3461
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-webhooks-endpoints-endpoint-id-test)
3462
+
3463
+ ### `mail.webhooks.endpoints.update(endpointId, params?, options?)`
3464
+
3465
+ `PATCH /v1/workspaces/{workspace_id}/webhooks/endpoints/{endpoint_id}` → `Mail.WebhookEndpoint`
3466
+
3467
+ Change a webhook endpoint: URL, description, events, on/off.
3468
+
3469
+ Only the fields sent change. `enabled: true` (or a new URL) also clears an automatic disable. Switching off cancels queued retries. The secret is unchanged; rotate it with `POST …/rotate-secret`.
3470
+
3471
+ - Params: `Mail.WebhookEndpointUpdateParams`
3472
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3473
+
3474
+ | Status | Error class | Codes | When |
3475
+ |---|---|---|---|
3476
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Validation failure, or `endpoint_id` is not a UUID. |
3477
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3478
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3479
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3480
+ | 409 | `ConflictError` | `CONFLICT`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | Idempotency-Key reused with a different body (`IDEMPOTENCY_KEY_REUSE`). Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3481
+ | 422 | `UnprocessableEntityError` | `UNPROCESSABLE_ENTITY` | The new URL points at a private, loopback or internal address. |
3482
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3483
+
3484
+ ```ts
3485
+ const webhookEndpoint = await mail.webhooks.endpoints.update("3f0c1a2b-4c5d-4e6f-8a9b-0c1d2e3f4a5b", { description: "Production receiver" });
3486
+ ```
3487
+
3488
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#patch-v1-workspaces-workspace-id-webhooks-endpoints-endpoint-id)
3489
+
3490
+ ## webhooks.eventTypes
3491
+
3492
+ ### `mail.webhooks.eventTypes.list(options?)`
3493
+
3494
+ `GET /v1/workspaces/{workspace_id}/webhooks/event-types` → `Mail.ListWebhookEventTypes`
3495
+
3496
+ List the events an endpoint can subscribe to.
3497
+
3498
+ Each type with a plain description, its group and an example `data` object (what "Send test" delivers).
3499
+
3500
+ | Status | Error class | Codes | When |
3501
+ |---|---|---|---|
3502
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | A resource id in the path is not a UUID, or the request failed validation (`BAD_REQUEST` / `VALIDATION_ERROR`). |
3503
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3504
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3505
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3506
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3507
+
3508
+ ```ts
3509
+ const listWebhookEventTypes = await mail.webhooks.eventTypes.list();
3510
+ ```
3511
+
3512
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-event-types)
3513
+
3514
+ ## webhooks.events
3515
+
3516
+ ### `mail.webhooks.events.list(params?, options?)`
3517
+
3518
+ `GET /v1/workspaces/{workspace_id}/webhooks/events` → `PagePromise<Mail.WebhookEvent>`
3519
+
3520
+ List webhook events, newest first (kept for 30 days).
3521
+
3522
+ Every event logged while at least one endpoint existed, with the latest state per endpoint and when the next retry is due.
3523
+
3524
+ - Params: `Mail.WebhookEventListParams`
3525
+ - Paginated: `for await` iterates every item; `await` gives one page
3526
+
3527
+ | Status | Error class | Codes | When |
3528
+ |---|---|---|---|
3529
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | Invalid filter value or cursor. |
3530
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3531
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3532
+ | 404 | `NotFoundError` | `NOT_FOUND` | Workspace not found: no workspace has this `workspaceId`, or it is not a UUID (`NOT_FOUND`). A workspace the caller is not a member of answers 403. Routes that read a resource also answer 404 when the resource is missing. |
3533
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3534
+
3535
+ ```ts
3536
+ for await (const event of mail.webhooks.events.list()) {
3537
+ console.log(event);
3538
+ }
3539
+ const page = await mail.webhooks.events.list({}, { timeout: 5_000 });
3540
+ ```
3541
+
3542
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-events)
3543
+
3544
+ ### `mail.webhooks.events.replay(eventId, params?, options?)`
3545
+
3546
+ `POST /v1/workspaces/{workspace_id}/webhooks/events/{event_id}/replay` → `Mail.ReplayWebhookEvent`
3547
+
3548
+ Send a logged event again, to its endpoints or to one endpoint.
3549
+
3550
+ Same body and `webhook-id`, freshly signed, to every switched-on endpoint that receives this event type now, or only to `endpoint_id`. Waits for the receivers (up to 15 s each) and returns the attempts. A success cancels a retry still queued for that endpoint.
3551
+
3552
+ - Params: `Mail.WebhookEventReplayParams`
3553
+ - Idempotent: an `Idempotency-Key` is sent automatically and reused on retries
3554
+
3555
+ | Status | Error class | Codes | When |
3556
+ |---|---|---|---|
3557
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `event_id` is not `evt_<uuid>`, or `endpoint_id` is not a UUID. |
3558
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3559
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3560
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_EVENT_NOT_FOUND`, `WEBHOOK_ENDPOINT_NOT_FOUND` | No such event here (`WEBHOOK_EVENT_NOT_FOUND`); events are kept for 30 days. Or No such endpoint here (`WEBHOOK_ENDPOINT_NOT_FOUND`). |
3561
+ | 409 | `ConflictError` | `CONFLICT`, `WEBHOOK_ENDPOINT_NOT_SUBSCRIBED`, `IDEMPOTENCY_KEY_REUSE`, `IDEMPOTENCY_IN_PROGRESS` | `endpoint_id` is switched off or does not receive this event type (`WEBHOOK_ENDPOINT_NOT_SUBSCRIBED`), or Idempotency-Key reused with a different body (`IDEMPOTENCY_KEY_REUSE`). Idempotency: `IDEMPOTENCY_KEY_REUSE` when this `Idempotency-Key` was already used with a different body or route; `IDEMPOTENCY_IN_PROGRESS` while another request with the same key is still running (retry after `Retry-After` seconds, with the same key). |
3562
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3563
+
3564
+ ```ts
3565
+ const replayWebhookEvent = await mail.webhooks.events.replay("event");
3566
+ ```
3567
+
3568
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#post-v1-workspaces-workspace-id-webhooks-events-event-id-replay)
3569
+
3570
+ ### `mail.webhooks.events.retrieve(eventId, options?)`
3571
+
3572
+ `GET /v1/workspaces/{workspace_id}/webhooks/events/{event_id}` → `Mail.WebhookEventDetail`
3573
+
3574
+ Get one webhook event with every delivery attempt.
3575
+
3576
+ The event as it was sent (`type`, `data`) and every delivery attempt to every endpoint, with its trigger, HTTP status, error and the start of the response body. Use it to debug a receiver, then `replay` to send it again.
3577
+
3578
+ | Status | Error class | Codes | When |
3579
+ |---|---|---|---|
3580
+ | 400 | `BadRequestError` | `BAD_REQUEST`, `VALIDATION_ERROR` | `event_id` is not `evt_<uuid>`. |
3581
+ | 401 | `AuthenticationError` | `UNAUTHORIZED` | Missing, expired or invalid bearer token. |
3582
+ | 403 | `PermissionDeniedError` | `FORBIDDEN` | The caller lacks the action this route requires on the resource, or Mail is not enabled for the workspace. |
3583
+ | 404 | `NotFoundError` | `NOT_FOUND`, `WEBHOOK_EVENT_NOT_FOUND` | No such event here (`WEBHOOK_EVENT_NOT_FOUND`); events are kept for 30 days. |
3584
+ | 429 | `RateLimitError` | `RATE_LIMITED` | More than 20 requests per second or 600 per minute from this client IP (`RATE_LIMITED`); wait `Retry-After` seconds. |
3585
+
3586
+ ```ts
3587
+ const webhookEventDetail = await mail.webhooks.events.retrieve("event");
3588
+ ```
3589
+
3590
+ [HTTP reference](https://productcraft.co/docs/mail/api-reference#get-v1-workspaces-workspace-id-webhooks-events-event-id)
3591
+
3592
+ ## Error codes
3593
+
3594
+ `err.code` on an `APIError` is one of these (an open set: new codes may appear).
3595
+
3596
+ | Code | Meaning |
3597
+ |---|---|
3598
+ | `BAD_REQUEST` | The request is malformed (not valid JSON, a bad header, an unusable parameter). |
3599
+ | `VALIDATION_ERROR` | One or more request fields are invalid; `errors` lists each field (`param`, `message`, `code`). |
3600
+ | `UNAUTHORIZED` | No credentials, or the token or API key is invalid, expired or revoked. |
3601
+ | `PAYMENT_REQUIRED` | The workspace has no credit or plan for this operation. |
3602
+ | `FORBIDDEN` | The caller is authenticated but not allowed to do this. |
3603
+ | `NOT_FOUND` | The resource does not exist, or belongs to another workspace or app. |
3604
+ | `METHOD_NOT_ALLOWED` | The route does not accept this HTTP method. |
3605
+ | `NOT_ACCEPTABLE` | The route cannot produce the requested content type. |
3606
+ | `REQUEST_TIMEOUT` | The request took too long to arrive. |
3607
+ | `CONFLICT` | The request conflicts with the current state of the resource. |
3608
+ | `GONE` | The resource existed but has been removed for good. |
3609
+ | `PRECONDITION_FAILED` | A precondition header did not match. |
3610
+ | `PAYLOAD_TOO_LARGE` | The request body is over the size limit. |
3611
+ | `UNSUPPORTED_MEDIA_TYPE` | The request body is not in a content type the route accepts. |
3612
+ | `UNPROCESSABLE_ENTITY` | The request is well-formed but cannot be carried out. |
3613
+ | `PRECONDITION_REQUIRED` | The route requires a precondition header. |
3614
+ | `RATE_LIMITED` | Too many requests; retry after the `Retry-After` header. |
3615
+ | `INTERNAL_SERVER_ERROR` | An unexpected server error. Retrying is safe for idempotent requests; quote `request_id` to support. |
3616
+ | `NOT_IMPLEMENTED` | The server does not support this operation. |
3617
+ | `BAD_GATEWAY` | An upstream service returned an invalid response. |
3618
+ | `SERVICE_UNAVAILABLE` | The service or one of its dependencies is temporarily unavailable; retry. |
3619
+ | `GATEWAY_TIMEOUT` | An upstream service did not answer in time; retry. |
3620
+ | `SERVICE_NOT_ENABLED` | The workspace has not enabled this product. |
3621
+ | `INVALID_CREDENTIALS` | The credentials were not accepted. |
3622
+ | `SESSION_REVOKED` | The session behind this token was signed out or revoked. |
3623
+ | `MCP_NOT_PERMITTED` | An MCP (agent) token cannot call this route. |
3624
+ | `MCP_WORKSPACE_MISMATCH` | The MCP token was issued for a different workspace. |
3625
+ | `PAK_NOT_PERMITTED` | A workspace API key cannot call this route. |
3626
+ | `ACTIONS_NOT_GRANTABLE` | The policy grants actions the caller does not hold. |
3627
+ | `IDEMPOTENCY_KEY_REUSE` | The `Idempotency-Key` was already used with a different body or route. |
3628
+ | `IDEMPOTENCY_KEY_REQUIRED` | This operation requires an `Idempotency-Key` header. |
3629
+ | `IDEMPOTENCY_IN_PROGRESS` | A request with the same `Idempotency-Key` is still running. Retry after `Retry-After` seconds with the same key to get its response. |
3630
+ | `WEBHOOK_ENDPOINT_LIMIT` | The workspace has the maximum number of webhook endpoints. |
3631
+ | `WEBHOOK_ENDPOINT_NOT_FOUND` | No webhook endpoint with this id in this workspace. |
3632
+ | `WEBHOOK_ENDPOINT_NOT_SUBSCRIBED` | The endpoint is not subscribed to this event type. |
3633
+ | `WEBHOOK_EVENT_NOT_FOUND` | No webhook event with this id in this workspace. |
3634
+ | `WEBHOOK_EVENT_TYPE_UNKNOWN` | The event type is not one this product sends. |
3635
+ | `AUDIENCE_REQUIRED` | Pass segment_id, topic_id, or both. |
3636
+ | `BROADCAST_AUDIENCE_EMPTY` | Nobody in this audience can receive the broadcast. |
3637
+ | `BROADCAST_CONTENT_CONFLICT` | Use a template or inline html, not both. |
3638
+ | `BROADCAST_NOT_CANCELABLE` | The broadcast cannot be canceled in its current status. |
3639
+ | `BROADCAST_NOT_DELETABLE` | The broadcast cannot be deleted in its current status; cancel it first. |
3640
+ | `BROADCAST_NOT_DRAFT` | The broadcast is no longer a draft (`details.status`). |
3641
+ | `BROADCAST_NOT_FOUND` | Broadcast not found. |
3642
+ | `BROADCAST_NOT_READY` | The broadcast cannot be sent yet (`details.problems`). |
3643
+ | `CAPABILITY_NOT_VERIFIED` | A verified domain cannot gain sending or receiving until the new records verify (`details.records`). |
3644
+ | `CONTACT_EXISTS` | A contact with this email already exists. |
3645
+ | `CONTACT_LIMIT` | The workspace has the maximum number of contacts. |
3646
+ | `CONTACT_NOT_FOUND` | Contact not found. |
3647
+ | `CONTACT_PROPERTY_EXISTS` | A contact property with this key already exists. |
3648
+ | `CONTACT_PROPERTY_LIMIT` | The workspace has the maximum number of contact properties. |
3649
+ | `CONTACT_PROPERTY_NOT_FOUND` | Contact property not found. |
3650
+ | `DATE_RANGE_BEYOND_RETENTION` | The date range reaches past the data retention window. |
3651
+ | `DATE_RANGE_TOO_WIDE` | The date range is wider than allowed. |
3652
+ | `DATE_WINDOW_TOO_WIDE` | The date window is longer than the message log retention. |
3653
+ | `DKIM_KEY_MISSING` | The domain has no DKIM key yet. |
3654
+ | `DNS_CONFLICTS` | Some records cannot be written without breaking existing ones; nothing was changed. |
3655
+ | `DOMAIN_CONNECT_DISABLED` | Domain Connect is turned off. |
3656
+ | `DOMAIN_CONNECT_MAIL_FROM_LABEL` | Domain Connect cannot apply a custom MAIL FROM label. |
3657
+ | `DOMAIN_CONNECT_NOT_SENDING` | Domain Connect needs a sending domain. |
3658
+ | `DOMAIN_CONNECT_UNAVAILABLE` | The DNS provider does not support Domain Connect for this domain. |
3659
+ | `DOMAIN_HAS_MAILBOXES` | The domain still has active mailboxes (`details.mailboxes`). |
3660
+ | `DOMAIN_NOT_FOUND` | Domain not found in this workspace. |
3661
+ | `EMAIL_CONTENT_CONFLICT` | Send either `html`/`text` or a `template`, not both, and at least one of them. |
3662
+ | `EMAIL_NOT_FOUND` | Email not found in this workspace. |
3663
+ | `EMAIL_UNAVAILABLE` | Emailing share links is temporarily unavailable. Create the link without email_to and send it yourself. |
3664
+ | `FROM_DOMAIN_MISMATCH` | The From address is not at this domain. |
3665
+ | `IDEMPOTENCY_KEY_CONFLICTS_WITH_ROW` | An `idempotency_key` on a batch row conflicts with the request key. |
3666
+ | `IMPORT_IN_PROGRESS` | Another contact import is running for this workspace. |
3667
+ | `IMPORT_NOT_FOUND` | Import not found. |
3668
+ | `IMPORT_TOO_LARGE` | The import file is over the size limit. |
3669
+ | `INVALID_CAPABILITIES` | At least one capability (sending or receiving) must be enabled. |
3670
+ | `INVALID_CONTACT_PROPERTY_VALUE` | A contact property value does not match the property type. |
3671
+ | `INVALID_CSV` | The first line must be a header with an "email" column. |
3672
+ | `INVALID_DATE` | A date is not valid ISO 8601. |
3673
+ | `INVALID_DATE_RANGE` | The date range is invalid (start after end). |
3674
+ | `INVALID_DATE_WINDOW` | The date window is invalid. |
3675
+ | `INVALID_FORMAT` | A path or query value is not in the expected format. |
3676
+ | `INVALID_FROM_ADDRESS` | The From address could not be parsed. |
3677
+ | `INVALID_SEGMENT_RULE` | The segment rule is invalid (`details.problems`). |
3678
+ | `INVALID_TIMEZONE` | The time zone is not a valid IANA name. |
3679
+ | `INVALID_ZONE` | The DNS zone is not one this domain belongs to. |
3680
+ | `MAIL_FROM_HOST_IN_USE` | The MAIL FROM host already has records that are not ours (`details.found`). |
3681
+ | `MAIL_FROM_NOT_SENDING` | A custom MAIL FROM needs the domain to send. |
3682
+ | `MAIL_HEADER_NOT_ALLOWED` | A custom header is not allowed, or its value is invalid (`details.headers`). |
3683
+ | `PROVIDER_ERROR` | The DNS provider returned an error. |
3684
+ | `PROVIDER_PERMISSION_DENIED` | The DNS provider token lacks permission to edit this zone. |
3685
+ | `PROVIDER_TOKEN_REJECTED` | The DNS provider rejected the token. |
3686
+ | `PROVIDER_UNAVAILABLE` | The DNS provider is unavailable; try again later. |
3687
+ | `PROVIDER_ZONE_NOT_FOUND` | The DNS provider has no zone for this domain. |
3688
+ | `RECIPIENT_LIMIT` | More than 50 recipients across `to`, `cc` and `bcc`. |
3689
+ | `RECIPIENT_SUPPRESSED` | The recipient is suppressed for this workspace or topic (`details.reason`). |
3690
+ | `REGION_NOT_AVAILABLE` | `region` is not the region this deployment sends from (`details.available`). |
3691
+ | `RESERVED_PROPERTY_KEY` | The property key is reserved. |
3692
+ | `SEGMENT_EXISTS` | A segment with this name already exists. |
3693
+ | `SEGMENT_LIMIT` | The workspace has the maximum number of segments. |
3694
+ | `SEGMENT_NOT_FOUND` | Segment not found. |
3695
+ | `SEGMENT_RULE_BASED` | Members of a rule segment follow its rule: change the contact or the rule instead. |
3696
+ | `SEGMENT_TYPE_FIXED` | A static segment cannot take a rule: create a rule segment instead. |
3697
+ | `SES_IDENTITY_DELETE_FAILED` | The sending identity could not be removed from the email provider; retry. |
3698
+ | `SHARE_LINK_NOT_FOUND` | This setup link does not exist, has expired or was revoked. |
3699
+ | `SMTP_AUTH_PARTIAL` | SMTP username and password must be set together. |
3700
+ | `SMTP_CONFIG_NOT_FOUND` | No SMTP override saved for this domain. |
3701
+ | `SMTP_PASSWORD_REQUIRED` | An SMTP password is required. |
3702
+ | `TEMPLATE_DATA_INVALID` | The render data does not match the template schema. |
3703
+ | `TEMPLATE_KIND_NOT_SENDABLE` | This template kind (a partial or layout) cannot be sent on its own. |
3704
+ | `TEMPLATE_NOT_FOUND` | No template with this name in this workspace. |
3705
+ | `TEMPLATE_RENDER_FAILED` | The template could not be rendered. |
3706
+ | `TLS_ENFORCED_NOT_AVAILABLE` | `tls: enforced` is not available yet. |
3707
+ | `TOPIC_EXISTS` | A topic with this name already exists. |
3708
+ | `TOPIC_LIMIT` | The workspace has the maximum number of topics. |
3709
+ | `TOPIC_NOT_FOUND` | Topic not found. |
3710
+ | `TRACKING_NOT_AVAILABLE` | Open and click tracking are not available yet. |
3711
+ | `UNKNOWN_CONTACT_PROPERTY` | The contact property is not defined; create it first. |
3712
+ | `UNSUBSCRIBE_LINK_REQUIRED` | A broadcast body must contain `{{{unsubscribe_url}}}`. |
3713
+ | `domain_not_verified_by_provider` | The From domain's DNS records have been broken for over 72 h, so it no longer sends. |
3714
+ | `from_domain_not_verified` | The From domain is not an active sending domain of this workspace (`details: { fqdn, reason }`). |
3715
+ | `no_default_sender` | No `from` and the workspace has no default sender. |
3716
+ | `sender_domain_not_in_workspace` | The address is not on a sending domain of this workspace (`details.fqdn`). |