Integrate an email sending API securely with cURL and TypeScript, then add validation, timeouts, retries, delivery webhooks, and a durable production path.
To send email with a REST API, your server makes an authenticated HTTPS request containing the sender, recipient, subject, and message content. The provider validates the request, submits the message to its delivery infrastructure, and returns an identifier you can use to follow later events.
The first successful request takes minutes. A production integration also needs a safe server boundary, domain authentication, runtime validation, deadlines, retry classification, delivery-event processing, suppression handling, and a way to survive an application restart. This guide builds those pieces in order.
REST email API vs SMTP
OPTION BEST WHEN TRADEOFF
REST API New application integration Provider-specific contract
Provider SDK SDK adds types or useful helpers Dependency and upgrade surface
SMTP relay Existing framework already speaks SMTP Less structured request/errors
Gmail API Acting inside a user's Gmail mailbox OAuth and mailbox quotas
Graph mail API Acting inside Microsoft 365 mailboxes Tenant and permission model
REST and SMTP can use the same provider. Choosing HTTP for one service
does not prevent a legacy application from using its SMTP relay.A REST API is usually the clearest default for product email because HTTP exposes status codes, request IDs, structured errors, templates, scheduling, metadata, and event models directly. SMTP remains a sound submission protocol when portability, a corporate relay, or an existing library matters more than provider-specific features.
Before the first API call
- Create separate development and production projects or credentials.
- Verify a domain you control and publish the provider’s DKIM and return-path records.
- Store the API key in a deployment secret store, not source code or frontend environment variables.
- Choose a fixed application From address for the first test.
- Send only to an inbox you control until authentication and event handling are verified.
- Decide which application action owns the email, such as account verification or payment receipt.
Do not begin by exposing POST /send-email to the browser with arbitrary to, from, subject, and html fields. That is an email relay. Begin with a named business action whose server-side handler loads the authoritative recipient and chooses an approved sender and template.
Send email through a REST API with cURL
This example uses Email Bump’s POST /api/v1/emails endpoint. Replace the addresses with a verified sender and a test inbox. Supplying HTML and text gives mail clients a useful alternative when HTML is disabled or unavailable.
curl --request POST 'https://emailbump.com/api/v1/emails' \
--header "Authorization: Bearer $EMAIL_BUMP_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"from": "Acme <[email protected]>",
"to": "[email protected]",
"reply_to": "[email protected]",
"subject": "Verify your email address",
"html": "<h1>Verify your email</h1><p>Your code is <strong>482913</strong>.</p>",
"text": "Verify your email\n\nYour code is 482913."
}'A successful response includes an Email Bump ID, the underlying provider message ID, and status sent. Sent means accepted by the delivery provider; it does not mean delivered to the receiving server, placed in the inbox, opened, or read.
{
"object": "email",
"id": "6dde5322-c940-43e5-84cc-97a2d8c69c08",
"message_id": "provider-message-id",
"to": "[email protected]",
"from": "Acme <[email protected]>",
"subject": "Verify your email address",
"status": "sent",
"created_at": "2026-08-04T14:20:00Z"
}Integrate the email API in TypeScript
Put provider details behind one server-only adapter. The rest of the application should pass a typed message intent and receive a normalized result; it should not know credential names, URLs, raw status codes, or provider response shapes.
export type EmailMessage = {
from: string;
to: string;
replyTo?: string;
subject: string;
html: string;
text: string;
};
export type EmailAccepted = {
id: string;
providerMessageId: string;
};
export class EmailApiError extends Error {
constructor(
message: string,
readonly kind: "retryable" | "permanent" | "ambiguous",
readonly status?: number,
readonly retryAfterMs?: number,
) {
super(message);
}
}
function apiKey() {
const key = process.env.EMAIL_BUMP_API_KEY;
if (!key) throw new Error("EMAIL_BUMP_API_KEY is missing");
return key;
}
export async function submitEmail(message: EmailMessage): Promise<EmailAccepted> {
let response: Response;
try {
response = await fetch("https://emailbump.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: message.from,
to: message.to,
reply_to: message.replyTo,
subject: message.subject,
html: message.html,
text: message.text,
}),
signal: AbortSignal.timeout(10_000),
});
} catch {
throw new EmailApiError("Request ended without a response", "ambiguous");
}
if (!response.ok) {
const retryAfter = response.headers.get("retry-after");
const retryAfterMs = retryAfter ? Number(retryAfter) * 1_000 : undefined;
const kind = response.status === 429 || response.status >= 500
? "retryable"
: "permanent";
throw new EmailApiError("Email API rejected the request", kind, response.status, retryAfterMs);
}
const body = await response.json() as { id: string; message_id: string };
return { id: body.id, providerMessageId: body.message_id };
}The adapter intentionally does not log the full response or message. Email bodies can contain reset links, addresses, invoices, health information, or other personal data. Log the internal intent ID, provider ID, HTTP status, duration, and a safe error class instead.
Validate a named email action on the server
A browser can request resend verification. The server authenticates the session, loads the user’s current email address, checks whether verification is still needed, rotates or reuses a bounded token, renders a known template, and applies a purpose-specific rate limit. The browser never chooses the recipient or HTML.
UNTRUSTED REQUEST
POST /account/resend-verification
session cookie
SERVER DECIDES
authenticated user
authoritative recipient
approved From address
verification template + current version
token lifetime
per-user and per-IP rate limit
unique message intent
PROVIDER RECEIVES
one fully resolved email- Validate all runtime input even when the codebase uses TypeScript.
- Keep public request bodies small and reject unsupported content types before parsing.
- Escape untrusted variables before placing them in HTML; template literals do not escape markup.
- Return neutral recovery responses so callers cannot enumerate accounts.
- Rate-limit by purpose, account, and network signal rather than only one global counter.
- Use a stable semantic key so repeated clicks reuse one active verification intent.
Handle email API errors and retries
RESULT ACTION
2xx accepted Store provider ID; wait for events
400 / 422 invalid request Permanent; fix payload or template
401 / 403 Permanent; alert on key or permission
404 endpoint/template Permanent until configuration changes
408 / 429 Retry with bounded backoff and jitter
5xx Retry within an attempt and age budget
Timeout / connection reset Ambiguous; may already have been accepted
Delivered event Receiving server accepted the message
Bounce / complaint event Suppress or route to policy handlingA network timeout is not proof that nothing happened. The provider may accept the message and lose the response on its way back. Use a provider-supported idempotency key when available. Without one, retain an ambiguous state and reconcile provider activity before blindly resending a high-impact receipt or security alert.
Retry only while the message is still useful. A login code that expires after ten minutes should not wait in a queue for an hour. Record both an attempt limit and a maximum message age, honor Retry-After, add jitter, and stop on permanent validation or authentication errors.
Use a transactional outbox for important email
Your database commit and the provider’s API call cannot be one atomic transaction. For receipts, invitations, security alerts, and other important messages, commit business state and a unique email intent in the same database transaction. A worker can then submit it after the request returns and recover unfinished work after a restart.
APPLICATION TRANSACTION
update business state
insert unique email intent
commit
|
v
DURABLE WORKER
claim with lease -> render -> POST REST API -> store provider ID
| |
| retry / ambiguous v
+------------------------- signed delivery webhooks
|
v
message ledger + suppressionProcess signed delivery webhooks
Email Bump can send email.delivered, email.bounced, and email.complained events to an HTTPS endpoint. Verify the signature against the exact raw request body before parsing JSON. Acknowledge valid events quickly, enqueue slower work, and deduplicate by event ID because webhook delivery is at least once.
import crypto from "node:crypto";
export function verifyEmailBumpWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const actualBytes = Buffer.from(parts.v1 ?? "", "hex");
const expectedBytes = Buffer.from(expected, "hex");
return actualBytes.length === expectedBytes.length
&& crypto.timingSafeEqual(expectedBytes, actualBytes);
}Delivery means the receiving mail server accepted the message. It does not prove inbox placement or human attention. Opens can be generated by privacy proxies and security scanners; clicks can be generated by link inspection. Use delivery, bounce, complaint, and suppression events for operations, and treat engagement events as directional product signals.
REST email API production checklist
- Server-only API key with separate development and production credentials.
- Verified domain with SPF, DKIM, and DMARC configured and monitored.
- Narrow business commands rather than a public arbitrary-send endpoint.
- Typed templates that render both HTML and plain text from validated variables.
- Request deadline plus explicit permanent, retryable, and ambiguous error classes.
- Durable outbox, worker leases, message age limit, and bounded retry budget.
- Provider idempotency where supported and semantic deduplication in your database.
- Signed, deduplicated, out-of-order-safe delivery-event processing.
- Automatic suppression for permanent bounces, complaints, and unsubscribes.
- Alerts for queue age, error rate, deferrals, bounces, complaints, and exhausted quota.
Frequently asked questions
Can a REST API send email?
Yes. Transactional email providers expose authenticated HTTP endpoints that accept message content and submit it to managed email delivery infrastructure. An HTTP success response normally means accepted or queued, while later webhook events report delivery, bounce, and complaint outcomes.
Can I call an email sending API from React or browser JavaScript?
Do not call a privileged provider API directly from the browser. Anyone can extract the credential and use your account. Call your own narrow server endpoint from React, authenticate or constrain the request there, and keep the provider key and message policy in server-only code.
Should I use REST or SMTP to send email?
Use REST for a new product integration when structured errors, metadata, scheduling, templates, and provider events are valuable. Use SMTP when an existing framework already supports it, a corporate relay is mandated, or standards-level portability matters. Both can be production-ready with the same domain authentication and operational controls.