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.

POSTapi/v1/messages/text

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)
POSTapi/v1/messages/template

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: SendMessageResult
POSTapi/v1/messages/media

Invia 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: SendMessageResult
POSTapi/v1/messages/media/upload

Come 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: SendMessageResult
GETapi/v1/messages?take=25

Ultimi messaggi del Tenant (tutti i numeri/contatti insieme), più recenti prima. take clampato tra 1 e 100.

GETapi/v1/messages/{messageId}

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);
GETapi/v1/messages/{messageId}/status-events

Storico delle transizioni di stato di un messaggio (Accepted → Sent → Delivered → Read, o → Failed).

GETapi/v1/messages/{messageId}/media

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)

GETapi/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);
GETapi/v1/conversations/{phoneNumberId}/{contact}?take=100

Tutti i messaggi di una conversazione specifica, in ordine cronologico.

GETapi/v1/conversations/{phoneNumberId}/{contact}/window

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);
DELETEapi/v1/conversations/{phoneNumberId}/{contact}

Nasconde la conversazione (soft delete — ricompare da sola al prossimo messaggio, in entrata o in uscita). 204 No Content.

DELETEapi/v1/conversations/{phoneNumberId}/{contact}/permanent

Cancella definitivamente tutti i messaggi della conversazione — irreversibile. 204 No Content.

Template (api/v1/templates)

GETapi/v1/templates?phoneNumberId={guid}

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);
POSTapi/v1/templates

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 parametro
POSTapi/v1/templates/synchronize?phoneNumberId={guid}

Riallinea il mirror locale con lo stato reale su Meta (approvazione, categoria, ecc.) — da chiamare periodicamente o dopo modifiche fatte altrove.

PUTapi/v1/templates/{id}/variables

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)

GETapi/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);
PUTapi/v1/phone-numbers/{phoneNumberId}/alias

Imposta un alias interno testuale per un numero (o null per rimuoverlo).

Rubrica (api/v1/contacts)

GETapi/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);
PUTapi/v1/contacts/{phoneNumber}

Crea/aggiorna solo il nome visualizzato — richiede che il numero abbia già una conversazione.

PUTapi/v1/contacts/{phoneNumber}/details

Rubrica completa (nome, cognome, indirizzo, note) — funziona anche su un numero mai contattato.

DELETEapi/v1/contacts/{phoneNumber}

204 No Content se cancellato, 404 se non esisteva.

Tenant e Provider

GETapi/v1/tenant

Il Tenant a cui appartiene la chiave API corrente.

record TenantDetails(Guid Id, string Slug, string DisplayName, string Status);
GETapi/v1/provider

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).

Ricollegamento al server in corso...

Ricollegamento non riuscito... nuovo tentativo tra secondi.

Ricollegamento non riuscito.
Riprova o ricarica la pagina.

La sessione è stata messa in pausa dal server.

Impossibile riprendere la sessione.
Riprova o ricarica la pagina.