API
Documentazione API
Riferimento tecnico completo delle API REST di Messivo (`api/v1/*`): autenticazione, tutti gli endpoint, forme di richiesta/risposta, convenzioni di errore e le regole di dominio da conoscere prima di integrare.
Autenticazione
Ogni richiesta porta questo header:
X-Api-Key: ms_<KeyId>.<secret>
Una singola chiave identifica sia il chiamante sia il Tenant (l'azienda/spazio operativo) su cui opera — non serve nessun altro parametro di contesto, nessun OAuth/JWT/cookie. Una chiave è sempre scoped a un solo Tenant fisso: non può operare su Tenant diversi, e non ha bisogno di saperlo — la piattaforma lo risolve internamente dalla chiave.
Risposta 401 Unauthorized: chiave assente, malformata, revocata, o Tenant associato non più attivo.
Le chiavi se le genera da solo il tenant che le userà, dal Portal (sezione self-service "Integrazioni API") — un reseller genera allo stesso modo una chiave per ciascun cliente finale che gestisce, dalla propria pagina Clienti. Un endpoint Provider-only resta disponibile solo per casi di supporto eccezionali. Un client deve solo accettare la stringa completa (ms_...) come credenziale in ingresso, tipicamente da configurazione.
Pacchetto NuGet per .NET
Se il tuo backend è .NET, non serve chiamare la REST API a mano: il pacchetto KeyInteractive.Messivo (.NET 8 o superiore) offre un client pronto — IMessivoApiConsumer/MessivoApiConsumer — che incapsula invio messaggi, lettura conversazioni/cronologia e gestione template/numeri/contatti in chiamate a metodo.
dotnet add package KeyInteractive.Messivo
Registrazione come typed client, con la tua chiave e l'URL base:
// Program.cs
builder.Services.AddHttpClient<IMessivoApiConsumer, MessivoApiConsumer>();
builder.Services.AddSingleton(new MessivoApiConsumerOptions
{
BaseUri = "https://messivo.api.keyinteractive.it/",
ApiKey = "ms_...", // generata dal Portal, Integrazioni API
});
Esempio — controllare la finestra di servizio prima di inviare:
var window = await messivoApiConsumer.GetServiceWindowStatus(phoneNumberId, contact: "+391234567890");
if (window.IsOpen)
{
await messivoApiConsumer.SendText(new SendTextMessageRequest(
phoneNumberId, Recipient: "+391234567890", Text: "Ciao!",
ClientRequestId: myEventId, RepliedToProviderMessageId: null));
}
else
{
await messivoApiConsumer.SendTemplate(new SendTemplateMessageRequest(
phoneNumberId, Recipient: "+391234567890", TemplateName: "welcome",
LanguageCode: "it", BodyParameters: null, ClientRequestId: myEventId));
}
Gli invii non hanno retry automatico: passa sempre un ClientRequestId stabile (derivato dal tuo evento di origine, non casuale ad ogni tentativo) — se già usato per lo stesso Tenant, un invio duplicato restituisce 409 Conflict invece di mandare due volte lo stesso messaggio.
Convenzioni generali
- Tutti gli endpoint applicativi restituiscono 401 Unauthorized se la X-Api-Key non risolve un Tenant valido.
- Errori di validazione: 400 Bad Request con ProblemDetails standard ASP.NET Core.
- Errori dal provider WhatsApp (Meta) quando l'invio fallisce lato Meta: 502 Bad Gateway, con un campo esteso providerErrorCode.
- Conflitto di idempotenza (vedi sotto): 409 Conflict, ProblemDetails.
- Tutti i timestamp sono DateTimeOffset in UTC, formato ISO 8601 in JSON (es. "2026-09-03T10:15:00Z").
- Gli ID di dominio (PhoneNumberId, MessageId, ecc.) sono sempre Guid interni a Messivo — diversi dagli ID che usa Meta (MetaPhoneNumberId, ProviderMessageId), che sono stringhe. Vanno trattati come tipi distinti, mai intercambiabili.
- Idempotenza sugli invii: gli endpoint di invio accettano un ClientRequestId opzionale — se già usato in precedenza per lo stesso Tenant, una richiesta duplicata (es. un retry di rete) restituisce 409 Conflict invece di inviare due volte lo stesso messaggio.
Finestra di servizio WhatsApp (24h)
Regola di dominio Meta, non specifica di Messivo: un messaggio di testo libero può essere inviato solo se il destinatario ha scritto qualcosa negli ultimi 24h ("finestra di servizio aperta"). Fuori da quella finestra — incluso il primissimo contatto mai fatto con un numero — serve un template approvato: un testo libero fuori finestra viene rifiutato da Meta, non silenziosamente accettato.
Controlla sempre GET /conversations/{phoneNumberId}/{contact}/window prima di decidere quale dei due invocare, piuttosto che assumere lo stato.
Messaggi (api/v1/messages)
Invio, lettura e stato dei messaggi WhatsApp.
Invia un messaggio di testo libero (solo a finestra aperta).
record SendTextMessageRequest(
Guid PhoneNumberId, // required
string Recipient, // required, 8-32 caratteri (E.164)
string Text, // required, max 4096
string? ClientRequestId, // opzionale, max 100 — idempotenza
string? RepliedToProviderMessageId);
// 200 OK: SendMessageResult(string ProviderMessageId, Guid MessageId, string Status)Avvia/continua una conversazione con un template approvato — funziona anche a finestra chiusa, è l'unico modo per iniziare un contatto.
record SendTemplateMessageRequest(
Guid PhoneNumberId,
string Recipient,
string TemplateName, // max 512
string LanguageCode, // max 20 — codice lingua Meta
IReadOnlyList<string>? BodyParameters, // max 10 — {{1}}, {{2}}, ...
string? ClientRequestId);
// 200 OK: SendMessageResultInvia un'immagine o un documento già ospitato altrove — Messivo lo scarica dall'URL indicato e lo inoltra a Meta.
record SendMediaMessageRequest(
Guid PhoneNumberId,
string Recipient,
string MediaType, // "image" o "document"
string MediaUrl, // required, HTTPS assoluto
string? Caption, // max 1024
string? FileName, // max 255
string? ClientRequestId);
// 200 OK: SendMessageResultCome sopra, ma il file è incluso in base64 nella richiesta — utile quando non hai un URL pubblico da dare. Limite 5MB decodificato; image/jpeg, image/png, application/pdf.
record SendUploadedMediaRequest(
Guid PhoneNumberId,
string Recipient,
string ContentType, // image/jpeg | image/png | application/pdf
string FileName,
string Base64Content, // required, max 5MB decodificato
string? Caption,
string? ClientRequestId,
string? RepliedToProviderMessageId);
// 200 OK: SendMessageResultUltimi messaggi del Tenant (tutti i numeri/contatti insieme), più recenti prima. take clampato tra 1 e 100.
Un singolo messaggio per id. 404 se non trovato (o non del Tenant corrente).
record MessageDetails(
Guid Id, Guid PhoneNumberId, string ProviderMessageId,
string Direction, // "Inbound" | "Outbound"
string Type, // "Text" | "Image" | "Document" | "Template" | ...
string Contact, // E.164
string? Content, string? ProviderMediaId, string? MediaUrl,
string? MimeType, string? FileName, string? ClientRequestId,
string Status, // "Accepted" | "Sent" | "Delivered" | "Read" | "Failed" | ...
string? ErrorCode, string? ErrorMessage,
DateTimeOffset CreatedAtUtc, DateTimeOffset StatusUpdatedAtUtc,
string? RepliedToProviderMessageId, RepliedToMessagePreview? RepliedToMessage);Storico delle transizioni di stato di un messaggio (Accepted → Sent → Delivered → Read, o → Failed).
Scarica il contenuto binario di un allegato (proxato da Meta). 404 se non più disponibile, 502 se Meta rifiuta il download.
Conversazioni (api/v1/conversations)
Tutte le conversazioni del Tenant (una per coppia numero+contatto), più recenti prima, max 100.
record ConversationDetails(
Guid PhoneNumberId, string Contact, string? ContactDisplayName,
string? LastContent, string LastMessageType, string LastDirection, string LastStatus,
DateTimeOffset LastActivityAtUtc, int MessageCount,
DateTimeOffset? LastInboundAtUtc, DateTimeOffset? ServiceWindowExpiresAtUtc,
bool IsServiceWindowOpen);Tutti i messaggi di una conversazione specifica, in ordine cronologico.
Stato della finestra di servizio per un contatto specifico, senza scaricare l'intera lista conversazioni. Funziona anche per un contatto mai scritto (nessun 404: isOpen false).
record ServiceWindowStatus(bool IsOpen, DateTimeOffset? ExpiresAtUtc);
Nasconde la conversazione (soft delete — ricompare da sola al prossimo messaggio, in entrata o in uscita). 204 No Content.
Cancella definitivamente tutti i messaggi della conversazione — irreversibile. 204 No Content.
Template (api/v1/templates)
Template disponibili per un numero (mirror locale, allineato a Meta solo tramite synchronize — non è sempre "live").
record TemplateDetails(
Guid Id, Guid WhatsAppBusinessAccountId, string MetaTemplateId,
string Name, string Language,
string Status, // "Approved" | "Pending" | "Rejected" | ...
string Category, // "UTILITY" | "MARKETING"
string? BodyText, int BodyParameterCount, bool IsSupportedForSending,
DateTimeOffset SyncedAtUtc,
IReadOnlyDictionary<int, TemplateVariableSourceDetails> VariableSources);Crea un nuovo template e lo sottopone a Meta per approvazione.
record CreateTemplateRequest(
Guid PhoneNumberId,
string Name, // solo minuscole/numeri/underscore
string Language,
string Category, // "UTILITY" o "MARKETING"
string BodyText, // {{1}}, {{2}}, ... mai a inizio/fine testo
IReadOnlyList<string> ExampleValues); // un esempio per ogni parametroRiallinea il mirror locale con lo stato reale su Meta (approvazione, categoria, ecc.) — da chiamare periodicamente o dopo modifiche fatte altrove.
Configura come compilare le variabili di un template all'invio (da rubrica, valore fisso, o manuale a ogni invio). 204 No Content.
Numeri di telefono (api/v1/phone-numbers)
Numeri WhatsApp collegati al Tenant. Id è il PhoneNumberId da usare in tutte le altre chiamate.
record PhoneNumberDetails(Guid Id, string DisplayPhoneNumber, string? Alias, bool IsEnabled);
Imposta un alias interno testuale per un numero (o null per rimuoverlo).
Rubrica (api/v1/contacts)
Tutti i contatti del Tenant.
record ContactDetails(
Guid Id, string PhoneNumber, string DisplayName,
string? FirstName, string? LastName, string? Address, string? Notes,
DateTimeOffset CreatedAtUtc, DateTimeOffset? UpdatedAtUtc);Crea/aggiorna solo il nome visualizzato — richiede che il numero abbia già una conversazione.
Rubrica completa (nome, cognome, indirizzo, note) — funziona anche su un numero mai contattato.
204 No Content se cancellato, 404 se non esisteva.
Tenant e Provider
Il Tenant a cui appartiene la chiave API corrente.
record TenantDetails(Guid Id, string Slug, string DisplayName, string Status);
Nome del provider WhatsApp configurato lato Messivo (utile solo per diagnostica).
Limiti noti
- Nessun webhook in uscita: Messivo non notifica push un'app esterna quando arriva un nuovo messaggio. Un client che vuole reagire ai messaggi in entrata deve fare polling (GET /messages o GET /conversations) a intervalli.
- Un solo numero per Tenant nella pratica: il modello dati supporta più numeri, ma l'onboarding guidato oggi assume un numero solo. Non è un limite dell'API di invio/lettura (già multi-numero via PhoneNumberId), solo del provisioning.
- Nessun rate limiting/retry-policy documentato lato Messivo oggi — un client robusto dovrebbe comunque implementare un proprio backoff sui 5xx, specialmente sui 502 (errori Meta).