Version v1 Developer guides

API reference

An OpenAPI-driven reference frame with endpoints, schemas, examples, scopes and normalized errors.

Base URLhttps://messagevia.com/api/v1

Authentication

Every request carries an API key as a bearer token. The key decides the workspace, so there is no workspace parameter to get wrong.

Create a key in the app under Developers → API keys. The full value is shown once at creation and can never be read again — store it in your secret manager.

A new key can use the whole API. You can narrow it to specific scopes when an integration should reach less than that; a request outside its scopes is refused with 403. Keys are workspace-bound either way: they cannot read or write another workspace, and an X-Workspace-Id header is ignored on this API.

Revoking a key is permanent — it starts failing with 401 immediately and cannot be restored. To rotate, create the replacement first, switch your systems over, then revoke the old one.

Send keys only from your server. A key in browser or mobile code is a leaked key.

contacts:read scope

Read the workspace and list contacts. Granted by default.

contacts:write scope

Create and update contacts.

messages:write scope

Upload media and queue messages.

messages:read scope

Read the message log — every message and its delivery outcome.

media:read scope

Read and download inbound attachments.

groups:read scope

List the groups your numbers are in, and refresh that list.

groups:send scope

Post a message into a group. Separate from messages:write because one call reaches every member.

Idempotency

Optional, and worth sending: an Idempotency-Key makes a retry safe. Without one a request simply runs.

Networks time out after the message was already queued. Retrying then sends it twice — and your customer sees it twice. An Idempotency-Key removes that risk: repeat the same key and body and you get the stored response back with Idempotency-Replayed: true, nothing is sent again.

Use a value your own system can regenerate for the same intent, such as the order you are notifying about — order-4472-shipped, not a fresh random string. That is what makes the retry match.

Reusing a key with a different body is refused with 409: it means two different messages were about to share one identity. Keys are scoped to the API key, method and path, and must be 8 to 255 characters of A-Z, a-z, 0-9, dot, underscore, colon or hyphen.

Idempotency-Key header

Stable per intent, 8–255 characters. Send it and retries are safe; omit it and the request just runs.

Errors

Errors are JSON with a human-readable message. Validation failures also carry a field-keyed errors object.

Send Accept: application/json on every request so failures come back as JSON rather than an HTML error page.

Messages are safe to show to your own operators. They never contain bearer values, request bodies or engine internals.

Method Description
400 Bad Request The request could not be read.
401 Unauthorized The key is missing, unknown, revoked or expired.
403 Forbidden The key does not carry the scope this endpoint requires.
404 Not Found The connection, contact or media does not exist in this workspace.
409 Conflict The Idempotency-Key was reused with a different request body.
422 Unprocessable The body failed validation, the Idempotency-Key was malformed, or the send was refused — for example an opted-out recipient or a connection that is offline.
429 Too Many Requests The rate limit was exceeded. Back off and retry.

Retrieve the workspace

GET/workspace

Returns the workspace the key belongs to. Useful as a health check right after you create a key.

contacts:read scope · required

Required scope.

List contacts

GET/contacts

Paginated contacts for the workspace, newest activity first.

per_page integer

Page size, 1–100. Defaults to 25.

page integer

Page number, starting at 1.

contacts:read scope · required

Required scope.

Create or update a contact

POST/contacts

Upserts by phone number. Sending a message to a new number creates the contact for you, so call this only when you want to set a name, consent or metadata.

Consent is preserved unless you send it explicitly. Set opted_in only when you hold a real opt-in for that number — it is what unlocks campaigns, and it is recorded in the workspace consent ledger with this API key as the source.

phone string · required

E.164 format, for example +923289720627.

name string

Display name for your team.

consent_status string

unknown, opted_in or opted_out. Omit to leave unchanged.

metadata object

Your own key/value fields.

contacts:write scope · required

Required scope.

Upload media

POST/media

Uploads a file, scans it for malware and stores it — in one call. The returned id is ready to attach to a message. Only needed when you send the same file more than once; otherwise attach it straight to the send.

Send the file as multipart/form-data under the field name file. The response returns only once the file is scanned and retained, so a 201 means the media is safe to send.

An infected or unsupported file is refused and never stored. Media you upload is retained for the period your plan allows and then deleted.

file file · required

The media to send. Supported types and the size limit come from your plan.

messages:write scope · required

Required scope.

Send a message

POST/messages

Queues one WhatsApp message. Address it by phone number or contact id, and send text, a file, or both.

Give a recipient phone number and the contact is created for you — no separate call. Send contact_id instead when your system already stores our ids. Send one or the other, never both.

Attach a file directly as file and it is uploaded and scanned inline — one call, nothing to wire up first. Send the same file to many people instead by uploading it once through POST /media and passing media.attachments[].media_id, up to 10 of them.

A message needs at least one of body, file or media.attachments.

A 202 means the message is queued and handed to the number you chose. It is not proof of delivery: delivery and read state arrive later through webhooks.

Sends are paced per number to protect these session-based numbers from spam detection — a queued message is released within your workspace sending policy (per-number hourly/daily cap and a small random gap), so on a busy number or large batch it can leave a little after this call. Configure it in your workspace settings.

This endpoint is for messages your relationship justifies — order updates, replies, receipts. It sends to contacts whose consent is still unknown, but it will never reach a contact who opted out or is on the suppression list. Marketing to a list is what campaigns are for, and those require a recorded opt-in.

connection_id string (uuid) · required

Which of your WhatsApp numbers sends. Copy it from Connections → Manage. Sending works whether or not the number receives.

recipient string

Phone number in E.164 format. Required unless contact_id is given.

contact_id string (uuid)

An existing contact. Required unless recipient is given.

body string

Message text, up to 4096 characters.

file file

A file to upload, scan and send in this call. Use multipart/form-data.

media.attachments[].media_id string (uuid)

An id returned by POST /media, for media you send more than once. Up to 10.

messages:write scope · required

Required scope.

Message log

GET/messages

Paginated log of every message the workspace sent or received, newest first, with delivery outcome and timeline.

direction string

Filter to inbound or outbound. Omit for both.

status string

Filter by queued, sending, sent, delivered, read or failed.

per_page integer

Page size, 1–100. Defaults to 25.

page integer

Page number, starting at 1.

messages:read scope · required

Required scope.

List groups

GET/groups

The WhatsApp groups your connected numbers belong to, largest first.

Groups are joined, renamed and left on the phone, so this list is a mirror of what your numbers report — there is nothing here to create or delete. Call POST /groups/sync to re-read it.

participant_count is a count and nothing more. Group members never contacted your workspace and never consented to being on file here, so their numbers are counted when the list is read and then discarded. There is no endpoint that returns them because there is no table that holds them.

can_send tells you whether this number may post. It is false for an announcement group where your number is not an admin — WhatsApp would drop such a message without reporting an error.

search string

Match on group name, case-insensitive.

per_page integer

Page size, 1-100. Defaults to 25.

page integer

Page number, starting at 1.

groups:read scope · required

Required scope.

Refresh groups from the phone

POST/groups/sync

Re-reads every connected number's group list from its phone and replaces the stored mirror.

This runs across every connected number, because the API deliberately exposes no connection ids. Each number is attempted on its own, so one being offline does not cost you the others — the numbers array reports each outcome.

A group your number has left is removed, not left behind as a row that has quietly stopped being true. Messages already sent to it go with it.

The call is a refresh, so running it twice is the same as running it once. It needs no Idempotency-Key.

groups:read scope · required

Required scope.

Message a group

POST/groups/{id}/messages

Posts one text message into a group. Every member receives it.

Use the id from GET /groups. Text only — attachments to a group are not offered.

It costs one message from the same plan allowance as any other send, however many members are in the group.

A 202 means the message was queued and handed to the number. Its status ends at sent: WhatsApp acknowledges a group message once but then returns a receipt per member, so there is no honest delivered or read state for a group and none is reported.

The send is refused with 422 when the group only lets admins post and your number is not one, and when the number the group belongs to is not connected. Both are refused before anything is queued, because WhatsApp would otherwise accept the message and drop it silently.

It carries its own scope, groups:send, rather than riding on messages:write. One call here reaches every member of a group, none of whom opted in to anything with you — that is a different power from messaging one customer.

body string · required

Message text, up to 4096 characters.

groups:send scope · required

Required scope.