When to use
Use this when you need to send transactional or marketing email through Twilio SendGrid's v3 Mail Send API — a single POST /v3/mail/send with Bearer API-key auth. Use it when building personalizations for multiple recipients, rendering dynamic templates (d-... template IDs), attaching files, or tagging sends with categories for analytics. Use it when you want a safe dry-run via sandbox mode before real delivery.
Not for: SMS/voice — use twilio-webhook-architecture and twilio-reliability-patterns. SPF/DKIM/DMARC, domain authentication, IP warmup, and inbox placement — use twilio-email-deliverability-advisor.
Method
- Create a SendGrid API key (Settings → API Keys) with least privilege — "Mail Send" scope is enough to send. Store it as
SENDGRID_API_KEY; never embed it client-side.
- Authenticate every request with
Authorization: Bearer <SENDGRID_API_KEY> and Content-Type: application/json. The endpoint is POST https://api.sendgrid.com/v3/mail/send.
- Set a
from address on an authenticated domain. The from-domain must pass domain authentication (DKIM/SPF) or mail lands in spam / is rejected (see twilio-email-deliverability-advisor). A verified single-sender works only for low-volume testing.
- Build
personalizations. Each personalization is one envelope: its own to/cc/bcc, subject, and (for templates) dynamic_template_data. Multiple personalizations = multiple independent messages in one API call. Decision point: to prevent recipients seeing each other, use separate personalizations, not multiple to entries.
- Choose content mode. Either provide
content (text/plain and/or text/html) OR set template_id to a dynamic template (d-...) and pass dynamic_template_data per personalization — do not hand-write body content when using a template.
- Add optional features:
attachments (base64 content, type, filename, disposition), categories (up to 10, for analytics), custom_args, send_at (unix epoch for scheduled send), and reply_to.
- Dry-run first with
mail_settings.sandbox_mode.enable = true: SendGrid validates the request and returns 2xx without delivering. Flip it off for real sends.
- Handle the response. Success is
202 Accepted with an empty body and an X-Message-Id header — capture it for correlation with the Event Webhook. On 4xx, read the errors[] array (field + message).
- Prefer the official SDK (
@sendgrid/mail) for retries/serialization, but the raw curl shape below is the source of truth.
Example
curl -X POST https://api.sendgrid.com/v3/mail/send \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"personalizations": [
{ "to": [{"email":"ada@example.com"}],
"dynamic_template_data": { "first_name": "Ada", "order_id": "1024" } }
],
"from": { "email": "receipts@mail.example.com", "name": "Example Receipts" },
"reply_to": { "email": "support@example.com" },
"template_id": "d-abc123def4567890abc123def4567890",
"categories": ["receipts", "prod"],
"attachments": [
{ "content": "'"$(base64 -w0 invoice.pdf)"'",
"type": "application/pdf", "filename": "invoice.pdf", "disposition": "attachment" }
],
"mail_settings": { "sandbox_mode": { "enable": false } }
}'
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const [res] = await sgMail.send({
to: 'ada@example.com',
from: { email: 'receipts@mail.example.com', name: 'Example Receipts' },
templateId: 'd-abc123def4567890abc123def4567890',
dynamicTemplateData: { first_name: 'Ada', order_id: '1024' },
categories: ['receipts', 'prod'],
});
console.log(res.statusCode, res.headers['x-message-id']); // 202, <id>
Pitfalls
- Unauthenticated from-domain. Sending from a domain that hasn't passed domain authentication (DKIM/SPF CNAMEs) means spam-foldering or rejection, regardless of a correct API call. Authenticate the domain first.
- Mixing template and content. If you set
template_id for a dynamic template, don't also send conflicting content/subject unless the template intends it — the template's Handlebars owns the body.
- CC/BCC leak. Multiple
to entries in one personalization expose recipients to each other. Use one personalization per recipient for privacy and per-recipient template data.
- Forgetting sandbox is on.
sandbox_mode.enable=true returns 202 but sends nothing — a classic "why didn't my email arrive" that is actually success-without-delivery.
- Silent 202. 202 means accepted, not delivered. Bounces/blocks/spam show up later via the Event Webhook, not in the send response. Capture
X-Message-Id to correlate.
- Attachment encoding.
content must be base64 with no line wrapping; the wrong type MIME or a huge attachment (SendGrid caps total message ~30MB) fails the send.
Output format
SEND: endpoint=POST /v3/mail/send sandbox=<on|off>
FROM: <email@authenticated-domain> template=<d-... | inline-content>
RECIPIENTS: <count personalizations> categories=[..]
RESULT: http=<202|4xx> x-message-id=<id | none>
ERRORS: <field: message, if 4xx>
FOLLOW-UP: <correlate via Event Webhook message_id>
Reference
- Endpoint:
POST https://api.sendgrid.com/v3/mail/send. Auth: Authorization: Bearer <API key>. Success: 202 Accepted, empty body, X-Message-Id header.
- Request body keys:
personalizations[] (to, cc, bcc, subject, dynamic_template_data, send_at, custom_args), from, reply_to, subject, content[] (type: text/plain|text/html, value), template_id (d-...), attachments[] (content base64, type, filename, disposition, content_id), categories[] (max 10), send_at, batch_id, asm (unsubscribe groups), mail_settings.sandbox_mode, tracking_settings.
- Dynamic templates: created in the SendGrid UI/API, IDs prefixed
d-; use Handlebars ({{first_name}}) rendered from dynamic_template_data. Legacy transactional templates are separate.
- Sandbox mode:
mail_settings.sandbox_mode.enable=true validates and returns 202 without sending.
- Categories vs custom_args: categories (≤10) group sends for Stats/analytics;
custom_args are opaque key/values echoed in the Event Webhook.
- Limits: up to 1000 recipients per request across personalizations; total message size ~30MB.
- Events: delivery outcome (delivered, bounce, dropped, spamreport, open, click) arrives via the SendGrid Event Webhook, correlated by
sg_message_id.
- Compliance: transactional email is exempt from CAN-SPAM unsubscribe requirements but still must not be deceptive; marketing email must include a physical address and unsubscribe (use ASM unsubscribe groups).
1---2name: twilio-email-send3description: Send email via the Twilio SendGrid v3 Mail Send API with API-key auth, dynamic templates, attachments, and categories.4---56## When to use78Use this when you need to send transactional or marketing email through Twilio SendGrid's v3 Mail Send API — a single `POST /v3/mail/send` with Bearer API-key auth. Use it when building personalizations for multiple recipients, rendering dynamic templates (`d-...` template IDs), attaching files, or tagging sends with categories for analytics. Use it when you want a safe dry-run via sandbox mode before real delivery.910**Not for:** SMS/voice — use twilio-webhook-architecture and twilio-reliability-patterns. SPF/DKIM/DMARC, domain authentication, IP warmup, and inbox placement — use twilio-email-deliverability-advisor.1112## Method13141. Create a SendGrid API key (Settings → API Keys) with least privilege — "Mail Send" scope is enough to send. Store it as `SENDGRID_API_KEY`; never embed it client-side.152. Authenticate every request with `Authorization: Bearer <SENDGRID_API_KEY>` and `Content-Type: application/json`. The endpoint is `POST https://api.sendgrid.com/v3/mail/send`.163. Set a `from` address on an authenticated domain. The from-domain must pass domain authentication (DKIM/SPF) or mail lands in spam / is rejected (see twilio-email-deliverability-advisor). A verified single-sender works only for low-volume testing.174. Build `personalizations`. Each personalization is one envelope: its own `to`/`cc`/`bcc`, `subject`, and (for templates) `dynamic_template_data`. Multiple personalizations = multiple independent messages in one API call. Decision point: to prevent recipients seeing each other, use separate personalizations, not multiple `to` entries.185. Choose content mode. Either provide `content` (text/plain and/or text/html) OR set `template_id` to a dynamic template (`d-...`) and pass `dynamic_template_data` per personalization — do not hand-write body content when using a template.196. Add optional features: `attachments` (base64 `content`, `type`, `filename`, `disposition`), `categories` (up to 10, for analytics), `custom_args`, `send_at` (unix epoch for scheduled send), and `reply_to`.207. Dry-run first with `mail_settings.sandbox_mode.enable = true`: SendGrid validates the request and returns 2xx without delivering. Flip it off for real sends.218. Handle the response. Success is `202 Accepted` with an empty body and an `X-Message-Id` header — capture it for correlation with the Event Webhook. On 4xx, read the `errors[]` array (field + message).229. Prefer the official SDK (`@sendgrid/mail`) for retries/serialization, but the raw curl shape below is the source of truth.2324## Example2526```bash27curl -X POST https://api.sendgrid.com/v3/mail/send \28 -H "Authorization: Bearer $SENDGRID_API_KEY" \29 -H "Content-Type: application/json" \30 -d '{31 "personalizations": [32 { "to": [{"email":"ada@example.com"}],33 "dynamic_template_data": { "first_name": "Ada", "order_id": "1024" } }34 ],35 "from": { "email": "receipts@mail.example.com", "name": "Example Receipts" },36 "reply_to": { "email": "support@example.com" },37 "template_id": "d-abc123def4567890abc123def4567890",38 "categories": ["receipts", "prod"],39 "attachments": [40 { "content": "'"$(base64 -w0 invoice.pdf)"'",41 "type": "application/pdf", "filename": "invoice.pdf", "disposition": "attachment" }42 ],43 "mail_settings": { "sandbox_mode": { "enable": false } }44 }'45```4647```javascript48import sgMail from '@sendgrid/mail';49sgMail.setApiKey(process.env.SENDGRID_API_KEY);5051const [res] = await sgMail.send({52 to: 'ada@example.com',53 from: { email: 'receipts@mail.example.com', name: 'Example Receipts' },54 templateId: 'd-abc123def4567890abc123def4567890',55 dynamicTemplateData: { first_name: 'Ada', order_id: '1024' },56 categories: ['receipts', 'prod'],57});58console.log(res.statusCode, res.headers['x-message-id']); // 202, <id>59```6061## Pitfalls6263- **Unauthenticated from-domain.** Sending from a domain that hasn't passed domain authentication (DKIM/SPF CNAMEs) means spam-foldering or rejection, regardless of a correct API call. Authenticate the domain first.64- **Mixing template and content.** If you set `template_id` for a dynamic template, don't also send conflicting `content`/`subject` unless the template intends it — the template's Handlebars owns the body.65- **CC/BCC leak.** Multiple `to` entries in one personalization expose recipients to each other. Use one personalization per recipient for privacy and per-recipient template data.66- **Forgetting sandbox is on.** `sandbox_mode.enable=true` returns 202 but sends nothing — a classic "why didn't my email arrive" that is actually success-without-delivery.67- **Silent 202.** 202 means accepted, not delivered. Bounces/blocks/spam show up later via the Event Webhook, not in the send response. Capture `X-Message-Id` to correlate.68- **Attachment encoding.** `content` must be base64 with no line wrapping; the wrong `type` MIME or a huge attachment (SendGrid caps total message ~30MB) fails the send.6970## Output format7172```73SEND: endpoint=POST /v3/mail/send sandbox=<on|off>74FROM: <email@authenticated-domain> template=<d-... | inline-content>75RECIPIENTS: <count personalizations> categories=[..]76RESULT: http=<202|4xx> x-message-id=<id | none>77ERRORS: <field: message, if 4xx>78FOLLOW-UP: <correlate via Event Webhook message_id>79```8081## Reference8283- **Endpoint:** `POST https://api.sendgrid.com/v3/mail/send`. Auth: `Authorization: Bearer <API key>`. Success: `202 Accepted`, empty body, `X-Message-Id` header.84- **Request body keys:** `personalizations[]` (`to`, `cc`, `bcc`, `subject`, `dynamic_template_data`, `send_at`, `custom_args`), `from`, `reply_to`, `subject`, `content[]` (`type`: `text/plain`|`text/html`, `value`), `template_id` (`d-...`), `attachments[]` (`content` base64, `type`, `filename`, `disposition`, `content_id`), `categories[]` (max 10), `send_at`, `batch_id`, `asm` (unsubscribe groups), `mail_settings.sandbox_mode`, `tracking_settings`.85- **Dynamic templates:** created in the SendGrid UI/API, IDs prefixed `d-`; use Handlebars (`{{first_name}}`) rendered from `dynamic_template_data`. Legacy transactional templates are separate.86- **Sandbox mode:** `mail_settings.sandbox_mode.enable=true` validates and returns 202 without sending.87- **Categories vs custom_args:** categories (≤10) group sends for Stats/analytics; `custom_args` are opaque key/values echoed in the Event Webhook.88- **Limits:** up to 1000 recipients per request across personalizations; total message size ~30MB.89- **Events:** delivery outcome (delivered, bounce, dropped, spamreport, open, click) arrives via the SendGrid Event Webhook, correlated by `sg_message_id`.90- **Compliance:** transactional email is exempt from CAN-SPAM unsubscribe requirements but still must not be deceptive; marketing email must include a physical address and unsubscribe (use ASM unsubscribe groups).