Email

Transactional email sending.

POST/api/v1/emailsInvia un'email transazionale. `idempotencyKey` nel body obbligatoria (min 8 char); `from`/`replyTo` accettano 'Nome <a@b.com>'; `replyTo` anche piu' indirizzi (virgola o array, max 10); solo `text` senza `html` = email di solo testo.(scope: email:send)
GET/api/v1/emailsElenca gli invii col riassunto di engagement (aperture umane e da proxy, click, click sul contenuto); filtri campaignId, contactId, status, engagement, q, since.(scope: emails:read|email:send)
GET/api/v1/emails/:idStato di un invio: metadati, engagement, link cliccati col ruolo e timeline eventi (sui click: linkUrl, linkId, linkRole).
POST/api/v1/messagesInvio transazionale unificato email/WhatsApp/Telegram.(scope: messages:send)

POST /api/v1/emails

Body parameters (JSON):

FieldTypeRequiredNotes
tostring or arrayyesOne or more recipients: a@b.com, Name <a@b.com>, several addresses comma separated in the same string, or an array.
fromstringyesa@b.com or Name <a@b.com>. The domain must be verified.
subjectstringyes*Min 1 char. Optional if the template supplies the subject.
htmlstringyes*HTML body. Use templateId instead if you prefer, or text alone for a text-only email.
textstringnoPlain text version (multipart part). Sent alone, without html or templateId, it makes a text-only email: no open tracking, links left as written.
replyTostring | string[]noReply-To: one or more addresses (a@b.com, Name <a@b.com>, comma-separated or an array, at most 10). A plain "Reply" reaches all of them.
ccstring or arraynoVisible copies: they appear in the Cc header. Same forms as to.
bccstring or arraynoBlind copies: they appear in no header of the message. Same forms as to.
templateIdstringnoId (uuid) or name of a tenant template; alternative to html.
variablesobjectnoInterpolation variables for the template.
attachmentsarraynoAttachments: { id }, { filename, content } (base64) or { filename, url }. See Attachments.
idempotencyKeystringyesMin 8 characters. Prevents double sends on retries. Goes in the body, not in a header.
disableTrackingbooleannoTurns off pixel and click redirects for this send (default false).
disableUtmbooleannoSkips UTM auto-tagging of links (default false).
* subject is required unless a templateId supplies it. The body is html, a templateId, or text alone (text-only email).

Max 50 recipients per message (to plus cc plus bcc), an SES limit: past that the response is 422 too_many_recipients. Every recipient consumes one unit of the plan's monthly quota. Copies in the suppression list (bounce, complaint, unsubscribe) are removed from the send and reported back in droppedRecipients; the request is a 409 only when no to recipient is left to send to. Opens and clicks are per message, not per recipient: they cannot tell which of the copies opened.

Every address field accepts both a@b.com and Name <a@b.com>, and to, cc and bcc also take several addresses at once, as an array or comma separated inside one string ("sara@acme.io, boss@acme.io"). A comma inside a quoted display name does not split the list: "Neri, Sara" <sara@acme.io> is one recipient. Multiple to addresses land in the To header, not in Cc, and GET /api/v1/emails/{id} returns them in toAddresses.

Responses: 202 ({ id, status: "queued" }), 200 ({ status: "duplicate", idempotencyKey, id }), 422 (validation), 402 (plan quota), 403 (domain_not_verified), 409 (suppressed).

Example

import { Sending } from "sending-sdk";
 
const sending = new Sending({ apiKey: process.env.SENDING_API_KEY! });
 
const { id } = await sending.emails.send({
  from: "Acme <hello@yourdomain.dev>",
  to: "sara@acme.io",
  subject: "Hello",
  html: "<p>Hey</p>",
  replyTo: "Support <support@yourdomain.dev>",
  idempotencyKey: "hello-sara-001",
});

POST /api/v1/emails/batch

Up to 100 emails in one request. The body is { "emails": [...] } and each item is exactly the body of POST /api/v1/emails, idempotencyKey included.

The outcome is partial, not all-or-nothing: the response is 207 and results is aligned by index with the array you sent, so one invalid address out of 60 does not stop the other 59.

Result fieldNotes
indexPosition in the emails array you sent.
statusaccepted or failed.
idMessage id, present on accepted items (duplicates included).
duplicatetrue if that idempotencyKey had already been accepted: this is not an error.
droppedRecipientsCopies removed because they are suppressed.
error{ code, message } on failures: validation_error, domain_not_verified, suppressed, template_not_found, quota_exceeded, too_many_recipients.

Idempotency applies per item: a retried batch produces no double sends, and items already accepted come back accepted with duplicate: true. For rate limiting purposes the call counts as one request.

const outcome = await sending.emails.sendBatch([
  { from: "Acme <hello@yourdomain.dev>", to: "sara@acme.io", subject: "Hello", html: "<p>hey</p>", idempotencyKey: "b-001" },
  { from: "Acme <hello@yourdomain.dev>", to: "boss@acme.io", subject: "Hello", html: "<p>hey</p>", idempotencyKey: "b-002" },
]);
for (const r of outcome.results) {
  if (r.status === "failed") console.error(`item ${r.index}: ${r.error?.code}`);
}

Response:

{
  "accepted": 2,
  "failed": 1,
  "results": [
    { "index": 0, "status": "accepted", "id": "8f1c…" },
    { "index": 1, "status": "accepted", "id": "0b22…", "duplicate": true },
    { "index": 2, "status": "failed", "error": { "code": "suppressed", "message": "complaint" } }
  ]
}

GET /api/v1/emails/:id

State of one send: use the id returned when you sent it.

const status = await sending.emails.get(id);
console.log(status.status, status.events);

It returns status, metadata (to, from, subject, provider, createdAt, kind: transactional, campaign or automation), the attachments of the send (id, filename, contentType, size), an engagement summary (opensHuman, opensMachine, clicks, contentClicks, lastLinkUrl, bounced, unsubscribed), the links clicked grouped by canonical URL (with role and isContent) and the events timeline (sent, delivered, open, click, bounce, complaint). Every click event carries linkUrl, linkId and linkRole (logo, image, cta, text, social, video, chrome): three clicks on the logo are not interest in the content. Opens count as human only when classification is human: Apple Mail Privacy Protection and Gmail's proxy open every email on their own.

GET /api/v1/emails

The sends of the workspace, newest first, each with the same engagement summary. Same filters as the dashboard list: campaignId, contactId, status (queued, sent, delivered, failed, bounced), engagement (clicked, opened_not_clicked, not_opened among the delivered, bounced, unsubscribed), q on recipient and subject, since (ISO 8601), limit (max 100) and offset. Requires the emails:read scope (or email:send).

// Who clicked in this campaign?
const page = await sending.emails.list({ campaignId, engagement: "clicked", limit: 100 });
console.log(page.total, page.data.map((m) => m.to));
Full schema and a "try it" console in the interactive API reference (OpenAPI).