DenSend API

One HTTPS endpoint for sending email, and a webhook for what happens next. There are official libraries for eight languages, and every example on this page is also written out as a plain HTTP request, so anything that can reach the internet can use it.

Base URL
https://api.densend.com/v1
Auth
Bearer token in the Authorization header
Format
JSON in, JSON out
Sending domain
Must be verified before you can send from it

Quickstart

  1. 1Add and verify a domain under Domains. Publish the DNS records it gives you and it verifies itself, usually within a minute.
  2. 2Create an API key under API Keys. It is shown once, so copy it then.
  3. 3Install the package for your language, then send your first email with the call below. Switch language with the tabs.
$npm install @densend/node
import { DenSend } from "@densend/node";

const densend = new DenSend(process.env.DENSEND_API_KEY);

const { id } = await densend.emails.send({
  from: "You <[email protected]>",
  to: "[email protected]",
  subject: "Hello from DenSend",
  html: "<p>Your order has shipped.</p>",
});

console.log(id);

Requires Node 18 or newer.

A successful send returns 202 with the id of each message created. One id per recipient, because a bounce belongs to exactly one address.

Response

{
  "id": "8f2b1c40-9a3e-4a1b-9f0c-2d1e5a7b3c4d",
  "status": "queued",
  "ids": [
    "8f2b1c40-9a3e-4a1b-9f0c-2d1e5a7b3c4d"
  ]
}

Authentication

Every request carries an API key as a bearer token. Keys start with mf_denis_ and are scoped to one workspace.

Authorization: Bearer mf_denis_xxxxxxxxxxxxxxxx

Keep keys on your server

A key can send as any verified domain in its workspace. Never put one in frontend code, a mobile app, or a public repository. If one leaks, revoke it under API Keys and create another.

Keys carry a scope: full, send or read. A read key cannot send.

Send an email

POSThttps://api.densend.com/v1/emails
fromstringrequiredVerified sender. `Name <[email protected]>` also works.
tostring or arrayrequiredOne address, or several.
subjectstringoptionalDefaults to (no subject).
htmlstringoptionalHTML body.
textstringoptionalPlain text body. Send both where you can.
ccarrayoptionalCopied recipients.
bccarrayoptionalBlind copied recipients.
reply_tostringoptionalWhere replies should go.
tagsarrayoptionalYour own labels, returned on the message.
scheduled_atstringoptionalISO 8601. See scheduling.
attachmentsarrayoptional7MB total, after decoding. See attachments.
$npm install @densend/node
const { ids } = await densend.emails.send({
  from: "You <[email protected]>",
  to: ["[email protected]", "[email protected]"],
  cc: ["[email protected]"],
  bcc: ["[email protected]"],
  replyTo: "[email protected]",
  subject: "Your receipt",
  html: "<p>Thanks for your order.</p>",
  text: "Thanks for your order.",
});

// One id per recipient, because a bounce belongs to exactly one address.
console.log(ids);

Requires Node 18 or newer.

Scheduling

Pass scheduled_at as an ISO 8601 timestamp and the message waits until then. Times are UTC unless you say otherwise.

$npm install @densend/node
await densend.emails.send({
  from: "You <[email protected]>",
  to: "[email protected]",
  subject: "A reminder",
  text: "This was scheduled.",
  scheduledAt: new Date("2026-09-01T09:00:00Z"),
});

Requires Node 18 or newer.

Attachments

Each attachment is filename, base64-encoded content, and an optional content_type (defaults to application/octet-stream). 7MB total across all attachments combined, after decoding - not the size of the base64 text itself. Going over returns a 422 naming the actual size, so you find out before the request succeeds, not after.

$npm install @densend/node
import { readFileSync } from "node:fs";

await densend.emails.send({
  from: "You <[email protected]>",
  to: "[email protected]",
  subject: "Your invoice",
  text: "Attached.",
  attachments: [
    {
      filename: "invoice.pdf",
      content: readFileSync("./invoice.pdf"), // a Buffer is encoded for you
      contentType: "application/pdf",
    },
  ],
});

Requires Node 18 or newer.

Retrieve an email

GEThttps://api.densend.com/v1/emails/{email_id}

Returns the message and its timeline: queued, sent, delivered, bounced, complained.

$npm install @densend/node
const email = await densend.emails.get("8f2b1c40-9a3e-4a1b-9f0c-2d1e5a7b3c4d");

console.log(email.status, email.opens, email.clicks);

Requires Node 18 or newer.

Response

{
  "id": "8f2b1c40-9a3e-4a1b-9f0c-2d1e5a7b3c4d",
  "to": "[email protected]",
  "from_addr": "[email protected]",
  "subject": "Hello",
  "status": "delivered",
  "direction": "outbound",
  "opens": 0,
  "clicks": 0,
  "created_at": "2026-08-21T09:14:22Z",
  "events": [
    {
      "type": "delivered",
      "detail": null,
      "ts": "2026-08-21T09:14:25Z"
    }
  ]
}

List emails

GEThttps://api.densend.com/v1/emails

Newest first. Use limit and offset to page, and direction to filter. The total sits in the X-Total-Count header, so you can tell how much is left.

$npm install @densend/node
const emails = await densend.emails.list({ limit: 20 });

Requires Node 18 or newer.

SMTP

Anything that speaks SMTP can send through DenSend without touching the API. Useful for WordPress, Django, Rails or any framework with mail already wired up.

Host
smtp.densend.com
Port
587 with STARTTLS
Username
densend
Password
your API key

The password is the API key itself, so it needs the send or full scope. Your exact settings are on the SMTP page in the dashboard.

Errors

Failures come back as JSON with a detail field written to be shown to a person, not parsed by a machine.

{
  "detail": "acme.com is not verified yet. Publish its DNS records, then verify it."
}
401UnauthorizedMissing or invalid API key.
402Payment requiredPlan limit reached, or the subscription lapsed.
403ForbiddenSending from a domain you have not verified.
422UnprocessableSomething in the body is wrong. The detail says what.
429Too many requestsSlow down. Retry after the header says.

Rate limits

Limits come from your plan rather than from the API: a monthly allowance, and on some plans a daily one. Going past either returns 402 with a message saying which limit you hit.

Extra emails bought on top of a plan are used only once the monthly allowance is gone, and they do not expire when the period rolls over.