Introduction

The Seen API lets you send WhatsApp messages, manage contacts and templates, read your inbox conversations, and receive real-time events — all through your own WhatsApp Business Account, via the official Meta Cloud API underneath.

Every request is scoped to a single tenant (your workspace) and authenticated with an API key. All endpoints below are relative to your API base URL and return JSON.

Authentication

Authenticate by sending your API key in the x-api-key header on every request. Create and manage keys from Dashboard → API Keys.

Authentication
curl "https://wa.vsartech.com/api/v1/contacts" \
  -H "x-api-key: YOUR_API_KEY"

Requests without a valid key return 401 Unauthorized. Keys are hashed at rest and never shown again after creation, so store yours securely.

Paste a key here to run real requests from the "Try it" tab on every endpoint below. It's kept only in this browser tab's session storage — never sent anywhere except directly to the API — and is cleared when you close the tab.

Base URL & versioning

All endpoints are versioned under a single path prefix:

Base URL
https://wa.vsartech.com/api/v1

The current version is v1. Breaking changes will be introduced under a new version prefix rather than changing existing behavior in place.

Errors

The API uses conventional HTTP status codes. 2xx indicates success, 4xx indicates a problem with the request (bad input, missing auth, not found), and 5xx indicates an error on our side.

Error response
{
  "success": false,
  "error": "Contact not found"
}
400Bad Request — missing or invalid parameters
401Unauthorized — missing or invalid API key
403Forbidden — key does not have permission for this action
404Not Found — the resource does not exist for your tenant
429Too Many Requests — rate limit exceeded
500Internal Server Error

Rate limits

Requests are limited to 1,000 requests per 15 minutes per IP address. Current usage is reported on every response via standard rate-limit headers:

Response headers
RateLimit-Limit: 1000
RateLimit-Remaining: 998
RateLimit-Reset: 842

Exceeding the limit returns 429 Too Many Requests. If you need a higher limit, contact us.

SDKs

Official client libraries for Node.js, Python, Dart/Flutter, and PHP wrap the REST API above. All four require the same API key used for direct REST calls — there is no separate SDK-only credential, and the client cannot be constructed without one.

Install (Node.js / TypeScript)
npm install seenwa
Node.js / TypeScript
import { SeenClient } from 'seenwa';

const client = new SeenClient({ apiKey: 'YOUR_API_KEY' });

await client.messages.sendText({
  to: '+15551234567',
  phone_number_id: '1143523638846055',
  message: 'Hi! Your order has shipped.',
});
Install (Python)
pip install seenwa
Python
from seenwa import SeenClient

client = SeenClient(api_key='YOUR_API_KEY')

client.messages.send_text(
    to='+15551234567',
    phone_number_id='1143523638846055',
    message='Hi! Your order has shipped.',
)
Install (Dart / Flutter)
dart pub add seenwa
Dart / Flutter
import 'package:seenwa/seenwa.dart';

final client = SeenClient(apiKey: 'YOUR_API_KEY');

await client.messages.sendText(
  to: '+15551234567',
  phoneNumberId: '1143523638846055',
  message: 'Hi! Your order has shipped.',
);

The Flutter SDK should be called from a backend you control, not bundled directly into a distributed app binary — a compiled app can be decompiled and any embedded API key extracted.

Install (PHP)
composer require vsartech/seenwa
PHP
use Seen\SeenClient;

$client = new SeenClient('YOUR_API_KEY');

$client->messages->sendText(
    to: '+15551234567',
    phoneNumberId: '1143523638846055',
    message: 'Hi! Your order has shipped.'
);

Every resource shown below (Messages, Conversations, Contacts, Templates, Media, Webhooks) has an equivalent example in all four languages — switch tabs on any endpoint's code panel to see it.

MCP Server

Seen runs a hosted Model Context Protocol server at mcp.seen.vsartech.com, so an MCP-compatible AI client (Claude Desktop, Claude Code, or any other MCP client) can read and act on your WhatsApp data directly — sending messages, looking up contacts and conversations, checking campaign status, and more, all in natural language.

Authentication uses the same API key as the REST API and SDKs above, sent as an x-api-key header on the MCP connection. A key's scopes, rate limit, and IP allowlist apply to MCP tool calls exactly as they do to direct API requests.

MCP Server URL
https://mcp.seen.vsartech.com/mcp
Claude Desktop / Claude Code (claude_desktop_config.json)
{
  "mcpServers": {
    "seen": {
      "url": "https://mcp.seen.vsartech.com/mcp",
      "headers": {
        "x-api-key": "YOUR_API_KEY"
      }
    }
  }
}

Available tools include list_conversations, get_conversation_messages, list_contacts, create_contact, list_phone_numbers, send_message, send_template_message, list_messages, list_templates, list_campaigns, get_campaign, and list_segments.

Messages

Send WhatsApp messages and read message history.

POST/api/v1/messages/text

Send a text message

Sends a free-form text message to a WhatsApp number. Requires an active 24-hour customer service window, or use the template endpoint instead.

Requires scope messages:send

Body parameters

tostringrequired

Recipient phone number in E.164 format.

phone_number_idstringrequired

Your WhatsApp phone number ID (see the WhatsApp page in the dashboard).

messagestringrequired

Message text, up to 4096 characters.

reply_tostring

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned. An id that can't be found sends without the quote.

Recipient phone number in E.164 format.

Your WhatsApp phone number ID (see the WhatsApp page in the dashboard).

Message text, up to 4096 characters.

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned. An id that can't be found sends without the quote.

Response
{
  "success": true,
  "data": { "id": "b6b6c1b0-...", "status": "queued" }
}
POST/api/v1/messages/template

Send a template message

Sends a pre-approved WhatsApp template message. Use this to message a customer outside the 24-hour window. For a carousel template, components must include a "carousel" entry with one card per card in the template (card_index from 0), each with a header parameter ({ "type": "image", "image": { "link": "https://..." } }), its body parameters, and a button parameter for each quick-reply or URL button. A send with a different number of cards is rejected with the reason.

Requires scope messages:send

Body parameters

tostringrequired

Recipient phone number in E.164 format.

phone_number_idstringrequired

Your WhatsApp phone number ID.

template_namestringrequired

Name of an approved template.

language_codestring

Template language code. Defaults to "en".

componentsarray

Template variable substitutions, in Meta's components format.

Recipient phone number in E.164 format.

Your WhatsApp phone number ID.

Name of an approved template.

Template language code. Defaults to "en".

Template variable substitutions, in Meta's components format.

Response
{
  "success": true,
  "data": { "id": "b6b6c1b0-...", "status": "queued" }
}
POST/api/v1/messages/media

Send a media message

Sends an image, video, audio, document, or sticker message. Provide either a previously uploaded media_id (see the Media resource) or a publicly reachable link for Seen to fetch.

Requires scope messages:send

Body parameters

tostringrequired

Recipient phone number in E.164 format.

phone_number_idstringrequired

Your WhatsApp phone number ID.

typestringrequired

One of image, video, audio, document, sticker.

media_idstring

ID of a media file previously uploaded via POST /media/upload. Either this or link is required.

linkstring

Publicly reachable URL for Seen to fetch and send. Either this or media_id is required.

captionstring

Caption text. Not supported for audio or sticker.

filenamestring

Displayed file name, for document messages.

reply_tostring

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned. An id that can't be found sends without the quote.

Recipient phone number in E.164 format.

Your WhatsApp phone number ID.

One of image, video, audio, document, sticker.

ID of a media file previously uploaded via POST /media/upload. Either this or link is required.

Publicly reachable URL for Seen to fetch and send. Either this or media_id is required.

Caption text. Not supported for audio or sticker.

Displayed file name, for document messages.

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned. An id that can't be found sends without the quote.

Response
{
  "success": true,
  "data": { "id": "b6b6c1b0-...", "status": "sent" }
}
POST/api/v1/messages/interactive

Send an interactive message

Sends reply buttons (up to 3), a list menu (up to 10 rows), or a link button. WhatsApp only accepts these within 24 hours of the customer's last message, and not into groups; outside that window the request fails with the reason WhatsApp gives. When the customer taps a button or picks a row, the reply arrives as a normal incoming message (message.received) whose text is the button or row title.

Requires scope messages:send

Body parameters

tostringrequired

Recipient phone number in E.164 format.

phone_number_idstringrequired

Your WhatsApp phone number ID.

typestringrequired

One of button, list, cta_url.

bodystringrequired

The message text (up to 1024 characters).

headerstring

Short title above the text (up to 60 characters).

header_mediaobject

A file above the text, for button and cta_url messages (a list takes a text header only): { type: image | video | document, media_id or link, filename (documents) }. Use instead of header.

footerstring

Small text below (up to 60 characters).

buttonsarray

type button: 1 to 3 reply buttons, each { id (up to 256), title (up to 20) }. Ids must differ.

button_labelstring

type list: the text (up to 20 characters) on the button that opens the menu.

sectionsarray

type list: sections of rows, 10 rows in all. Each row is { id, title (up to 24), description (up to 72, optional) }. With more than one section, each needs a title.

cta_labelstring

type cta_url: the text (up to 20 characters) on the link button.

cta_urlstring

type cta_url: the link the button opens.

reply_tostring

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned.

Recipient phone number in E.164 format.

Your WhatsApp phone number ID.

One of button, list, cta_url.

The message text (up to 1024 characters).

Short title above the text (up to 60 characters).

A file above the text, for button and cta_url messages (a list takes a text header only): { type: image | video | document, media_id or link, filename (documents) }. Use instead of header.

Small text below (up to 60 characters).

type button: 1 to 3 reply buttons, each { id (up to 256), title (up to 20) }. Ids must differ.

type list: the text (up to 20 characters) on the button that opens the menu.

type list: sections of rows, 10 rows in all. Each row is { id, title (up to 24), description (up to 72, optional) }. With more than one section, each needs a title.

type cta_url: the text (up to 20 characters) on the link button.

type cta_url: the link the button opens.

Quote an earlier message, like Reply in WhatsApp: its wamid, or the Seen message id a send returned.

Response
{
  "success": true,
  "data": { "id": "b6b6c1b0-...", "status": "sent", "conversation_id": "9c1f...", "wamid": "wamid.HBg..." }
}
GET/api/v1/messages

List messages

Returns a paginated list of messages, optionally filtered by contact, conversation, status, or type.

Requires scope messages:read

Query parameters

contactstring

Filter by contact ID.

conversationstring

Filter by conversation ID.

statusstring

Filter by status (queued, sent, delivered, read, failed).

typestring

Filter by message type (text, template, image, ...).

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20, at most 100.

cursorstring

For long lists, page with a cursor instead of page numbers: send an empty cursor for the first page, then the nextCursor from the previous response until hasMore is false. Faster, returns no total, and never skips or repeats rows while new ones arrive.

Filter by contact ID.

Filter by conversation ID.

Filter by status (queued, sent, delivered, read, failed).

Filter by message type (text, template, image, ...).

Page number, defaults to 1.

Results per page, defaults to 20, at most 100.

For long lists, page with a cursor instead of page numbers: send an empty cursor for the first page, then the nextCursor from the previous response until hasMore is false. Faster, returns no total, and never skips or repeats rows while new ones arrive.

Response
{
  "success": true,
  "data": [{ "id": "...", "direction": "inbound", "content": "hi", "status": "received", "createdAt": "..." }],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
GET/api/v1/messages/:id

Get a message

Returns a single message by ID.

Requires scope messages:read

Path parameters

idstringrequired

Message ID.

Message ID.

Response
{
  "success": true,
  "data": { "id": "...", "direction": "outbound", "content": "hi", "status": "sent" }
}

Conversations

Read and reply to inbox conversations.

GET/api/v1/conversations

List conversations

Returns a paginated list of conversations, each with the linked contact and phone number.

Requires scope messages:read

Query parameters

statusstring

Filter by status (open, pending, resolved).

assigned_tostring

Filter by assigned user ID, or "unassigned".

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20, at most 100.

cursorstring

For long lists, page with a cursor instead of page numbers: send an empty cursor for the first page, then the nextCursor from the previous response until hasMore is false. Faster, returns no total, and never skips or repeats rows while new ones arrive.

Filter by status (open, pending, resolved).

Filter by assigned user ID, or "unassigned".

Page number, defaults to 1.

Results per page, defaults to 20, at most 100.

For long lists, page with a cursor instead of page numbers: send an empty cursor for the first page, then the nextCursor from the previous response until hasMore is false. Faster, returns no total, and never skips or repeats rows while new ones arrive.

Response
{
  "success": true,
  "data": [{ "id": "...", "status": "open", "lastMessagePreview": "hi", "contacts": { "name": "Kundan" } }],
  "pagination": { "limit": 20, "hasMore": true, "nextCursor": "WyIyMDI2LTA4LTMx..." }
}
GET/api/v1/conversations/:id

Get a conversation

Returns a single conversation with its contact, phone number, and notes.

Requires scope messages:read

Path parameters

idstringrequired

Conversation ID.

Conversation ID.

Response
{
  "success": true,
  "data": { "id": "...", "status": "open", "contacts": { "name": "Kundan" } }
}
POST/api/v1/conversations/:id/send

Reply in a conversation

Sends an outbound text message within an existing conversation, to the conversation's contact.

Requires scope messages:send

Path parameters

idstringrequired

Conversation ID.

Body parameters

messagestringrequired

Message text.

Conversation ID.

Message text.

Response
{
  "success": true,
  "data": { "id": "...", "status": "queued" }
}
POST/api/v1/conversations/:id/resolve

Resolve a conversation

Marks a conversation as resolved.

Requires scope messages:send

Path parameters

idstringrequired

Conversation ID.

Conversation ID.

Response
{
  "success": true,
  "data": { "id": "...", "status": "resolved" }
}

Contacts

Create, read, update, and manage your contacts.

GET/api/v1/contacts

List contacts

Returns a paginated list of contacts, optionally searched or filtered by opt-in status.

Requires scope contacts:read

Query parameters

searchstring

Matches against phone, name, or email.

opted_inboolean

Filter by opt-in status.

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20.

Matches against phone, name, or email.

Filter by opt-in status.

Page number, defaults to 1.

Results per page, defaults to 20.

Response
{
  "success": true,
  "data": [{ "id": "...", "phone": "+15551234567", "name": "Jane Doe", "optedIn": true }],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
GET/api/v1/contacts/:id

Get a contact

Returns a single contact, including its tags.

Requires scope contacts:read

Path parameters

idstringrequired

Contact ID.

Contact ID.

Response
{
  "success": true,
  "data": { "id": "...", "phone": "+15551234567", "name": "Jane Doe", "tags": ["vip"] }
}
POST/api/v1/contacts

Create or update a contact

Creates a new contact, or updates the existing one if the phone number already exists for your account.

Requires scope contacts:write

Body parameters

phonestringrequired

Phone number in E.164 format.

namestring

Contact name.

emailstring

Contact email.

languagestring

Preferred language code. Defaults to "en".

tagsarray

Tags to attach to the contact.

opted_inboolean

Whether the contact has opted in to receive messages. Defaults to false.

Phone number in E.164 format.

Contact name.

Contact email.

Preferred language code. Defaults to "en".

Tags to attach to the contact.

Whether the contact has opted in to receive messages. Defaults to false.

Response
{
  "success": true,
  "data": { "id": "...", "phone": "+15551234567", "name": "Jane Doe" }
}
PUT/api/v1/contacts/:id

Update a contact

Updates a contact's name, email, or custom fields.

Requires scope contacts:write

Path parameters

idstringrequired

Contact ID.

Body parameters

namestring

Contact name.

emailstring

Contact email.

Contact ID.

Contact name.

Contact email.

Response
{
  "success": true,
  "data": { "id": "...", "name": "Jane Doe" }
}
DELETE/api/v1/contacts/:id

Delete a contact

Permanently deletes a contact.

Requires scope contacts:write

Path parameters

idstringrequired

Contact ID.

Contact ID.

Response
{
  "success": true,
  "message": "Contact deleted"
}
POST/api/v1/contacts/import

Bulk import contacts

Creates multiple contacts at once. Contacts with a phone number that already exists are skipped.

Requires scope contacts:write

Body parameters

contactsarrayrequired

Array of { phone, name?, email? } objects.

Array of { phone, name?, email? } objects.

Response
{
  "success": true,
  "data": { "imported": 1 }
}
POST/api/v1/contacts/opt-out/:phone

Opt out a contact

Marks a contact as opted out, by phone number.

Requires scope contacts:write

Path parameters

phonestringrequired

Contact phone number.

Contact phone number.

Response
{
  "success": true,
  "message": "Contact opted out"
}

Templates

Create and read your WhatsApp message templates.

POST/api/v1/templates

Create a template

Submits a new WhatsApp message template to Meta for approval. Newly created templates start in the "pending" status until Meta reviews them. For a carousel (a message with 2 to 10 swipeable cards), add a CAROUSEL component whose cards each have an IMAGE or VIDEO HEADER (with example.header_handle from POST /templates/media-upload), an optional BODY of up to 160 characters, and 1 or 2 BUTTONS; every card must have the same shape. A card that breaks a rule is rejected with the reason before the template reaches Meta.

Requires scope templates:write

Body parameters

namestringrequired

Template name (lowercase, unique per language).

categorystringrequired

Template category.

languagestring

Template language code. Defaults to "en".

componentsarrayrequired

Template structure in Meta's components format (header, body, footer, buttons, carousel).

Template name (lowercase, unique per language).

Template category.

Template language code. Defaults to "en".

Template structure in Meta's components format (header, body, footer, buttons, carousel).

Response
{
  "success": true,
  "data": { "id": "...", "name": "order_confirmation", "status": "pending" }
}
GET/api/v1/templates

List templates

Returns a paginated list of templates, optionally filtered by status or category.

Requires scope templates:read

Query parameters

statusstring

Filter by status (pending, approved, rejected).

categorystring

Filter by category (marketing, utility, authentication).

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20.

Filter by status (pending, approved, rejected).

Filter by category (marketing, utility, authentication).

Page number, defaults to 1.

Results per page, defaults to 20.

Response
{
  "success": true,
  "data": [{ "id": "...", "name": "order_confirmation", "status": "approved" }],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
GET/api/v1/templates/:id

Get a template

Returns a single template, including its component structure.

Requires scope templates:read

Path parameters

idstringrequired

Template ID.

Template ID.

Response
{
  "success": true,
  "data": { "id": "...", "name": "order_confirmation", "components": [] }
}
GET/api/v1/templates/:id/variables

Get a template's required variables

Returns the {{1}}, {{2}}, ... placeholders used by a template's header, body, and button URLs, plus the CSV column names a campaign audience upload needs (e.g. "var1", "var2"). Use this before building a CSV audience for the Create a campaign endpoint, so the file has exactly the right columns.

Requires scope templates:read

Path parameters

idstringrequired

Template ID.

Template ID.

Response
{
  "success": true,
  "data": {
    "templateId": "...",
    "templateName": "order_confirmation",
    "variables": [{ "placeholder": "{{1}}", "column": "var1", "component": "BODY" }],
    "requiredCsvColumns": ["phone", "var1"]
  }
}

Campaigns

Create and send bulk template campaigns, including CSV-uploaded audiences with per-recipient template variables.

GET/api/v1/campaigns

List campaigns

Returns a paginated list of campaigns, optionally filtered by status.

Query parameters

statusstring

Filter by status (draft, scheduled, running, completed, paused, failed).

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20.

Filter by status (draft, scheduled, running, completed, paused, failed).

Page number, defaults to 1.

Results per page, defaults to 20.

Response
{
  "success": true,
  "data": [{ "id": "...", "name": "Summer Sale", "status": "completed", "sentCount": 120 }],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
GET/api/v1/campaigns/:id

Get a campaign

Returns a single campaign, including its template and phone number.

Path parameters

idstringrequired

Campaign ID.

Campaign ID.

Response
{
  "success": true,
  "data": { "id": "...", "name": "Summer Sale", "status": "draft", "audienceType": "csv", "contactCount": 3 }
}
POST/api/v1/campaigns

Create a campaign

Creates a draft campaign targeting either all opted-in contacts, a specific audience_type, or a CSV-style uploaded list of recipients with per-recipient template variables. For a "csv" audience, first call Get a template's required variables to find out which variables the template needs, then supply one entry in recipients per row with a "phone" and a variables map keyed by placeholder number (e.g. { "1": "Kundan", "2": "ORD-42" } for a template using {{1}} and {{2}}). Every recipient must supply a value for every variable the template requires, or the request is rejected.

Requires scope campaigns:write

Body parameters

namestringrequired

Campaign name.

template_idstringrequired

Approved template to send.

phone_number_idstringrequired

Phone number to send from.

audience_typestringrequired

One of "all" (all opted-in contacts), "segment", "custom" (contact_ids), or "csv" (recipients).

segment_idstring

Required when audience_type is "segment".

contact_idsarray

Required when audience_type is "custom".

recipientsarray

Required when audience_type is "csv". Array of { phone, variables }, where variables is an object keyed by placeholder number as a string.

csv_filenamestring

Original CSV filename, stored for reference on the campaign.

Campaign name.

Approved template to send.

Phone number to send from.

One of "all" (all opted-in contacts), "segment", "custom" (contact_ids), or "csv" (recipients).

Required when audience_type is "segment".

Required when audience_type is "custom".

Required when audience_type is "csv". Array of { phone, variables }, where variables is an object keyed by placeholder number as a string.

Original CSV filename, stored for reference on the campaign.

Response
{
  "success": true,
  "data": { "id": "...", "name": "Summer Sale Announcement", "status": "draft", "audienceType": "csv", "contactCount": 2 }
}
POST/api/v1/campaigns/:id/send

Send a campaign

Starts sending a draft or scheduled campaign to its full audience. For a "csv" audience, each recipient's message is personalized with the variable values supplied at creation time.

Requires scope campaigns:write

Path parameters

idstringrequired

Campaign ID.

Campaign ID.

Response
{
  "success": true,
  "message": "Campaign started"
}
GET/api/v1/campaigns/:id/analytics

Get campaign analytics

Returns send/delivery/read counts and rates for a campaign.

Path parameters

idstringrequired

Campaign ID.

Campaign ID.

Response
{
  "success": true,
  "data": { "sent": 2, "delivered": 2, "read": 1, "failed": 0, "deliveryRate": "100.00", "readRate": "50.00" }
}
DELETE/api/v1/campaigns/:id

Delete a campaign

Permanently deletes a campaign and its recipient/message records.

Requires scope campaigns:write

Path parameters

idstringrequired

Campaign ID.

Campaign ID.

Response
{
  "success": true,
  "message": "Campaign deleted"
}

Calls

WhatsApp Calling (VoIP) — a separate Meta approval from base messaging. Requires a Cloud-API-native phone number, calling explicitly enabled on that number, and either 2,000+ daily unique message recipients or Tech Partner sandbox access. Outbound/inbound audio uses WebRTC (SDP offer/answer relayed through these endpoints); Seen does not provide a media relay of its own.

GET/api/v1/calls

List calls

Returns a paginated list of inbound and outbound calls, optionally filtered by status.

Query parameters

statusstring

Filter by status (ringing, in_progress, completed, rejected, no_answer, failed).

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20.

Filter by status (ringing, in_progress, completed, rejected, no_answer, failed).

Page number, defaults to 1.

Results per page, defaults to 20.

Response
{
  "success": true,
  "data": [{ "id": "...", "direction": "inbound", "status": "completed", "durationSeconds": 42 }],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
GET/api/v1/calls/:id

Get a call

Returns a single call.

Path parameters

idstringrequired

Call ID.

Call ID.

Response
{
  "success": true,
  "data": { "id": "...", "direction": "inbound", "status": "completed", "durationSeconds": 42 }
}
POST/api/v1/calls

Place an outbound call

Places an outbound WhatsApp call, relaying your WebRTC SDP offer through Meta. Meta assigns and returns a call ID, which is then used to accept/reject/terminate this call.

Body parameters

phone_number_idstringrequired

Your connected phone number ID to call from.

tostringrequired

Recipient WhatsApp number in E.164 format.

sdp_offerobjectrequired

WebRTC SDP offer: { type: "offer", sdp: "..." }.

Your connected phone number ID to call from.

Recipient WhatsApp number in E.164 format.

WebRTC SDP offer: { type: "offer", sdp: "..." }.

Response
{
  "success": true,
  "data": { "id": "...", "direction": "outbound", "status": "ringing" }
}
POST/api/v1/calls/:id/accept

Accept an inbound call

Answers a ringing inbound call by relaying your WebRTC SDP answer through Meta.

Path parameters

idstringrequired

Call ID.

Body parameters

sdp_answerobjectrequired

WebRTC SDP answer: { type: "answer", sdp: "..." }.

Call ID.

WebRTC SDP answer: { type: "answer", sdp: "..." }.

Response
{
  "success": true,
  "data": { "id": "...", "status": "in_progress" }
}
POST/api/v1/calls/:id/reject

Reject an inbound call

Declines a ringing inbound call before it connects.

Path parameters

idstringrequired

Call ID.

Call ID.

Response
{
  "success": true,
  "data": { "id": "...", "status": "rejected" }
}
POST/api/v1/calls/:id/terminate

End a call

Ends an in-progress call, or cancels one that is still ringing.

Path parameters

idstringrequired

Call ID.

Call ID.

Response
{
  "success": true,
  "data": { "id": "...", "status": "completed", "durationSeconds": 42 }
}

Media

Upload and manage media files for use in messages and templates.

GET/api/v1/media

List media files

Returns a paginated list of uploaded media files.

Query parameters

pagenumber

Page number, defaults to 1.

limitnumber

Results per page, defaults to 20.

Page number, defaults to 1.

Results per page, defaults to 20.

Response
{
  "success": true,
  "data": [{ "id": "...", "originalName": "logo.png", "publicUrl": "https://..." }]
}
POST/api/v1/media/upload

Upload a media file

Uploads a file (as base64) and, if a WABA is connected, also uploads it to WhatsApp so it can be referenced in outbound media messages.

Body parameters

filestringrequired

Base64-encoded file content.

file_typestring

MIME type of the file.

file_namestring

Original file name.

Base64-encoded file content.

MIME type of the file.

Original file name.

Response
{
  "success": true,
  "data": { "id": "...", "publicUrl": "https://...", "whatsappMediaId": "..." }
}
DELETE/api/v1/media/:id

Delete a media file

Permanently deletes an uploaded media file.

Path parameters

idstringrequired

Media file ID.

Media file ID.

Response
{
  "success": true,
  "message": "Media file deleted"
}

Webhooks

Register a URL to receive real-time events (inbound messages, delivery status) instead of polling.

GET/api/v1/webhooks

List webhooks

Returns every webhook registered for your account. The signing secret is not included — it is only ever returned once, at creation.

Response
{
  "success": true,
  "data": [{ "id": "...", "url": "https://example.com/webhooks/seen", "events": ["message.received"], "is_active": true }]
}
POST/api/v1/webhooks

Register a webhook

Registers a URL to receive events. The response is the only time the signing secret is returned — store it, since it is needed to verify the X-Webhook-Signature header on every delivery. Owner/admin only.

Body parameters

urlstringrequired

HTTPS URL that will receive event POSTs.

eventsarrayrequired

Event types to subscribe to: message.received, message.sent, message.delivered, message.read, message.failed.

HTTPS URL that will receive event POSTs.

Event types to subscribe to: message.received, message.sent, message.delivered, message.read, message.failed.

Response
{
  "success": true,
  "data": { "id": "...", "url": "https://example.com/webhooks/seen", "events": ["message.received"], "is_active": true, "secret": "b6b6c1b0-..." }
}
PATCH/api/v1/webhooks/:id

Update a webhook

Updates a webhook's URL, subscribed events, or active status. This is the way to point an existing webhook at a new URL (e.g. a new tunnel/domain) without losing its signing secret — delete-and-recreate would issue a new secret instead. Owner/admin only.

Path parameters

idstringrequired

Webhook ID.

Body parameters

urlstring

New HTTPS URL for this webhook.

eventsarray

Replaces the full set of subscribed event types.

is_activeboolean

Pause (false) or resume (true) deliveries without deleting the webhook.

Webhook ID.

New HTTPS URL for this webhook.

Replaces the full set of subscribed event types.

Pause (false) or resume (true) deliveries without deleting the webhook.

Response
{
  "success": true,
  "data": { "id": "...", "url": "https://new-tunnel.example.com/webhooks/seen", "events": ["message.received"], "is_active": true }
}
DELETE/api/v1/webhooks/:id

Delete a webhook

Permanently deletes a webhook registration. Owner/admin only.

Path parameters

idstringrequired

Webhook ID.

Webhook ID.

Response
{
  "success": true,
  "message": "Webhook deleted"
}
POST/api/v1/webhooks/:id/test

Send a test event

Queues a synthetic message.received event to this webhook, so you can verify your endpoint and signature verification without waiting for a real message.

Path parameters

idstringrequired

Webhook ID.

Webhook ID.

Response
{
  "success": true,
  "message": "Test event queued"
}

Integrations

Helpers for third-party products (like a helpdesk) that let their users connect their own Seen account.

POST/api/v1/integrations/validate-key

Validate an API key + phone number pair

Lets a third-party product confirm that a phone number and API key a user pasted into its own UI are genuine and belong to the same tenant, before storing them. Unauthenticated — the API key is passed in the body, not the x-api-key header, since the caller is verifying credentials on someone else's behalf.

Body parameters

apiKeystringrequired

The API key to validate.

phoneNumberstringrequired

The phone number to check against the key's tenant. Matches on either the Meta phone_number_id or the display number.

The API key to validate.

The phone number to check against the key's tenant. Matches on either the Meta phone_number_id or the display number.

Response
{
  "success": true,
  "data": { "valid": true, "phoneNumberId": "1143523638846055", "displayNumber": "+1 555 123 4567", "status": "connected" }
}