@sendgrid/mail and @sendgrid/client (8.1.6), and verify webhook signatures with @sendgrid/eventwebhook (8.0.0).
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000.
Point @sendgrid/client and @sendgrid/mail at the emulator with the client’s baseUrl. setApiKey resets the client’s base URL, so set the base URL after it:
URL mapping
Auth
Every/v3 request needs Authorization: Bearer <key>. Keys seeded under api_keys or created with POST /v3/api_keys carry their scopes, and a route answers 403 access forbidden when the key lacks the scope it needs: Mail Send needs mail.send, templates need templates.*, Marketing needs marketing.read, and webhook settings need user.webhooks.event.settings.*. Any other token the CLI accepts acts as a Full Access key. A missing header is 401 authorization required; an unknown key is 401.
Errors
Errors use SendGrid’s{ errors: [{ message, field, help }] } body with its messages, for example The from object must be provided for every email send. with field from. Unknown /v3 paths answer 404.
Mail Send
POST /v3/mail/send takes:
personalizations(1 to 1000), each withto,cc,bcc,subject,headers,substitutions,dynamic_template_data,custom_args, andsend_atfrom,reply_toorreply_to_list,subject, andcontent(text/plainfirst, thentext/html)attachmentsin base64;disposition: inlineneeds acontent_idtemplate_id,categories(up to 10),custom_args,headers(reserved headers are refused),send_at,batch_id,asm, andip_pool_namemail_settings.sandbox_modeandmail_settings.footer
X-Message-Id header. Sandbox mode validates the request and answers 200 without sending. A body over 30 MB answers 413. Each personalization becomes one message in the inbox.
A dynamic template (d-...) renders its active version’s subject, HTML, and text with each personalization’s dynamic_template_data. A legacy template fills <%subject%> and <%body%>.
The Handlebars subset matches what SendGrid supports:
{{name}}(HTML escaped) and{{{html}}}{{#if}},{{else}},{{else if}},{{#unless}}, and{{#with}}{{#each}}withthis,@index,@key,@first,@last,../, and@root{{#equals a b}},{{#notEquals}},{{#greaterThan}},{{#lessThan}},{{#and}}, and{{#or}}{{insert x "default=y"}},{{formatDate d "YYYY-MM-DD"}}, and{{length list}}- comments and
~whitespace control
Scheduling
send_at (unix seconds, up to 72 hours ahead) holds mail until then, and it goes out on its own timer. POST /v3/mail/batch creates a batch_id. POST /v3/user/scheduled_sends with { batch_id, status: "pause" | "cancel" } holds or discards the batch, and PATCH and DELETE /v3/user/scheduled_sends/{batch_id} change or lift it. Paused mail more than 72 hours past its send time is discarded.
Templates
/v3/templates creates, lists, gets, renames, and deletes templates (generation: legacy | dynamic). Listing needs page_size (1 to 200), filters with generations, and pages with page_token. POST /v3/templates/{id} duplicates a template. /v3/templates/{id}/versions creates, gets, updates, and deletes versions, and POST /v3/templates/{id}/versions/{version_id}/activate makes one active. Versions with Handlebars errors carry warnings.
API keys
GET /v3/scopes returns the caller’s scopes. /v3/api_keys creates a key with name and optional scopes (Full Access when left out), lists, gets, renames with PATCH, replaces name and scopes with PUT, and deletes. A key cannot grant scopes it lacks, and billing scopes cannot be mixed with others.
Marketing
Contacts
PUT /v3/marketing/contacts— Upsert contacts: takes{ list_ids?, contacts: [{ email, first_name, ..., custom_fields }] }, upserts by email, and returns{ job_id }; read the job atGET /v3/marketing/contacts/imports/{job_id}GET /v3/marketing/contacts— List contactsGET /v3/marketing/contacts/{id}— Get contactDELETE /v3/marketing/contacts?ids=or?delete_all_contacts=true— Delete contactsGET /v3/marketing/contacts/count— Count contactsPOST /v3/marketing/contacts/batch— Get contacts by id: with{ ids }POST /v3/marketing/contacts/search/emails— Search contacts by email: with{ emails }POST /v3/marketing/contacts/search— Search contacts: with an SGQLquery, such asemail LIKE 'ada%' AND CONTAINS(list_ids, '<list id>')
Lists
/v3/marketing/lists: create, list, get (contact_sample=true), rename, and delete (delete_contacts=true)DELETE /v3/marketing/lists/{id}/contacts?contact_ids=— Remove contacts from a listGET /v3/marketing/lists/{id}/contacts/count— Count list contacts
Suppressions
Bounces, spam reports, and unsubscribes from the simulate routes or the seed join their suppression list. Later mail to the address isdropped with reason Bounced Address, Spam Reporting Address, or Unsubscribed Address.
GETandDELETE /v3/suppression/bounces({ delete_all: true }or{ emails }), andGETandDELETE /v3/suppression/bounces/{email}GET /v3/suppression/spam_reportsandDELETE /v3/suppression/spam_reports/{email}— Spam reports: list them, or delete one by emailGET /v3/suppression/unsubscribes— List unsubscribes
Event Webhook
POST /v3/user/webhooks/event/settings creates a webhook (up to five) with url, enabled, a boolean per event (processed, delivered, open, click, bounce, dropped, deferred, spam_report, unsubscribe, group_unsubscribe, group_resubscribe), friendly_name, and an optional OAuth client (oauth_client_id, oauth_client_secret, oauth_token_url). GET /v3/user/webhooks/event/settings/all lists them, and GET, PATCH, and DELETE /v3/user/webhooks/event/settings/{id} manage one. The older GET and PATCH /v3/user/webhooks/event/settings act on the first webhook.
PATCH /v3/user/webhooks/event/settings/signed/{id} with { enabled: true } turns on signing and returns { id, public_key }. Events go out as one JSON array per request (a send posts processed and delivered for every recipient together), signed with ECDSA P-256 over timestamp + body in X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp:
Authorization: Bearer <token>. Failed posts are retried three times. POST /v3/user/webhooks/event/test with { id?, url } sends a sample batch, signed with that webhook’s key when it has one.
Simulate
POST /_sendgrid/simulate/{event} with { email, message_id?, url?, reason?, useragent? } makes a recipient of sent mail do what a real inbox does. Events: delivered, open, click, bounce, blocked, deferred, dropped, spamreport, unsubscribe, group_unsubscribe, and group_resubscribe (group events need mail sent with asm.group_id). It updates the recipient, posts the event to the webhooks, and returns it. Without message_id, the latest mail to the address is used.
Seed config
id: generated when left out
generation: legacy; the last version with active: true (or the first) is active. Webhook events default to all; read a signed webhook’s public key from GET /v3/user/webhooks/event/settings/signed. Seeding again does not duplicate entries.
Inbox and inspector
GET /inbox lists sent mail, and /inbox/{message_id} previews it. GET /_inspector has tabs for messages, templates, contacts and lists, webhooks with recent deliveries, suppressions, and API keys.
Limits
- No SMTP relay, Single Sends, Automations, segments, custom field definitions, subusers, IP pools, or stats.
- Open and click tracking rewrite nothing; use the simulate routes for engagement events.
- Webhook retries run three times over about 20 seconds, where SendGrid retries for 24 hours.
Conformance
See CONFORMANCE.md for every provider.