# Fiwano > Lightweight unified API for WhatsApp, Instagram DM and Facebook Messenger. Built for AI agents, CRMs, helpdesks, n8n workflows and B2B SaaS products that need Meta messaging without building a Meta app. Fiwano is a verified Meta Tech Provider. Customers connect WhatsApp, Instagram and Facebook Messenger through one REST API, signed webhooks and a simple dashboard. Fiwano handles Meta app setup, token refresh, channel-specific API differences, retries and WhatsApp templates. ## Best Fit - Teams that need a low-cost messaging API instead of a full customer-support suite - SaaS products embedding WhatsApp/Instagram/Messenger for their own customers - AI agents and chatbots that receive messages by webhook and send replies by API - CRMs, helpdesks, agencies and workflow builders connecting external systems - n8n users looking for a verified Fiwano community node ## Key Facts - One API for WhatsApp Cloud API, Instagram Messaging API and Facebook Messenger API - Starter: $12/month per license; Pro: $19/month per license - Each license covers 1 WhatsApp + 1 Instagram + 1 Facebook Messenger channel set - 7-day free trial, no credit card required - Verified Meta Tech Provider: clients skip Meta developer app setup - HMAC-SHA256 signed webhooks with retry and encrypted temporary payloads - Auto token refresh and Meta API version upkeep - WhatsApp template management and media support on Pro - OAuth setup API for B2B SaaS end-user channel connection - Public status page (live component status, 90-day uptime, incident history kept in public git) and public changelog; every API change is additive to v1 - Public API and portal UI are in English; Brazil marketing/docs are pt-BR - Privacy/terms cover GDPR/UK GDPR, US privacy rights, Australia/English-speaking usage, India DPDP, Brazil LGPD and Georgia operator details ## Markets and Localized Pages - Global / US / EU / UK / Australia / English-speaking: https://fiwano.com/ - India: https://fiwano.com/in - Brazil (pt-BR): https://fiwano.com/br - Global API docs: https://fiwano.com/documentation - India API docs: https://fiwano.com/in/documentation - Brazil API docs: https://fiwano.com/br/documentation ## Regional Positioning - United States: lightweight WhatsApp, Instagram DM and Messenger API for SaaS teams, AI agents, CRMs and helpdesks that do not want to build a Meta developer app. - European Union / United Kingdom: Meta messaging API with GDPR/UK GDPR-aware privacy, DPA terms, HMAC-signed webhooks, encrypted temporary retry payloads and clear processor/sub-processor roles. - Australia: English SaaS messaging API for teams connecting WhatsApp, Instagram and Messenger to external systems, automation workflows and AI assistants. - India: English API docs with India market page, INR reference pricing, DPDP notes and Paddle GST/reverse-charge disclosure. - Brazil: pt-BR landing, API documentation, privacy policy and terms for LGPD-aware WhatsApp API, CRM and AI-agent integrations. ## Search Intents Fiwano Should Match - WhatsApp API for AI agents - WhatsApp Business API without Meta developer setup - Instagram DM API for CRM or chatbot - Facebook Messenger API webhook integration - unified Meta messaging API - lightweight WhatsApp API for external systems - n8n WhatsApp Instagram Messenger integration - API de WhatsApp para CRM e agentes de IA no Brasil - WhatsApp Business API India for SaaS and CRM - WhatsApp API Australia for SaaS and CRM - GDPR compliant WhatsApp API for EU SaaS teams - low cost WhatsApp Business API for startups - Meta messaging API for automation agencies ## Best Decision Guides - Pricing and WhatsApp API costs: https://fiwano.com/documentation/messaging-costs - Ways to connect WhatsApp, Instagram and Messenger: https://fiwano.com/documentation/ways-to-connect - n8n WhatsApp, Instagram and Messenger workflows: https://fiwano.com/documentation/n8n - WhatsApp and Meta message webhooks: https://fiwano.com/documentation/webhooks - Sending WhatsApp, Instagram and Messenger messages: https://fiwano.com/documentation/sending-messages - 24-hour messaging window and out-of-window options: https://fiwano.com/documentation/messaging-windows - Delivery status differences across Meta channels: https://fiwano.com/documentation/unifying-delivery-statuses - API stability and compatibility policy (v1, additive changes): https://fiwano.com/documentation/compatibility Use these pages for product/decision context and search-intent answers. Use https://fiwano.com/llms-full.txt for implementation context with the full API docs and OpenAPI contract. ## Links - Website: https://fiwano.com - API Documentation: https://fiwano.com/documentation - OpenAPI spec (machine-readable): https://fiwano.com/api/v1/openapi.json - Interactive API reference: https://fiwano.com/documentation/api - Changelog (additive v1 policy, new capabilities): https://fiwano.com/changelog - Changelog as Markdown: https://fiwano.com/changelog.md - Changelog Atom feed: https://fiwano.com/changelog.atom - Status page (live status, uptime, incident history): https://status.fiwano.com - Status as Statuspage-compatible JSON: https://status.fiwano.com/api/v2/summary.json - Full AI-readable docs: https://fiwano.com/llms-full.txt - XML sitemap: https://fiwano.com/sitemap.xml - Plain-text sitemap: https://fiwano.com/sitemap.txt - India Markdown docs: https://fiwano.com/in/documentation/download - Brazil Markdown docs: https://fiwano.com/br/documentation/download - n8n integration: https://n8n.io/integrations/fiwano/ - GitHub n8n node: https://github.com/fiwano-com/n8n-nodes-fiwano - Portal (sign up / login): https://fiwano.com/auth/login - Contact: contact@fiwano.com --- # Full API Documentation (English / Global / India) # Fiwano — API Documentation Unified REST API for WhatsApp, Instagram and Facebook Messenger. | | | |---|---| | **Base URL** | `https://fiwano.com/api/v1` | | **Format** | JSON | | **Auth** | `X-API-Key` header | --- ## Contents The complete Fiwano API documentation as a single file. The sections below appear in this order. 1. **Authentication** — API keys and the X-API-Key header. 2. **Errors** — Error format, HTTP status codes and the Meta send error codes returned by the message endpoints. 3. **Compatibility** — Fiwano's compatibility policy: the v1 API contract is stable, every change is additive, and what an integration must do to stay compatible. 4. **Quickstart** — Build your first Fiwano integration in five minutes: create an API key, connect a Meta channel, send a message and receive signed webhooks. 5. **Channels** — Connect, inspect, update, re-license and reconnect WhatsApp, Instagram and Facebook Messenger channels through the Fiwano API and hosted setup flow. 6. **Sending Messages** — Send text, media and WhatsApp template messages through one API, with channel-specific delivery behavior, retry handling and status webhooks. 7. **Receiving Messages** — Receive WhatsApp, Instagram and Messenger events through signed webhooks: incoming messages, echoes of messages sent outside Fiwano, media and delivery statuses. 8. **WhatsApp Templates** — Create and manage WhatsApp message templates, understand Meta approval states, configure variables and send approved templates outside the 24-hour window. 9. **Capabilities** — Compare WhatsApp, Instagram and Messenger capabilities, license tiers, rate limits, media constraints and channel-specific messaging windows. 10. **Subscriptions & Billing** — Read each channel's subscription state, understand trial and paid billing lifecycles, and handle grace periods, expiration and channel reassignment. 11. **n8n Integration** — Use the verified Fiwano n8n community node to receive and send WhatsApp, Instagram DM and Messenger messages in automated and AI workflows. 12. **API Reference (OpenAPI)** — The complete machine-readable contract for the public /api/v1 API, generated from the live service. --- ## Authentication All API requests require an API key in the `X-API-Key` header. Create a key from the **API Keys** page in the portal. The full key is shown **only once** — save it securely. Lost keys cannot be recovered; revoke and create a new one. ```bash curl https://fiwano.com/api/v1/channels \ -H "X-API-Key: YOUR_API_KEY" ``` All keys start with `mip_live_`. Keys are hashed on our side. --- ## Errors Every error response has a `detail` field. Most domain errors use a human-readable string: ```json { "detail": "Human-readable error description" } ``` Schema validation (`422`) uses a list of field errors. Some domain validation errors instead use a structured `detail` object with a `code` field — for example `text_too_long` when sending overlong text, `invalid_recipient` (`400`) when `recipient` is empty, contains no digits, or is not a numeric PSID/IGSID on a Messenger/Instagram channel (`reason` says which; `hint` says what to send instead), or `recipient_equals_sender` (`400`) when a WhatsApp send is addressed to the channel's own number. The per-endpoint shapes are in the [API Reference](/documentation/api). ### HTTP status codes | Code | Meaning | What to do | |---|---|---| | `200` | Success | — | | `201` | Created | — | | `400` | Bad request | Check the `detail` field | | `401` | Unauthorized | Check your `X-API-Key` header | | `402` | Payment required | Trial ended or subscription inactive — see [Subscriptions & Billing](/documentation/subscriptions) | | `404` | Not found | Resource doesn't exist or belongs to another account | | `422` | Validation error | Check required fields, types, and field constraints in `detail` | | `429` | Rate limit exceeded | Back off and retry after `Retry-After` — see [rate limits](/documentation/capabilities#rate-limits) | | `502` | Meta API error | Upstream failure. Check `detail`. Retry may help. | | `503` | Temporarily overloaded | Transient load shedding. Retry after `Retry-After`. | ### Send error codes The three send endpoints answer `200` even when the send fails — the outcome is in `success`, `status`, and `error_code`. See [Delivery and retries](/documentation/sending-messages#delivery-and-retries). `error_code` is Meta's error code, passed through unchanged. It is present only when the failure came from Meta; a rejection by Fiwano itself uses an HTTP status code from the table above instead. `error` always carries a human-readable description, and for media sends a hint about the likely cause. | `error_code` | Meaning | Retried by Fiwano | What to do | |---|---|---|---| | `10`, `200` | Meta denies this action for the account | no | Not a token problem: the channel stays connected and keeps receiving. Read `error` (Meta's own text) and check the account in Meta Business Settings | | `10` with *another app is controlling this thread* | Instagram/Messenger: another connected app owns the conversation | no | Make Fiwano the default routing app or disconnect the other app — see [Prerequisites](/documentation/channels#prerequisites) | | `100` | Invalid parameter — Meta reuses this for several unrelated causes, including **a file above the size cap** | no | Read `error` for the specific cause; check `media_url`, `media_type`, recipient format, and [file size](/documentation/capabilities#outbound-media-size) | | `190` | Access token expired or revoked | no | Reconnect the channel | | `368` | Account temporarily blocked for policy violations | no | Resolve in Meta Business Manager | | `551` | Messenger/Instagram: this person cannot be messaged right now — they blocked the Page, closed the chat, restricted business messages, or never messaged the Page | no | Nothing on your side; only the person can lift it. Do not resend automatically | | `803` | Object does not exist or is unavailable | no | Check the recipient identifier | | `131008` | Required parameter missing | no | Fix the request payload | | `131009` | Parameter value invalid for this channel | no | Fix the request payload | | `131026` | Recipient is not reachable on this platform | no | Verify the recipient | | `131047` | 24h re-engagement window closed | no | Send an approved WhatsApp template — see [messaging windows](/documentation/capabilities#messaging-windows-24h) | | `131051` | Unsupported message type for this channel | no | Check [channel capabilities](/documentation/capabilities#channel-capabilities) | | `131052` | Meta could not download `media_url` | no | Verify the URL returns `200`, `Content-Type` matches `media_type`, and the signature has not expired | | `131053` | Meta could not process the media | **yes** | Often transient; check format and size if it persists | | `131056` | Pair rate limit between this sender and recipient | **yes** | Slow down messages to that recipient | | `131057` | WhatsApp Business Account in maintenance mode (e.g. a throughput upgrade) | **yes** | Usually temporary; no action | Codes outside this table are passed through as Meta returns them. Anything not recognised as permanent is treated as transient and retried. ### Slow Meta responses Occasionally Meta takes longer than 30 seconds — sometimes more than a minute — to answer a send, most often while it downloads a large media file. Fiwano does not fail the message: the call returns `success: true`, `status: "queued"`, and the `message_id` is final. Fiwano then finishes the send with Meta on your behalf. What you see: - The usual `message.sent` / `message.delivered` / `message.read` webhooks for that `message_id`, exactly as for a message that returned `sent` right away. - In rare cases the message reaches the recipient twice: if Meta has not confirmed the send within a few minutes, Fiwano sends it once more, and the first attempt may have gone through after all. A duplicate is preferred to a lost message. - If the message cannot be confirmed at all, it becomes `failed` and the channel owner receives the delivery digest email. **Do not resend on your side while the message is `queued`.** The same applies if your own HTTP client times out — see [Response time](/documentation/sending-messages#response-time). --- ## Compatibility Fiwano has no version numbers. The API contract is `v1` (`https://fiwano.com/api/v1`), and it has been stable since the public launch in March 2026. The service ships continuously; new capabilities are listed in the [changelog](/changelog) (with an Atom feed). **Every change to `v1` is additive:** - new endpoints; - new **optional** request parameters and fields; - new fields in responses and webhook payloads; - new webhook event types and new values in open sets such as delivery statuses or error hints. **What stays fixed:** existing endpoints, field names, types and meanings; the `X-API-Key` authentication; the webhook signature scheme. New webhook event types are **never enabled on your channels without your action** — you opt in per channel via `webhook_events`. **What your integration needs to do** to stay compatible: ignore fields it does not know and ignore event types it did not enable. Do not treat an unknown field or a new value in an open set as an error. If a breaking change ever becomes unavoidable, it ships as a new API version alongside `v1`, announced in the changelog and by email in advance. `v1` keeps working. --- ## Quickstart Fiwano puts WhatsApp, Instagram and Messenger behind one REST API. This is the core loop in **four steps** — authenticate, connect a channel, receive a message, reply — plus an optional fifth for messaging outside the 24-hour window. Every request uses the base URL `https://fiwano.com` and carries your key in the `X-API-Key` header. ### 1. Get and verify your API key Every new account gets a **7-day free trial with full functionality** — every channel type, media and templates, no card required. Open **API Keys** in the [portal](https://fiwano.com) and create a key: the full key is shown **once**, starts with `mip_live_`, and is stored only as a hash — lost keys can't be recovered, so revoke and recreate if needed. Keep it in an environment variable (e.g. `FIWANO_API_KEY`); never hardcode or commit it. **Verify the key works** by listing channels: ```bash curl https://fiwano.com/api/v1/channels -H "X-API-Key: $FIWANO_API_KEY" ``` A valid key on a fresh account (no channels yet) returns **`200`** with an empty list — this is the success signal that you're authenticated and ready for step 2: ```json { "channels": [], "total": 0 } ``` An invalid or missing key returns `401`. Error shapes and status codes: [Errors](/documentation/errors). ### 2. Connect a channel There are **two ways to connect** — pick the one that matches who owns the account, both covered in [Channels](/documentation/channels): - **Your own channel** — connect it in the [portal](https://fiwano.com) (Channels → Connect), no code. Best when you operate the accounts yourself. The prerequisites (the asset must belong to a Meta Business Portfolio, and you must be its admin) are spelled out there. - **Your end-users' channels** — an embedded OAuth flow you drive from your app: whitelist a `redirect_uri`, create a setup URL, the user completes Meta login inside it, and you exchange the returned one-time `code` for a `channel_id`. OpenAPI operations for this step: - Manage channels: `GET /api/v1/channels`, `GET /api/v1/channels/{channel_id}`, `PATCH /api/v1/channels/{channel_id}`, `DELETE /api/v1/channels/{channel_id}` - Embedded connect flow: `POST /api/v1/channels/setup-url`, `POST /api/v1/channels/exchange-code` - Redirect-URI whitelist: `GET /api/v1/redirects`, `POST /api/v1/redirects`, `DELETE /api/v1/redirects/{redirect_id}` **Success:** you have a `channel_id` (the embedded flow returns it straight from `exchange-code`), and `GET /api/v1/channels` now lists the channel with `"is_active": true` — it can send and receive. That `channel_id` is what you pass to every send and receive call from here on. ### 3. Receive a message Replying to inbound is Fiwano's core use case, so set up receiving **before** sending. Two parts: **1. Enable events on the channel.** Delivery is opt-in — **by default no events are delivered**. Set `webhook_events` (and a `webhook_url`) on the channel, and enable only the events you actually handle (start with `message.received`). The per-channel event list and how to configure it are in [Channels](/documentation/channels). **2. Handle the webhook.** Fiwano POSTs each enabled event to your `webhook_url`. Your endpoint **must verify the `X-Webhook-Signature`** (HMAC-SHA256 with the channel's `webhook_secret`) and **respond HTTP 2xx within ~5 seconds** — otherwise Fiwano retries with backoff and emails you. Payload shapes and the signature check are in [Receiving Messages](/documentation/webhooks). An inbound `message.received` carries the two identifiers you need to reply (marked below): ```jsonc { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", // ← which of your channels received it "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "wamid.xxx", "from": "1234567890", // ← who sent it — reply to this "from_name": "John Doe", "type": "text", "text": "Hello!" } } ``` - **`channel_id`** (top level) — the channel the message arrived on. - **`data.from`** — the sender's id: phone number (WhatsApp), IGSID (Instagram), or PSID (Facebook). This is exactly what you pass back as `recipient`. From a handler you'll often also call: - `PATCH /api/v1/channels/{channel_id}` — set or update `webhook_events` / `webhook_url` - `GET /api/v1/media/{media_id}` — download received media - `GET /api/v1/channels/{channel_id}/profile/{user_id}` — look up the sender's profile **Success:** message your connected channel from a real device; your endpoint receives a `message.received` webhook with a valid signature and returns 2xx. You're now receiving. ### 4. Reply to it With receiving in place, send outbound. The everyday case is a **free-form reply within the 24-hour window** after a user messages you — plain text or media, no approval needed. This is the core move: answer the sender by feeding the **same** identifiers straight back — the webhook's `channel_id` as `channel_id`, and `data.from` as `recipient`: ```bash curl -X POST https://fiwano.com/api/v1/messages/send \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Thanks for your message!"}' ``` - Send: `POST /api/v1/messages/send` (text), `POST /api/v1/messages/send-media` (media) **Success:** the call returns an accepted message with a `message_id`; if you enabled delivery events in step 3, you'll then receive `message.sent` / `message.delivered` webhooks tracking it. Read [Sending Messages](/documentation/sending-messages) for media and more. That covers the core loop — connect, receive, reply. Step 5 is optional. ### 5. WhatsApp templates — messaging outside the 24-hour window (optional) Free-form messages only reach a user **inside** the 24-hour window. To start a conversation, or to reply after the window has closed, WhatsApp requires a pre-approved **template** (WhatsApp only). Skip this step if you only ever reply within the window — see the 24-hour window in [Capabilities](/documentation/capabilities#messaging-windows-24h). Read [WhatsApp Templates](/documentation/templates) for the create/review lifecycle, then send the approved template. - Send a template: `POST /api/v1/messages/send-template` - Manage templates: `GET /api/v1/channels/{channel_id}/templates`, `POST /api/v1/channels/{channel_id}/templates`, `GET|PUT|DELETE /api/v1/channels/{channel_id}/templates/{template_id}` ### Next steps - **No code?** Use the verified [n8n node](/documentation/n8n) — same channels, same events, drag-and-drop. - **Media and templates** — [Sending Messages](/documentation/sending-messages). - **Limits, windows and tiers** — [Capabilities](/documentation/capabilities). - **The full machine-readable contract** — [API Reference](/documentation/api). --- ## Channels A **channel** is one connected Meta asset — a WhatsApp number, an Instagram account, or a Facebook Page — that you send and receive messages through. This page covers how to connect, manage and reconnect channels. For the exact request/response schema of every channel endpoint (fields, types, status codes), see the **[API Reference](/documentation/api)**. This page is the task-level guide; it does not repeat the field tables. ### Prerequisites Before connecting any channel — WhatsApp, Instagram or Facebook Messenger — make sure the conditions below are met. They apply equally to the Portal flow and the API flow; if the first two are missing, Meta stops the OAuth popup before a channel can be created. - **The asset belongs to a Meta Business Portfolio** (Business Manager). The "asset" is the WhatsApp number's WABA, the Facebook Page, or — for Instagram — a Business or Creator Instagram account linked to a Facebook Page that is owned by a Business Portfolio. - **The Facebook user signing in has full admin rights** on that Business Portfolio and on the asset itself. A user without admin role sees the relevant choice in the popup greyed out. - **Fiwano is the default messaging app** (Instagram and Facebook Messenger). If another tool — a chatbot, CRM or inbox — is connected to the same account, Meta routes each conversation to one app only. Set Fiwano as the *Default routing app* (Facebook Page → Settings → Page setup → *Instagram conversation routing* / *Messenger conversation routing*; for Instagram accounts without a Page: Meta Business Suite → Settings → Integrations → *Conversation Routing*) and turn off *Take control of conversations* for the other apps, or disconnect them. Otherwise incoming messages may not reach your webhook and replies are rejected with error `10`. ### Option A: Via Portal (self-service) Use this to connect **your own** channels, no code required. 1. Go to **Channels → Connect Channel** in the portal. 2. Select the channel type (WhatsApp, Instagram, or Facebook Messenger). 3. Complete the Meta OAuth flow in the popup window. 4. Configure the **Webhook URL** and select **Webhook Events** in channel settings. 5. By default, no events are enabled — select which events to forward to your endpoint. Saving a webhook URL in the Portal does **not** create a `webhook_secret`. Set one explicitly so incoming deliveries are signed — see [Webhook secret](#webhook-secret). The URL must be an absolute HTTPS URL reachable from Fiwano. Explicit ports from 1 to 65535 are supported; embedded credentials and URL fragments are not. ### Option B: Via API (programmatic) Use this when your application connects channels **on behalf of your end users**. **Step 1 — Whitelist your redirect URI.** For security, the user can only be redirected back to a URL you have pre-registered for your API key. Register the URL(s) where users land after OAuth (wildcards are allowed, e.g. `https://*.example.com/callback`): ```bash curl -X POST https://fiwano.com/api/v1/redirects \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"uri_pattern": "https://yourapp.com/callback"}' ``` Redirect URI patterns must use HTTPS and cannot target localhost or a loopback address. Explicit ports from 1 to 65535 are supported, including non-standard HTTPS ports such as `https://yourapp.com:4426/callback`. Exact URIs are safest and recommended. When a wildcard is necessary, it may appear in the path/query or as one complete left-most hostname label (`*.example.com`), but cannot replace the whole hostname, part of a label, or the port. Embedded credentials, URL fragments, and the reserved query keys `code`, `status`, `channel_type`, and `error` are rejected. To associate a setup flow with your authenticated tenant or administrator, generate a high-entropy, single-use opaque nonce, store it server-side with that context, and put only the nonce in the redirect URI. Register a narrowly scoped pattern such as `https://yourapp.com/callback?state=*`, then request the setup URL with `https://yourapp.com/callback?state=BASE64URL_NONCE`. Fiwano preserves `state` and appends its own parameters, for example `?state=BASE64URL_NONCE&code=...&status=success&channel_type=whatsapp`. Use a URL-safe value and do not place tenant/user identifiers or other sensitive data directly in the URI. You manage these with `GET /api/v1/redirects` and `DELETE /api/v1/redirects/{id}`. **Step 2 — Request a setup URL.** Pass one of your whitelisted redirect URIs. The URL is valid until the `expires_at` returned in the response — open it in a browser or popup for the user: ```bash curl -X POST https://fiwano.com/api/v1/channels/setup-url \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"channel_type": "whatsapp", "redirect_uri": "https://yourapp.com/callback"}' ``` The same endpoint also reconnects channels; there is no separate reconnect API. If the Meta identity already belongs to one of your inactive channels, Fiwano reactivates that row and `exchange-code` returns the existing `channel_id`. When both a genuinely new asset and an inactive asset are available, the new asset is preferred. **Step 3 — User completes Meta OAuth.** After approval, the user is redirected to your `redirect_uri` with a one-time `code` parameter: ``` https://yourapp.com/callback?code=abc123... ``` On failure, the redirect instead carries two query params — branch your logic on `error` only: | Query param | How to use it | |---|---| | `error` | Machine-readable code. **Branch on this.** `access_denied` — the user cancelled the Meta dialog. `setup_failed` — setup could not complete (e.g. no Instagram Business account was accessible with the permissions granted). | | `message` | URL-encoded, human-readable English explanation, safe to display to the user. **Free-form and may change — never parse or branch on its text.** | Example failure redirect: ``` https://yourapp.com/callback?error=setup_failed&message=We%20couldn%27t%20access%20any%20Instagram%20Business%20account... ``` **Step 4 — Exchange the code.** Within 5 minutes (single-use), exchange the code for the channel. You can configure the webhook in the same call: ```bash curl -X POST https://fiwano.com/api/v1/channels/exchange-code \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "abc123...", "webhook_url": "https://yourapp.com/webhooks/meta", "webhook_events": ["message.received", "message.delivered", "message.failed"] }' ``` The response returns your `channel_id` (store it — every other call uses it). When you set `webhook_url` and pass no `webhook_secret`, Fiwano **auto-generates** one and returns it here. It is returned **only in this response** — `GET` never shows it again — so store it to verify webhook signatures ([Webhook secret](#webhook-secret)). All fields except `code` are optional and can be set later via `PATCH /api/v1/channels/{id}`. ### Webhook events Webhook delivery is **opt-in per channel**: by default **no events are delivered**. You choose what you receive by setting `webhook_events` — in the connect call, in the Portal, or later via `PATCH /api/v1/channels/{id}`. Until you do, your endpoint gets nothing. The available events depend on the channel type — WhatsApp exposes more (`message.sent`, `message.failed`) than Instagram and Facebook. The full list with payloads is on the **[Webhooks](/documentation/webhooks#event-types)** page. An event you list that isn't valid for the channel type is simply ignored, not an error. One event has an extra knob: `message.echo` (copies of messages your business sends outside Fiwano) delivers just the message by default. Set the channel's boolean `echo_statuses` field — in the connect call, via `PATCH /api/v1/channels/{id}`, or in the Portal — to also receive delivered/read statuses for echoed messages through your regular status events. Details: **[message.echo](/documentation/webhooks#message-echo)**. **Your endpoint owns the other half of this contract.** Once events are enabled, Fiwano POSTs each one to your `webhook_url`, and your endpoint **must respond with HTTP 2xx within ~5 seconds**. A non-2xx response or a timeout counts as a failed delivery: Fiwano **retries with backoff and emails you** — a warning after the 3rd failed attempt and an alert when retries are exhausted. So enable **only the events you actually handle**, and return 2xx as soon as you've accepted the payload (do slower work afterwards). Successfully delivered webhook payloads are not retained for relay; failures are stored encrypted for retries. Full behavior: **[Webhooks → Retry Policy](/documentation/webhooks#retry-policy)**. ### Webhook secret The `webhook_secret` is the HMAC key Fiwano uses to **sign webhook deliveries**, so your endpoint can confirm a request genuinely came from Fiwano and was not altered in transit. When a channel has a secret, every delivery carries an `X-Webhook-Signature: sha256=` header — see **[Webhooks](/documentation/webhooks)** for the verification snippet. A channel with no secret receives **unsigned** deliveries. How a secret first appears differs by how you connect — and this is the one place the Portal and the API deliberately behave differently: - **Portal (Option A):** a new channel has **no secret**, and saving a webhook URL does not create one. Set it yourself in channel settings: click **Generate random** for a random 64-character secret, or type your own and **Save** (16–64 characters). The value is revealed **once**, immediately after. - **API (Option B):** when you set `webhook_url` and the channel has no secret yet, Fiwano **auto-generates** one (64-character hex) and returns it in the `exchange-code` / `PATCH /api/v1/channels/{id}` response — so channels you connect by API are **signed by default**. To use a specific value instead, pass your own `webhook_secret` (**at most 64 characters**) in that same call. **Reading it back.** The value is only returned the moment it is set or changed — in the Portal's one-time reveal, or in the `exchange-code` and `PATCH /api/v1/channels/{id}` responses. `GET /api/v1/channels` and `GET /api/v1/channels/{id}` never return it; they only report `has_webhook_secret: true | false`. **Store the value when it is shown** — if you lose it, your only option is to set a new one. **Rotating it.** Set a new secret any time by passing a new `webhook_secret` to `PATCH /api/v1/channels/{id}`, or with the Portal's **Generate random** / **Save** actions. Updating only `webhook_url`/`webhook_events` leaves the secret untouched. A change takes effect on the **very next delivery** — there is no overlap window, so switch your verifier to the new secret at the same moment, or signatures will mismatch. **Constraints and recommendations.** - Use a high-entropy random string of **16–64 characters** (the Portal enforces the 16-character minimum; the field stores up to 64). Auto-generated secrets are 64-character hex — prefer those unless you have a reason to bring your own. - The secret is **per channel** — each channel has its own, independent of the rest. - Reconnecting an inactive channel **keeps** its existing secret (see [Reconnecting an inactive channel](#reconnecting-an-inactive-channel) below). - Treat it like a password: store it in a secret manager, never commit it, and verify signatures using a constant-time comparison (as in the Webhooks example). ### Managing channels | Task | Endpoint | |---|---| | List all channels (active and inactive), each with its current subscription state | `GET /api/v1/channels` | | Inspect one channel | `GET /api/v1/channels/{id}` | | Update webhook URL / secret / events, or the subscription binding | `PATCH /api/v1/channels/{id}` | | Deactivate a channel | `DELETE /api/v1/channels/{id}` | Each channel carries a `subscription` block describing its billing state — see **[Subscriptions & Billing](/documentation/subscriptions)** for what the combinations mean. Full field lists live in the **[API Reference](/documentation/api)**. **Deactivation is a soft delete.** `DELETE` stops the channel from sending and receiving, but does not erase it — its `channel_id` and history are preserved so you can reconnect later. The channel also remains owned by the same Fiwano account: deactivation does not release its WhatsApp number, Instagram account or Facebook Page for connection to another Fiwano account. If the channel must move between accounts, contact `contact@fiwano.com`. Fiwano also unsubscribes the channel's Meta webhook resource only when it is safe to: a WABA subscription is kept if another active WhatsApp channel uses the same WABA, and a Page subscription is kept if another active Instagram/Facebook channel uses the same Page. ### Subscription slots Each subscription grants **one slot per channel type** — one WhatsApp, one Instagram, one Facebook. A slot stays occupied while a channel is bound to it, **including a deactivated channel**: the binding is what lets you reconnect that channel later without buying another subscription. `GET /api/v1/subscriptions` shows which channel sits in each slot and how many slots are free; each channel reports its own `subscription.id` in return. Send `subscription_id` to `PATCH /api/v1/channels/{channel_id}` to change that. A subscription ID moves the channel there — no downtime, and it does not have to be deactivated first, but a move to a Starter subscription stops media and template sending immediately. An empty string releases the slot, and that is allowed only for a channel already deactivated with `DELETE /api/v1/channels/{channel_id}`, so a slot is never freed as a side effect of a settings update. **Releasing a slot is effectively permanent.** Once another channel takes the freed slot, the released one can no longer be reconnected until a slot is free again. It is not erased and its Meta identity stays owned by your Fiwano account — but treat the release as retiring that channel, not pausing it. Replacing a channel when you have a single subscription: ```text GET /api/v1/subscriptions → find the subscription and its occupied slot DELETE /api/v1/channels/{old_id} → deactivate the channel you are replacing PATCH /api/v1/channels/{old_id} → {"subscription_id": ""} frees the slot POST /api/v1/channels/setup-url → user connects the new Meta account POST /api/v1/channels/exchange-code → new channel takes the free slot ``` ### Reconnecting an inactive channel A channel goes inactive when it is deactivated (`DELETE /api/v1/channels/{id}`) or when its Meta connection can no longer be maintained (for example, the account owner revoked access in Meta). To bring it back, run the **same connection flow again for the same Meta account** (same WhatsApp number, Instagram account, or Facebook Page): - The existing channel is **reactivated in place** — its `channel_id`, webhook URL/secret/events and history are preserved. No new channel is created and your stored `channel_id` mapping stays valid. - Reconnecting requires an **active license**: the channel must still hold one, or you must have a free license slot. Otherwise the flow is refused — attach a license in Billing first. - A Meta account owned by a different Fiwano account cannot be connected, even when that channel is inactive. If it is your channel, contact `contact@fiwano.com` to request an ownership release. --- ## Sending Messages Fiwano has three send endpoints — plain text, media, and WhatsApp templates. All take a `channel_id` and a `recipient`. The `recipient` format depends on the channel (phone number for WhatsApp, IGSID for Instagram, PSID for Facebook) — see the recipient row in [Capabilities](/documentation/capabilities#channel-capabilities). Full request/response schemas are in the [API Reference](/documentation/api); this page is the task guide. ### Send WhatsApp messages with the API For a normal WhatsApp reply inside the 24-hour customer service window, use the plain text endpoint below with a WhatsApp `channel_id` and the recipient's phone number. You authenticate with your Fiwano `X-API-Key`; you do not need a separate Meta or WhatsApp API key in your application. Outside the 24-hour window, WhatsApp requires an approved template message. That uses `/api/v1/messages/send-template` and is described in [Template messages](#template-messages). Instagram DM and Facebook Messenger use the same text endpoint for ordinary replies, with IGSID or PSID as `recipient`. ### Text messages `POST /api/v1/messages/send` — works on all channel types. ```bash curl -X POST https://fiwano.com/api/v1/messages/send \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Hello! Your order is ready."}' ``` The response carries a `message_id` (a Fiwano UUID) that every later delivery-status webhook references. Text has a per-platform length cap (WhatsApp 4096, Facebook 2000, Instagram 1000) — oversize text is rejected with `400 text_too_long` before Meta is called. Fiwano does **not** auto-split; split on your side to preserve your own chunking and ordering. The `text` value must contain at least one non-whitespace character; empty or whitespace-only values are rejected with `422` before Meta is called. Leading and trailing whitespace in otherwise valid text is preserved. See [Capabilities](/documentation/capabilities). `recipient` is trimmed of surrounding whitespace, then checked before Meta is called: an empty value, a value without any digit (for example a serialized object such as `[object Object]`), or a non-numeric PSID/IGSID on a Messenger/Instagram channel is rejected with `400 invalid_recipient` (see [Errors](/documentation/errors)). No further shape rules are applied — a WhatsApp number that Meta can interpret is passed through as-is, and any rejection Meta makes itself still comes back as `status: "failed"`. ### Media messages `POST /api/v1/messages/send-media` — **Pro license required.** Meta fetches the file directly from `media_url`; Fiwano never downloads or stores it. Pass `media_type` (`image`, `audio`, `video`, `document`) and an HTTPS `media_url`. **Use a signed URL for non-public content** — S3/GCS/R2 presigned, Azure SAS, or an HMAC-signed URL on your own server, with expiry ≥ 20 min so background retries can still fetch it. A public URL is reachable by anyone who learns it. **Keep files small and hosting fast.** Meta downloads the file while your request is waiting, so a compressed image on a fast host is accepted in a couple of seconds, while a large file on a slow host can take Meta a minute or more. Smaller files mean faster, more predictable delivery and fewer sends that finish in the background — see [Response time](#response-time). ```bash curl -X POST https://fiwano.com/api/v1/messages/send-media \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{ "channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "media_type": "image", "media_url": "https://my-bucket.s3.amazonaws.com/photo.jpg?X-Amz-Signature=...&X-Amz-Expires=1800", "caption": "Your order photo" }' ``` Always check `success` and `status`. Permanent failures include a Meta `error_code` — for example `131052` when Meta cannot download the URL. Failures that can recover, including `131053` processing/fetcher failures and `131056` pair rate limits, return `queued` and use the same durable retry schedule as text. File-size caps are in [Capabilities](/documentation/capabilities#outbound-media-size) and the full error-code table is in [Errors](/documentation/errors#send-error-codes). ### Response time `POST /messages/send-media` is **synchronous and can be slow**. Fiwano never downloads your file: we hand Meta the `media_url` and **Meta fetches it inside your request**. The wait is therefore proportional to the file size and to how fast your own hosting serves it. A 12-second call for a large file is normal. Fiwano waits for Meta for up to **30 seconds**; if Meta has not answered by then, the call returns `queued` and Fiwano completes the send in the background — see [Delivery and retries](#delivery-and-retries). Text and template sends are not affected — they carry no file and typically complete in well under a second. **Set your HTTP client timeout to at least 35 seconds** for `send-media`. Some environments cap this for you and cannot wait that long — AWS API Gateway stops at 29 seconds, and serverless functions often default to 10–15 seconds. > **If your client times out, the message may still have been sent.** Meta can > accept it after you stopped waiting. Resending then delivers it twice. > Retry only after confirming the message is absent from the conversation. To keep media sends fast, serve `media_url` from storage close to your users (S3/GCS/R2 with a CDN) and keep files well under the [size caps](/documentation/capabilities#outbound-media-size). ### Template messages `POST /api/v1/messages/send-template` — **WhatsApp only, Pro required.** Use a pre-approved template to start a conversation outside the 24-hour window (see [Capabilities](/documentation/capabilities#messaging-windows-24h)). Only `APPROVED` templates can be sent — to create and manage them, see [WhatsApp Templates](/documentation/templates). Provide variable values keyed by component. **Positional** templates (`{{1}}`, `{{2}}`) take arrays: ```bash curl -X POST https://fiwano.com/api/v1/messages/send-template \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{ "channel_id": "a1b2c3d4e5f67890", "template_name": "order_confirmation", "language": "en_US", "recipient": "1234567890", "variables": { "header": ["Summer Sale"], "body": ["Pablo", "ORD-123", "25%"], "buttons": [{"index": 0, "value": "promo25"}] } }' ``` **Named** templates (`{{customer_name}}`) take objects: ```bash -d '{ "channel_id": "a1b2c3d4e5f67890", "template_name": "welcome_message", "language": "en_US", "recipient": "1234567890", "variables": {"body": {"customer_name": "Pablo", "order_number": "ORD-123"}} }' ``` Omit `variables` entirely if the template has none. Template sends return the same response shape as text and media sends. The `message_id` is a Fiwano UUID; keep it to correlate later delivery webhooks: ```json { "success": true, "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error": null, "error_code": null, "status": "sent" } ``` ### Delivery and retries All three send endpoints return `200` with the common `success`, `message_id`, `error`, `error_code`, and `status` fields, because what happens after Meta accepts the request matters: - **`sent`** — Meta accepted it. Track the rest via delivery-status webhooks (`message.delivered` / `read` / `failed`) — see [Receiving Messages](/documentation/webhooks#delivery-status-tracking). - **`queued`** — Fiwano is completing the send in the background and `message_id` is already final. This happens after a transient Meta failure (network, 5xx, rate limit) — retried up to 7 times over ~20 min, with an early-warning email after 3 failed retries and a final email if they're exhausted — and when Meta takes longer than 30 seconds to answer, which occasionally happens with large media — see [slow Meta responses](/documentation/errors#unverified-send-outcomes). Track the outcome through the delivery-status webhooks; do not resend on your side. - **`failed`** (`success: false`) — the request will not be retried. For `send` and `send-media`, this means Meta rejected the message permanently (a recipient the Page or number cannot message, oversize text or media, malformed payload, a closed 24h window, or an action Meta denies for the account — see [send error codes](/documentation/errors#send-error-codes)), and the channel owner is emailed. A request Fiwano itself refuses before calling Meta (`text_too_long`, `invalid_recipient`, `recipient_equals_sender`) answers with an HTTP `400` instead, not with `failed`. `send-template` does not retry automatically, so any Meta send error is returned as `failed`; the caller can decide whether and when to resend. So `200` does not by itself mean "delivered" — always read `success` and `status`. --- ## Receiving Messages When a user messages your connected channel, Fiwano delivers the message — and later its delivery statuses — to your channel's `webhook_url` as a `POST` request. This page covers verifying webhooks, the payload formats per channel, downloading inbound media, and looking up a sender's profile. Set `webhook_url` and choose which `webhook_events` to receive when you connect a channel (see [Channels](/documentation/channels)); by default no events are enabled. If the channel has a [`webhook_secret`](/documentation/channels#webhook-secret), each delivery is signed so you can verify it came from Fiwano — strongly recommended. Until you set a secret, deliveries are sent unsigned. ### WhatsApp webhook setup and payload For WhatsApp, enable `message.received` on the channel and point `webhook_url` at your public HTTPS endpoint. Fiwano receives the original Meta webhook from the WhatsApp Cloud API, resolves the connected channel, normalizes the payload, signs it if you configured a `webhook_secret`, and delivers it to you. The useful difference from wiring Meta directly is that the webhook envelope is the same shape across all three channels: - WhatsApp senders arrive as phone numbers in `data.from`. - Instagram senders arrive as IGSID values in `data.from`. - Facebook Messenger senders arrive as PSID values in `data.from`. The top-level fields (`event`, `channel_id`, `channel_type`, `timestamp`, `data`) stay stable, so one receiver can handle WhatsApp webhooks, Instagram webhooks and Messenger webhooks without three separate Meta parsers. ### Verifying Signatures When the channel has a `webhook_secret`, every webhook request includes an `X-Webhook-Signature` header: ``` X-Webhook-Signature: sha256= ``` To verify: compute `HMAC-SHA256` of the raw request body using your `webhook_secret` as the key, then compare the hex digest. (If no secret is configured, this header is absent — set one to enable verification.) ```python import hmac, hashlib def verify_signature(body: bytes, secret: str, header: str) -> bool: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() received = header.replace("sha256=", "") return hmac.compare_digest(expected, received) ``` ### Event Types Each channel type supports a specific set of webhook events. **Only events you explicitly enable via `webhook_events` are delivered.** By default, no events are enabled — you must configure them after connecting a channel. | Event | Description | Channels | |---|---|---| | `message.received` | Incoming message from a user | All | | `message.echo` | Copy of a message your business sent **outside Fiwano** (WhatsApp Business App, Instagram inbox, Facebook Page Inbox, Meta Business Suite, another integration) | All | | `message.sent` | Your message was accepted by Meta | WhatsApp | | `message.delivered` | Message delivered to recipient's device | WhatsApp, Instagram, Facebook | | `message.read` | Message read by recipient * | WhatsApp, Instagram, Facebook | | `message.failed` | Message delivery failed | WhatsApp | \* `message.read` depends on the recipient's privacy settings on WhatsApp and Instagram — if they have disabled read receipts, the `read` status will never arrive. Treat `delivered` as a terminal success state. New event types may be added over time; they are never enabled on an existing channel until you add them to `webhook_events`. Ignore payload fields you do not know — see [Compatibility](/documentation#compatibility). ### Delivery Status Tracking When you send text, media, or a WhatsApp template through any send endpoint, you receive a `message_id` (UUID). All subsequent status webhooks reference this same UUID; Meta's provider ID remains internal to Fiwano. - `message_id` is always present in all status events — it's a UUID generated by Fiwano, not a Meta internal ID. - Status progression: `sent → delivered → read`. Each status implies all previous ones. - `data.recipient` is the user identifier: phone number (WhatsApp), IGSID (Instagram), or PSID (Facebook). - All channels use the exact same webhook format. - **Read cascading:** when a user reads a conversation, Fiwano sends a separate `message.read` webhook for *each* unread message — not just the latest one. Facebook and Instagram read receipts are thread-level (Meta reports "read up to this moment", Instagram naming only the last message), so Fiwano resolves them against every message you sent to that user; WhatsApp reports each message on its own. - Statuses can arrive out of order — on Instagram a `read` may reach you before the `delivered` of the same message. Treat the highest status seen as the current one and upsert by `message_id`. ### Payload Format All payloads share the same top-level structure: ```json { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { ... } } ``` #### message.received (WhatsApp) ```json { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "wamid.xxx", "from": "1234567890", "from_name": "John Doe", "type": "text", "text": "Hello!" } } ``` `data.from` — sender's phone number without `+`. Use directly as `recipient` when replying. #### message.received (Instagram) ```json { "event": "message.received", "channel_id": "b2c3d4e5f6789012", "channel_type": "instagram", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "mid.xxx", "from": "6543217890123456", "from_name": null, "type": "text", "text": "Hi there!" } } ``` `data.from` — IGSID. Use as `recipient` when replying. `from_name` is always `null` (Meta does not include sender name in IG webhooks). #### message.received (Facebook Messenger) ```json { "event": "message.received", "channel_id": "c3f8a1b2e4d56789", "channel_type": "facebook", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "mid.xxx", "from": "7890123456789012", "from_name": null, "type": "text", "text": "Hello from Messenger!" } } ``` `data.from` — PSID. Use as `recipient` when replying. `from_name` is always `null` (Meta does not include sender name in FB webhooks). #### message.received — media (Pro) With a **Pro** license, media messages include the file content. The media file is downloaded from Meta and stored temporarily. Use the `download_url` to fetch the file before it expires. WhatsApp image example: ```json { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "wamid.xxx", "from": "1234567890", "from_name": "John Doe", "type": "image", "caption": "Check this photo", "media": { "media_id": "m1b2c3d4e5f67890", "mime_type": "image/jpeg", "file_size": 245760, "filename": null, "sha256": "abc123...", "duration_ms": null, "download_url": "https://fiwano.com/api/v1/media/m1b2c3d4e5f67890", "expires_at": "2025-01-15T11:30:00Z" } } } ``` WhatsApp voice message example: ```json { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "wamid.xxx", "from": "1234567890", "from_name": "John Doe", "type": "audio", "media": { "media_id": "m2b3c4d5e6f78901", "voice": true, "mime_type": "audio/ogg; codecs=opus", "file_size": 12345, "filename": null, "sha256": "def456...", "duration_ms": 5200, "download_url": "https://fiwano.com/api/v1/media/m2b3c4d5e6f78901", "expires_at": "2025-01-15T11:30:00Z" } } } ``` Instagram and Facebook Messenger deliver the same `data.media` block; only `data.from` differs (IGSID or PSID instead of a phone number). `data.type` is the message type on every channel and always comes from one fixed set: `text`, `image`, `audio`, `video`, `document`, `sticker` (WhatsApp only), or `unsupported`. Route on it. The four media types are exactly the values accepted as the outbound `media_type`, so an inbound media event can be forwarded without a mapping table — except `sticker`, which is inbound-only and has to be re-encoded to go out as `image`. Instagram and Facebook Messenger also use attachments for things that are not a file, such as a shared post or a location pin. Those arrive as `type: "unsupported"` with no `data.media` block — see **unsupported type** below. When Meta includes text together with media, Fiwano exposes that accompanying text as `data.caption` on the media event. Plain text messages continue to use `data.text`. This rule is the same across WhatsApp, Instagram and Facebook Messenger. **Multiple attachments:** every attachment is delivered as its own `message.received` webhook and its own HTTP `POST`; Fiwano never sends an array of webhook events. All files from the source message are prepared before the first event is delivered, then the events are sent in Meta's attachment order. The first event keeps Meta's message ID and carries the caption, if present. Later events use deterministic IDs with `.2`, `.3`, and so on, and omit the caption: ```text mid.xxx image + caption mid.xxx.2 image mid.xxx.3 video ``` Treat inbound `message_id` as an opaque idempotency key; do not parse the suffix or pass the ID to Meta. Delivery retries remain independent per event, so a failing client endpoint can still observe a later part before a retried earlier part. A failed media download does not suppress the other attachments: its event has `media.download_url: null` and `media.error`. Fiwano preserves Meta's original file format and does not transcode media. `media.mime_type` describes the downloaded file bytes, not the message semantics. For example, Facebook Messenger voice-style clips commonly download as OGG/Opus (`audio/ogg`), while Instagram audio messages can download as audio-only MP4 served with `video/mp4`. In both cases the message type is still `data.type: "audio"`. For inbound WhatsApp only, Meta provides a reliable voice-message flag. Fiwano exposes it as `media.voice: true` when present. Instagram and Facebook Messenger do not expose an equivalent reliable voice flag through the webhook payload, so `media.voice` is omitted for those channels. The `download_url` is authenticated; fetch it with your `X-API-Key`. Do not pass it directly as an outbound `media_url` because Meta will not send your API key header — re-host the bytes behind a public or signed HTTPS URL first. **Media payload fields:** | Field | Type | Description | |---|---|---| | `media_id` | string | Media file ID — use in `GET /api/v1/media/{media_id}` to download | | `voice` | bool | Present only for WhatsApp voice messages (`true`). Omitted for IG/FB because Meta does not provide a reliable voice flag there. | | `mime_type` | string | MIME type (e.g. `image/jpeg`, `audio/ogg; codecs=opus`) | | `file_size` | int | File size in bytes | | `filename` | string\|null | Original filename (documents only) | | `sha256` | string\|null | SHA-256 hash from Meta (WhatsApp only) | | `duration_ms` | int\|null | Duration in milliseconds (audio/video only) | | `download_url` | string\|null | Authenticated download URL. `null` if download from Meta failed. | | `error` | string | Present only when download failed — describes the error | | `expires_at` | string | ISO 8601 timestamp — file is deleted after this time | > **Note:** Treat voice messages as audio messages. `data.type: "audio"` is the stable cross-channel value for routing and forwarding. `media.voice` is an optional WhatsApp-only hint for UI/UX. #### Downloading inbound media Fetch the file from `data.media.download_url` (which is `GET /api/v1/media/{media_id}`) with your `X-API-Key`: ```bash curl https://fiwano.com/api/v1/media/m1b2c3d4e5f67890 \ -H "X-API-Key: YOUR_API_KEY" \ --output photo.jpg ``` The response is the raw file bytes with the original `Content-Type` (and a `Content-Disposition` filename when known). Files expire about 60 minutes after Fiwano retrieves them from Meta; `media.expires_at` is authoritative. Download promptly and re-host anything you need to keep; after expiry the URL returns `410 Gone`. Sizes are in [Capabilities](/documentation/capabilities#media-limits); status codes in the [API Reference](/documentation/api). #### message.received — unsupported type (all channels) A message arrives as `type: "unsupported"` when Fiwano cannot give you the content as a file. `unsupported_type` says what it was, and there is no `data.media` block. There are two reasons, and `upgrade_required` tells them apart. **Media on a Starter license.** The file exists but your tier does not include it. `unsupported_type` is the media type Pro would have delivered, and `upgrade_required` names the tier that unlocks it: ```json { "event": "message.received", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:00Z", "data": { "message_id": "wamid.xxx", "from": "1234567890", "from_name": "John Doe", "type": "unsupported", "unsupported_type": "image", "upgrade_required": "pro" } } ``` Upgrade via the Billing page in the portal to receive full media content. **Content that is not a file.** No tier delivers these, so `upgrade_required` is absent. `unsupported_type` carries Meta's own name for the content: | Channel | `unsupported_type` values | |---|---| | WhatsApp | `location`, `contacts`, and other non-media message types | | Instagram, Facebook Messenger | `share` and `ig_reel` (a shared post or reel), `story_mention`, `location`, `fallback` (a shared link), `template`, `unsupported` | Any type not listed here arrives the same way, so an unfamiliar `unsupported_type` is still just unsupported content. Message reactions are ignored and are not delivered as webhook events. #### message.echo — messages sent outside Fiwano When someone on your side answers a customer **without going through Fiwano**, Meta echoes that message back — and Fiwano can deliver you a copy, so your system sees the whole conversation, not just its own half. Sources per channel: | Channel | Where the message was sent from | |---|---| | WhatsApp | WhatsApp Business App or a linked device, on a Coexistence number | | Instagram | Instagram app inbox, Meta Business Suite, or another integration | | Facebook Messenger | Facebook Page Inbox, Meta Business Suite, or another integration | Enable it per channel by adding `message.echo` to `webhook_events` (off by default, available on every plan). Messages sent through Fiwano never arrive as echoes — you already have them. ```json { "event": "message.echo", "channel_id": "b2c3d4e5f6789012", "channel_type": "instagram", "timestamp": "2026-09-01T10:30:00Z", "data": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "recipient": "6543217890123456", "status": "sent", "type": "text", "text": "Operator reply" } } ``` - `message_id` — a Fiwano UUID, exactly like the one you get when sending through the API. It is **stable**: if Meta redelivers the same echo, you receive the same UUID, so deduplicate on it. - `recipient` — the user the message was sent to, in the same format the send endpoints accept (phone number for WhatsApp, IGSID for Instagram, PSID for Facebook). You can reply to `recipient` directly. - `status: "sent"` — the initial lifecycle state. An echo confirms the message exists in the conversation, not that it reached the recipient's device. No separate `message.sent` event is emitted for echoes. - Who exactly sent the message (which operator, device, or app) is not exposed — Meta does not provide a reliable identity for it. **Status tracking for echoes.** By default an echo is a one-off copy: no `delivered`/`read` follow-ups. Set the channel's `echo_statuses` field to `true` (via `PATCH /api/v1/channels/{id}` or the Portal) and echoed messages get the same status lifecycle as messages you send through Fiwano: subsequent `message.delivered` / `message.read` / `message.failed` webhooks reference the same echo `message_id` and are filtered by your `webhook_events` exactly like ordinary statuses. Delivered and read statuses for WhatsApp echoes are delivered the same way as for messages sent through Fiwano. Meta does not formally guarantee status delivery for messages sent from the WhatsApp Business App, so treat a missing status as normal, not as an error. Instagram has no delivery receipt; the echo itself is the equivalent of the synthetic `delivered` Fiwano emits for your own Instagram sends, so no separate `message.delivered` follows an Instagram echo. An Instagram read receipt covers the whole thread: one `message.read` follows for every echoed message the user had not read yet, the same way as for messages sent through Fiwano. Statuses and echoes are delivered independently and at-least-once: a status can occasionally arrive before the echo it belongs to. Correlate by `message_id` and upsert rather than relying on arrival order. **Media in echoes is not delivered.** An echoed media message keeps its real `data.type` (`image`, `audio`, `video`, `document`, `sticker`) and a caption when present, but the file itself is skipped — `data.media` arrives with no download: ```json { "data": { "message_id": "550e8400-e29b-41d4-a716-446655440000", "recipient": "6543217890123456", "status": "sent", "type": "image", "caption": "Invoice photo", "media": {"media_id": null, "download_url": null, "unavailable": "echo_media_not_supported", "kind": "image"} } } ``` The rule you already apply to inbound media — *check `media.download_url` before fetching* — covers this case with no extra code, and keeps your handler compatible if echo media becomes available later. Instagram/Messenger multi-attachment messages are split into separate `message.echo` events per attachment (each with its own `message_id`), and non-file attachments arrive as `type: "unsupported"` with `unsupported_type` — same as `message.received`. **Not delivered as echoes:** reactions, message edits, and message deletions (unsend). They are changes to an existing message, not new messages, and are silently skipped. On WhatsApp, echoes exist only for Coexistence numbers — a channel connected purely through the Cloud API has no source of external messages, so `message.echo` never fires there. > **Warning:** never mirror an echo back into the same conversation automatically. > Your reply would generate no echo (Fiwano sends are filtered out), but a bot on > the other side — or a second integration mirroring echoes too — can create a > loop. Always deduplicate by `message_id` before acting on an echo. #### message.delivered / message.read (all channels) ```json { "event": "message.read", "channel_id": "b2c3d4e5f6789012", "channel_type": "instagram", "timestamp": "2025-01-15T10:30:10Z", "data": { "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "read", "recipient": "6543217890123456" } } ``` Same format for all channels and all statuses (`sent`, `delivered`, `read`). `message_id` is the UUID from the send response. #### message.failed (WhatsApp) ```json { "event": "message.failed", "channel_id": "a1b2c3d4e5f67890", "channel_type": "whatsapp", "timestamp": "2025-01-15T10:30:05Z", "data": { "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "recipient": "1234567890", "status": "failed", "error": "Message undeliverable", "errors": [{"code": 131047, "title": "Message undeliverable"}] } } ``` ### Retry Policy If your webhook URL returns a non-2xx status or is unreachable, the system retries automatically: - **7 retry attempts** with exponential backoff: 30s, 1m, 2m, 2m, 2m, 2m, 2m (~12 minutes total) - **20-minute hard deadline** — after which the delivery is marked as permanently failed - **Email warning** sent after the 3rd failed attempt (retries still in progress) - **Email alert** sent when all retries are exhausted (permanent failure) - Payloads are encrypted at rest during retry and cleared after delivery or expiry **Important:** Your endpoint **must respond with HTTP 2xx within 5 seconds**. Non-2xx responses or timeouts trigger the retry queue. Fiwano does not retain successfully delivered webhook payloads for relay; after a failed initial delivery, the encrypted payload is stored temporarily for automatic retries. > **Tip:** Only enable the webhook events you actually handle. Unhandled events that receive non-2xx responses will fill your retry queue unnecessarily. ### Sender Profile WhatsApp includes the sender's name inline in every webhook (`data.from_name`) — no extra call needed. Instagram and Facebook do **not** (`data.from_name` is always `null`); to get a name or avatar, call the profile endpoint: ``` GET /api/v1/channels/{channel_id}/profile/{user_id} ``` Pass the `data.from` value (IGSID for Instagram, PSID for Facebook) as `user_id`. It returns: - **Instagram** — `username`, `name`, `profile_pic`, `follower_count`, `is_verified_user` - **Facebook** — display name in `first_name`; `last_name` and `profile_pic` when available from Meta WhatsApp is not supported (the name is already in the webhook). Successful results are cached for 5 minutes; unavailable results are cached briefly so a newly indexed conversation can be retried soon. The response's `cached` flag tells you if it was a cache hit. Full request/response and status codes are in the [API Reference](/documentation/api). > **Tip:** call this once when you first see a new `data.from`, then cache the result on your side — no need to call it on every message. --- ## WhatsApp Templates WhatsApp requires **pre-approved templates** to start a conversation outside the 24-hour window (see [Capabilities](/documentation/capabilities#messaging-windows-24h)). Templates are WhatsApp-only and require a **Pro license**. This page is about managing them; to *send* an approved template see [Sending → Template messages](/documentation/sending-messages#template-messages). ### Lifecycle ``` Create → PENDING (Meta review, ~24h) → APPROVED (sendable) → REJECTED (fix & resubmit) ``` Templates belong to the channel's WhatsApp Business Account (WABA). Manage them through these endpoints — full request/response schemas are in the [API Reference](/documentation/api): | Task | Endpoint | |---|---| | List (filter by status; syncs from Meta by default) | `GET /api/v1/channels/{id}/templates` | | Get one (components + variable definitions) | `GET /api/v1/channels/{id}/templates/{template_id}` | | Create (→ submitted to Meta, starts `PENDING`) | `POST /api/v1/channels/{id}/templates` | | Update components | `PUT /api/v1/channels/{id}/templates/{template_id}` | | Delete | `DELETE /api/v1/channels/{id}/templates/{template_id}` | ### Creating a template A template is a `name` + `category` (`MARKETING`, `UTILITY`, or `AUTHENTICATION`) + `language` + `components`. `BODY` is required; `HEADER` (text only), `FOOTER` and `BUTTONS` are optional. Variables are `{{1}}, {{2}}` (positional) or `{{name}}` (named) — Meta requires `example` values for review. ```bash curl -X POST https://fiwano.com/api/v1/channels/a1b2c3d4e5f67890/templates \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "order_confirmation", "category": "UTILITY", "language": "en_US", "components": [ {"type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed.", "example": {"body_text": [["Pablo", "ORD-123"]]}} ], "parameter_format": "positional" }' ``` ### Rules to know - **Editing an approved template** re-submits it for review (back to `PENDING`) and is rate-limited by Meta: **max 10 edits per 30 days, 1 per 24 hours**. You can't change the category of an approved template. - **Deleting an approved template** locks its **name for 30 days** — you can't recreate a template with the same name until then (Meta restriction). - **Creating** is capped at ~100 templates per WABA per hour. Once a template is `APPROVED`, send it with [`POST /api/v1/messages/send-template`](/documentation/sending-messages#template-messages). --- ## Capabilities What each channel supports, the license tiers, and the platform limits. ### Channel Capabilities All three channels are connected the same way (OAuth). The table below shows what each channel supports. | Feature | WhatsApp | Instagram | Facebook Messenger | |---|---|---|---| | Outbound text — max length | 4096 chars | 1000 chars | 2000 chars | | Outbound media (Pro) | image, audio, video, document | image, audio, video, document | image, audio, video, document | | Template messages (Pro) | ✅ Required outside 24h window | ❌ Not supported | ❌ Not supported | | Incoming webhooks — text | ✅ `type: "text"` | ✅ `type: "text"` | ✅ `type: "text"` | | Incoming webhooks — media (Pro) | image, audio, video, document, sticker | image, audio, video, document | image, audio, video, document | | Delivery statuses | `sent` `delivered` `read` `failed` | `delivered` `read` | `delivered` `read` | | Recipient format | Phone number without `+` | IGSID | PSID — | | 24h window workaround | Use approved templates | None — wait for user to message | None — wait for user to message | | Channel identifier | `phone_number_id` | `ig_account_id` | `page_id` | | Sender profile | `data.from_name` (from Meta contacts) | Via [profile endpoint](/documentation/webhooks#sender-profile) | Via [profile endpoint](/documentation/webhooks#sender-profile) | > **Note:** Each Meta account (phone number, Instagram account, or Facebook Page) can only be connected to one Fiwano user at a time. ### License Tiers Fiwano offers two license tiers. Each connected channel requires an active license. | Tier | Monthly | Capabilities | |---|---|---| | **Starter** | $12 | Unlimited inbound and outbound text messages, delivery statuses | | **Pro** | $19 | Everything in Starter **+** inbound media with files, outbound media via HTTPS URL (signed URLs supported), WhatsApp template management and sending | New accounts start with a 7-day free trial (Pro tier). For the billing lifecycle and how a channel's subscription state is reported, see [Subscriptions & Billing](/documentation/subscriptions). For how this flat fee relates to Meta's own per-message charges, see [Messaging Costs Explained](/documentation/messaging-costs). ### Rate limits Message sends are limited to **10 accepted send attempts per second per channel** across all API keys. The limit is shared by text, media, and template sends, so creating another key does not increase one channel's allowance while one key can drive many channels independently. Exceeding it returns HTTP `429` with `Retry-After`. Other public API operations do not share a product-wide RPS cap. During exceptional outbound saturation, a send can briefly return HTTP `503` + `Retry-After`; honor the header and retry. Meta also enforces its own channel and recipient limits (shown in Meta Business Manager, not controlled by Fiwano). ### Messaging windows (24h) Meta restricts when you can message a user outside an open conversation: - **WhatsApp** — you can send regular text only within **24 hours** of the customer's last message. Outside the window, use an approved template via `POST /api/v1/messages/send-template`. This is a Meta policy. - **Instagram & Facebook Messenger** — you can reply only within **24 hours** of the user's last message. There is no template workaround — wait for the user to message again. ### Media limits #### Outbound file size Meta downloads your `media_url` and enforces its own per-platform caps. Fiwano does not re-check the file, so an oversize file is rejected by Meta with `error_code` `100` and the message is **not** retried — see [Errors](/documentation/errors#send-error-codes). | Media type | WhatsApp | Instagram | |---|---|---| | Image | 5 MB (JPEG, PNG) | 8 MB (JPEG, PNG) | | Video | 16 MB (MP4, 3GPP) | 25 MB (MP4, OGG, AVI, MOV, WebM) | | Audio | 16 MB (AAC, AMR, MP3, MP4, OGG) | 25 MB (AAC, M4A, WAV, MP4) | | Document | 100 MB (PDF, Office, text) | 25 MB (PDF) | Meta does not publish per-type caps for the Facebook Messenger Send API. Treat the Instagram figures as a safe working assumption for Messenger and handle the oversize rejection rather than relying on a fixed number. These are Meta's limits and Meta may change them. Note that encoding overhead can push a file over the cap even when its size on disk looks safe. #### Inbound file size - **Inbound media** (images, audio, video, documents) is stored temporarily for **60 minutes**. Download it via `GET /api/v1/media/{media_id}` promptly after the webhook; files are cleaned up automatically after expiry. Maximum file size: **10 MB**. - **Pro license required** for sending/receiving media and using WhatsApp templates. With a Starter license, inbound media arrives as `type: "unsupported"` with `upgrade_required: "pro"`. See [Subscriptions & Billing](/documentation/subscriptions). --- ## Subscriptions & Billing Every channel returned by `GET /api/v1/channels` carries a `subscription` object describing its current billing state. This page explains what those states mean and how they change over a channel's lifecycle. For the field types, see the **[API Reference](/documentation/api)**. A channel can **send and receive messages only while its subscription is `active`.** When it is not, send/receive calls are rejected until a license is (re)attached. ### The subscription object ```json "subscription": { "status": "active", "source": "paddle", "tier": "pro", "expires_at": "2025-02-15T10:30:00", "auto_renew": true } ``` - **`status`** — `active`, `expired`, `canceled`, or `none` (no license bound; the channel cannot send/receive). - **`source`** — where the entitlement came from: `trial` (auto-granted on signup), `paddle` (paid subscription), or `enterprise` (custom subscription provisioned by Fiwano staff, e.g. a partner deal or invoice billing). `null` when `status` is `none`. - **`tier`** — `starter` or `pro`. `pro` is required for media messages and WhatsApp template CRUD/send. `null` when `status` is `none`. - **`expires_at`** — ISO-8601 UTC timestamp when the current period ends. If `auto_renew` is `true`, this is the next renewal date; otherwise it is the cutoff after which the channel stops working. - **`auto_renew`** — `true` only for an active Paddle subscription that will renew at `expires_at`. Always `false` for trial and Enterprise. ### What the combinations mean - **Active Paddle subscription** — `{status: "active", source: "paddle", auto_renew: true, expires_at: }`. - **Paddle renewal being retried** — `{status: "active", source: "paddle", auto_renew: true, expires_at: }`. While a renewal payment is retried, `status` stays `active` and `expires_at` may sit slightly in the past — **service continues during this short grace window.** It then resolves to renewed (future `expires_at`) or, if payment keeps failing, lapses. - **Paddle with cancellation scheduled** — `{status: "active", source: "paddle", auto_renew: false, expires_at: }`. The customer cancelled in Paddle; service continues until `expires_at`, then the channel becomes orphaned. - **Trial** — `{status: "active", source: "trial", tier: "pro", auto_renew: false, expires_at: }`. - **Enterprise** — `{status: "active", source: "enterprise", auto_renew: false, expires_at: }`. Renewals are arranged with Fiwano staff before `expires_at`. - **No active subscription** — `{status: "none", source: null, tier: null, expires_at: null, auto_renew: false}`. Send/receive will fail; attach a license to restore service. > **Tip.** Treat `status` as the single source of truth for whether a channel can > operate. Do not infer it yourself from `expires_at` — during the Paddle grace > window an `active` channel can legitimately have an `expires_at` in the past. ### Checking available slots Use `GET /api/v1/subscriptions` when an external service needs to decide whether it can start a new channel connection flow. The endpoint is read-only and returns all subscriptions plus aggregate slot availability: ```bash curl -H "X-API-Key: $FIWANO_API_KEY" \ https://fiwano.com/api/v1/subscriptions ``` ```json { "available_slots": { "whatsapp": { "total": 0, "starter": 0, "pro": 0 }, "instagram": { "total": 1, "starter": 0, "pro": 1 }, "facebook": { "total": 1, "starter": 0, "pro": 1 } }, "subscriptions": [ { "id": "a1b2c3d4e5f67890", "status": "active", "source": "trial", "tier": "pro", "auto_renew": false, "assigned_channels": { "whatsapp": { "channel_id": "1111222233334444", "channel_type": "whatsapp", "name": "Acme Support", "is_active": false }, "instagram": null, "facebook": null } } ] } ``` Use `available_slots..total > 0` as the signal that a new channel of that type can be connected. An inactive channel can still occupy a slot because Fiwano preserves the binding for reconnect. In `available_slots`, `total` is the sum of the currently free `starter` and `pro` slots for that channel type. In each subscription, `assigned_channels` shows which channel is assigned to the subscription for each type; `null` means no channel is assigned there. The reverse mapping is on the channel itself: `subscription.id` in `GET /api/v1/channels`. To move a channel to a different subscription, or to release a slot so another channel of the same type can take it, use `subscription_id` in `PATCH /api/v1/channels/{channel_id}` — see [subscription slots](/documentation/channels#subscription-slots). --- ## n8n Integration Fiwano is a **verified n8n community node** — listed on [n8n.io/integrations/fiwano/](https://n8n.io/integrations/fiwano/). Use it to build WhatsApp, Instagram and Facebook Messenger automations, AI agent workflows, and chatbots. ### Install #### From the n8n editor (recommended) 1. Open the nodes panel with **+** or **N** 2. Search for **Fiwano** 3. Select **Fiwano** under **More from the community** 4. Click **Install** On n8n Cloud, installation may need to be enabled by the instance owner in the Cloud Admin Panel first. #### Manual fallback (npm) Use only when in-app installation is unavailable in your environment (e.g. restricted self-hosted setup). ```bash mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes npm install n8n-nodes-fiwano # Restart n8n ``` For self-hosted Docker: build this package into a custom n8n image — see the [GitHub repository](https://github.com/fiwano-com/n8n-nodes-fiwano) for details. ### Nodes | Node | Type | Description | |---|---|---| | **Fiwano** | Action | Send messages, manage channels, WhatsApp templates, contact profile enrichment, redirect URIs | | **Fiwano Trigger** | Webhook Trigger | Receive incoming messages and delivery status webhooks with optional HMAC signature verification | ### Action node — operations | Resource | Operations | |---|---| | Message | Send Text, Send Template (WhatsApp), Send Media (image/audio/video/document) | | Media | Download (fetch a received media file; expires 60 min after the webhook) | | Channel | Get Many, Get, Generate OAuth URL, Exchange OAuth Code, Update (webhook settings, echo status tracking, and subscription binding), Deactivate | | Subscription | Get Many (subscriptions, the channel assigned to each slot, and free slots per channel type and tier) | | Contact | Get Profile (Instagram & Facebook — returns name/username and profile picture; Instagram also follower count) | | Template | Get Many, Get, Create, Update, Delete (WhatsApp only) | | Redirect URI | Get Many, Add, Delete | **Deactivate is a soft delete.** It stops a channel sending and receiving but keeps its ID, history and its subscription slot, so the same Meta account can be reconnected later. To free the slot for a different channel, deactivate it and then send an empty **Subscription ID** in **Update** — see [subscription slots](/documentation/channels#subscription-slots). ### Trigger node — events Starts your workflow for any of these events (filter by event type in node settings): | Event | Channels | |---|---| | `message.received` | WhatsApp, Instagram, Facebook | | `message.echo` | WhatsApp (Coexistence only), Instagram, Facebook | | `message.delivered` | WhatsApp, Instagram, Facebook | | `message.read` | WhatsApp, Instagram, Facebook | | `message.sent` | WhatsApp | | `message.failed` | WhatsApp | ### Common workflow patterns The Fiwano node is intentionally small: it gives n8n a reliable transport layer for WhatsApp, Instagram DM and Facebook Messenger, then leaves the workflow logic to n8n and the tools you connect around it. | Pattern | How to build it | |---|---| | WhatsApp AI agent or chatbot | `Fiwano Trigger` receives `message.received` → your AI/model/tool nodes decide the answer → `Fiwano` sends the reply. Use WhatsApp templates only when you need to start or reopen a conversation outside the 24-hour window. | | n8n WhatsApp trigger | Use `Fiwano Trigger` with **Specific Channel** for one WhatsApp number, or **All Active Channels** when one workflow should handle every connected channel. | | Instagram DM automation | Use the same trigger/action pair on an Instagram channel. Keep the workflow focused on inbound support, opt-in lead qualification and customer replies; do not build cold-DM scraping or spam automation. | | Facebook Messenger workflow | Use `channel_type: "facebook"` branches when a Messenger Page needs different copy or routing. It is lower-volume than WhatsApp, but useful when customers already start on Messenger. | | One workflow for all Meta channels | Use **All Active Channels**, then branch on `channel_type` (`whatsapp`, `instagram`, `facebook`) only where the channel rules differ. | ### When to use Fiwano with n8n Use it when you want n8n to own the automation — AI logic, routing, CRM updates, memory, approvals, escalation — and you only need a clean way to receive and send messages on Meta's official channels. Do not use it as a bulk cold-outreach engine. WhatsApp, Instagram and Messenger all have messaging-window and opt-in rules; Fiwano follows the official APIs and does not bypass Meta policy. ### Webhook auto-setup The trigger can wire its own webhook onto your channels, so you don't have to call **Update** by hand. Pick a **Webhook Auto-Setup** mode and attach a Fiwano API credential. The auto modes (**All Active Channels** / **Specific Channel**) need it to call the API — if it's missing, activation fails with a clear error. In **Manual** the credential is optional, used only to read a default webhook secret: | Mode | What happens on activation | On deactivation | |---|---|---| | **All Active Channels** | Points every active channel that **isn't already wired elsewhere** (WhatsApp + Instagram + Facebook) at this trigger — one workflow handles all three. Channels already pointing at another URL are **left untouched**. | Clears the webhook on the channels that still point at this trigger. | | **Specific Channel** | Points one **Channel ID** at this trigger — **takes it over** even if it already has a webhook. | Clears that channel's webhook (only if it still points here). | | **Manual** *(default)* | Nothing — you set `webhook_url` yourself via **Exchange OAuth Code** / **Update**. No credential needed. | Nothing. | The trigger's selected **Event Types** are registered as the channel's `webhook_events` (events that don't apply to a channel type are ignored — e.g. `message.sent`/`message.failed` on Instagram). Channels start with no events enabled, so auto-setup turns them on for you. **Webhook secret.** Set a **Webhook Secret** to verify incoming signatures (HMAC-SHA256; mismatches are rejected with HTTP 401) and, in auto-setup, to register on your channels. You can set it in two places: the trigger's own **Webhook Secret** field, or — to reuse one secret everywhere — the **Webhook Secret** field on the Fiwano API credential. The trigger's field wins; if it's empty, the credential's secret is used. That same credential secret also backs the **Exchange OAuth Code** and **Update** operations when you leave their secret empty. Leave both empty to skip verification (not recommended in production). **When it runs:** only on workflow **activation / deactivation** (and when n8n restarts active workflows) — **never per message**, so it adds no overhead to message handling. A few points to keep in mind: - **Deactivating removes the webhook** from the channels that point at this trigger. This only clears the webhook URL — it does **not** delete the channel or existing data. While deactivated, new inbound webhook events are neither relayed nor stored; reactivate to resume delivery. - **All Active Channels skips channels silently.** A channel already pointing at another URL is left alone and the workflow still activates without an error. So if one channel isn't responding, check whether its webhook points somewhere else — clear it or use **Specific Channel** to take it over. - **Clean up before removing.** Deactivate the workflow (don't just delete it, and don't remove the credential first) so the trigger can clear the webhook. If cleanup can't run, a channel keeps pointing at an inactive n8n URL — Fiwano then logs delivery failures and emails you until you clear it (via **Update** or the portal). - Connect a **new channel** after activating? Re-activate the workflow (toggle off/on) so the trigger wires it. - Two **All Active Channels** workflows won't fight over a channel — whichever claims an unwired channel first owns it; the other leaves it alone. To move a channel deliberately, clear its webhook or use **Specific Channel**. - Your n8n must be **publicly reachable** — Fiwano delivers webhooks over the internet to the URL the trigger registers. ### Example workflows Two ready-to-import workflows are in the [GitHub repository](https://github.com/fiwano-com/n8n-nodes-fiwano/tree/main/workflows). Use them in order: 1. **Connect a Channel** — generate a Meta setup link per channel and capture the connected `channel_id` automatically via a webhook callback. (You can also connect channels in the [Fiwano portal](https://fiwano.com).) 2. **Universal Auto-Responder** — one trigger answers **every** message across WhatsApp, Instagram and Facebook: echoes text and replies to attachments with file details. The unified ping-pong pattern — **needs at least one connected channel** (step 1). Import them from the editor (**Workflows → Import from File…**) or the CLI. --- ## API Reference (OpenAPI) The complete machine-readable contract for the public `/api/v1` API, generated from the live service (also at https://fiwano.com/api/v1/openapi.json and https://fiwano.com/api/v1/openapi.yaml). ```yaml openapi: 3.1.0 info: title: Fiwano API description: 'Unified REST API for WhatsApp, Instagram and Facebook Messenger. Authenticate every request with the `X-API-Key` header (keys start with `mip_live_`, created on the API Keys page in the portal). Base URL: `https://fiwano.com`. Human-readable guides: https://fiwano.com/documentation' version: 2.0.0 servers: - url: https://fiwano.com paths: /api/v1/subscriptions: get: tags: - api summary: List Subscriptions description: 'List subscriptions and free channel slots for the authenticated user. Use this endpoint before starting a channel setup flow to check whether a new WhatsApp, Instagram, or Facebook channel can be connected right now. A positive `available_slots..total` means a new channel can be added. POST /api/v1/channels/setup-url also permits reconnecting an existing inactive channel that still has an operable reserved slot.' operationId: list_subscriptions_api_v1_subscriptions_get responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SubscriptionsResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels: get: tags: - api summary: List Channels description: 'List all channels for the authenticated user. Returns both active and inactive channels. Each channel includes its current `subscription` (billing) state and the channel-type-specific identifiers (WhatsApp: phone_number_id/waba_id; Instagram: ig_account_id/ig_username; Instagram & Facebook: page_id).' operationId: list_channels_api_v1_channels_get responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ChannelListResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/{channel_id}: get: tags: - api summary: Get Channel description: 'Get one channel by ID. Same shape as the list endpoint — a single channel object including its `subscription` (billing) state. Returns 404 if the channel does not belong to the authenticated account.' operationId: get_channel_api_v1_channels__channel_id__get parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ChannelOut' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' patch: tags: - api summary: Update Channel description: 'Update channel webhook settings, or move the channel between subscriptions. Set webhook_url to receive incoming messages. Must use HTTPS and cannot target loopback. Optionally provide a custom webhook_secret for HMAC verification. `subscription_id` binds the channel to another subscription, or — when sent as an empty string — releases its subscription slot so a different channel of the same type can be connected. Releasing a slot requires the channel to be deactivated first via DELETE /api/v1/channels/{channel_id}; this is deliberate, so a slot is never freed as a side effect of an unrelated settings update. Fields left out (or sent as null) are not changed, and the whole request is applied atomically: if the subscription change is rejected, the webhook settings are not updated either.' operationId: update_channel_api_v1_channels__channel_id__patch parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChannelUpdateRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ChannelUpdateResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' delete: tags: - api summary: Deactivate channel description: 'Deactivate a channel (soft delete). The channel stops sending and receiving, but is not erased — its channel_id, history and ownership claim are preserved so the same Fiwano account can reconnect it later via a new OAuth flow. Another Fiwano account cannot claim the Meta identity after deactivation; ownership release requires support. Fiwano unsubscribes the channel''s Meta webhook resource only when safe: a WABA subscription is kept if another active WhatsApp channel uses the same WABA, and a Page subscription is kept if another active Instagram/Facebook channel uses the same Page.' operationId: delete_channel_api_v1_channels__channel_id__delete parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/setup-url: post: tags: - api summary: Create Setup Url description: 'Generate a URL for connecting or reconnecting a channel on behalf of a user. The client opens the returned `setup_url` in a popup or browser. After the user completes Meta OAuth (or WhatsApp Embedded Signup), they are redirected to `redirect_uri` with a one-time `code`. Exchange that code via POST /api/v1/channels/exchange-code to obtain the channel_id. `redirect_uri` must already be whitelisted for this API key (add it via POST /api/v1/redirects), otherwise the request is rejected. The setup URL is valid until the `expires_at` returned in the response. There is intentionally no separate API reconnect endpoint. When Meta returns the identity of an existing inactive channel, setup reactivates that same channel row and returns its existing channel_id. A genuinely new eligible identity is preferred when both new and inactive assets are available.' operationId: create_setup_url_api_v1_channels_setup_url_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetupUrlRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SetupUrlResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/exchange-code: post: tags: - api summary: Exchange Code description: 'Exchange a one-time completion code for channel data. After the user completes the OAuth flow, the redirect_uri receives a `code` parameter. This endpoint exchanges that code for the channel_id and basic details. The code is one-time use and expires after 5 minutes.' operationId: exchange_code_api_v1_channels_exchange_code_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ExchangeCodeRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ExchangeCodeResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/{channel_id}/profile/{user_id}: get: tags: - api summary: Get Sender Profile description: "Fetch sender profile from Meta Graph API.\n\nReturns profile data\ \ for a user who messaged your channel.\nSuccessful results are cached for\ \ 5 minutes to reduce Meta API calls.\nUnavailable results are cached briefly\ \ so newly indexed conversations recover quickly.\n\n- Instagram: username,\ \ name, profile_pic, follower_count, is_verified_user\n- Facebook: display\ \ name in first_name; last_name and profile_pic when available\n- WhatsApp:\ \ not supported (name comes inline in webhooks via data.from_name)\n\nArgs:\n\ \ channel_id: Channel ID\n user_id: Sender identifier (IGSID for Instagram,\ \ PSID for Facebook)\n\nReturns:\n SenderProfileResponse with profile data\n\ \nRaises:\n HTTPException 404: Channel not found or WhatsApp (not supported)\n\ \ HTTPException 400: Channel inactive or missing token\n HTTPException\ \ 502: Meta API error" operationId: get_sender_profile_api_v1_channels__channel_id__profile__user_id__get parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id - name: user_id in: path required: true schema: type: string title: User Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SenderProfileResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/redirects: get: tags: - api summary: List Redirects description: 'List the redirect URIs whitelisted for this API key. These gate the programmatic channel-connection flow — the `redirect_uri` in POST /api/v1/channels/setup-url must match one of these patterns.' operationId: list_redirects_api_v1_redirects_get responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RedirectListResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' post: tags: - api summary: Add Redirect description: 'Add an allowed redirect URI. Must be HTTPS. Explicit ports are supported. Exact URIs are preferred; constrained wildcard patterns like https://*.example.com/* remain supported.' operationId: add_redirect_api_v1_redirects_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RedirectCreateRequest' responses: '201': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RedirectCreateResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/redirects/{redirect_id}: delete: tags: - api summary: Remove Redirect description: Remove an allowed redirect URI. operationId: remove_redirect_api_v1_redirects__redirect_id__delete parameters: - name: redirect_id in: path required: true schema: type: string title: Redirect Id responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/messages/send: post: tags: - api summary: Send Message description: 'Send a text message through a connected channel. Recipient format is a phone number without `+` for WhatsApp, an IGSID for Instagram, and a PSID for Messenger. Text must be non-blank and stay within the platform limit. Transient failures are queued for background retry; permanent errors return `success: false` and are not retried. Two caller mistakes are rejected before reaching Meta with `400` and a structured detail: `invalid_recipient` (recipient empty, without digits, or not a numeric PSID/IGSID on Instagram/Facebook; `reason` and `hint` included) and `recipient_equals_sender` (a WhatsApp send addressed to the channel''s own number). Surrounding whitespace in `recipient` is trimmed.' operationId: send_message_api_v1_messages_send_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendMessageRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SendMessageResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/media/{media_id}: get: tags: - api summary: Download Media description: 'Download a media file previously received via webhook. Requires a Pro license. Files are temporary and expire after the configured TTL (default 60 minutes). Response: raw file bytes with appropriate Content-Type header.' operationId: download_media_api_v1_media__media_id__get parameters: - name: media_id in: path required: true schema: type: string title: Media Id responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/messages/send-media: post: tags: - api summary: Send Media Message description: 'Send a media message through a connected Pro channel. Meta fetches `media_url` directly; Fiwano does not store the outbound file. Use a signed URL for non-public content and keep it valid for the retry window. Supported types are image, audio, video, and document. Transient failures are queued; permanent payload or policy errors are not retried. `recipient` follows the same preflight as the text endpoint: trimmed, then `400 invalid_recipient` / `recipient_equals_sender` before any Meta call.' operationId: send_media_message_api_v1_messages_send_media_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendMediaRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SendMessageResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/{channel_id}/templates: get: tags: - api summary: List Templates description: 'List message templates for a WhatsApp channel. By default, syncs templates from Meta before returning (sync=true). Set sync=false to return cached data only (faster, but may be stale). Templates are tied to the WhatsApp Business Account (WABA). Only WhatsApp channels support templates.' operationId: list_templates_api_v1_channels__channel_id__templates_get parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id - name: sync in: query required: false schema: type: boolean description: Sync templates from Meta before returning. Set false for faster cached data (may be stale). default: true title: Sync description: Sync templates from Meta before returning. Set false for faster cached data (may be stale). - name: status in: query required: false schema: anyOf: - type: string - type: 'null' description: 'Filter by status: APPROVED, PENDING, or REJECTED.' title: Status description: 'Filter by status: APPROVED, PENDING, or REJECTED.' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TemplateListResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' post: tags: - api summary: Create Template description: 'Create a new WhatsApp message template. The template is submitted to Meta for review (status=PENDING). Review typically takes up to 24 hours. Template name must be lowercase alphanumeric with underscores. BODY component is required. HEADER (TEXT only), FOOTER, and BUTTONS are optional. Variables use {{1}}, {{2}} syntax (positional) or {{name}} (named). Example values are required for Meta review. Rate limit: 100 templates created per WABA per hour.' operationId: create_template_api_v1_channels__channel_id__templates_post parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateCreateRequest' responses: '201': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TemplateOut' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/channels/{channel_id}/templates/{template_id}: get: tags: - api summary: Get Template description: 'Get a specific template by its ID. Returns cached template data including variable definitions with example values for each variable.' operationId: get_template_api_v1_channels__channel_id__templates__template_id__get parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id - name: template_id in: path required: true schema: type: string title: Template Id responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TemplateOut' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' put: tags: - api summary: Update Template description: 'Update a template''s components. All components are replaced entirely — partial update is not supported by Meta. Restrictions: - Only APPROVED, REJECTED, or PAUSED templates can be edited - Approved templates: max 10 edits per 30 days, 1 per 24 hours - Cannot change category of an approved template After editing an approved template, it goes back to PENDING for re-review.' operationId: update_template_api_v1_channels__channel_id__templates__template_id__put parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id - name: template_id in: path required: true schema: type: string title: Template Id requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TemplateUpdateRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/TemplateOut' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' delete: tags: - api summary: Delete Template description: 'Delete a message template. By default, deletes only the specific language version. Set all_languages=true to delete ALL language versions of this template. WARNING: After deleting an approved template, you cannot create a template with the same name for 30 days.' operationId: delete_template_api_v1_channels__channel_id__templates__template_id__delete parameters: - name: channel_id in: path required: true schema: type: string title: Channel Id - name: template_id in: path required: true schema: type: string title: Template Id - name: all_languages in: query required: false schema: type: boolean description: Delete all language versions of this template, not just this one. default: false title: All Languages description: Delete all language versions of this template, not just this one. responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/messages/send-template: post: tags: - api summary: Send Template Message description: 'Send an approved WhatsApp template without automatic retry. Variables must match the cached template definition. Positional variables use arrays such as `{"body": ["Pablo", "ORD-123"]}`; named variables use objects such as `{"body": {"customer_name": "Pablo"}}`. `recipient` follows the same preflight as the text endpoint: trimmed, then `400 invalid_recipient` / `recipient_equals_sender` before any Meta call.' operationId: send_template_message_api_v1_messages_send_template_post requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SendTemplateRequest' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/SendMessageResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' components: schemas: AvailableSlotsByTier: properties: total: type: integer title: Total description: Total free active slots for this channel type. starter: type: integer title: Starter description: Free Starter subscription slots for this channel type. pro: type: integer title: Pro description: Free Pro subscription slots for this channel type. type: object required: - total - starter - pro title: AvailableSlotsByTier description: Free active subscription slots for one channel type. ChannelListResponse: properties: channels: items: $ref: '#/components/schemas/ChannelOut' type: array title: Channels total: type: integer title: Total type: object required: - channels - total title: ChannelListResponse description: List of channels. ChannelOut: properties: id: type: string title: Id description: Channel ID. Use this value in every other channel/message API call. channel_type: type: string title: Channel Type description: 'Channel type: ''whatsapp'', ''instagram'', or ''facebook''.' name: anyOf: - type: string - type: 'null' title: Name description: 'Display name: business name (WhatsApp), username (Instagram), or Page name (Facebook).' is_active: type: boolean title: Is Active description: True if the channel can currently send and receive messages. phone_number_id: anyOf: - type: string - type: 'null' title: Phone Number Id description: WhatsApp only — Meta's phone number ID. phone_number: anyOf: - type: string - type: 'null' title: Phone Number description: WhatsApp only — human-readable phone number. waba_id: anyOf: - type: string - type: 'null' title: Waba Id description: WhatsApp only — WhatsApp Business Account (WABA) ID. quality_rating: anyOf: - type: string - type: 'null' title: Quality Rating description: WhatsApp only — Meta's current quality rating for the number, when available. ig_account_id: anyOf: - type: string - type: 'null' title: Ig Account Id description: Instagram only — Instagram account ID. ig_username: anyOf: - type: string - type: 'null' title: Ig Username description: Instagram only — Instagram username. page_id: anyOf: - type: string - type: 'null' title: Page Id description: Instagram/Facebook — linked Facebook Page ID. webhook_url: anyOf: - type: string - type: 'null' title: Webhook Url description: Where incoming messages and delivery statuses are delivered. has_webhook_secret: type: boolean title: Has Webhook Secret description: Whether a webhook secret is configured for HMAC signature verification. default: false webhook_events: anyOf: - items: type: string type: array - type: 'null' title: Webhook Events description: Enabled event types, e.g. ["message.received", "message.delivered"]. echo_statuses: type: boolean title: Echo Statuses description: 'message.echo status mode: true = tracked (echoed external messages get a durable identity and their delivered/read/failed statuses are delivered per your webhook_events), false = relay-only (echo events without status tracking; the default). Has no effect until "message.echo" is enabled in webhook_events.' default: false connected_at: anyOf: - type: string - type: 'null' title: Connected At description: ISO-8601 UTC timestamp when the channel was connected. created_at: anyOf: - type: string - type: 'null' title: Created At description: ISO-8601 UTC timestamp when the channel record was first created. subscription: $ref: '#/components/schemas/SubscriptionInfo' description: Current subscription/billing state. The channel can send/receive only while status='active'. type: object required: - id - channel_type - is_active - subscription title: ChannelOut description: 'A connected channel — one WhatsApp number, Instagram account, or Facebook Page. Channel-type-specific fields are populated only for the relevant type (e.g. `phone_number_id`/`waba_id` for WhatsApp, `ig_username` for Instagram); the rest are null.' ChannelUpdateRequest: properties: webhook_url: anyOf: - type: string maxLength: 500 - type: 'null' title: Webhook Url description: HTTPS URL for incoming webhook delivery. Explicit ports are supported. webhook_secret: anyOf: - type: string maxLength: 64 - type: 'null' title: Webhook Secret description: Set a specific HMAC secret (max 64 characters; also use this to rotate to a new value). Omit to keep the current secret — except that setting webhook_url for the first time with no secret auto-generates one, returned in the response. webhook_events: anyOf: - items: type: string type: array - type: 'null' title: Webhook Events description: List of event types to deliver. Available events depend on channel type. echo_statuses: anyOf: - type: boolean - type: 'null' title: Echo Statuses description: 'message.echo status mode: true = tracked (echoed external messages get a durable identity and their delivered/read/failed statuses are delivered per your webhook_events), false = relay-only (echo events without status tracking; the default). Has no effect until "message.echo" is enabled in webhook_events.' subscription_id: anyOf: - type: string maxLength: 16 - type: 'null' title: Subscription Id description: 'Move the channel to another subscription, or release its subscription slot. Pass a subscription ID from GET /api/v1/subscriptions to bind the channel to it — allowed for an active or a deactivated channel, and the target subscription must be active with a free slot for this channel type. Pass an empty string ("") to unbind: this frees the slot so a different channel of the same type can be connected, and is allowed only for a channel already deactivated via DELETE /api/v1/channels/{channel_id}. Omit the field (or send null) to leave the current binding untouched. Note that moving a channel from a Pro to a Starter subscription immediately removes access to media and template sending, and that unbinding is effectively permanent: reconnecting that channel later requires a free slot again.' type: object title: ChannelUpdateRequest description: Request to update channel settings. ChannelUpdateResponse: properties: id: type: string title: Id webhook_url: anyOf: - type: string - type: 'null' title: Webhook Url webhook_secret: anyOf: - type: string - type: 'null' title: Webhook Secret webhook_events: anyOf: - items: type: string type: array - type: 'null' title: Webhook Events echo_statuses: anyOf: - type: boolean - type: 'null' title: Echo Statuses subscription: anyOf: - $ref: '#/components/schemas/SubscriptionInfo' - type: 'null' description: Subscription state of the channel after the update. Same shape as `subscription` on GET /api/v1/channels/{channel_id}. type: object required: - id title: ChannelUpdateResponse description: Response after channel update. ExchangeCodeRequest: properties: code: type: string title: Code description: One-time completion code from OAuth redirect webhook_url: anyOf: - type: string maxLength: 500 - type: 'null' title: Webhook Url description: HTTPS URL for incoming webhook delivery. Explicit ports are supported. If provided, webhook is configured automatically — no separate PATCH needed. webhook_secret: anyOf: - type: string maxLength: 64 - type: 'null' title: Webhook Secret description: 'HMAC secret used to sign webhook deliveries (verify it via the X-Webhook-Signature header). Optional: if you set webhook_url without supplying this, Fiwano auto-generates a secret and returns it in the response. Pass your own value (max 64 characters) to use a specific secret instead.' webhook_events: anyOf: - items: type: string type: array - type: 'null' title: Webhook Events description: List of event types to deliver. Available events depend on channel type. If omitted, no events are delivered until configured. echo_statuses: anyOf: - type: boolean - type: 'null' title: Echo Statuses description: 'message.echo status mode: true = tracked (echoed external messages get a durable identity and their delivered/read/failed statuses are delivered per your webhook_events), false = relay-only (echo events without status tracking; the default). Has no effect until "message.echo" is enabled in webhook_events.' type: object required: - code title: ExchangeCodeRequest description: Request to exchange completion code for channel data. ExchangeCodeResponse: properties: channel_id: type: string title: Channel Id channel_type: type: string title: Channel Type name: anyOf: - type: string - type: 'null' title: Name phone_number_id: anyOf: - type: string - type: 'null' title: Phone Number Id phone_number: anyOf: - type: string - type: 'null' title: Phone Number ig_account_id: anyOf: - type: string - type: 'null' title: Ig Account Id ig_username: anyOf: - type: string - type: 'null' title: Ig Username page_id: anyOf: - type: string - type: 'null' title: Page Id webhook_url: anyOf: - type: string - type: 'null' title: Webhook Url webhook_secret: anyOf: - type: string - type: 'null' title: Webhook Secret webhook_events: anyOf: - items: type: string type: array - type: 'null' title: Webhook Events echo_statuses: anyOf: - type: boolean - type: 'null' title: Echo Statuses type: object required: - channel_id - channel_type title: ExchangeCodeResponse description: Response with channel data after code exchange. HTTPValidationError: properties: detail: items: $ref: '#/components/schemas/ValidationError' type: array title: Detail type: object title: HTTPValidationError RedirectCreateRequest: properties: uri_pattern: type: string maxLength: 500 title: Uri Pattern description: HTTPS redirect URI. Explicit ports are supported. Prefer an exact URI; wildcards are limited to paths, queries, or a complete left-most hostname label such as https://*.example.com/* type: object required: - uri_pattern title: RedirectCreateRequest description: Request to add redirect URI. RedirectCreateResponse: properties: id: type: string title: Id uri_pattern: type: string title: Uri Pattern type: object required: - id - uri_pattern title: RedirectCreateResponse description: Response after creating redirect. RedirectListResponse: properties: redirects: items: $ref: '#/components/schemas/RedirectOut' type: array title: Redirects type: object required: - redirects title: RedirectListResponse description: List of redirect URIs. RedirectOut: properties: id: type: string title: Id uri_pattern: type: string title: Uri Pattern created_at: anyOf: - type: string - type: 'null' title: Created At type: object required: - id - uri_pattern title: RedirectOut description: Allowed redirect URI. SendMediaRequest: properties: channel_id: type: string title: Channel Id description: Channel ID to send from recipient: type: string maxLength: 100 title: Recipient description: Recipient identifier media_type: type: string title: Media Type description: 'Media type: image, audio, video, or document' media_url: type: string maxLength: 2048 title: Media Url description: HTTPS URL of the media file. For non-public content, use a signed URL (S3 presigned, GCS Signed, R2 signed, or HMAC). Max length 2048. caption: anyOf: - type: string maxLength: 1024 - type: 'null' title: Caption description: Caption (WhatsApp image/video/document) filename: anyOf: - type: string maxLength: 255 - type: 'null' title: Filename description: Filename (WhatsApp document only) type: object required: - channel_id - recipient - media_type - media_url title: SendMediaRequest description: 'Request to send a media message. Requires a Pro license. Meta fetches the file directly from `media_url`. For non-public content, use a signed URL — presigned S3, GCS Signed URL, Cloudflare R2 signed URL, or HMAC-signed URL on your own server. Public URLs are accessible to anyone who learns them; only use them for non-sensitive content. Use image, audio, video, or document across all supported channels. Provider-specific attachment names are handled internally.' SendMessageRequest: properties: channel_id: type: string title: Channel Id description: Channel ID to send from recipient: type: string maxLength: 100 title: Recipient description: Recipient identifier (phone number for WhatsApp, IGSID for Instagram, PSID for Facebook) text: type: string minLength: 1 title: Text description: Message text containing at least one non-whitespace character type: object required: - channel_id - recipient - text title: SendMessageRequest description: Request to send a message. Only text messages are supported. SendMessageResponse: properties: success: type: boolean title: Success message_id: anyOf: - type: string - type: 'null' title: Message Id error: anyOf: - type: string - type: 'null' title: Error error_code: anyOf: - type: integer - type: 'null' title: Error Code description: Meta error code (when failure originates at Meta). See documentation for common send error codes. status: anyOf: - type: string - type: 'null' title: Status description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (transient text/media failure — retried in the background), or ''failed'' (terminal for this request; see error/error_code). Template sends are never queued.' type: object required: - success title: SendMessageResponse description: Response after sending a message. SendTemplateRequest: properties: channel_id: type: string title: Channel Id description: WhatsApp channel ID to send from template_name: type: string title: Template Name description: Template name (e.g., 'order_confirmation') language: type: string title: Language description: Template language code (e.g., 'en_US') recipient: type: string maxLength: 100 title: Recipient description: Recipient phone number (without +, e.g., '1234567890') variables: anyOf: - type: object - type: 'null' title: Variables description: 'Variable values keyed by component type. Positional: {"header": ["Sale"], "body": ["Pablo", "ORD-123"], "buttons": [{"index": 0, "value": "promo"}]}. Named: {"body": {"customer_name": "Pablo", "order_number": "ORD-123"}}. Omit if template has no variables.' type: object required: - channel_id - template_name - language - recipient title: SendTemplateRequest description: "Request to send a WhatsApp template message.\n\nVariables must\ \ match the template's parameter definitions.\nOnly APPROVED templates can\ \ be sent.\n\nFor positional templates, provide variables as arrays:\n \ \ {\"body\": [\"Pablo\", \"ORD-123\"]}\n\nFor named templates, provide variables\ \ as objects:\n {\"body\": {\"customer_name\": \"Pablo\", \"order_number\"\ : \"ORD-123\"}}" SenderProfileResponse: properties: channel_id: type: string title: Channel Id channel_type: type: string title: Channel Type user_id: type: string title: User Id profile: anyOf: - type: object - type: 'null' title: Profile description: 'Profile data from Meta, normalized and minimized by channel type. null if profile is unavailable. Instagram: username, name, profile_pic, follower_count, is_verified_user. Facebook: display name in first_name; last_name and profile_pic when available from Meta. WhatsApp is not supported.' examples: - follower_count: 46 is_verified_user: false name: Roman Babakin profile_pic: https://scontent.cdninstagram.com/v/t51.2885-19/... username: winnerzzz - first_name: Roman last_name: Babakin profile_pic: https://platform-lookaside.fbsbx.com/platform/profilepic/... cached: type: boolean title: Cached description: Whether the result was served from cache default: false type: object required: - channel_id - channel_type - user_id title: SenderProfileResponse description: Sender profile from Meta Graph API. SetupUrlRequest: properties: channel_type: type: string pattern: ^(whatsapp|instagram|facebook)$ title: Channel Type description: 'Channel type: whatsapp, instagram, or facebook' redirect_uri: type: string maxLength: 500 title: Redirect Uri description: Exact HTTPS URL to redirect after OAuth completion. Explicit ports are supported. Must match allowed_redirects. type: object required: - channel_type - redirect_uri title: SetupUrlRequest description: Request to generate channel setup URL. SetupUrlResponse: properties: setup_url: type: string title: Setup Url description: URL to open in popup/browser for channel setup session_id: type: string title: Session Id expires_at: type: string title: Expires At type: object required: - setup_url - session_id - expires_at title: SetupUrlResponse description: Response with channel setup URL. SubscriptionAssignedChannel: properties: channel_id: type: string title: Channel Id description: Fiwano channel ID assigned to this subscription. channel_type: type: string title: Channel Type description: 'Channel type: whatsapp, instagram, or facebook.' name: anyOf: - type: string - type: 'null' title: Name description: Human-readable channel name when available. is_active: type: boolean title: Is Active description: Whether the bound channel is currently active. type: object required: - channel_id - channel_type - is_active title: SubscriptionAssignedChannel description: 'Channel currently assigned to this subscription for one channel type. An inactive channel still occupies its slot while it remains bound to an active subscription. This preserves the owner''s reconnect path and matches the billing gate used by channel setup.' SubscriptionInfo: properties: id: anyOf: - type: string - type: 'null' title: Id description: Subscription ID this channel is bound to — the same value as `subscriptions[].id` in GET /api/v1/subscriptions. Pass it back in PATCH /api/v1/channels/{channel_id} to move the channel between subscriptions. Null when status='none'. status: type: string title: Status description: 'Subscription status: ''active'', ''expired'', ''canceled'', or ''none'' (no license bound — channel cannot send/receive messages).' source: anyOf: - type: string - type: 'null' title: Source description: 'Origin of the license: ''trial'' (auto-created on signup), ''paddle'' (paid subscription), or ''enterprise'' (admin-granted custom subscription). Null when status=''none''.' tier: anyOf: - type: string - type: 'null' title: Tier description: 'License tier: ''starter'' or ''pro''. Null when status=''none''.' expires_at: anyOf: - type: string - type: 'null' title: Expires At description: ISO-8601 UTC timestamp when the current billing period / license ends. Null when status='none' or for perpetual enterprise licenses. auto_renew: type: boolean title: Auto Renew description: True only for an active Paddle subscription that is set to renew automatically at `expires_at`. False for trial, enterprise, or any Paddle subscription where the user has scheduled/performed a cancellation. default: false type: object required: - status title: SubscriptionInfo description: 'Subscription/license state for a channel. Always present on ChannelOut. When the channel is not bound to any license (orphaned after expiry/cancellation, or briefly between connect and auto-assign), `status` is `"none"` and all other fields are null/false. `expires_at` is `null` when no license is bound; in normal operation every active license (trial / Paddle / Enterprise) carries an explicit expiration date. (The schema still permits `NULL` for legacy admin- granted rows — clients should treat that as "no announced end date".) `auto_renew` is `true` only for an active Paddle subscription with no scheduled cancellation. Once the user cancels in Paddle (or the subscription enters a non-renewing state) it flips to `false` and the license will lapse at `expires_at` unless resumed.' SubscriptionOut: properties: id: type: string title: Id description: Fiwano subscription ID. status: type: string title: Status description: 'Subscription status: active, expired, or canceled.' source: type: string title: Source description: 'Origin: trial, paddle, or enterprise.' tier: type: string title: Tier description: 'Subscription tier: starter or pro.' starts_at: anyOf: - type: string - type: 'null' title: Starts At description: ISO-8601 UTC timestamp when the subscription started. expires_at: anyOf: - type: string - type: 'null' title: Expires At description: ISO-8601 UTC timestamp when the current period ends. Treat status as the source of truth for operability; Paddle grace can leave an active subscription with expires_at in the past. auto_renew: type: boolean title: Auto Renew description: True only for an active Paddle subscription set to renew automatically. default: false assigned_channels: additionalProperties: anyOf: - $ref: '#/components/schemas/SubscriptionAssignedChannel' - type: 'null' type: object title: Assigned Channels description: Channels assigned to this subscription by channel type. Values are null when no channel is assigned for that type. Use top-level available_slots for current connection availability. type: object required: - id - status - source - tier - assigned_channels title: SubscriptionOut description: 'A Fiwano subscription/license and its channel-slot availability. Public API uses the product term "subscription"; internally this maps to a License row. One subscription grants one slot for each channel type.' SubscriptionsResponse: properties: subscriptions: items: $ref: '#/components/schemas/SubscriptionOut' type: array title: Subscriptions total: type: integer title: Total description: Total number of subscriptions returned. active_total: type: integer title: Active Total description: Number of currently active subscriptions. available_slots: additionalProperties: $ref: '#/components/schemas/AvailableSlotsByTier' type: object title: Available Slots description: Free active subscription slots by channel type, split by tier. `total` is the sum of Starter and Pro free slots. type: object required: - subscriptions - total - active_total - available_slots title: SubscriptionsResponse description: Subscriptions and aggregate slot availability for the authenticated user. TemplateComponentInput: properties: type: type: string title: Type description: 'Component type: HEADER, BODY, FOOTER, BUTTONS' format: anyOf: - type: string - type: 'null' title: Format description: 'Header format: TEXT (media not yet supported)' text: anyOf: - type: string - type: 'null' title: Text description: Component text. Use {{1}}, {{2}} for positional or {{name}} for named variables example: anyOf: - type: object - type: 'null' title: Example description: Example values for variables (required by Meta for review) buttons: anyOf: - items: type: object type: array - type: 'null' title: Buttons description: Button definitions (for BUTTONS component) type: object required: - type title: TemplateComponentInput description: 'A single template component for creation/update. Components define the structure of a WhatsApp message template.' TemplateCreateRequest: properties: name: type: string maxLength: 512 pattern: ^[a-z0-9_]+$ title: Name description: Template name. Lowercase alphanumeric and underscores only. Max 512 chars. category: type: string pattern: ^(MARKETING|UTILITY|AUTHENTICATION)$ title: Category description: 'Template category: MARKETING, UTILITY, or AUTHENTICATION' language: type: string title: Language description: Language code (e.g., en_US, ru, es) components: items: $ref: '#/components/schemas/TemplateComponentInput' type: array title: Components description: Template components (HEADER, BODY, FOOTER, BUTTONS). BODY is required. parameter_format: type: string pattern: ^(positional|named)$ title: Parameter Format description: 'Variable format: ''positional'' for {{1}}, {{2}} or ''named'' for {{customer_name}}' default: positional type: object required: - name - category - language - components title: TemplateCreateRequest description: 'Request to create a WhatsApp message template. The template will be submitted to Meta for review (status=PENDING). Review typically takes up to 24 hours.' TemplateListResponse: properties: templates: items: $ref: '#/components/schemas/TemplateOut' type: array title: Templates total: type: integer title: Total synced: type: boolean title: Synced default: false type: object required: - templates - total title: TemplateListResponse description: List of templates. TemplateOut: properties: id: type: string title: Id meta_template_id: type: string title: Meta Template Id name: type: string title: Name language: type: string title: Language category: type: string title: Category status: type: string title: Status components: items: {} type: array title: Components parameter_format: type: string title: Parameter Format default: positional variables: anyOf: - $ref: '#/components/schemas/TemplateVariablesSummary' - type: 'null' synced_at: anyOf: - type: string - type: 'null' title: Synced At created_at: anyOf: - type: string - type: 'null' title: Created At type: object required: - id - meta_template_id - name - language - category - status - components title: TemplateOut description: Template data returned by API. TemplateUpdateRequest: properties: components: items: $ref: '#/components/schemas/TemplateComponentInput' type: array title: Components description: New components (replaces all existing) category: anyOf: - type: string - type: 'null' title: Category description: New category (only for REJECTED or PAUSED templates) type: object required: - components title: TemplateUpdateRequest description: 'Request to update a template''s components. All components are replaced entirely (partial update not supported by Meta). Approved templates: max 10 edits per 30 days, 1 per 24 hours.' TemplateVariableInfo: properties: position: type: integer title: Position name: anyOf: - type: string - type: 'null' title: Name example: anyOf: - type: string - type: 'null' title: Example type: object required: - position title: TemplateVariableInfo description: Variable info for display/documentation. TemplateVariablesSummary: properties: total_count: type: integer title: Total Count default: 0 parameter_format: type: string title: Parameter Format default: positional header: anyOf: - items: $ref: '#/components/schemas/TemplateVariableInfo' type: array - type: 'null' title: Header body: anyOf: - items: $ref: '#/components/schemas/TemplateVariableInfo' type: array - type: 'null' title: Body buttons: anyOf: - items: type: object type: array - type: 'null' title: Buttons type: object title: TemplateVariablesSummary description: Summary of all variables in a template. ValidationError: properties: loc: items: anyOf: - type: string - type: integer type: array title: Location msg: type: string title: Message type: type: string title: Error Type type: object required: - loc - msg - type title: ValidationError securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: API key in the format mip_live_xxx. Create one on the API Keys page in the portal. security: - ApiKeyAuth: [] ```