Documentazione
API pubblica e server MCP
Costruisci agenti, CLI e integrazioni di back-office su qmailing. PLUS e superiori sbloccano l'autenticazione con token Bearer su ogni endpoint pubblico, oltre a un server MCP pronto da collegare a Claude Desktop, Cursor o qualsiasi altro client compatibile.
Per iniziare
- 1. Passa a PLUS — l'API pubblica è disponibile su
PLUS,PRO,PRO_PLUSoPRO_MAX. Le chiamate da FREE ricevono402 Payment Requiredindipendentemente dal token. - 2. Emetti un token — vai in Impostazioni → Sviluppatori, clicca su Nuovo token, scegli gli scope di cui hai bisogno e copia il valore
qm_live_…quando viene mostrato. I token compaiono una sola volta. - 3. Effettua la tua prima chiamata:
curl https://qmailing.com/api/v1/pub/mailboxes \ -H "Authorization: Bearer qm_live_your_token_here"
Autenticazione
Ogni chiamata all'API pubblica trasporta un header Authorization: Bearer qm_live_<…>. Il formato del token usa un namespace (qm_live_ seguito da 32 caratteri casuali) così che eventuali fughe nei log e nei repository risultino evidenti.
I token sono memorizzati con hash (SHA-256). Il testo in chiaro viene mostrato esattamente una volta al momento dell'emissione e mai più — se lo perdi, generane uno nuovo. Ogni richiesta innesca un confronto di hash a tempo costante; last_used_at si aggiorna in caso di successo, così la UI per gli sviluppatori può evidenziare credenziali obsolete.
apiAccess a ogni richiesta firmata — non c'è bisogno di revocare i token uno per uno quando cambi piano.Scope
Ogni token porta una o più stringhe di scope. Gli endpoint dichiarano quali scope richiedono; gli scope mancanti restituiscono 403 InsufficientScope. Il jolly (*) concede tutto — abilitalo solo se davvero necessario.
| Scope | Consente |
|---|---|
| mailboxes:read | Elencare e ispezionare le caselle di posta |
| mailboxes:write | Creare, modificare, eliminare le caselle di posta |
| domains:read | Elencare domini e record DNS |
| domains:write | Aggiungere, verificare, eliminare domini personalizzati (solo FE in v1) |
| email:read | Elencare e ispezionare i messaggi delle caselle |
| email:send | Inviare posta in uscita tramite l'API |
| webhooks:read | Elencare gli endpoint webhook |
| webhooks:write | Registrare, elencare e revocare gli endpoint webhook |
| * | Jolly — concede tutto. Usalo con parsimonia. |
Caselle di posta
Elenca
GET /api/v1/pub/mailboxes
# scope: mailboxes:readOttieni una
GET /api/v1/pub/mailboxes/{id}
# scope: mailboxes:readCrea
POST /api/v1/pub/mailboxes
Content-Type: application/json
# scope: mailboxes:write
{
"localPart": "support",
"domain": "yourbrand.com",
"displayName": "Support team",
"forwardTo": "ops@yourbrand.com"
}Il campo domain è opzionale — ometterlo crea la casella su qmailing.com. Su un dominio personalizzato il dominio deve essere sia claimed CHE fullyVerified; un dominio non ancora pronto restituisce 400 invece di creare silenziosamente una casella non instradabile.
Domini e DNS
Elenca i domini personalizzati
GET /api/v1/pub/domains
# scope: domains:readLista dei record DNS
GET /api/v1/pub/domains/{id}/dns-records
# scope: domains:readRestituisce la lista completa degli 8 record (TXT di challenge, MX, SPF, tre CNAME DKIM, DMARC, opzionalmente _amazonses) con un status per riga tra PENDING / FOUND / MISMATCH / NOT_REQUIRED. L'agente può usarla per dire all'utente esattamente cosa pubblicare presso il suo provider DNS.
Leggi il contenuto delle caselle e invia posta in uscita. La paginazione per cartella combacia con l'API interna del frontend — gli agenti che migrano da auth cookie-JWT a Bearer non vedono cambi di forma.
Elenca per cartella
GET /api/v1/pub/email?folder=INBOX&offset=0&limit=25
# scope: email:read
# Optional query params:
# mailboxId filter to one mailbox (omit for all-mailboxes view)
# folder INBOX | SENT | DRAFTS | TRASH | STARRED | SPAM
# offset 0-based index, default 0
# limit 1..100, default 25folder di default è INBOX; valori ammessi: INBOX, SENT, DRAFTS, TRASH, STARRED, SPAM. mailboxId è opzionale — ometterlo elenca tutte le caselle del chiamante. Paginazione con offset + limit; limit è limitato all'intervallo 1–100 (default 25).
Ottieni uno
GET /api/v1/pub/email/{id}
# scope: email:readRestituisce il EmailDetailDto completo: corpo HTML / testo, lista di etichette, metadati degli allegati. Gli allegati NON sono inline — scarica ciascuno con GET /{emailId}/attachments/{index} qui sotto, che trasmette i byte grezzi con il nome originale del file e il content-type.
Scaricare un allegato
GET /api/v1/pub/email/{emailId}/attachments/{index}
# scope: email:read
# response: streamed bytes, Content-Type from the attachmentTrasmette i byte grezzi dell'allegato (indice basato su zero nella lista degli allegati del messaggio). Stessa forma dell'endpoint basato su sessione: Content-Type in pass-through, Content-Disposition secondo RFC 6266 con il nome originale (UTF-8 percent-encoded per Unicode). Gli agenti MCP che chiamano tramite @qmailing/mcp-server usano lo strumento qmailing_get_attachment, che restituisce i byte codificati in base64 inline (limite 5 MiB).
Invia
Endpoint di invio. Body multipart/form-data con una parte JSON command (destinatari / oggetto / corpo) e zero o più parti-file attachments:
POST /api/v1/pub/email/send
Content-Type: multipart/form-data
# scope: email:send
--boundary
Content-Disposition: form-data; name="command"
Content-Type: application/json
{
"mailboxId": "11111111-2222-3333-4444-555555555555",
"to": ["alice@example.com"],
"cc": [],
"bcc": [],
"subject": "Order #1428 confirmed",
"bodyText": "Plain-text body",
"bodyHtml": "<p>Rich body</p>",
"replyToId": null
}
--boundary
Content-Disposition: form-data; name="attachments"; filename="receipt.pdf"
Content-Type: application/pdf
<binary bytes>
--boundary--Forma del JSON command:
{
"mailboxId": "uuid (required) — must be SES-verified",
"to": "string[] (required, max 50)",
"cc": "string[] (optional, max 50)",
"bcc": "string[] (optional, max 50)",
"subject": "string (max 998 chars)",
"bodyText": "string — plain-text body",
"bodyHtml": "string — HTML body",
"replyToId": "uuid | null — set for in-thread replies"
}Ogni chiamata viene scalata dal limite giornaliero di invio del piano, dalla quota di invio SES della casella e dalla quota di archiviazione dell'utente per la copia risultante in SENT. I destinatari sono validati come indirizzi RFC 5322; quelli non validi restituiscono 400 con l'indirizzo problematico. La casella riferita da mailboxId deve essere verificata in SES — altrimenti, 409 MailboxNotVerified con un suggerimento verso la pagina dei domini.
multipart/form-data in uscita, così il formato sul filo resta identico.filename sulla parte command è opzionale — Node fetch stampa automaticamente filename="blob" su ogni Blob, i client browser e Node possono passare command.json, e i client curl --form conformi possono ometterlo. Tutte e tre le forme vengono analizzate allo stesso modo; conta solo Content-Type: application/json sui byte della parte.Scope: email:read per elencare e ottenere uno, email:send per l'invio.
Webhook
Registra un endpoint HTTPS e QMailing farà POST di un envelope JSON firmato ogni volta che si verifica uno degli eventi sottoscritti. La consegna è attiva — il worker interroga una coda per-evento ogni 10s, firma ogni richiesta con HMAC-SHA256 e ritenta i fallimenti con backoff esponenziale (1m / 5m / 15m / 1h / 6h) prima di parcheggiare la riga nel DLQ per revisione manuale.
Registra un endpoint
POST /api/v1/pub/webhooks
Content-Type: application/json
# scope: webhooks:write
{
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"]
}
# Response (201):
{
"endpoint": {
"id": "uuid",
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"],
"secretPrefix": "whk_AbCd",
"createdAt": "2026-05-04T20:30:00Z",
"active": true
},
"plaintext": "whk_AbCd…<32 random chars>" // shown ONCE
}url deve essere un endpoint HTTPS (HTTP è permesso solo per localhost in dev). L'host è verificato contro un'allow-list anti-SSRF — gli indirizzi privati / loopback / cloud-metadata sono respinti già in fase di registrazione. La risposta porta plaintext esattamente una volta; conservalo lato server e usalo per verificare le firme HMAC delle consegne in arrivo.
Catalogo degli eventi
| Evento | Si attiva quando |
|---|---|
| email.received | Un'email in arrivo atterra in una delle tue caselle. |
| email.sent | Un'email in uscita è stata accettata da SES. |
| email.bounced | Un'email in uscita è rimbalzata (hard o soft). |
| domain.verified | Un dominio personalizzato ha completato la verifica DNS end-to-end. |
Elenca gli endpoint
GET /api/v1/pub/webhooks
# scopes: webhooks:read OR webhooks:writeRestituisce gli endpoint attivi e revocati, dai più recenti. L'accesso in lettura funziona con webhooks:read o webhooks:write — un token ispettore evita così lo scope di scrittura più ampio.
Forma della consegna
Ogni consegna è un singolo POST al tuo endpoint con Content-Type: application/json. Il body è l envelope dell evento con i campi event (nome dell evento), occurred_at (timestamp ISO) e un blocco data la cui forma dipende dall evento (vedi tabella sopra). Tre header trasportano metadati di routing + verifica:
POST https://your-server.example.com/qmailing-events
Content-Type: application/json
User-Agent: qmailing-webhook/1 (+https://qmailing.com)
X-Qmailing-Event: email.received
X-Qmailing-Delivery: 5f3b2a91-7c4d-4d52-9c3e-aa1bcd8a4f12
X-Qmailing-Signature: t=1685120800,v1=abc123def…
{
"event": "email.received",
"occurred_at": "2026-05-23T19:46:40Z",
"data": {
"email_id": "uuid",
"mailbox_id": "uuid",
"from": "alice@example.com",
"subject": "Hello",
"preview_text": "Just checking in…"
}
}Schema di firma
# Receiver side (Node example)
const crypto = require("node:crypto");
function verify(headers, body, secret) {
const sig = headers["x-qmailing-signature"]; // "t=1685120800,v1=abc123…"
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(sig);
if (!m) return false;
const ts = Number(m[1]);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-min replay window
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}Rifiuta la richiesta se il timestamp è più vecchio di 5 minuti (finestra anti-replay) o se l'HMAC ricalcolato non corrisponde al v1 hex — il nostro firmatario BE riproduce esattamente le convenzioni di Stripe / GitHub, quindi qualunque delle librerie standard di verifica funziona out-of-the-box.
Politica di retry
Risposte non-2xx (o errori di trasporto — DNS, TLS, connessione rifiutata, timeout di 10s) incrementano attempt_count e spostano next_attempt_at allo slot successivo in 1m / 5m / 15m / 1h / 6h. Dopo il quinto fallimento la consegna passa a FAILED (DLQ) — visibile su /settings/developers, dove lo sviluppatore può ritentarla manualmente. Idempotenza: ogni retry porta lo stesso UUID in X-Qmailing-Delivery — i ricevitori deduplicano su quell'header.
Elenca le consegne recenti
GET /api/v1/pub/webhooks/{endpointId}/deliveries?limit=100
# scopes: webhooks:read OR webhooks:writeRestituisce le limit consegne più recenti di un endpoint (default 100, max 100), dalle più recenti. Ogni riga porta stato, contatore tentativi, ultimo codice HTTP, ultimo errore e il JSON originale — alimenta la vista cronologica nella dashboard dello sviluppatore.
Invia consegna di prova
POST /api/v1/pub/webhooks/{endpointId}/test
# scope: webhooks:write
# Response 202: WebhookDeliveryDto (enqueued, picked up next worker tick)Spara un evento sintetico webhook.test all'endpoint — stesso percorso di dispatch degli eventi reali, utile per verificare firma + TLS prima di sottoscrivere il traffico live.
Ritenta una consegna
POST /api/v1/pub/webhooks/deliveries/{deliveryId}/retry
# scope: webhooks:write
# Response 200: WebhookDeliveryDto (status=PENDING, attempt=MAX-1)Rimette manualmente in coda una riga di consegna. Non puoi ritentare una riga già consegnata (409). Il retry concede esattamente un tentativo aggiuntivo (contatore a MAX-1), così un endpoint rotto in modo persistente non può ciclare indefinitamente per clic ripetuti.
Elimina consegna dalla cronologia
DELETE /api/v1/pub/webhooks/deliveries/{deliveryId}
# scope: webhooks:write
# Response: 204 No ContentHard-delete di una riga di consegna dalla tabella di cronologia. Il log di audit di compliance cattura la cancellazione stessa, quindi la tracciabilità non si perde quando la riga sparisce.
Revoca un endpoint
DELETE /api/v1/pub/webhooks/{id}
# scope: webhooks:write
# Response: 204 No ContentIdempotente — revocare un endpoint già revocato riesce in silenzio. La riga viene conservata (timestamp revoked-at impostato) così che i log di audit mantengano i loro riferimenti.
409 WebhookEndpointLimitExceeded / 400 InvalidWebhookEvent.Errori
Ogni risposta non-2xx è un ProblemDetail RFC-7807 con un campo code stabile su cui il tuo agente può ramificarsi:
| HTTP | code | Quando |
|---|---|---|
| 400 | InvalidApiTokenScope | Stringa di scope sconosciuta in fase di creazione. |
| 400 | InvalidEmailAddress | Uno dei destinatari non ha superato la validazione di sintassi RFC 5322. |
| 400 | InvalidWebhookEvent | L'elenco eventi registrati contiene un nome sconosciuto. |
| 401 | — | Header Bearer mancante, malformato, o token revocato / scaduto. |
| 402 | PlanFeatureRequired | Il chiamante è su un piano che non include l'accesso API — fai l'upgrade. |
| 403 | InsufficientScope | Il token non contiene nessuno degli scope richiesti. |
| 404 | ApiTokenNotFound | L'obiettivo della revoca non esiste o appartiene a un altro account. |
| 409 | MailboxNotVerified | Il dominio della casella di invio non è ancora completamente verificato — completa il DNS in /settings/domains. |
| 409 | ApiTokenLimitExceeded | L'account ha il massimo (20) di token attivi; revocane uno prima. |
| 409 | WebhookEndpointLimitExceeded | L'account ha già 10 endpoint webhook attivi; revocane uno prima. |
| 429 | RateLimitExceeded | Il token ha superato le 300 richieste al minuto. Retry-After in secondi. |
Limiti di frequenza
Ogni token ottiene il proprio bucket di 300 richieste al minuto. Configurazioni multi-agente (CLI + cron + plugin IDE) che usano token diversi per lo stesso account funzionano in modo indipendente — non si contendono mai un unico bucket condiviso. 429 RateLimitExceeded include un header Retry-After in secondi.
Server MCP
Il pacchetto @qmailing/mcp-server gira in locale ed espone l'API pubblica come strumenti del Model Context Protocol. Collegalo a Claude Desktop, Cursor, Continue o a qualsiasi client compatibile con MCP.
@qmailing/mcp-server@0.2.0) se non vuoi aggiornamenti automatici.{
"mcpServers": {
"qmailing": {
"command": "npx",
"args": ["-y", "@qmailing/mcp-server"],
"env": {
"QMAILING_API_TOKEN": "qm_live_your_token_here"
}
}
}
}Strumenti inclusi: qmailing_list_mailboxes, qmailing_get_mailbox, qmailing_create_mailbox, qmailing_list_domains, qmailing_get_dns_records, qmailing_list_emails, qmailing_get_email, qmailing_get_attachment, qmailing_send_email, qmailing_register_webhook, qmailing_list_webhooks, qmailing_delete_webhook. Il catalogo si amplia man mano che arrivano nuovi endpoint.