Email

Invio email transazionale.

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

Parametri del body (JSON):

CampoTipoObblig.Note
tostring o arraysìUno o più destinatari: a@b.com, Nome <a@b.com>, più indirizzi separati da virgola nella stessa stringa, oppure un array.
fromstringsìa@b.com oppure Nome <a@b.com>. Il dominio deve essere verificato.
subjectstringsì*Min 1 char. Opzionale se il template fornisce il subject.
htmlstringsì*HTML del messaggio. In alternativa usa templateId, oppure solo text per un'email di solo testo.
textstringnoVersione testo semplice (parte multipart). Da solo, senza html né templateId, invia un'email di solo testo: niente tracciamento delle aperture, link lasciati come sono.
replyTostring | string[]noReply-To: uno o più indirizzi (a@b.com, Nome <a@b.com>, separati da virgola o in un array, al massimo 10). Il semplice "Rispondi" li raggiunge tutti.
ccstring o arraynoCopie visibili: compaiono nell'header Cc. Stesse forme di to.
bccstring o arraynoCopie nascoste: non compaiono in nessun header del messaggio. Stesse forme di to.
templateIdstringnoId (uuid) o nome del template del tenant; alternativo a html.
variablesobjectnoVariabili di interpolazione del template.
attachmentsarraynoAllegati: { id }, { filename, content } (base64) o { filename, url }. Vedi Allegati.
idempotencyKeystringsìMin 8 caratteri. Previene i doppi invii sui retry. Va nel body, non in un header.
disableTrackingbooleannoDisattiva pixel/redirect per questo invio (default false).
disableUtmbooleannoSalta l'auto-tagging UTM dei link (default false).
* subject è richiesto se non usi un templateId che lo fornisce. Il corpo è html, un templateId, oppure solo text (email di solo testo).

Massimo 50 destinatari per messaggio (to + cc + bcc), limite di SES: oltre, la risposta è 422 too_many_recipients. Ogni destinatario consuma una unità della quota mensile del piano. Le copie in suppression (bounce, complaint, unsubscribe) vengono tolte dall'invio e riportate nella risposta in droppedRecipients; la richiesta è un 409 solo quando non resta nemmeno un destinatario to a cui inviare. Aperture e click sono per messaggio, non per destinatario: non distinguono chi delle copie ha aperto.

Ogni campo indirizzo accetta sia a@b.com sia Nome <a@b.com>, e to, cc e bcc prendono anche più indirizzi insieme, come array o separati da virgola dentro una sola stringa ("sara@acme.io, capo@acme.io"). Una virgola dentro un display name fra virgolette non separa niente: "Neri, Sara" <sara@acme.io> è un destinatario solo. Più to finiscono nell'header To, non in Cc, e GET /api/v1/emails/{id} li restituisce in toAddresses.

Risposte: 202 ({ id, status: "queued" }), 200 ({ status: "duplicate", idempotencyKey, id }), 422 (validazione), 402 (quota piano), 403 (domain_not_verified), 409 (suppressed).

Esempio

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

POST /api/v1/emails/batch

Fino a 100 email in una richiesta. Il body è { "emails": [...] } e ogni elemento è esattamente il body di POST /api/v1/emails, idempotencyKey compresa.

L'esito è parziale, non tutto o niente: la risposta è 207 e results è allineato per indice all'array inviato, quindi un indirizzo invalido su 60 non impedisce gli altri 59.

Campo del risultatoNote
indexPosizione nell'array emails inviato.
statusaccepted oppure failed.
idId del messaggio, presente sugli accettati (duplicati compresi).
duplicatetrue se quella idempotencyKey era già stata accettata: non è un errore.
droppedRecipientsCopie escluse perché in suppression.
error{ code, message } sui falliti: validation_error, domain_not_verified, suppressed, template_not_found, quota_exceeded, too_many_recipients.

L'idempotenza vale per elemento: un batch ritentato non produce doppi invii, e gli elementi già accettati tornano accepted con duplicate: true. Ai fini dei limiti di frequenza la chiamata conta una richiesta.

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

Risposta:

{
  "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

Stato di un invio: usa l'id restituito dall'invio.

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

Ritorna status, metadati (to, from, subject, provider, createdAt, kind: transactional, campaign o automation), gli attachments dell'invio (id, filename, contentType, size), il riassunto engagement (opensHuman, opensMachine, clicks, contentClicks, lastLinkUrl, bounced, unsubscribed), i links cliccati raggruppati per URL canonica (con role e isContent) e la timeline events (sent, delivered, open, click, bounce, complaint). Ogni evento click porta linkUrl, linkId e linkRole (logo, image, cta, text, social, video, chrome): tre click sul logo non sono interesse per il contenuto. Le aperture contano come umane solo con classification uguale a human: Apple Mail Privacy Protection e il proxy di Gmail aprono ogni email da soli.

GET /api/v1/emails

Gli invii del workspace, dal piu' recente, ognuno con lo stesso riassunto engagement. Stessi filtri della lista in dashboard: campaignId, contactId, status (queued, sent, delivered, failed, bounced), engagement (clicked, opened_not_clicked, not_opened fra le consegnate, bounced, unsubscribed), q su destinatario e oggetto, since (ISO 8601), limit (max 100) e offset. Richiede lo scope emails:read (oppure email:send).

// Chi ha cliccato in questa campagna?
const page = await sending.emails.list({ campaignId, engagement: "clicked", limit: 100 });
console.log(page.total, page.data.map((m) => m.to));
Schema completo e "try it" nella API reference interattiva (OpenAPI).