skills/twilio/twilio-email/SKILL.md
SendGrid: transactional email, templates Handlebars, event webhooks, suppression, SPF/DKIM/DMARC
npx skillsauth add alphaonedev/openclaw-graph twilio-emailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Enable OpenClaw to implement, operate, and troubleshoot SendGrid (Twilio) transactional email in production:
This skill is for engineers building reliable email delivery pipelines and maintaining them under real traffic, compliance constraints, and deliverability requirements.
mail.send (required)templates.read, templates.write (if managing templates)suppression.read, suppression.write (if managing suppressions)eventwebhook.read, eventwebhook.write (if managing webhook settings)@sendgrid/[email protected], @sendgrid/[email protected]sendgrid==6.11.0@sendgrid/[email protected] (or implement ECDSA verify manually)github.com/sendgrid/[email protected] (mail send), custom verifier for webhookhttps://api.sendgrid.com (TCP 443).prod-mail-send-2026-02Mail Send (and others as needed)prod/sendgrid/api_keyprod-sendgrid-api-keysecret/data/prod/sendgridexport SENDGRID_API_KEY='SG.xxxxxx.yyyyyy'
This skill focuses on transactional via /v3/mail/send and Dynamic Templates.
A send request is a JSON payload with:
from (must be verified or domain-authenticated)personalizations[]:
to[], cc[], bcc[]dynamic_template_data (for dynamic templates)custom_args (for correlation IDs; appears in event webhook)template_id (dynamic template)categories[] (for analytics grouping)asm (unsubscribe groups)mail_settings, tracking_settingstemplate_id.{{var}} HTML-escaped by default{{{var}}} unescaped (dangerous; avoid unless sanitized){{#if}}, {{#each}}, {{else}}SendGrid posts batched JSON events to your endpoint, e.g.:
delivered, open, click, bounce, dropped, deferred, spamreport, unsubscribe, group_unsubscribe, group_resubscribe, processedKey production requirements:
(sg_event_id) or (sg_message_id, event, timestamp).Suppression lists prevent delivery:
Your system must:
p=none, move to quarantine/reject.Repository: https://github.com/twilio/twilio-python
PyPI: pip install twilio sendgrid · Supported: Python 3.7–3.13
# SendGrid is the email layer (separate package, same Twilio ecosystem)
import sendgrid
from sendgrid.helpers.mail import Mail
import os
sg = sendgrid.SendGridAPIClient(api_key=os.environ["SENDGRID_API_KEY"])
message = Mail(
from_email="[email protected]",
to_emails="[email protected]",
subject="Hello from Python!",
html_content="<strong>Hello!</strong>"
)
response = sg.send(message)
print(response.status_code) # 202 = queued
Source: sendgrid/sendgrid-python (official SendGrid Python SDK, part of the Twilio family)
Install Node 20, Python 3.11, and tools:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg jq python3.11 python3.11-venv python3-pip
# NodeSource Node 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # v20.11.1 (or later 20.x)
npm -v # 10.2.4 (or later)
Project dependencies (Node):
mkdir -p services/email-sender
cd services/email-sender
npm init -y
npm install @sendgrid/[email protected] @sendgrid/[email protected] [email protected] [email protected]
Python venv:
mkdir -p services/email-worker
cd services/email-worker
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip==23.3.2
pip install sendgrid==6.11.0 fastapi==0.109.2 uvicorn==0.27.1 pydantic==2.6.1
sudo dnf install -y nodejs-20.11.1 npm jq python3.11 python3.11-pip python3.11-virtualenv openssl
node -v
python3.11 -V
Using Homebrew:
brew update
brew install node@20 jq [email protected] openssl@3
echo 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc # Apple Silicon
echo 'export PATH="/usr/local/opt/node@20/bin:$PATH"' >> ~/.zshrc # Intel
source ~/.zshrc
node -v
python3.11 -V
Create .env (do not commit):
Path: services/email-sender/.env
SENDGRID_API_KEY=SG.xxxxxx.yyyyyy
[email protected]
SENDGRID_FROM_NAME=Example Notifications
SENDGRID_TEMPLATE_PASSWORD_RESET=d-2f3c4b5a6d7e8f90123456789abcdeff
SENDGRID_TEMPLATE_RECEIPT=d-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
SENDGRID_EVENT_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
EMAIL_ENV=prod
Load it (bash):
set -a
source .env
set +a
Requirements:
custom_args.Node example sender (production-safe skeleton):
Path: services/email-sender/src/send.ts
import sgMail from "@sendgrid/mail";
import { z } from "zod";
import pino from "pino";
import crypto from "crypto";
const log = pino({ level: process.env.LOG_LEVEL ?? "info" });
const Env = z.object({
SENDGRID_API_KEY: z.string().min(20),
SENDGRID_FROM_EMAIL: z.string().email(),
SENDGRID_FROM_NAME: z.string().min(1),
EMAIL_ENV: z.enum(["dev", "staging", "prod"]).default("prod"),
});
const env = Env.parse(process.env);
sgMail.setApiKey(env.SENDGRID_API_KEY);
export type SendTemplateEmailInput = {
to: string;
templateId: string;
dynamicTemplateData: Record<string, unknown>;
categories?: string[];
customArgs?: Record<string, string>;
};
function makeIdempotencyKey(input: SendTemplateEmailInput): string {
// Stable key for same logical send (avoid duplicates on retries)
const h = crypto.createHash("sha256");
h.update(input.to);
h.update(input.templateId);
h.update(JSON.stringify(input.dynamicTemplateData));
return h.digest("hex");
}
export async function sendTemplateEmail(input: SendTemplateEmailInput) {
const idempotencyKey = makeIdempotencyKey(input);
const msg = {
to: input.to,
from: { email: env.SENDGRID_FROM_EMAIL, name: env.SENDGRID_FROM_NAME },
templateId: input.templateId,
dynamicTemplateData: input.dynamicTemplateData,
categories: input.categories ?? ["transactional"],
customArgs: {
...input.customArgs,
idempotency_key: idempotencyKey,
env: env.EMAIL_ENV,
},
mailSettings: {
sandboxMode: { enable: env.EMAIL_ENV !== "prod" },
},
};
// SendGrid does not support an explicit idempotency header for /mail/send.
// You must implement idempotency in your app (e.g., outbox table).
try {
const [resp] = await sgMail.send(msg as any, false);
log.info(
{ statusCode: resp.statusCode, headers: resp.headers, to: input.to, templateId: input.templateId },
"sendgrid mail.send accepted"
);
return { accepted: true, statusCode: resp.statusCode, idempotencyKey };
} catch (err: any) {
const statusCode = err?.code ?? err?.response?.statusCode;
const body = err?.response?.body;
log.error({ err, statusCode, body, to: input.to, templateId: input.templateId }, "sendgrid mail.send failed");
throw err;
}
}
Operational notes:
mailSettings.sandboxMode.enable=true prevents actual delivery (use in dev/staging).(idempotency_key) and only send once.Rules:
{{var}} (escaped) for user-provided content.{{{var}}} unless content is sanitized HTML.Example template data contract:
{
"app_name": "Example",
"reset_url": "https://app.example.com/reset?token=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"support_email": "[email protected]",
"expires_minutes": 30
}
Handlebars snippet:
<p>Reset your {{app_name}} password:</p>
<p><a href="{{reset_url}}">Reset password</a></p>
<p>This link expires in {{expires_minutes}} minutes.</p>
SendGrid Event Webhook uses ECDSA signatures:
X-Twilio-Email-Event-Webhook-SignatureX-Twilio-Email-Event-Webhook-Timestamptimestamp + payload (raw request body)FastAPI example (raw body + verify):
Path: services/email-worker/app/webhook.py
import base64
import hashlib
from fastapi import APIRouter, Request, HTTPException
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature
router = APIRouter()
def verify_sendgrid_signature(public_key_pem: str, signature_b64: str, timestamp: str, payload: bytes) -> None:
public_key = serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
if not isinstance(public_key, ec.EllipticCurvePublicKey):
raise ValueError("public key is not EC")
signed = timestamp.encode("utf-8") + payload
sig = base64.b64decode(signature_b64)
try:
public_key.verify(sig, signed, ec.ECDSA(hashes.SHA256()))
except InvalidSignature as e:
raise
@router.post("/webhooks/sendgrid/events")
async def sendgrid_events(request: Request):
sig = request.headers.get("X-Twilio-Email-Event-Webhook-Signature")
ts = request.headers.get("X-Twilio-Email-Event-Webhook-Timestamp")
if not sig or not ts:
raise HTTPException(status_code=400, detail="missing signature headers")
raw = await request.body()
public_key_pem = request.app.state.sendgrid_event_public_key_pem
try:
verify_sendgrid_signature(public_key_pem, sig, ts, raw)
except Exception:
raise HTTPException(status_code=401, detail="invalid signature")
events = await request.json()
# events is a list of dicts
# Idempotency: dedupe by sg_event_id
# Persist first, then ack 2xx.
return {"ok": True, "count": len(events)}
Replay protection:
ts to current time; allow small skew.(sg_event_id) with unique constraint; ignore duplicates.Use suppression endpoints to:
Common endpoints:
/v3/asm/suppressions/global/v3/asm/groups/{group_id}/suppressions/v3/suppression/bounces/v3/suppression/blocks/v3/suppression/spam_reportsProduction baseline:
v=DMARC1; p=none; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1; adkim=s; aspf=squarantine then reject after monitoring.If using dedicated IPs:
This section covers SendGrid API via curl (no official SendGrid CLI is assumed) and common operational commands.
All API calls:
Authorization: Bearer $SENDGRID_API_KEYapplication/jsonPOST /v3/mail/sendcurl -sS -D /tmp/sg_headers.txt -o /tmp/sg_body.txt \
-X POST "https://api.sendgrid.com/v3/mail/send" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{
"from": { "email": "[email protected]", "name": "Example Notifications" },
"personalizations": [
{
"to": [{ "email": "[email protected]", "name": "Alice" }],
"dynamic_template_data": {
"app_name": "Example",
"reset_url": "https://app.example.com/reset?token=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"expires_minutes": 30
},
"custom_args": {
"user_id": "u_12345",
"request_id": "req_01HPQ7ZK8Z7WQ2J9R8D2E6M3QK"
}
}
],
"template_id": "d-2f3c4b5a6d7e8f90123456789abcdeff",
"categories": ["password_reset", "transactional"],
"tracking_settings": {
"click_tracking": { "enable": true, "enable_text": true },
"open_tracking": { "enable": true }
}
}
JSON
Relevant behaviors:
202 Accepted with empty body.4xx/5xx with JSON error details.GET /v3/templatesFlags:
?generations=dynamic filter (if supported in your account)?page_size=... paginationcurl -sS "https://api.sendgrid.com/v3/templates?generations=dynamic&page_size=50" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" | jq .
GET /v3/templates/{template_id}curl -sS "https://api.sendgrid.com/v3/templates/d-2f3c4b5a6d7e8f90123456789abcdeff" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" | jq .
POST /v3/templatescurl -sS -X POST "https://api.sendgrid.com/v3/templates" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"name":"prod_password_reset","generation":"dynamic"}' | jq .
POST /v3/templates/{template_id}/versionscurl -sS -X POST "https://api.sendgrid.com/v3/templates/d-2f3c4b5a6d7e8f90123456789abcdeff/versions" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'JSON' | jq .
{
"active": 0,
"name": "v2026-02-21",
"subject": "Reset your password",
"html_content": "<p>Reset your password: <a href=\"{{reset_url}}\">Reset</a></p>",
"plain_content": "Reset your password: {{reset_url}}"
}
JSON
PATCH /v3/templates/{template_id}/versions/{version_id}curl -sS -X PATCH "https://api.sendgrid.com/v3/templates/d-2f3c4b5a6d7e8f90123456789abcdeff/versions/3c1b2a9d-0f2a-4c7b-9d2a-1a2b3c4d5e6f" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"active":1}' | jq .
GET /v3/user/webhooks/event/settingscurl -sS "https://api.sendgrid.com/v3/user/webhooks/event/settings" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" | jq .
PATCH /v3/user/webhooks/event/settingsKey fields:
enabled (boolean)url (string)group_resubscribe, group_unsubscribe, spam_report, bounce, deferred, delivered, dropped, open, click, processed, unsubscribe (booleans)oauth_client_id, oauth_client_secret, oauth_token_url (if using OAuth; uncommon)curl -sS -X PATCH "https://api.sendgrid.com/v3/user/webhooks/event/settings" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'JSON' | jq .
{
"enabled": true,
"url": "https://email-hooks.example.com/webhooks/sendgrid/events",
"delivered": true,
"bounce": true,
"dropped": true,
"deferred": true,
"spam_report": true,
"unsubscribe": true,
"group_unsubscribe": true,
"group_resubscribe": true,
"open": true,
"click": true,
"processed": true
}
JSON
GET /v3/asm/suppressions/globalPagination:
?offset=0&limit=500curl -sS "https://api.sendgrid.com/v3/asm/suppressions/global?offset=0&limit=500" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" | jq .
POST /v3/asm/suppressions/globalcurl -sS -X POST "https://api.sendgrid.com/v3/asm/suppressions/global" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"recipient_emails":["[email protected]"]}' | jq .
DELETE /v3/asm/suppressions/global/{email}curl -sS -X DELETE "https://api.sendgrid.com/v3/asm/suppressions/global/alice%40example.net" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" -i
GET /v3/suppression/bouncesFilters:
?start_time=1708473600&end_time=1708560000 (unix seconds)[email protected]curl -sS "https://api.sendgrid.com/v3/suppression/[email protected]" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" | jq .
DELETE /v3/suppression/bounces/{email}curl -sS -X DELETE "https://api.sendgrid.com/v3/suppression/bounces/alice%40example.net" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" -i
dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short CNAME s1._domainkey.mg.example.com
dig +short CNAME s2._domainkey.mg.example.com
openssl s_client -connect email-hooks.example.com:443 -servername email-hooks.example.com -tls1_2 </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates
Path: services/email-sender/config/email.config.toml
[sendgrid]
api_base_url = "https://api.sendgrid.com"
from_email = "[email protected]"
from_name = "Example Notifications"
[templates]
password_reset = "d-2f3c4b5a6d7e8f90123456789abcdeff"
receipt = "d-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
[webhook]
event_public_key_pem_path = "/etc/email-sender/sendgrid_event_webhook_public_key.pem"
[delivery]
categories = ["transactional"]
sandbox_mode = false
[retry]
max_attempts = 5
base_delay_ms = 200
max_delay_ms = 5000
retry_on_status = [429, 500, 502, 503, 504]
Path: /etc/systemd/system/email-sender.service
[Unit]
Description=Email Sender Service (SendGrid)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=email
Group=email
WorkingDirectory=/opt/email-sender
Environment=NODE_ENV=production
EnvironmentFile=/etc/email-sender/email-sender.env
ExecStart=/usr/bin/node /opt/email-sender/dist/index.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/email-sender
AmbientCapabilities=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target
Path: /etc/email-sender/email-sender.env
SENDGRID_API_KEY=SG.xxxxxx.yyyyyy
[email protected]
SENDGRID_FROM_NAME=Example Notifications
LOG_LEVEL=info
EMAIL_ENV=prod
Path: /etc/nginx/conf.d/sendgrid-webhook.conf
server {
listen 443 ssl http2;
server_name email-hooks.example.com;
ssl_certificate /etc/letsencrypt/live/email-hooks.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/email-hooks.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 2m;
location /webhooks/sendgrid/events {
proxy_pass http://127.0.0.1:8080/webhooks/sendgrid/events;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
}
}
Use when email sends are triggered by DB transactions.
Pattern:
email_outbox.sent_at and stores provider_response.PostgreSQL schema:
CREATE TABLE email_outbox (
id bigserial PRIMARY KEY,
idempotency_key text NOT NULL UNIQUE,
to_email text NOT NULL,
template_id text NOT NULL,
dynamic_template_data jsonb NOT NULL,
categories text[] NOT NULL DEFAULT ARRAY['transactional'],
created_at timestamptz NOT NULL DEFAULT now(),
sent_at timestamptz,
last_error text,
attempt_count int NOT NULL DEFAULT 0
);
CREATE INDEX email_outbox_pending_idx ON email_outbox (created_at) WHERE sent_at IS NULL;
request_id (ingress)user_idnotification_id (logical notification)custom_args so they appear in Event Webhook payloads.Recommended flow:
Benefits:
Example SQS enqueue (pseudo):
SendMessageBatch with raw JSON linesTreat templates as code:
templates/sendgrid/password_reset/v2026-02-21.htmlInclude the exact error text and what to do.
HTTP/2 401
{"errors":[{"message":"Permission denied, wrong credentials","field":null,"help":null}]}
Root cause:
SENDGRID_API_KEY invalid/revoked, or missing required scopes.Fix:
mail.send.HTTP/2 403
{"errors":[{"message":"access forbidden","field":null,"help":null}]}
Root cause:
Fix:
{"errors":[{"message":"The email address is invalid.","field":"personalizations.0.to.0.email","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.personalizations.to.email"}]}
Root cause:
to address formatting.Fix:
{"errors":[{"message":"The template_id is not a valid template ID.","field":"template_id","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html"}]}
Root cause:
Fix:
GET /v3/templates/{id}.d-.HTTP/2 429
{"errors":[{"message":"Too many requests","field":null,"help":null}]}
Root cause:
Fix:
NGINX:
413 Request Entity Too Large
Root cause:
client_max_body_size.Fix:
client_max_body_size (e.g., 2m or 5m).Your service logs:
HTTP 401 {"detail":"invalid signature"}
Root cause:
Fix:
Event webhook includes:
{
"event":"dropped",
"reason":"Bounced Address",
"email":"[email protected]"
}
Root cause:
Fix:
Event webhook bounce:
{
"event":"bounce",
"status":"5.1.1",
"reason":"550 5.1.1 The email account that you tried to reach does not exist."
}
Root cause:
Fix:
{
"event":"deferred",
"response":"451 4.7.1 Try again later",
"attempt":"2"
}
Root cause:
Fix:
SENDGRID_API_KEY only in a secret manager; never in repo or container image.prod-mail-send (mail.send only)prod-templates-admin (templates.* only, restricted to CI)User=email).NoNewPrivileges=true, ProtectSystem=strict, ProtectHome=true in systemd.npm audit in CI; pin versions with lockfile.pip-audit and hash-locked requirements for prod.List-Unsubscribe where applicable; for transactional, still consider preference center./v3/mail/send supports multiple personalizations in one request.Expected impact:
Constraints:
custom_args: key/value, appears in event webhook; best for correlation IDs.categories: array of strings; used for SendGrid stats/analytics grouping.Production guidance:
password_reset, receipt), not per-user.If you use SendGrid Inbound Parse:
from, to, subject, text, html, attachments.Hardening:
If sending on behalf of multiple brands/domains:
INSERT INTO email_outbox (idempotency_key, to_email, template_id, dynamic_template_data, categories)
VALUES (
'email:password_reset:u_12345:req_01HPQ7ZK8Z7WQ2J9R8D2E6M3QK',
'[email protected]',
'd-2f3c4b5a6d7e8f90123456789abcdeff',
'{"app_name":"Example","reset_url":"https://app.example.com/reset?token=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d","expires_minutes":30}',
ARRAY['password_reset','transactional']
);
Worker reads pending rows, calls sendTemplateEmail, marks sent.
Event webhook updates delivery status keyed by custom_args.request_id.
SendGrid supports attachments (base64). Keep size small; large attachments hurt deliverability.
PDF_B64="$(base64 -w 0 receipt_2026-02-21.pdf)"
curl -sS -X POST "https://api.sendgrid.com/v3/mail/send" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"from": { "email": "[email protected]", "name": "Example Billing" },
"personalizations": [
{
"to": [{ "email": "[email protected]" }],
"dynamic_template_data": {
"amount": "49.00",
"currency": "USD",
"receipt_id": "rcpt_9f3a2c1d"
},
"custom_args": { "receipt_id": "rcpt_9f3a2c1d" }
}
],
"template_id": "d-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"attachments": [
{
"content": "${PDF_B64}",
"type": "application/pdf",
"filename": "receipt_rcpt_9f3a2c1d.pdf",
"disposition": "attachment"
}
],
"categories": ["receipt","transactional"]
}
JSON
Schema:
CREATE TABLE sendgrid_events (
sg_event_id text PRIMARY KEY,
sg_message_id text,
event text NOT NULL,
email text NOT NULL,
ts bigint NOT NULL,
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now()
);
Insert with conflict ignore:
INSERT INTO sendgrid_events (sg_event_id, sg_message_id, event, email, ts, payload)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (sg_event_id) DO NOTHING;
For critical emails (e.g., legal notices), you may pre-check suppression:
curl -sS "https://api.sendgrid.com/v3/asm/suppressions/global/alice%40example.net" \
-H "Authorization: Bearer ${SENDGRID_API_KEY}" -i
200 OK, user is globally unsubscribed → do not send.404 Not Found, not suppressed globally → proceed (still could be bounced/blocked).Use sparingly; it adds latency and API load.
dig +short CNAME s1._domainkey.mg.example.com
dig +short CNAME s2._domainkey.mg.example.com
dig +short TXT _dmarc.example.com
spf=passdkim=passdmarc=passPseudo-policy:
min(max_delay, base * 2^attempt) + random(0, 100ms)| Task | Command / Endpoint | Key flags / fields |
|---|---|---|
| Send email | POST /v3/mail/send | template_id, personalizations[], dynamic_template_data, custom_args, categories, mail_settings.sandboxMode |
| List templates | GET /v3/templates | generations=dynamic, page_size |
| Create template | POST /v3/templates | name, generation:"dynamic" |
| Add template version | POST /v3/templates/{id}/versions | name, subject, html_content, plain_content, active |
| Activate version | PATCH /v3/templates/{id}/versions/{vid} | active:1 |
| Get webhook settings | GET /v3/user/webhooks/event/settings | n/a |
| Update webhook settings | PATCH /v3/user/webhooks/event/settings | enabled, url, event toggles |
| List global unsub | GET /v3/asm/suppressions/global | offset, limit |
| Add global unsub | POST /v3/asm/suppressions/global | recipient_emails[] |
| Remove global unsub | DELETE /v3/asm/suppressions/global/{email} | URL-encode email |
| List bounces | GET /v3/suppression/bounces | email, start_time, end_time |
| Remove bounce | DELETE /v3/suppression/bounces/{email} | URL-encode email |
| DNS check | dig | _dmarc, _domainkey, SPF TXT |
twilio-email DEPENDS_ON:
twilio-email COMPOSES with:
twilio-messaging (SMS fallback when email bounces or is suppressed)twilio-verify (email channel for OTP; verify + transactional email coordination)twilio-studio (trigger flows that send email + SMS; webhook-driven orchestration)tools
Root web development: project structure, tooling selection, deployment decisions
development
WebAssembly: Rust/Go/C to WASM, wasm-bindgen, Emscripten, WASM Component Model
development
Vue 3: Composition API script setup, Pinia, Vue Router 4, SFCs, Vite, Nuxt 3
tools
Tailwind CSS 4: utility classes, config, JIT, arbitrary values, darkMode, plugins, shadcn/ui