SendGrid

Lightweight, dependency-free, in-memory SendGrid v3 Web API fake for testing code that uses the real @sendgrid/mail client.

Default port: 4650

Implemented Operations

Mail (the surface @sendgrid/mail actually calls)

Account / key management (broader @sendgrid/client surface)

Unsubscribe groups & suppressions (ASM)

Sender verification

Service & inspection operations

Quick Start

The fake speaks the exact wire protocol of the real client. Point @sendgrid/mail at the local server by overriding the client base URL:

import sgMail from "@sendgrid/mail";
import { SendgridServer } from "./services/sendgrid/src/server.js";

const server = new SendgridServer(4650);
await server.start();

sgMail.setApiKey("SG.parlel");
// Route the client to the local fake instead of api.sendgrid.com:
sgMail.client.setDefaultRequest("baseUrl", "http://127.0.0.1:4650");

const [response] = await sgMail.send({
  to: "test@example.com",
  from: "verified@parlel.dev",
  subject: "Sending with parlel is fun",
  text: "and easy to do anywhere, even with Node.js",
  html: "<strong>and easy to do anywhere, even with Node.js</strong>",
});

console.log(response.statusCode); // 202
console.log(response.headers["x-message-id"]);

await server.stop();

To assert what was "sent" in a test, read the captured mailbox:

const res = await fetch("http://127.0.0.1:4650/__parlel/messages");
const { messages, count } = await res.json();

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ supported · ◐ accepted (stored, not enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatusNotes
@sendgrid/mail send / sendMultiple / send([...])All route through POST /v3/mail/send; 202 + empty body + X-Message-Id.
setApiKey (Bearer auth)Any non-empty Bearer token is accepted; missing/malformed → 401.
setTwilioEmailAuth (Basic auth)Any non-empty Basic credential is accepted.
Mail payload validationValidates personalizations, to, from, subject, and content with real SendGrid error envelopes and help URLs.
Template sends (template_id)subject and content become optional when a template is used.
cc / bcc / reply_to / attachments / categories / custom_args / asm / settingsPreserved verbatim in the captured message; not interpreted.
API keys CRUD (/v3/api_keys)List/create/get/update/delete; api_key secret returned once on create.
ASM unsubscribe groups (/v3/asm/groups)CRUD; group objects include unsubscribes.
Global suppressions (/v3/asm/suppressions/global)Add/check/delete; check returns { recipient_email } or {}.
Verified senders (/v3/verified_senders)List/create; { results: [...] }.
Scopes (/v3/scopes){ scopes: [...] }.
Mail batch (/v3/mail/batch)Create { batch_id } (201); validate { batch_id } (200).
Message capture / inspection/__parlel/messages exposes everything that was sent.
Actual email deliveryNothing leaves the process — zero side effects by design.
Unverified-sender 403 enforcementNo real sender verification; any valid from is accepted.
Event/Inbound Parse webhooksNo outbound callbacks are made.
Rate limiting / quotasLocal tests should not pay SendGrid costs or hit side effects.
PersistenceState is ephemeral by design.
Stats, Marketing Campaigns, Contacts, Templates CRUDOutside the @sendgrid/mail surface; not required for app tests.

Error Shapes

All JSON errors use SendGrid v3 framing — an errors array where each entry has message, field, and help:

{
  "errors": [
    {
      "message": "The subject is required. You can get around this requirement if you use a template with a subject defined or if every personalization has a subject defined.",
      "field": "subject",
      "help": "http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.subject"
    }
  ]
}

Returned status codes:

StatusWhen
202POST /v3/mail/send accepted (empty body, X-Message-Id header).
200Successful reads / list operations.
201Resource created (api key, asm group, global suppression, verified sender, mail batch).
204Successful delete / CORS preflight.
400Validation failure (invalid/missing mail fields, missing required name, malformed JSON body).
401Missing or unrecognized Authorization header.
404Unknown endpoint or missing resource.
405Endpoint exists but the HTTP method is unsupported.
500Unexpected server exception.
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

SENDGRID_API_KEY=SG.parlel
SENDGRID_BASE_URL=http://parlel-bridge:4650
SENDGRID_HOST=http://parlel-bridge:4650
<!-- parlel:testenv:end -->