Invio email transazionale.
| POST | /api/v1/emails | Invia 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/emails | Elenca 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/:id | Stato di un invio: metadati, engagement, link cliccati col ruolo e timeline eventi (sui click: linkUrl, linkId, linkRole). |
| POST | /api/v1/messages | Invio transazionale unificato email/WhatsApp/Telegram.(scope: messages:send) |
POST /api/v1/emails
Parametri del body (JSON):
| Campo | Tipo | Obblig. | Note |
|---|---|---|---|
to | string o array | sì | Uno o più destinatari: a@b.com, Nome <a@b.com>, più indirizzi separati da virgola nella stessa stringa, oppure un array. |
from | string | sì | a@b.com oppure Nome <a@b.com>. Il dominio deve essere verificato. |
subject | string | sì* | Min 1 char. Opzionale se il template fornisce il subject. |
html | string | sì* | HTML del messaggio. In alternativa usa templateId, oppure solo text per un'email di solo testo. |
text | string | no | Versione testo semplice (parte multipart). Da solo, senza html né templateId, invia un'email di solo testo: niente tracciamento delle aperture, link lasciati come sono. |
replyTo | string | string[] | no | Reply-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. |
cc | string o array | no | Copie visibili: compaiono nell'header Cc. Stesse forme di to. |
bcc | string o array | no | Copie nascoste: non compaiono in nessun header del messaggio. Stesse forme di to. |
templateId | string | no | Id (uuid) o nome del template del tenant; alternativo a html. |
variables | object | no | Variabili di interpolazione del template. |
attachments | array | no | Allegati: { id }, { filename, content } (base64) o { filename, url }. Vedi Allegati. |
idempotencyKey | string | sì | Min 8 caratteri. Previene i doppi invii sui retry. Va nel body, non in un header. |
disableTracking | boolean | no | Disattiva pixel/redirect per questo invio (default false). |
disableUtm | boolean | no | Salta 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 risultato | Note |
|---|---|
index | Posizione nell'array emails inviato. |
status | accepted oppure failed. |
id | Id del messaggio, presente sugli accettati (duplicati compresi). |
duplicate | true se quella idempotencyKey era già stata accettata: non è un errore. |
droppedRecipients | Copie 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));