Skip to content

Commit 43b7d7d

Browse files
Merge pull request #406 from geturbackend/mintlify/6dc16288
Update from code changes: document mail replyTo and stricter validation
2 parents 560690f + e79827f commit 43b7d7d

3 files changed

Lines changed: 62 additions & 25 deletions

File tree

‎mintlify/docs/api-reference/mail/send.mdx‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ Sends transactional email from your project.
2424

2525
### Request body
2626

27-
<ParamField body="to" type="string" required>
28-
Recipient email address.
27+
<ParamField body="to" type="string | string[]" required>
28+
Recipient email address, or an array of recipient addresses. Each address must be a valid email. An array must contain at least one entry.
29+
</ParamField>
30+
31+
<ParamField body="replyTo" type="string | string[]">
32+
Optional reply-to address, or an array of addresses. Each address must be a valid email. When provided as an array, it must contain at least one entry. Forwarded to Resend as the message's `Reply-To` header.
2933
</ParamField>
3034

3135
<ParamField body="subject" type="string">
@@ -57,7 +61,9 @@ Sends transactional email from your project.
5761
- Template mode: `templateId` or `templateName`
5862
- Direct mode: `subject` + (`html` or `text`)
5963
- Do not pass both `templateId` and `templateName`.
60-
- If rendered subject/body becomes empty after variable interpolation, request is rejected with `400`.
64+
- `to` and `replyTo` must contain valid email addresses. Array forms cannot be empty (`Recipient list cannot be empty`, `Reply-to list cannot be empty`).
65+
- Subject is required after any template resolution and variable interpolation (`Subject is required`).
66+
- The rendered message must include a non-empty `html` or `text` body. Requests where both come out empty after interpolation are rejected with `Provide html or text` (or `Provide at least one of html or text content.` when neither field was supplied).
6167

6268
### Template resolution order
6369

@@ -123,6 +129,7 @@ const res = await fetch('https://api.ub.bitbros.in/api/mail/send', {
123129
},
124130
body: JSON.stringify({
125131
to: 'user@example.com',
132+
replyTo: 'support@acme.com',
126133
subject: 'Welcome to Acme',
127134
html: '<h1>Hello</h1><p>Your account is ready.</p>'
128135
})
@@ -212,10 +219,14 @@ public class Main {
212219

213220
### Errors
214221

215-
| Status | Cause |
216-
| :--- | :--- |
217-
| `400 Bad Request` | Invalid payload, invalid template ID, or empty rendered subject/body |
218-
| `401 Unauthorized` | Missing project context |
219-
| `403 Forbidden` | Non-secret key used |
220-
| `404 Not Found` | Template or project not found |
221-
| `429 Too Many Requests` | Monthly mail limit exceeded |
222+
| Status | Message | Cause |
223+
| :--- | :--- | :--- |
224+
| `400 Bad Request` | `Invalid recipient email format` / `Recipient list cannot be empty` | `to` is missing, malformed, or an empty array |
225+
| `400 Bad Request` | `Invalid replyTo email format` / `Reply-to list cannot be empty` | `replyTo` is malformed or an empty array |
226+
| `400 Bad Request` | `Subject is required` | Subject missing, blank, or empty after template/variable interpolation |
227+
| `400 Bad Request` | `Provide at least one of html or text content.` | Neither `html` nor `text` supplied (or both empty after interpolation) |
228+
| `400 Bad Request` | `Template not found.` | `templateId` / `templateName` does not resolve |
229+
| `401 Unauthorized` | `Project context missing.` | Request has no resolved project |
230+
| `403 Forbidden` | `Forbidden. This action requires a Secret Key (sk_live_...).` | Non-secret key used |
231+
| `404 Not Found` | `Project not found.` | Project record missing |
232+
| `429 Too Many Requests` | `Monthly mail limit exceeded.` | Project quota exhausted for the month |

‎mintlify/docs/guides/mail-platform.mdx‎

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ description: "Comprehensive developer guide for urBackend Mail Platform: BYOK, b
77

88
urBackend Mail Platform extends transactional sending into a full delivery workflow with:
99

10-
- async queue-backed single sending (`/api/mail/send`)
11-
- direct provider batch sending (`/api/mail/send-batch`)
10+
- async queue-backed single sending (`/api/mail/send`), with optional `replyTo`
11+
- direct provider batch sending (`/api/mail/send-batch`, 1..100 items, per-item `replyTo`)
1212
- BYOK (Bring Your Own Key) with encrypted project-level Resend keys
1313
- delivery tracking via persistent `MailLog`
1414
- audience/contact management (BYOK-gated)
@@ -101,14 +101,21 @@ All `/api/mail/*` send endpoints require your **Secret Key** (`sk_live_...`) in
101101

102102
| Field | Type | Required | Notes |
103103
|---|---|---:|---|
104-
| `to` | `string` | ✅ | recipient email |
104+
| `to` | `string \| string[]` | ✅ | recipient email(s); array form must have at least one entry |
105+
| `replyTo` | `string \| string[]` | Optional | reply-to address(es); forwarded to Resend as the `Reply-To` header |
105106
| `subject` | `string` | Direct mode ✅ | required when not using template fields |
106107
| `html` | `string` | Direct mode one of `html/text` | HTML body |
107108
| `text` | `string` | Direct mode one of `html/text` | Text body |
108109
| `templateId` | `string` | Template mode one of `templateId/templateName` | 24-char ObjectId |
109110
| `templateName` | `string` | Template mode one of `templateId/templateName` | key/name lookup |
110111
| `variables` | `object` | Optional | template vars |
111112

113+
Validation notes:
114+
115+
- `to` and `replyTo` must be valid email addresses. Array forms cannot be empty (`Recipient list cannot be empty`, `Reply-to list cannot be empty`).
116+
- Subject must be non-empty after any template/variable interpolation (`Subject is required`).
117+
- Rendered body must contain a non-empty `html` or `text` value; requests where both are empty after interpolation are rejected (`Provide at least one of html or text content.`).
118+
112119
#### Quota and limits
113120

114121
- per-request monthly quota slot is reserved in Redis
@@ -169,13 +176,30 @@ Array of 1..100 items:
169176
[
170177
{
171178
"to": "u1@example.com",
179+
"replyTo": "support@acme.com",
172180
"subject": "Campaign",
173181
"html": "<p>Hello</p>"
174182
}
175183
]
176184
```
177185

178-
Each item supports `to`, `subject`, `html?`, `text?`.
186+
Each item supports:
187+
188+
| Field | Type | Required | Notes |
189+
|---|---|---:|---|
190+
| `to` | `string \| string[]` | ✅ | recipient email(s); array form must have at least one entry |
191+
| `replyTo` | `string \| string[]` | Optional | reply-to address(es); array form must have at least one entry |
192+
| `subject` | `string` | ✅ | non-empty after trimming |
193+
| `html` | `string` | one of `html/text` ✅ | HTML body |
194+
| `text` | `string` | one of `html/text` ✅ | Text body |
195+
196+
Validation is enforced by a strict Zod schema on every batch call:
197+
198+
- The batch array itself must contain **1..100** items. Empty batches are rejected with `Batch cannot be empty`; larger batches are rejected with `Max 100 emails per batch`.
199+
- Each `to` and `replyTo` value is validated as a proper email address.
200+
- Each item must supply a non-empty `subject` (`Subject is required`).
201+
- Each item must supply at least one of `html` or `text` (`Provide html or text`).
202+
- Templates (`templateId` / `templateName` / `variables`) are **not** supported in batch mode. Batch items always go directly to Resend with the exact `subject` / `html` / `text` you provide.
179203

180204
#### Quota behavior
181205

@@ -196,8 +220,8 @@ curl -X POST "https://api.ub.bitbros.in/api/mail/send-batch" \
196220
-H "Content-Type: application/json" \
197221
-H "x-api-key: sk_live_YOUR_SECRET_KEY" \
198222
-d '[
199-
{"to":"a@example.com","subject":"Hi A","html":"<p>A</p>"},
200-
{"to":"b@example.com","subject":"Hi B","html":"<p>B</p>"}
223+
{"to":"a@example.com","replyTo":"support@acme.com","subject":"Hi A","html":"<p>A</p>"},
224+
{"to":"b@example.com","subject":"Hi B","text":"Hello B"}
201225
]'
202226
```
203227

@@ -207,8 +231,8 @@ import { UrBackendClient } from 'urbackend-sdk';
207231
const client = new UrBackendClient({ apiKey: process.env.URB_SECRET_KEY! });
208232
const result = await client.request('POST', '/api/mail/send-batch', {
209233
body: [
210-
{ to: 'a@example.com', subject: 'Hi A', html: '<p>A</p>' },
211-
{ to: 'b@example.com', subject: 'Hi B', html: '<p>B</p>' }
234+
{ to: 'a@example.com', replyTo: 'support@acme.com', subject: 'Hi A', html: '<p>A</p>' },
235+
{ to: 'b@example.com', subject: 'Hi B', text: 'Hello B' }
212236
]
213237
});
214238
console.log(result);
@@ -568,12 +592,12 @@ Add product screenshots/annotated walkthrough images in this section if your doc
568592

569593
| Endpoint | Status | Typical message |
570594
|---|---:|---|
571-
| `POST /api/mail/send` | `400` | Invalid mail payload / template not found / subject required |
572-
| `POST /api/mail/send` | `403` | Secret key required |
573-
| `POST /api/mail/send` | `429` | Monthly mail limit exceeded |
574-
| `POST /api/mail/send-batch` | `400` | Invalid batch mail payload |
575-
| `POST /api/mail/send-batch` | `403` | Secret key required |
576-
| `POST /api/mail/send-batch` | `429` | Monthly mail limit exceeded |
595+
| `POST /api/mail/send` | `400` | `Recipient list cannot be empty` / `Reply-to list cannot be empty` / `Subject is required` / `Provide at least one of html or text content.` / `Template not found.` |
596+
| `POST /api/mail/send` | `403` | `Forbidden. This action requires a Secret Key (sk_live_...).` |
597+
| `POST /api/mail/send` | `429` | `Monthly mail limit exceeded.` |
598+
| `POST /api/mail/send-batch` | `400` | `Batch cannot be empty` / `Max 100 emails per batch` / `Recipient list cannot be empty` / `Reply-to list cannot be empty` / `Subject is required` / `Provide html or text` |
599+
| `POST /api/mail/send-batch` | `403` | `Forbidden. This action requires a Secret Key (sk_live_...).` |
600+
| `POST /api/mail/send-batch` | `429` | `Monthly mail limit exceeded.` |
577601
| `GET /api/mail/logs/:resendId` | `400` | Invalid resendId format |
578602
| `GET /api/mail/logs/:resendId` | `404` | Mail log entry not found for this project |
579603
| `GET /api/mail/audiences` + audience mutations | `403` | This feature requires a BYOK Resend key |

‎mintlify/docs/sdk/mail.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ send(payload: SendMailPayload): Promise<SendMailResponse>
2222

2323
| Field | Type | Required | Description |
2424
|---|---|---|---|
25-
| `to` | `string \| string[]` | Yes | Recipient email address(es). |
25+
| `to` | `string \| string[]` | Yes | Recipient email address(es). Array form must contain at least one entry. |
26+
| `replyTo` | `string \| string[]` | No | Reply-to address(es). Forwarded to Resend as the `Reply-To` header. Array form must contain at least one entry. |
2627
| `subject` | `string` | Template mode: No, Direct mode: Yes | Email subject line. |
2728
| `text` | `string` | Direct mode: one of `text` or `html` is required | Plain text body. |
2829
| `html` | `string` | Direct mode: one of `text` or `html` is required | HTML body. |
@@ -85,6 +86,7 @@ console.log(response.id);
8586
// Direct mode
8687
const response = await client.mail.send({
8788
to: 'customer@example.com',
89+
replyTo: 'support@acme.com',
8890
subject: 'Welcome to urBackend',
8991
html: '<h1>Hi there!</h1><p>Thanks for joining.</p>'
9092
});

0 commit comments

Comments
 (0)