API

API documentation

Full technical reference for Messivo's REST API (`api/v1/*`): authentication, every endpoint, exact request/response shapes, error conventions, and the domain rules you need to know before integrating.

Authentication

Every request carries this header:

X-Api-Key: ms_<KeyId>.<secret>

A single key identifies both the caller and the Tenant (the business/workspace) it operates on — no other context parameter is needed, no OAuth/JWT/cookie. A key is always scoped to one fixed Tenant: it can't operate on a different one, and doesn't need to know it — the platform resolves it internally from the key.

401 Unauthorized response: missing, malformed or revoked key, or the associated Tenant is no longer active.

A tenant generates its own keys from the Portal (self-service "API Integrations" section) — a reseller generates one key per managed client the same way, from their Clients page. A Provider-only endpoint remains available only for exceptional support cases. A client only needs to accept the full string (ms_...) as an incoming credential, typically from configuration.

NuGet package for .NET

If your backend is .NET, you don't need to call the REST API by hand: the KeyInteractive.Messivo package (.NET 8 or later) provides a ready-to-use client — IMessivoApiConsumer/MessivoApiConsumer — wrapping message sending, conversation/history reading and template/phone number/contact management behind simple method calls.

dotnet add package KeyInteractive.Messivo

Register it as a typed client, with your key and the base URL:

// 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
});

Example — checking the service window before sending:

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));
}

Sends have no automatic retry: always pass a stable ClientRequestId (derived from your originating event, not random per attempt) — if already used for the same Tenant, a duplicate send returns 409 Conflict instead of sending the same message twice.

General conventions

  • Every application endpoint returns 401 Unauthorized if X-Api-Key doesn't resolve a valid Tenant.
  • Validation errors: 400 Bad Request with standard ASP.NET Core ProblemDetails.
  • Errors from the WhatsApp provider (Meta) when sending fails on Meta's side: 502 Bad Gateway, with an extended providerErrorCode field.
  • Idempotency conflict (see below): 409 Conflict, ProblemDetails.
  • All timestamps are DateTimeOffset in UTC, ISO 8601 in JSON (e.g. "2026-09-03T10:15:00Z").
  • Domain IDs (PhoneNumberId, MessageId, etc.) are always Guids internal to Messivo — different from the IDs Meta uses (MetaPhoneNumberId, ProviderMessageId), which are strings. Treat them as distinct types, never interchangeable.
  • Send idempotency: sending endpoints accept an optional ClientRequestId — if already used for the same Tenant, a duplicate request (e.g. a network retry) returns 409 Conflict instead of sending the same message twice.

WhatsApp service window (24h)

A Meta domain rule, not specific to Messivo: a free-text message can only be sent if the recipient has written something in the last 24h ("service window open"). Outside that window — including the very first contact ever made with a number — an approved template is required: a free-text message outside the window is rejected by Meta, not silently accepted.

Always check GET /conversations/{phoneNumberId}/{contact}/window before deciding which of the two to call, instead of assuming the state.

Messages (api/v1/messages)

Sending, reading and status of WhatsApp messages.

POSTapi/v1/messages/text

Sends a free-text message (window open only).

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

Starts/continues a conversation with an approved template — works even with the window closed, it's the only way to start a contact.

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

Sends an image or document already hosted elsewhere — Messivo downloads it from the given URL and forwards it to 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

Same as above, but the file is included as base64 in the request — useful when you don't have a public URL to give. 5MB decoded limit; 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

Latest messages for the Tenant (all numbers/contacts together), most recent first. take is clamped between 1 and 100.

GETapi/v1/messages/{messageId}

A single message by id. 404 if not found (or not owned by the current Tenant).

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

Status transition history for a message (Accepted → Sent → Delivered → Read, or → Failed).

GETapi/v1/messages/{messageId}/media

Downloads the binary content of an attachment (proxied from Meta). 404 if no longer available, 502 if Meta refuses the download.

Conversations (api/v1/conversations)

GETapi/v1/conversations

All of the Tenant's conversations (one per number+contact pair), most recent first, 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

All messages of a specific conversation, in chronological order.

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

Service window status for a specific contact, without downloading the whole conversation list. Works even for a contact who never wrote (no 404: isOpen false).

record ServiceWindowStatus(bool IsOpen, DateTimeOffset? ExpiresAtUtc);
DELETEapi/v1/conversations/{phoneNumberId}/{contact}

Hides the conversation (soft delete — it reappears on its own at the next message, inbound or outbound). 204 No Content.

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

Permanently deletes every message in the conversation — irreversible. 204 No Content.

Templates (api/v1/templates)

GETapi/v1/templates?phoneNumberId={guid}

Templates available for a number (local mirror, kept in sync with Meta only through synchronize — not always "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

Creates a new template and submits it to Meta for approval.

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}

Re-syncs the local mirror with the real state on Meta (approval status, category, etc.) — call periodically or after changes made elsewhere.

PUTapi/v1/templates/{id}/variables

Configures how a template's variables are filled in when sending (from contacts, a fixed value, or manually on each send). 204 No Content.

Phone numbers (api/v1/phone-numbers)

GETapi/v1/phone-numbers

WhatsApp numbers connected to the Tenant. Id is the PhoneNumberId to use in every other call.

record PhoneNumberDetails(Guid Id, string DisplayPhoneNumber, string? Alias, bool IsEnabled);
PUTapi/v1/phone-numbers/{phoneNumberId}/alias

Sets an internal text alias for a number (or null to remove it).

Contacts (api/v1/contacts)

GETapi/v1/contacts

All of the Tenant's contacts.

record ContactDetails(
    Guid Id, string PhoneNumber, string DisplayName,
    string? FirstName, string? LastName, string? Address, string? Notes,
    DateTimeOffset CreatedAtUtc, DateTimeOffset? UpdatedAtUtc);
PUTapi/v1/contacts/{phoneNumber}

Creates/updates only the display name — requires the number to already have a conversation.

PUTapi/v1/contacts/{phoneNumber}/details

Full contact record (first/last name, address, notes) — works even for a number never contacted.

DELETEapi/v1/contacts/{phoneNumber}

204 No Content if deleted, 404 if it didn't exist.

Tenant and Provider

GETapi/v1/tenant

The Tenant the current API key belongs to.

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

Name of the WhatsApp provider configured on the Messivo side (diagnostics only).

Known limits

  • No outgoing webhook: Messivo does not push-notify an external app when a new message arrives. A client that wants to react to inbound messages must poll (GET /messages or GET /conversations) at intervals.
  • One number per Tenant in practice: the data model supports multiple numbers, but the guided onboarding today assumes a single one. Not a limit of the send/read API (already multi-number via PhoneNumberId), only of provisioning.
  • No documented rate limiting/retry policy on the Messivo side today — a robust client should still implement its own backoff on 5xx responses, especially 502s (Meta errors).

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.