SendGrid Email API Integration
Integrates Twilio SendGrids Mail Send API, Dynamic Templates, Marketing Campaigns, and Inbound Parse using the sendgrid Python SDK v6.x. When loaded, this skill makes the model implement email delivery with proper Mail helper construction, dynamic template personalization, attachment handling, async sending, event webhook processing, and deliverability optimization.
TL;DR for Code Generation
When to Use
Use this skill when:
- Sending transactional emails (welcome, password reset, receipts, notifications) from Python applications
- Implementing Dynamic Template-based emails with Handlebars template variables and per-recipient personalization
- Building Marketing Campaigns with contact management, list segmentation, and campaign scheduling
- Processing Inbound Parse webhooks to receive emails with attachments programmatically
- Handling SendGrid Event Webhooks (delivered, bounced, opened, clicked, spam reported) for delivery monitoring
- Sending batched emails with substitution per recipient using the Mail Send API v3
When NOT to Use
Avoid this skill for:
- SMS or WhatsApp messaging (use
coding-twilio-api instead)
- Basic SMTP relay for low-volume internal alerts (use
smtplib + standard SMTP instead)
- Team chat or collaboration notifications (use
coding-slack-api instead)
- Hosting your own email infrastructure or open relay configuration
Core Workflow
Initialize the Client — Create a SendGridAPIClient(os.environ["SENDGRID_API_KEY"]). Validate the key on startup by calling client.client._get("/v3/scopes"). Checkpoint: Verify the API key has mail.send scope for sending, or asm.groups + templates for template management.
Construct the Mail Object — Use Mail() with from_email, to_emails, subject, and either html_content/plain_text_content or template_id. For bulk sends, build a personalization object per recipient with dynamic_template_data. Checkpoint: Test the mail JSON representation with mail.get() before calling send() to verify structure.
Apply Settings and Categories — Configure tracking_settings (click/open tracking), mail_settings (sandbox, bypass list management), and categories for analytics grouping. Attach files using Attachment helper with Base64-encoded content and proper MIME types. Checkpoint: Enable sandbox mode (mail_settings.sandbox_mode.enable = True) during development.
Send and Handle Response — Call client.send(mail.get()) and inspect the response. A 202 Accepted means the message is queued. Catch BadRequestsError and extract the response.body for validation error details. Checkpoint: Log the x-message-id header from successful responses for delivery tracing.
Process Event Webhooks — Accept POST requests at your webhook endpoint. Verify the signature using EventWebhook and EcdsaPublicKey. Parse the event array and dispatch based on event_type. Checkpoint: Store bounced and spam-report events in a suppression list to prevent re-sending.
Implementation Patterns
Pattern 1: Dynamic Template Email with Personalization
import os
import json
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Email, To, Content, TrackingSettings, ClickTracking, OpenTracking, MailSettings, SandBoxMode
from python_http_client.exceptions import BadRequestsError
# ❌ BAD — raw dict construction, no personalization, no tracking, no error handling
client = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])
response = client.client.mail.send.post(
request_body={
"personalizations": [{"to": [{"email": "user@example.com"}], "subject": "Hello"}],
"from": {"email": "noreply@example.com"},
"content": [{"type": "text/plain", "value": "Hello world"}],
}
)
print(response.status_code)
# ✅ GOOD — typed Mail helper, dynamic template with personalization, error handling, tracking
import logging
logger = logging.getLogger(__name__)
def send_dynamic_email(
to_email: str,
to_name: str | None,
template_id: str,
template_data: dict,
from_email: str = "noreply@example.com",
from_name: str = "Example App",
) -> str | None:
"""Send a dynamic template email and return the message ID."""
message = Mail(
from_email=Email(from_email, from_name),
to_emails=To(to_email, to_name or ""),
)
message.template_id = template_id
message.dynamic_template_data = template_data
# Explicit tracking settings
message.tracking_settings = TrackingSettings(
click_tracking=ClickTracking(enable=True, enable_text=True),
open_tracking=OpenTracking(enable=True),
)
# Attach category for analytics grouping
message.categories = ["transactional", "python-sdk"]
try:
sg = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])
response = sg.send(message)
if response.status_code == 202:
msg_id = response.headers.get("X-Message-Id")
logger.info("Email sent", extra={"message_id": msg_id, "to": to_email, "template": template_id})
return msg_id
else:
logger.warning("Unexpected status", extra={"status": response.status_code, "body": response.body})
return None
except BadRequestsError as exc:
body = json.loads(exc.body) if exc.body else {}
errors = body.get("errors", [])
logger.error("SendGrid API error", extra={"errors": errors, "status": exc.status_code})
raise RuntimeError(f"SendGrid rejected request: {errors}") from exc
Pattern 2: Inbound Parse Webhook Processing
import os
import json
import email
from sendgrid.helpers.inbound import parse
from sendgrid.helpers.inbound.attachment import Attachment
# ❌ BAD — assumes raw JSON body, no multipart handling, ignores attachments
@app.post("/inbound")
async def inbound_webhook(request):
data = await request.json()
print(f"From: {data['from']}, Subject: {data['subject']}")
return {"status": "ok"}
# ✅ GOOD — proper multipart parsing, attachment handling, validation
from sendgrid.helpers.inbound import parse as inbound_parse
from sendgrid.helpers.inbound.config import Config
def process_inbound_email(raw_body: bytes) -> dict:
"""Parse a SendGrid Inbound Parse webhook payload."""
config = Config(
raw_body=raw_body,
raw_headers=True,
)
parsed = inbound_parse(config)
result = {
"from": parsed.from_email or "",
"to": parsed.to or "",
"subject": parsed.subject or "",
"text": parsed.text or "",
"html": parsed.html or "",
"spam_score": parsed.spam_score,
"attachments": [],
}
for attachment in parsed.attachments or []:
result["attachments"].append({
"filename": attachment.filename or "unnamed",
"content_type": attachment.content_type or "application/octet-stream",
"size_bytes": len(attachment.content) if attachment.content else 0,
"content": attachment.content, # bytes, store to disk or S3
})
return result
Pattern 3: Sandbox Mode for Testing
# ✅ GOOD — sandbox mode enabled for testing; verifies JSON without sending
def send_test_email(to_email: str, template_id: str, template_data: dict) -> dict:
"""Send an email in sandbox mode. Validates the request without delivering."""
message = Mail(
from_email=Email("test@example.com", "Test Sender"),
to_emails=To(to_email),
)
message.template_id = template_id
message.dynamic_template_data = template_data
# Enable sandbox mode — validates request structure, does NOT deliver
message.mail_settings = MailSettings()
message.mail_settings.sandbox_mode = SandBoxMode(enable=True)
sg = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])
response = sg.send(message)
return {
"status_code": response.status_code,
"body": response.body,
"headers": dict(response.headers),
}
Constraints
MUST DO
- Store
SENDGRID_API_KEY as an environment variable — never hardcode it or commit it to version control
- Use
Mail helper objects from sendgrid.helpers.mail instead of raw request_body dicts (the helper validates at construction time)
- Prefer Dynamic Templates (
template_id + dynamic_template_data) over inline html_content for production — templates decouple design from code
- Set
tracking_settings explicitly (click tracking, open tracking) on every message to maintain deliverability analytics
- Validate sender email addresses — each
from address must be verified in the SendGrid Sender Authentication settings
- Enable sandbox mode during development to test without consuming email quota
MUST NOT DO
- Set
sandbox_mode in production — messages will not be delivered
- Assume 202 means immediate delivery — polling or callbacks are not available; rely on Event Webhooks for delivery state
- Override the
content field when using template_id — dynamic templates provide their own content
- Send to recipients who have previously hard-bounced or marked spam — always check the suppression list first
- Use the deprecated
sendgrid-py v2 API — always use the v3 Mail Send endpoint via client.send()
Output Template
When implementing SendGrid API code, the output must follow this structure:
- Client Initialization —
SendGridAPIClient instantiated from SENDGRID_API_KEY environment variable
- Mail Construction —
Mail() with from_email, to_emails, and either template_id or html_content
- Personalization —
dynamic_template_data dict with all template variables for each recipient
- Settings — Explicit
tracking_settings and optional mail_settings (sandbox mode for testing)
- Sending —
client.send(message) wrapped in try/except catching BadRequestsError
- Response Handling — 202 = queued; log
X-Message-Id; parse error body for validation failures
Related Skills
| Skill |
Purpose |
coding-twilio-api |
SMS, Voice, WhatsApp via Twilio — complement to SendGrid for multichannel comms |
coding-mailgun-api |
Alternative email delivery via Mailgun — compare for cost/features |
coding-slack-api |
Team notifications via Slack — use for internal alerts alongside email |
Live References
1---2name: sendgrid-api3description: Integrates Twilio SendGrid API (Mail Send, Dynamic Templates, Marketing Campaigns, Inbound Parse, Event Webhooks) using the sendgrid Python SDK v6.x with proper mail construction and deliverability patterns.4license: MIT5---678910# SendGrid Email API Integration1112Integrates Twilio SendGrids Mail Send API, Dynamic Templates, Marketing Campaigns, and Inbound Parse using the `sendgrid` Python SDK v6.x. When loaded, this skill makes the model implement email delivery with proper Mail helper construction, dynamic template personalization, attachment handling, async sending, event webhook processing, and deliverability optimization.1314## TL;DR for Code Generation1516- [ ] Initialize `SendGridAPIClient` from `SENDGRID_API_KEY` environment variable — never hardcode the key17- [ ] Use `Mail` helper from `sendgrid.helpers.mail` to construct messages, not raw dicts18- [ ] Use Dynamic Templates via `template_id` + `personalization.dynamic_template_data` for production emails19- [ ] Validate email addresses with `Email` helper — set a `from` name and email that is verified in SendGrid20- [ ] Set `tracking_settings` explicitly — enable click tracking, open tracking, and Google Analytics per-message21- [ ] Catch `sgrest.exceptions.BadRequestsError` for API errors and inspect the response body22- [ ] Use `mail_settings` to enable sandbox mode for testing (bypasses sending, validates the request)2324---2526## When to Use2728Use this skill when:2930- Sending transactional emails (welcome, password reset, receipts, notifications) from Python applications31- Implementing Dynamic Template-based emails with Handlebars template variables and per-recipient personalization32- Building Marketing Campaigns with contact management, list segmentation, and campaign scheduling33- Processing Inbound Parse webhooks to receive emails with attachments programmatically34- Handling SendGrid Event Webhooks (delivered, bounced, opened, clicked, spam reported) for delivery monitoring35- Sending batched emails with substitution per recipient using the Mail Send API v33637---3839## When NOT to Use4041Avoid this skill for:4243- SMS or WhatsApp messaging (use `coding-twilio-api` instead)44- Basic SMTP relay for low-volume internal alerts (use `smtplib` + standard SMTP instead)45- Team chat or collaboration notifications (use `coding-slack-api` instead)46- Hosting your own email infrastructure or open relay configuration4748---4950## Core Workflow51521. **Initialize the Client** — Create a `SendGridAPIClient(os.environ["SENDGRID_API_KEY"])`. Validate the key on startup by calling `client.client._get("/v3/scopes")`. **Checkpoint:** Verify the API key has `mail.send` scope for sending, or `asm.groups` + `templates` for template management.53542. **Construct the Mail Object** — Use `Mail()` with `from_email`, `to_emails`, `subject`, and either `html_content`/`plain_text_content` or `template_id`. For bulk sends, build a `personalization` object per recipient with `dynamic_template_data`. **Checkpoint:** Test the mail JSON representation with `mail.get()` before calling `send()` to verify structure.55563. **Apply Settings and Categories** — Configure `tracking_settings` (click/open tracking), `mail_settings` (sandbox, bypass list management), and `categories` for analytics grouping. Attach files using `Attachment` helper with Base64-encoded content and proper MIME types. **Checkpoint:** Enable sandbox mode (`mail_settings.sandbox_mode.enable = True`) during development.57584. **Send and Handle Response** — Call `client.send(mail.get())` and inspect the response. A 202 Accepted means the message is queued. Catch `BadRequestsError` and extract the `response.body` for validation error details. **Checkpoint:** Log the `x-message-id` header from successful responses for delivery tracing.59605. **Process Event Webhooks** — Accept POST requests at your webhook endpoint. Verify the signature using `EventWebhook` and `EcdsaPublicKey`. Parse the event array and dispatch based on `event_type`. **Checkpoint:** Store bounced and spam-report events in a suppression list to prevent re-sending.6162---6364## Implementation Patterns6566### Pattern 1: Dynamic Template Email with Personalization6768```python69import os70import json71from sendgrid import SendGridAPIClient72from sendgrid.helpers.mail import Mail, Email, To, Content, TrackingSettings, ClickTracking, OpenTracking, MailSettings, SandBoxMode73from python_http_client.exceptions import BadRequestsError7475# ❌ BAD — raw dict construction, no personalization, no tracking, no error handling76client = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])77response = client.client.mail.send.post(78 request_body={79 "personalizations": [{"to": [{"email": "user@example.com"}], "subject": "Hello"}],80 "from": {"email": "noreply@example.com"},81 "content": [{"type": "text/plain", "value": "Hello world"}],82 }83)84print(response.status_code)8586# ✅ GOOD — typed Mail helper, dynamic template with personalization, error handling, tracking87import logging8889logger = logging.getLogger(__name__)909192def send_dynamic_email(93 to_email: str,94 to_name: str | None,95 template_id: str,96 template_data: dict,97 from_email: str = "noreply@example.com",98 from_name: str = "Example App",99) -> str | None:100 """Send a dynamic template email and return the message ID."""101 message = Mail(102 from_email=Email(from_email, from_name),103 to_emails=To(to_email, to_name or ""),104 )105 message.template_id = template_id106 message.dynamic_template_data = template_data107108 # Explicit tracking settings109 message.tracking_settings = TrackingSettings(110 click_tracking=ClickTracking(enable=True, enable_text=True),111 open_tracking=OpenTracking(enable=True),112 )113114 # Attach category for analytics grouping115 message.categories = ["transactional", "python-sdk"]116117 try:118 sg = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])119 response = sg.send(message)120121 if response.status_code == 202:122 msg_id = response.headers.get("X-Message-Id")123 logger.info("Email sent", extra={"message_id": msg_id, "to": to_email, "template": template_id})124 return msg_id125 else:126 logger.warning("Unexpected status", extra={"status": response.status_code, "body": response.body})127 return None128 except BadRequestsError as exc:129 body = json.loads(exc.body) if exc.body else {}130 errors = body.get("errors", [])131 logger.error("SendGrid API error", extra={"errors": errors, "status": exc.status_code})132 raise RuntimeError(f"SendGrid rejected request: {errors}") from exc133```134135### Pattern 2: Inbound Parse Webhook Processing136137```python138import os139import json140import email141from sendgrid.helpers.inbound import parse142from sendgrid.helpers.inbound.attachment import Attachment143144# ❌ BAD — assumes raw JSON body, no multipart handling, ignores attachments145@app.post("/inbound")146async def inbound_webhook(request):147 data = await request.json()148 print(f"From: {data['from']}, Subject: {data['subject']}")149 return {"status": "ok"}150151# ✅ GOOD — proper multipart parsing, attachment handling, validation152from sendgrid.helpers.inbound import parse as inbound_parse153from sendgrid.helpers.inbound.config import Config154155156def process_inbound_email(raw_body: bytes) -> dict:157 """Parse a SendGrid Inbound Parse webhook payload."""158 config = Config(159 raw_body=raw_body,160 raw_headers=True,161 )162 parsed = inbound_parse(config)163164 result = {165 "from": parsed.from_email or "",166 "to": parsed.to or "",167 "subject": parsed.subject or "",168 "text": parsed.text or "",169 "html": parsed.html or "",170 "spam_score": parsed.spam_score,171 "attachments": [],172 }173174 for attachment in parsed.attachments or []:175 result["attachments"].append({176 "filename": attachment.filename or "unnamed",177 "content_type": attachment.content_type or "application/octet-stream",178 "size_bytes": len(attachment.content) if attachment.content else 0,179 "content": attachment.content, # bytes, store to disk or S3180 })181182 return result183```184185### Pattern 3: Sandbox Mode for Testing186187```python188# ✅ GOOD — sandbox mode enabled for testing; verifies JSON without sending189def send_test_email(to_email: str, template_id: str, template_data: dict) -> dict:190 """Send an email in sandbox mode. Validates the request without delivering."""191 message = Mail(192 from_email=Email("test@example.com", "Test Sender"),193 to_emails=To(to_email),194 )195 message.template_id = template_id196 message.dynamic_template_data = template_data197198 # Enable sandbox mode — validates request structure, does NOT deliver199 message.mail_settings = MailSettings()200 message.mail_settings.sandbox_mode = SandBoxMode(enable=True)201202 sg = SendGridAPIClient(os.environ["SENDGRID_API_KEY"])203 response = sg.send(message)204205 return {206 "status_code": response.status_code,207 "body": response.body,208 "headers": dict(response.headers),209 }210```211212---213214## Constraints215216### MUST DO217- Store `SENDGRID_API_KEY` as an environment variable — never hardcode it or commit it to version control218- Use `Mail` helper objects from `sendgrid.helpers.mail` instead of raw `request_body` dicts (the helper validates at construction time)219- Prefer Dynamic Templates (`template_id` + `dynamic_template_data`) over inline `html_content` for production — templates decouple design from code220- Set `tracking_settings` explicitly (click tracking, open tracking) on every message to maintain deliverability analytics221- Validate sender email addresses — each `from` address must be verified in the SendGrid Sender Authentication settings222- Enable sandbox mode during development to test without consuming email quota223224### MUST NOT DO225- Set `sandbox_mode` in production — messages will not be delivered226- Assume 202 means immediate delivery — polling or callbacks are not available; rely on Event Webhooks for delivery state227- Override the `content` field when using `template_id` — dynamic templates provide their own content228- Send to recipients who have previously hard-bounced or marked spam — always check the suppression list first229- Use the deprecated `sendgrid-py` v2 API — always use the v3 Mail Send endpoint via `client.send()`230231---232233## Output Template234235When implementing SendGrid API code, the output must follow this structure:2362371. **Client Initialization** — `SendGridAPIClient` instantiated from `SENDGRID_API_KEY` environment variable2382. **Mail Construction** — `Mail()` with `from_email`, `to_emails`, and either `template_id` or `html_content`2393. **Personalization** — `dynamic_template_data` dict with all template variables for each recipient2404. **Settings** — Explicit `tracking_settings` and optional `mail_settings` (sandbox mode for testing)2415. **Sending** — `client.send(message)` wrapped in try/except catching `BadRequestsError`2426. **Response Handling** — 202 = queued; log `X-Message-Id`; parse error body for validation failures243244---245246## Related Skills247248| Skill | Purpose |249|---|---|250| `coding-twilio-api` | SMS, Voice, WhatsApp via Twilio — complement to SendGrid for multichannel comms |251| `coding-mailgun-api` | Alternative email delivery via Mailgun — compare for cost/features |252| `coding-slack-api` | Team notifications via Slack — use for internal alerts alongside email |253254---255256## Live References257258- [SendGrid Python SDK Reference (v6.x)](https://github.com/sendgrid/sendgrid-python)259- [SendGrid Mail Send API v3](https://docs.sendgrid.com/api-reference/mail-send/mail-send)260- [SendGrid Dynamic Templates](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates)261- [SendGrid Event Webhook](https://docs.sendgrid.com/for-developers/tracking-events/event-webhook)262- [SendGrid Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/inbound-email)263- [SendGrid Sender Authentication](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication)264- [SendGrid Suppression Management](https://docs.sendgrid.com/ui/sending-email/blocked-emails)265- [PyPI: sendgrid package](https://pypi.org/project/sendgrid/)266- [GitHub: sendgrid/sendgrid-python](https://github.com/sendgrid/sendgrid-python)