Command Palette

Search for a command to run...

IntelliPortalDOCS

Start here

  • Overview
  • Quickstart
  • Test in the sandbox

Build

  • Authentication
  • Clients
  • Hosted onboarding
  • Webhooks

API reference

  • Messages
  • Templates
  • Usage
  • Status & observability

Reliability

  • WhatsApp limits
  • Errors & debugging

Resources

  • Changelog
  • OpenAPI 3.1
  • Postman collection
Developer support
IntelliPortalDOCS
Developer documentation/API v1

© 2026 Intelli Holdings Inc.

PrivacyTermsEmail support

Need a hand?

Share a request ID when you contact us so we can trace the call quickly.

Open developer support
  1. Docs
  2. Messages

API reference

messages:send

Messages

Send through a connected client using a Meta-compatible payload. IntelliPortal resolves the channel from client_ref and normalizes authentication, errors, request logging, and test mode.

POST/messages/send

Send a free-form message

Send from your backend with a key that has messages:send. The property named by type is forwarded in Meta's shape.

cURL
curl -X POST https://api.intelliconcierge.dev/v1/messages/send \
  -H "Authorization: Bearer $INTELLI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_ref": "customer_42",
    "to": "+254700000000",
    "type": "text",
    "text": { "body": "Your order is ready." }
  }'
PropertyTypeDescription
client_refrequiredstringYour stable identifier for the connected client and channel used to send.
torequiredstringWhatsApp recipient phone number or Instagram-scoped user ID from a webhook.
typerequiredenumSelects the matching payload object: text, image, video, audio, document, sticker, location, contacts, interactive, or reaction.
<type>requiredobjectThe payload whose property name matches type. Its shape mirrors Meta's Cloud API.
dry_runbooleanValidate without delivery. With an ik_test_ key, sends are simulated unless `to` is one of your registered test recipients — those deliver for real.

WhatsApp message types

Media can use a public HTTPS link or an uploaded Meta media id. Free-form sends require an open 24-hour customer-service window.

typePayload essentials
textbody; optional preview_url
imagelink or id; optional caption
videolink or id; optional caption
audiolink or id
documentlink or id; optional caption and filename
stickerlink or id
locationlatitude, longitude; optional name and address
contactsArray of Meta contact objects
interactivebutton, list, cta_url, and other Meta interactive shapes
reactionmessage_id and emoji; an empty emoji removes the reaction
Request body examples
{
  "client_ref": "customer_42",
  "to": "+254700000000",
  "type": "text",
  "text": {
    "body": "Your order is ready.",
    "preview_url": false
  }
}
POST/messages/send-template

Send an approved template

Templates are WhatsApp-only and can open a conversation outside the 24-hour service window. Reference a template you registered through the Templates API by template_ref and pass plain values — IntelliPortal resolves the template, checks that Meta has approved it, and builds the components for you. Only templates with status APPROVED are sendable; anything else returns template_not_approved instead of an opaque Meta error.

Prefer full control? Send a raw Meta template object instead — name, language, and components exactly as Meta's Cloud API takes them. Use one form or the other, not both.

cURL
curl -X POST https://api.intelliconcierge.dev/v1/messages/send-template \
  -H "Authorization: Bearer $INTELLI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_ref": "customer_42",
    "to": "+254700000000",
    "template_ref": "order_ready",
    "parameters": ["1842", "$12.50"]
  }'
PropertyTypeDescription
client_refrequiredstringYour stable identifier for the connected client whose WhatsApp Business Account holds the template.
torequiredstringWhatsApp recipient phone number.
template_refrequiredstringTemplate id from the Templates API, or the template name. Add language when the same name exists in several languages.
languagestringLanguage code (e.g. en_US) to disambiguate template_ref by name.
parametersarray | objectBody variable values. An array fills {{1}}, {{2}}… in order; an object matches named variables ({"name": "Ada"}) or numbered keys ({"1": "1842"}).
headerobjectHeader variable, when the template has one: {"text": ...} for a text header, {"link": ...} or {"id": ...} for image / video / document (optional "filename" for documents), {"location": {...}} for location headers.
buttonsarrayValues for dynamic URL buttons, in button order — one string per {{1}} placeholder in a button URL.
cardsarrayCarousel templates only: one object per card, in card order — {"header": {"link" | "id": ...}, "parameters": [...], "buttons": [...]}. Every card needs its header media at send time; quick replies take the payload your webhook receives on tap, dynamic URLs take their suffix.
dry_runbooleanValidate without delivery. With an ik_test_ key, sends are simulated unless `to` is a registered test recipient. The dry-run response echoes the assembled template object.
Request body examples
{
  "client_ref": "customer_42",
  "to": "+254700000000",
  "template_ref": "order_ready",
  "parameters": ["1842", "$12.50"]
}

Template send errors

These come back as 400 with a code your integration can branch on:
template_not_approvedThe template exists but Meta hasn't approved it (or rejected it — the reason is included).
template_not_foundNo template with that id or name on this client's WhatsApp Business Account.
ambiguous_templateThe name exists in several languages — pass language or use the template id.
missing_parametersThe template body has variables you didn't supply; the message lists them.
parameter_count_mismatchA positional parameters array has the wrong length.
missing_headerThe template has a dynamic header and header wasn't supplied.
missing_button_parametersThe template has dynamic URL buttons and buttons wasn't supplied.
empty_parameterA supplied body, header, button or card value is blank — Meta rejects empty parameters.
invalid_header_linkheader.link isn't a public http(s) URL Meta could fetch.
missing_cardsThe template is a carousel and cards wasn't supplied, or its length doesn't match the card count.
missing_card_buttonsA card's buttons list doesn't cover its quick replies and dynamic URL buttons; the error names the card.
template_layout_unsupportedAuthentication templates and copy-code buttons can't be assembled from template_ref yet — send them as a raw template object.
POST/media

Upload send media

Trade bytes for a media id you can reference from sends — header.id on a template, image.id and friends on a free-form message. Meta keeps the upload for 30 days, so one upload serves many sends.

Prefer an id over link in production: a linked URL is re-fetched by Meta at every delivery, after the API has already accepted the message — if the fetch fails, the message silently dies with a failed status on your webhook. WhatsApp-only: Instagram takes media as a public URL on the send itself.

cURL
curl -X POST https://api.intelliconcierge.dev/v1/media \
  -H "Authorization: Bearer $INTELLI_API_KEY" \
  -F client_ref=customer_42 \
  -F file=@menu.jpg
JSON · response
{
  "success": true,
  "media_id": "1013859600285441",
  "kind": "image",
  "expires_in_days": 30
}

Accepted uploads: image jpeg/png (≤5 MB), video mp4/3gpp (≤16 MB), audio aac/mp4/mpeg/amr/ogg (≤16 MB), documents pdf, Office formats and txt (≤100 MB), sticker webp (≤500 KB). Anything else returns media_type_unsupported or media_too_large before reaching Meta.

Channel routing

Do not send a separate channel parameter. IntelliPortal looks up the client and routes through its connected channel.

Instagram takes text and media

For an Instagram client, set to to the Instagram-scoped sender ID from a webhook. Text is limited to 1,000 characters and must be sent inside Instagram's reply window.

image, video, audio and document are supported and must carry link — Instagram fetches media from a public HTTPS URL and cannot use an uploaded media id, which returns media_link_required. A caption or filename has nowhere to go on an Instagram attachment, so the send succeeds and reports it under warnings; send the caption as its own text message if you need it.

sticker, location, contacts, interactive and reaction have no Instagram equivalent and return channel_type_unsupported.

Response

Store message_id to correlate later delivery status webhooks. Also capture the X-Request-Id response header for operational tracing.

JSON · live
{
  "success": true,
  "message_id": "wamid.xxx"
}
JSON · test
{
  "success": true,
  "message_id": "wamid.TEST_xxx",
  "dry_run": true
}

A warnings array appears when the message went out but the channel could not carry part of what you sent — a caption on an Instagram attachment, for instance. The send still succeeded; nothing needs retrying. Log these, because they mean content you supplied never reached the recipient.

JSON · sent with a dropped field
{
  "success": true,
  "message_id": "mid.xxx",
  "warnings": [
    "caption is not supported on Instagram attachments and was not sent — send it as a separate text message if you need it."
  ]
}

Delivery rules to handle

  • Use free-form WhatsApp messages only inside the 24-hour customer-service window; otherwise send an approved template.
  • Serve linked media over public HTTPS with a correct content type and without expiring before Meta downloads it.
  • Honor Retry-After on 429. Do not immediately repeat requests rejected by Meta without correcting the cause.
  • A 402 trial_expired blocks live delivery after trial grace; dry-runs remain available for debugging.
Review errors & debugging
Building with a coding assistant?

Copy the integration contract into your assistant, then let it tailor the implementation to your stack.

You are helping me build outbound messaging into my product using the Intelli Partner API (base URL: https://api.intelliconcierge.dev/v1). The WhatsApp/Instagram accounts I send from are already connected through Intelli; every request carries my Intelli API key and a client_ref naming the connected account.

Before writing code, ask me:
1. My backend framework/language.
2. Which channels I use (WhatsApp, Instagram or both) and whether I send templates, free-form messages, or both.

Environment variables (server-side only — the API key must NEVER reach the browser):
- INTELLI_API_KEY: ik_test_... keys simulate every send except to my registered test recipients (my own phone / Instagram username), which receive the real message; ik_live_... delivers for real. Auth header: Authorization: Bearer $INTELLI_API_KEY (scope messages:send).

A) Free-form messages — POST /messages/send
- Body: {"client_ref": "...", "to": "...", "type": "...", "<type>": {...}} mirroring Meta's Cloud API shape. WhatsApp types: text, image, video, audio, document, sticker, location, contacts, interactive, reaction. Media takes link (public HTTPS) or id (uploaded — see C).
- WhatsApp free-form only works inside the 24-hour customer-service window; outside it, send an approved template.
- Instagram: to is the Instagram-scoped user ID from a webhook; text (max 1000 chars) plus image/video/audio/document by link only. caption/filename can't be carried — the send succeeds and reports them under warnings.
- Never send a channel parameter: Intelli routes by client_ref.

B) Template messages — POST /messages/send-template
- Preferred shape: {"client_ref", "to", "template_ref": "<template id or name>", "language": "en_US" (only to disambiguate a name), "parameters": [...] or {...}, "header": {...}, "buttons": [...]}.
- parameters fills body variables: an array for positional {{1}}, {{2}}…, an object for named variables. header only when the template has a dynamic header: {"text": ...} or {"link"|"id": ...} (+ "filename" for documents). buttons: values for dynamic URL buttons, in order.
- Carousel templates add "cards": one object per card, in card order — {"header": {"link"|"id": ...}, "parameters": [...], "buttons": [...]}. Every card needs its header media at send time; quick replies take the payload my webhook receives on tap, dynamic URLs their suffix.
- Intelli resolves the template and refuses anything not APPROVED (400 template_not_approved, with Meta's rejection reason when there is one) and any missing or blank value (missing_parameters, parameter_count_mismatch, missing_header, missing_button_parameters, missing_cards, missing_card_buttons, empty_parameter, invalid_header_link — each error names exactly what's missing). Gate my own UI on the template being APPROVED.
- Full-control alternative: a raw Meta "template" object instead of template_ref (exactly one of the two). Authentication templates and copy-code buttons currently require the raw form (template_layout_unsupported otherwise).

C) Send media — POST /media (multipart/form-data: client_ref, file)
- Returns {"media_id", "kind", "expires_in_days": 30}. Reference it as header.id on template sends or image.id / video.id / … on free-form sends.
- Prefer an id over a link in production: Meta re-fetches a link at EVERY delivery, after the API has already returned 200 — an unreachable link kills the message asynchronously (a failed status on my webhook), which looks like silent non-delivery.
- Limits are enforced with clear errors: image jpeg/png ≤5 MB, video mp4/3gpp ≤16 MB, audio ≤16 MB, document pdf/Office/txt ≤100 MB, sticker webp ≤500 KB. WhatsApp-only — Instagram media travels by URL on the send.

D) Test mode
- dry_run: true validates everything but never delivers; the response carries dry_run: true and, on template_ref sends, the fully assembled template object so I can inspect what a live send would carry.
- With an ik_test_ key, only my registered test recipients receive real messages; any other recipient gets a simulated response with a hint. I register recipients in the Intelli portal (Sandbox → Test recipients — one WhatsApp number and one Instagram username).

E) Responses and errors
- Success: {"success": true, "message_id": "..."} — store message_id to correlate message.status webhooks (sent / delivered / read / failed). Delivery is asynchronous: 200 means Meta ACCEPTED the message, not that it arrived.
- A warnings array means the message went out but a field couldn't be carried — log it.
- Failures use one envelope with a stable code to branch on. 422 meta_rejected carries Meta's own code/message and sometimes a hint. Honor Retry-After on 429. 402 trial_expired blocks live sends after the trial; dry-run keeps working.

Write idiomatic, production-quality code for my stack, retrying only where the rules above allow it.
PreviousWebhooks
NextTemplates