Node.js / TypeScript SDK
SDK ufficiale Node.js per Tratto Email API
SDK ufficiale per Node.js e TypeScript: @tratto/email
Ogni esempio di questa pagina compila contro la 1.8.0 pubblicata. Dove una funzionalità richiede una versione precisa, la pagina dichiara la minima che ce l'ha, mai la più recente: quella invecchia da sola.
Installazione
npm install @tratto/emailInizializzazione
La classe esportata è Tratto, e la chiave API è un argomento posizionale,
non un oggetto di opzioni:
import { Tratto } from '@tratto/email';
const tratto = new Tratto(process.env.TRATTO_API_KEY!);Un secondo argomento facoltativo sovrascrive la base URL:
const tratto = new Tratto(process.env.TRATTO_API_KEY!, {
baseUrl: 'https://api.tratto.email',
});timeout fissa quanto può durare ogni richiesta, in millisecondi, e
richiede la 1.8.0 o successiva. Il valore predefinito è 60000 (60
secondi). Una richiesta che dura di più lancia TrattoError con
code: 'timeout' e statusCode: 0:
const tratto = new Tratto(process.env.TRATTO_API_KEY!, { timeout: 10_000 });Un invio andato in timeout può essere arrivato comunque all'API. Ritentalo con la stessa chiave di idempotenza, così non parte due volte.
Dalla 1.8.0 l'SDK codifica (percent-encoding) ogni id che inserisce nel path
dell'URL, quindi un id che contiene /, ? o # non può cambiare la rotta
chiamata.
Invia Email
emails.send risolve a { id }, nient'altro. Per leggere lo status, rileggi
l'email.
const { id } = await tratto.emails.send({
from: 'hello@tuodominio.it',
to: 'user@example.com',
subject: 'Ciao!',
text: 'Benvenuto a Tratto',
html: '<h1>Benvenuto!</h1>',
});
console.log(id); // email_abc123from si può omettere dalla 1.5.0 o successiva: Tratto usa allora il mittente transazionale del workspace, o il mittente generale se manca. Vedi Mittenti.
La chiave di idempotenza è il secondo argomento:
await tratto.emails.send(
{ from: 'hello@tuodominio.it', to: 'user@example.com', subject: 'Ciao', text: 'Ciao' },
'ordine-4711-conferma',
);Invia con Template
const { id } = await tratto.emails.send({
from: 'hello@tuodominio.it',
to: 'user@example.com',
subject: 'Benvenuta, Alice!',
templateId: 'tmpl_abc123',
variables: {
firstName: 'Alice',
code: 'SECRET123',
},
});subject è obbligatorio su ogni invio, con o senza template. Un template non
ha un oggetto proprio — vedi Template.
Invia con Markdown
Invia Markdown e lascia che il server
renderizzi HTML responsive per email — markdown è mutuamente esclusivo con
html:
const { id } = await tratto.emails.send({
from: 'hello@tuodominio.it',
to: 'user@example.com',
subject: 'Benvenuto!',
markdown: '# Benvenuto, {{firstName}}!',
variables: { firstName: 'Alice' },
});Anche templates.create e templates.update accettano markdown, e il tipo
di risposta Template espone format, source e renderWarnings.
Programma un'Email
const { id } = await tratto.emails.send({
from: 'hello@tuodominio.it',
to: 'user@example.com',
subject: 'Promemoria',
text: 'Ci vediamo domani!',
scheduledAt: new Date('2026-10-01T09:00:00Z'),
});Finché l'email è ancora scheduled puoi annullarla o spostarla a un'altra
data. Entrambi i metodi richiedono la 1.8.0 o successiva:
await tratto.emails.reschedule('email_abc123', new Date('2026-10-02T09:00:00Z'));
await tratto.emails.cancel('email_abc123'); // la sua quota torna al meseQuando l'email non è più scheduled, entrambi rispondono 409 CONFLICT.
Spostarla in un altro mese UTC può rispondere 429 QUOTA_EXCEEDED se quel mese
è pieno, e un 429 RATE_LIMITED si può ritentare senza rischi.
Invia un Batch
emails.sendBatch() invia fino a 100 email in una sola richiesta e richiede la
1.6.0 o successiva. Ogni email accetta i campi di emails.send() tranne
scheduledAt; il secondo argomento, facoltativo, è la
chiave di idempotenza, che vale per tutto il batch.
import { randomUUID } from 'node:crypto';
const batch = await tratto.emails.sendBatch(
[
{ from: 'hello@yourdomain.com', to: 'alice@example.com', subject: 'La tua ricevuta', text: 'Grazie, Alice.' },
{ from: 'hello@yourdomain.com', to: 'bob@example.com', subject: 'La tua ricevuta', text: 'Grazie, Bob.' },
],
randomUUID(),
);
console.log(`${batch.accepted} in coda, ${batch.failed} non inviate`);
for (const e of batch.emails) {
if (e.status === 'failed') console.error(e.to, e.error.code, e.error.message);
}La chiamata si risolve a ogni 202, anche quando alcune email non sono
partite: un'email non partita non ha id. Invia di nuovo solo quelle, in un
nuovo batch con una nuova chiave. Dopo un 503 o un 500 non è partito
niente: ripeti la stessa chiamata con la stessa chiave. Quota, limiti ed
errori sono in Invio in Batch.
Allegati
Richiede la @tratto/email 1.7.0 o successiva e un piano a pagamento: il piano Free
risponde 403 PLAN_UPGRADE_REQUIRED. Prima carica il file, poi indicane l'id
in emails.send() o emails.sendBatch().
import { readFile } from 'node:fs/promises';
const file = await tratto.attachments.upload(await readFile('fattura.pdf'), {
filename: 'fattura.pdf',
contentType: 'application/pdf',
});
await tratto.emails.send({
from: 'billing@yourdomain.com',
to: 'alice@example.com',
subject: 'La tua fattura',
html: '<p>In allegato.</p>',
attachments: [{ id: file.id }],
});
const meta = await tratto.attachments.get(file.id); // solo i metadati, mai i byte
await tratto.attachments.delete(file.id);Il contenuto può essere un Buffer, un Uint8Array, un ArrayBuffer o un Blob. Limiti, scadenza e spazio sono in Allegati.
Leggi lo Status di un'Email
const email = await tratto.emails.get('email_abc123');
console.log(email.status); // 'delivered'
console.log(email.events); // timeline completaElenca le Email
const emails = await tratto.emails.list({
limit: 50,
after: 'cursor_value', // per la paginazione
});
emails.data.forEach((email) => {
console.log(`${email.id}: ${email.status}`);
});Aggiungi un Dominio
Il metodo è add e prende il dominio come stringa:
const domain = await tratto.domains.add('hello.tuodominio.it');
// `records` è un array di record DNS, non un oggetto con una chiave per tipo
for (const record of domain.records) {
console.log(`${record.type} ${record.host} → ${record.value}`);
}Verifica un Dominio
const verified = await tratto.domains.verify('dom_abc123');
console.log(verified.status); // 'pending' | 'verified' | 'failed'Crea un Contatto
Le proprietà personalizzate stanno in customFields. Il campo metadata non
esiste:
const { id } = await tratto.contacts.create({
email: 'user@example.com',
firstName: 'Alice',
lastName: 'Smith',
tags: ['vip', 'newsletter'],
customFields: { plan: 'pro' },
});Crea una Campagna
CreateCampaignParams richiede ancora templateId, audienceId,
fromName, fromEmail e subjectA. create risolve a { id }:
const { id } = await tratto.campaigns.create({
name: 'Saldi Q3',
templateId: 'tmpl_abc123',
audienceId: 'aud_abc123',
fromName: 'Acme',
fromEmail: 'hello@tuodominio.it',
subjectA: 'I saldi estivi iniziano oggi',
subjectB: 'Saldi estivi: 30% di sconto, solo oggi', // oggetto A/B, facoltativo
});
const campaign = await tratto.campaigns.get(id);
console.log(campaign.status); // 'draft'L'API è più permissiva dei tipi della 1.8.0: accetta html inline al posto di
un templateId, e interpreta un audienceId omesso come «tutti i contatti del
workspace». Nessuna delle due cose è esprimibile con l'SDK: per quelle usa
HTTP.
Invia una Campagna
const sent = await tratto.campaigns.send('camp_xyz789');
console.log(sent.status); // 'sending'Per programmarla, passa una data:
await tratto.campaigns.send('camp_xyz789', {
scheduledAt: new Date('2026-10-01T09:00:00Z'),
});Ferma una Campagna
pause trattiene una campagna che sta spedendo o che è in attesa.
unschedule annulla un invio programmato e riporta la campagna a draft, e
richiede la 1.2.0 o successiva:
await tratto.campaigns.pause('camp_xyz789');
await tratto.campaigns.unschedule('camp_xyz789');unschedule vale solo mentre lo status è scheduled e non è uscito niente. Su
una campagna che sta spedendo, già in pausa, o che aspetta la sua
ondata di prova risponde 409 CONFLICT,
perché una parte della lista può già avere l'email. Mettila in pausa invece.
Campaign.pausedReason dice perché una campagna si è fermata da sola, e
richiede a sua volta la 1.2.0 o successiva. Vale 'quota_exceeded' | 'schedule_missed' | 'bounce_rate' | 'quota_unavailable' | null.
'bounce_rate' è lo
stop automatico sui rimbalzi.
'quota_unavailable' vuol dire che non è stato possibile leggere la quota
mensile per un errore nostro, non che sia esaurita, quindi la campagna si può
riprendere; il tipo lo include dalla 1.8.0 o successiva. Tutti e quattro
sono spiegati in
Perché una campagna è in pausa.
const campaign = await tratto.campaigns.get('camp_xyz789');
if (campaign.pausedReason === 'bounce_rate') {
// Pulisci la lista prima di richiamare send.
}Statistiche di una Campagna
Il metodo è getStats. I tassi sono percentuali: 50 significa 50%, non
0.5 — la stessa scala di Analytics.
const stats = await tratto.campaigns.getStats('camp_xyz789');
console.log(stats.stats.delivered); // 4950
console.log(stats.rates.openRate); // 50 → 50%
console.log(stats.rates.deliveryRate) // 99 → 99%Un alert scritto come rates.deliveryRate < 0.95 non scatterà mai. Il
confronto giusto è < 95.
Registra un Webhook
const webhook = await tratto.webhooks.create({
url: 'https://tuoapi.com/webhooks/tratto',
events: ['sent', 'delivered', 'opened', 'clicked', 'bounced'],
});
console.log(webhook.secret); // whsec_xyz789...Verifica i Webhook
verifyWebhook controlla l'header X-Tratto-Signature di una consegna e
richiede la 1.8.0 o successiva. Passagli il body grezzo esattamente come
arriva, prima di qualunque parsing JSON:
import { verifyWebhook } from '@tratto/email';
export async function POST(request: Request) {
const rawBody = await request.text(); // leggilo una volta, prima del parsing
const ok = verifyWebhook(
rawBody,
request.headers.get('x-tratto-signature'),
process.env.TRATTO_WEBHOOK_SECRET!,
);
if (!ok) return new Response('Firma non valida', { status: 401 });
const event = JSON.parse(rawBody);
console.log(event.type);
return Response.json({ ok: true });
}Il body può essere anche un Buffer o un Uint8Array, per esempio quello di
express.raw({ type: 'application/json' }).
Restituisce false per un header mancante, malformato, scaduto o che non
corrisponde, e lancia un'eccezione solo con un secret vuoto o una tolleranza
negativa. La tolleranza è di 5 minuti in entrambe le direzioni; la cambi con
{ toleranceSeconds: 600 } come quarto argomento. Come è costruita la firma è
spiegato in Verifica della firma dei webhook.
Analytics
AnalyticsPeriod è '7d' | '30d' | '90d' | '180d' | '1y' (gli ultimi due
richiedono la 1.1.1 o successiva):
const summary = await tratto.analytics.getSummary('30d');
console.log(summary.delivered);180d e 1y leggono l'aggregato di lungo periodo, che contiene solo dati live:
l'API li rifiuta per una chiave in modalità test.
Limiti del Workspace
workspace.get() restituisce in limits i tetti del piano del workspace. Il
tipo Workspace.limits (WorkspaceLimits) richiede la 1.8.0 o successiva:
const workspace = await tratto.workspace.get();
const { emailsPerMonth, domains, webhooks } = workspace.limits;
console.log(emailsPerMonth ?? 'nessun limite', domains ?? 'nessun limite', webhooks);I sette campi sono domains, emailsPerMonth, membersPerAccount,
membersPerWorkspace, workspaces, webhooks e testSendsPerDay.
domains ed emailsPerMonth valgono null quando il piano non ha limite.
Gestione degli Errori
TrattoError espone code, statusCode, docs, details e suggestion:
import { Tratto, TrattoError } from '@tratto/email';
const tratto = new Tratto(process.env.TRATTO_API_KEY!);
try {
await tratto.emails.send({
from: 'hello@nonverificato.it', // non verificato
to: 'user@example.com',
subject: 'Ciao',
text: 'Test',
});
} catch (error) {
if (error instanceof TrattoError) {
// Il messaggio nomina il dominio non verificato.
console.error(error.code, error.statusCode, error.message);
}
}Vedi Codici di Errore per l'elenco completo.
Su un 422 VALIDATION_ERROR, details indica quale campo è sbagliato, per
esempio [{ path: '/emails/3/to', message: '...' }] per la quarta email di un
batch. Richiede la 1.6.0 o successiva e vale undefined quando l'API non
lo invia.
suggestion è il consiglio dell'API su cosa fare, per esempio aspettare e
ritentare o correggere un'impostazione. Richiede la 1.8.0 o successiva e
vale undefined quando l'errore non ne porta uno. È il campo da leggere quando
un codice copre più situazioni, come il CONFLICT di campaigns.send:
try {
await tratto.campaigns.send('camp_xyz789');
} catch (error) {
if (error instanceof TrattoError) {
console.error(error.code, error.suggestion ?? error.message);
}
}Un errore con code: 'timeout' e statusCode: 0 è il
timeout del client, non una risposta dell'API.
Cosa l'SDK non copre ancora
Questi endpoint esistono nell'API e non hanno un metodo SDK nella 1.8.0. Chiamali via HTTP finché l'SDK non li recupera:
| Funzionalità | HTTP |
|---|---|
| Leggere un singolo contatto | GET /v1/contacts/{id} |
| Modificare / eliminare una campagna | PATCH, DELETE /v1/campaigns/{id} |
| Click sui link di una campagna | GET /v1/campaigns/{id}/links |
| Modificare / eliminare / ricalcolare un'audience | PATCH, DELETE, POST …/refresh su /v1/audiences/{id} |
| Elencare o rimuovere i contatti di un'audience | GET, DELETE su /v1/audiences/{id}/contacts |
| Anteprima di rendering del markdown | POST /v1/templates/render-preview |
| Gestione delle chiavi API | /v1/api-keys |
| Logo del marchio di workspace | PUT, DELETE /v1/workspace/brand/logo |
Mancano dai tipi della 1.8.0 anche alcuni campi di risposta più recenti:
Workspace non ha brand, Contact non ha trackingOptOut,
e CampaignStats non ha skipped,
untracked né variants. L'API li restituisce; i tipi non li descrivono
ancora.
Verificato sulla @tratto/email 1.8.0 pubblicata, il 2026-10-27: le otto righe
sono ancora scoperte, emails.cancel ed emails.reschedule sono coperti dalla
1.8.0, e ListContactsParams.q è coperto dalla 1.4.0
(contacts.list({ q })). Ogni riga di questa tabella è la
promessa che la lacuna c'è ancora: va riletta a ogni rilascio dell'SDK.
Link
Modifica questa pagina su GitHub
Ultimo aggiornamento