# 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, the X-API-Key header and Authorization: Bearer.
2. **Compatibility** — Fiwano's compatibility policy: the v1 API contract is stable, every change is additive, and what an integration must do to stay compatible.
3. **Quickstart** — Build your first Fiwano integration in five minutes: create an API key, connect a Meta channel, send a message and receive signed webhooks.
4. **Channels** — Connect, inspect, update, re-license and reconnect WhatsApp, Instagram and Facebook Messenger channels through the Fiwano API and hosted setup flow.
5. **Sending Messages** — Send text, media and WhatsApp template messages through one API, with channel-specific delivery behavior, retry handling and status webhooks.
6. **Receiving Messages** — Receive WhatsApp, Instagram and Messenger events through signed webhooks: incoming messages, echoes of messages sent outside Fiwano, media and delivery statuses.
7. **WhatsApp Templates** — Create and manage WhatsApp message templates, understand Meta approval states, configure variables and send approved templates outside the 24-hour window.
8. **Capabilities** — Compare WhatsApp, Instagram and Messenger capabilities, license tiers, rate limits, media constraints and channel-specific messaging windows.
9. **Errors** — HTTP errors from the Fiwano API and the Meta error codes of a failed WhatsApp, Instagram or Messenger send, with what each means and what to do.
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. Clients that only support bearer tokens can send the same key as `Authorization: Bearer YOUR_API_KEY`; when both headers are present, `X-API-Key` is used.

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.

API keys are secret: call the API from your server or backend functions, never from client-side code. The API does not accept cross-origin browser (CORS) requests.

---

## 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, error hints, and the inbound message type `data.type` (an unknown type is handled like `unsupported`).

**What stays fixed:** existing endpoints, field names, types and meanings; the `X-API-Key` authentication (`Authorization: Bearer` is an accepted alternative); 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` and `"health": {"status": "ok", …}` — 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. Acknowledge first, then do
slow work such as generating an AI reply. 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 `success: true` with a `message_id` — check it: a
`200` can still say `"status": "failed"` (see
[Delivery and retries](/documentation/sending-messages#delivery-and-retries)). 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).
</content>
</invoke>

---

## 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. Field-level schemas
are in the **[API Reference](/documentation/api)**.

### 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 app in control of conversations** (Instagram and Facebook
  Messenger). Meta gives one app control of each conversation at a time
  (*Conversation Routing*), so Fiwano must be the *Default routing app* to reply.
  Set it in Facebook Page → Settings → Page setup → *Instagram conversation
  routing* / *Messenger conversation routing* (for Instagram without a Page: Meta
  Business Suite → Settings → Integrations → *Conversation Routing*), turn off
  *Take control of conversations* for other apps or disconnect them, and don't
  work these chats from the Meta Business Suite / Page inbox or with Meta AI —
  either hands control to Meta's own inbox. If Fiwano is not in control, incoming
  messages still reach your webhook but replies are rejected with
  [error `10`](/documentation/errors#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.
6. Generate a **webhook secret** so deliveries are signed — see [Webhook secret](#webhook-secret).

The webhook 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 user connects a Meta account that is already one of your channels,
`exchange-code` returns that existing `channel_id` — see
[Reconnecting a channel](#reconnecting-an-inactive-channel).

The request is refused with `402` when the account has no active subscription,
and with `409` when every subscription slot for that channel type is already
taken and none of the occupying channels can be reconnected through this flow.
The `409` body is structured: `detail.code` is `no_free_slot` and
`detail.occupied_by` lists the channels holding the slots — see
[subscription slots](#subscription-slots) for how to free one.

**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 or did not complete the Meta dialog. `slot_occupied` — the user connected a *different* Meta account than the one holding your subscription slot; `message` names the channel to reconnect or release (see [subscription slots](#subscription-slots)). `session_expired` — the setup URL expired before the flow finished; request a new one. `setup_failed` — anything else that stopped setup (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.

Most events exist on every channel; `message.sent` is WhatsApp-only and
`conversation.referral` is Instagram and Facebook only. The full list with
payloads is on the **[Receiving Messages](/documentation/webhooks#event-types)** page.
An event you list that isn't valid for the channel type is 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**; otherwise Fiwano retries and emails you — see
**[Retry Policy](/documentation/webhooks#retry-policy)**. So enable **only the events
you actually handle**, and return 2xx as soon as you've accepted the payload (do
slower work afterwards).

### 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=<hmac>` header — see **[Verifying Signatures](/documentation/webhooks#verifying-signatures)**
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 a channel **keeps** its existing secret (see
  [Reconnecting a 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 verification snippet).

### 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 `support@fiwano.com`.

### 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
```

### Channel health

A channel can stay active and still be unable to work, when Meta no longer lets
Fiwano work with the account or number: the app was removed in Meta Business
settings, a required permission was revoked, the Page or WhatsApp account became
unavailable, the WhatsApp number is not on the WhatsApp Business Platform (the
*WhatsApp Business App* connection was not completed), or Meta blocked the
account. Fiwano then marks the channel **Action required** in the portal and
emails the account owner; the notice and the email say what to do. Until the
cause is fixed, sends may fail — for example with [error `190`](/documentation/errors#send-error-codes).

Every channel in the API carries the same state as `health`:

```json
"health": {
  "status": "action_required",
  "reason": "(#190) Error validating access token: …",
  "since": "2026-10-06T10:00:00Z"
}
```

`status` is `ok` or `action_required` — branch on it. `reason` is human-readable
text, usually Meta's own error with its code, which you can search in Meta's
documentation; do not branch on it. `since` is when the problem was detected.
`health` returns to `ok` by itself once Meta allows access again, and on
reconnect. `is_active` stays `true`: it turns `false` only when the channel is
deactivated. Failures of individual messages are not part of `health` — they
come in the send response and the `message.failed` webhook, and are listed on
the channel's **Delivery problems** page in the portal.

### Reconnecting a channel

Reconnect a deactivated channel, or one marked *Action required*, by running the
**same connection flow again for the same Meta account** (same WhatsApp number,
Instagram account, or Facebook Page). When Meta has blocked the account, resolve
it in Meta first — reconnecting alone does not clear it:

- The existing channel is **updated 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 subscription**: the channel must still hold
  one, or you must have a free slot. Otherwise `setup-url` is refused (`402` or
  `409`, see above) — bind a subscription first, in the portal's Billing page or
  via `PATCH /api/v1/channels/{channel_id}`.
- If the user completes the dialog for a **different** Meta account while the
  deactivated channel still holds the slot, the flow is refused at the end with
  `error=slot_occupied` and nothing is created. Reconnect the same account, or
  release the slot 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
  `support@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. Empty or whitespace-only `text` is rejected with `422`.

An obviously invalid `recipient` (empty, no digits, or a non-numeric PSID/IGSID) is
rejected with `400 invalid_recipient` before Meta is called; any other WhatsApp number
is passed to Meta as is, and Meta's own rejection comes back as `status: "failed"`.
See [HTTP errors](/documentation/errors#http-errors).

### Media messages

`POST /api/v1/messages/send-media` — **Pro license required.** Meta fetches the file
directly from `media_url`. Pass `media_type`
(`image`, `audio`, `video`, `document`, or `sticker` — see [Stickers](#stickers))
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; temporary
ones, such as `131053`, return `queued` and are retried like 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).

#### Stickers

`media_type: "sticker"` sends a sticker; what it takes depends on the channel,
because Meta's platforms differ:

| Channel | Field | What Meta accepts |
|---|---|---|
| WhatsApp | `media_url` | a WebP file, 512×512 px, up to 100 KB (static) or 500 KB (animated) |
| Facebook Messenger | `sticker_id` | a sticker from Meta's own catalog — `369239263222822` is the thumbs up — or the `media.sticker_id` of a sticker a user sent you |
| Instagram | — | no sticker message exists; send an `image` instead |

```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": "sticker", "media_url": "https://my-bucket.s3.amazonaws.com/thanks.webp?X-Amz-Signature=..."}'
```

`caption` and `filename` are ignored for stickers. The wrong field for the
channel — `sticker_id` on WhatsApp, `media_url` on Messenger, any sticker on
Instagram — is answered with `400 invalid_media_request` before Meta is called
(`reason` says which). A WebP that breaks WhatsApp's rules, or a WebP sent as
`image`, fails at once with Meta code `131053` and a hint; it is not retried.
An inbound sticker arrives as `image` with `media.sticker: true` — see
[Receiving Messages](/documentation/webhooks#media-messages).

### 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 answer `200` with `success`, `message_id`, `status`,
`error` and `error_code`. A `200` alone does not mean the message went out —
read `status`:

| `status` | `success` | Meaning | What to do |
|---|---|---|---|
| `sent` | `true` | Meta accepted the message | Follow it with [delivery-status webhooks](/documentation/webhooks#delivery-status-tracking) |
| `queued` | `true` | Fiwano is finishing the send in the background; `message_id` is final | Do not resend. The outcome arrives as webhooks: the usual statuses, or [`message.failed`](/documentation/webhooks#event-types) |
| `failed` | `false` | Not sent, and not retried | Read `error` and `error_code` — see [send error codes](/documentation/errors#send-error-codes) |

A request Fiwano refuses before calling Meta — `text_too_long`,
`invalid_recipient`, an inactive channel, a rate limit — gets an
[HTTP error](/documentation/errors#http-errors) instead of `failed`. On `send`
and `send-media`, a `failed` message is also reported to the channel owner by
email.

In the portal, each channel's **Delivery problems** page (Messages tab) lists
the last 7 days of messages that are `queued` for a retry (with the attempt
number and the next try) or `failed`, with Meta's code and text — the same
`error` the API and `message.failed` return. This includes WhatsApp messages
that Meta accepted and later reported as failed.

A send is `queued` for one of two reasons:

- **Meta failed temporarily** (text and media) — a network error, a Meta `5xx`,
  a rate limit, or a temporary error code. Fiwano retries up to 7 times over
  ~20 minutes. The channel owner is emailed after 3 failed retries, and again if
  all of them fail; you then receive `message.failed`. `send-template` does not
  retry: any error Meta returns is `failed`, and you decide whether and when to
  resend.
- **Meta answered slowly** (any send) — see below.

#### Slow Meta responses

Meta occasionally takes more than 30 seconds — sometimes over a minute — to
answer a send, most often while it downloads a large file. The call then returns
`queued` and Fiwano waits for Meta to confirm the message:

- Usually the confirmation comes and the normal `message.sent` /
  `message.delivered` / `message.read` webhooks follow.
- If Meta confirms nothing within a few minutes, Fiwano sends a text or media
  message once more (never a template). The first attempt may have gone through
  after all, so in rare cases the recipient gets the message twice — a duplicate
  is preferred to a lost message.
- If nothing is confirmed in the end, the message becomes `failed`: you receive
  `message.failed` and the channel owner is emailed.

Do not resend on your side while a message is `queued`. If your own HTTP client
times out before the answer, see [Response time](#response-time).

---

## 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. The webhook envelope has the same shape on 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=<hmac_hex>
```

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 |
| `conversation.referral` | A returning user clicked an ad or an m.me / ig.me link into an existing conversation without writing; carries the same [`referral`](#referral) block as `message.received` and reopens the 24-hour window (beta) | Instagram, Facebook |
| `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` | Your message could not be delivered | WhatsApp, Instagram, Facebook |

\* `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` (a Fiwano UUID). Every status webhook for that message
carries the same `message_id`, in the same format on all channels.

- 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).
- **Read cascading:** when a user reads a conversation, Fiwano sends a separate `message.read` webhook for *each* unread message you sent to that user — not just the latest one.
- 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: `text`, `image`, `audio`, `video`, `document`, `share`, or `unsupported`. Route on it. Should a type your integration does not recognize ever appear, handle it like `unsupported` (see [Compatibility](/documentation#compatibility)). The four media types are exactly the values accepted as the outbound `media_type`, so no type mapping is needed to forward a media message — only the file has to be re-hosted, because `download_url` is authenticated (see [Downloading inbound media](#downloading-inbound-media)).

**Stickers arrive as `image`** on every channel, with `media.sticker: true` so you can tell them from photos. A WhatsApp sticker is a WebP file (`mime_type: "image/webp"`); a Messenger sticker also carries Meta's persistent `media.sticker_id` (`369239263222822` is the thumbs up). Instagram does not deliver stickers at all. To send a sticker back use [`media_type: "sticker"`](/documentation/sending-messages#stickers) — forwarding a WebP as `image` is rejected by WhatsApp.

Instagram and Facebook Messenger also use attachments for things that are not a file. A shared post, reel or story mention arrives as [`type: "share"`](#shares); a location pin or a product card arrives 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. 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"`.

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. |
| `sticker` | bool | Present only when the image is a sticker (`true`) — WhatsApp (WebP) and Facebook Messenger. |
| `sticker_id` | string | Facebook Messenger stickers only — Meta's persistent sticker id. Send it back with `media_type: "sticker"`. |
| `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 — button taps and menu choices (all channels)

When a user picks one of the options you offered, the choice arrives as an ordinary `text` message whose `text` is the label they saw — a WhatsApp template quick-reply button, a WhatsApp interactive reply button or list row, an Instagram or Messenger quick reply, and Messenger / Instagram postbacks (Get Started, ice breakers, persistent menu, template buttons):

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:31:00Z",
  "data": {
    "message_id": "wamid.yyy",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "text",
    "text": "Confirm",
    "reply_to": {"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
  }
}
```

There is no separate event and no machine id for the button: one text handler covers typed answers and taps alike. Which message the button belonged to is in [`reply_to`](#reply-to) — on WhatsApp a template tap always quotes the template send, so `reply_to.message_id` is the UUID you got from `send-template`.

#### message.received — shared posts, reels and story mentions

On Instagram and Facebook Messenger a user can share a post or a reel into the conversation, or mention your account in their story. These arrive as `type: "share"`:

```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": "share",
    "share_type": "post",
    "caption": "Sunset at the pier",
    "share": {"url": "https://www.instagram.com/p/ABC123/", "expires_at": null}
  }
}
```

| Field | Description |
|---|---|
| `share_type` | `post` (a shared post), `reel` (a shared reel), `story_mention` (Instagram: the user mentioned you in their story) |
| `share.url` | Meta's link to the shared content. A post or reel link opens in Instagram / Facebook; a story mention links to the story media itself |
| `share.expires_at` | `story_mention` only: the story disappears about 24 hours after it was posted (possibly earlier). `null` for posts and reels |
| `caption` | The caption of the shared post or reel, when Meta provides it. Absent for story mentions |

Shared content is not downloaded and there is no `data.media` block: a post belongs to its author, and Meta does not allow apps to store story media. A user's own photo or video sent as a message is still an ordinary `image` / `video` event.

#### Replies and quoted messages — `reply_to`

When a user replies to a specific message (WhatsApp "Reply", Instagram / Messenger swipe-to-reply, a WhatsApp template button tap), the event carries `reply_to` next to `type`:

```json
"reply_to": {"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
```

`reply_to.message_id` is **the id you already have** for the quoted message: the Fiwano UUID if it is a message you sent through Fiwano (or received as `message.echo`), or the provider id from `data.message_id` if it is an earlier message from the user. Compare it with the ids you stored and you know which message was quoted; there is no separate origin flag. For a message with several attachments it is the id of the first part. In the rare case where a reply reaches Fiwano before the quoted send is recorded, the provider id is passed through as is.

An Instagram reply to your story carries the story instead of a message id:

```json
"reply_to": {
  "story": {
    "id": "17900000000000009",
    "url": "https://lookaside.fbsbx.com/…/story.jpg",
    "expires_at": "2025-01-16T10:30:00Z",
    "link_url": "https://shop.example/promo"
  }
}
```

`story.id` is the media id of your story, `story.url` a temporary link to its media, `story.expires_at` the end of its 24-hour lifetime, and `story.link_url` the link sticker the user tapped, when there was one (otherwise `null`).

`reply_to` is present on `message.received` and on [`message.echo`](#message-echo) (an operator replying to a specific message), on all three channels and for every message type. It is absent when the message is not a reply.

#### Referral context — ads and links (beta)

When a conversation starts from a Click-to-WhatsApp, Click-to-Instagram or Click-to-Messenger ad, or from an m.me / ig.me link with a `ref` parameter, Meta attaches attribution to the first inbound event. Fiwano passes it on as `data.referral` on the `message.received` that follows the click — normally the first message of the conversation; an ice breaker or Get Started tap on Instagram / Messenger arrives as `type: "text"` and carries it the same way. For a message with several attachments it is on the first part only.

> **Beta until November 2026.** The four normalised keys (`source`, `text`, `image_url`, `ref`) and the `conversation.referral` event may be adjusted; `raw` is guaranteed to stay exactly as it is, so anything built on `raw` is safe. If you plan to rely on the normalised keys or on `conversation.referral`, tell us at support@fiwano.com: should anything change, we will let you know before it does.

```json
"referral": {
  "source": "ad",
  "text": "Chat with us\nSummer Succulents are here!",
  "image_url": "https://scontent.xx.fbcdn.net/v/t45.1/...",
  "ref": null,
  "raw": {
    "source_url": "https://fb.me/3cr4Wqqkv",
    "source_id": "120226305854810726",
    "source_type": "ad",
    "headline": "Chat with us",
    "body": "Summer Succulents are here!",
    "media_type": "image",
    "image_url": "https://scontent.xx.fbcdn.net/v/t45.1/...",
    "ctwa_clid": "Aff-n8ZTODiE79d22KtAwQKj9e_mIEOOj27vDVwFjN80dp4...",
    "welcome_message": {"text": "Hi there! Let us know how we can help!"}
  }
}
```

| Field | Meaning |
|---|---|
| `source` | Where the user came from. `ad` — a paid ad (including Story placements); `link` — an m.me / ig.me link with `ref`; `product` — an Instagram Shop product page. The set is open: another source Meta reports is passed through in lower case, `unknown` means Meta sent none. |
| `text` | The ad copy the user saw. WhatsApp: headline and primary text joined by a newline. Instagram / Messenger: the ad title Meta provides. `null` for links. |
| `image_url` | The creative as an image: the picture of an image ad or the thumbnail of a video ad. `null` when Meta sends none. |
| `ref` | Your own marker from the `ref` parameter of an m.me / ig.me link or of an Instagram / Messenger ad. Always `null` on WhatsApp. |
| `raw` | Meta's referral object exactly as received: `source_id` / `ad_id`, `source_url`, `post_id`, `ctwa_clid`, `headline` / `body` / `ad_title`, `welcome_message`, `flow_id`, `product`. Field names differ per channel; see Meta's reference for your channel. |

`text` and `image_url` are meant for your model directly: the user is replying to an ad that said this and looked like this.

**A returning user — `conversation.referral` (Instagram, Messenger).** When a user who already has a conversation with you clicks an ad or an m.me / ig.me link without writing, Meta sends the attribution as a separate event with no message. The click reopens the 24-hour window, so you may reply. Enable `conversation.referral` in `webhook_events` to receive it; the payload is `data.from`, `data.from_name` and the same `referral` block. WhatsApp has no equivalent: there the attribution always arrives with a message.

```json
{
  "event": "conversation.referral",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "instagram",
  "timestamp": "2026-09-21T10:30:00Z",
  "data": {
    "from": "17841400000000777",
    "from_name": null,
    "referral": {"source": "link", "text": null, "image_url": null, "ref": "spring_promo", "raw": {"ref": "spring_promo", "source": "SHORTLINKS", "type": "OPEN_THREAD"}}
  }
}
```

What to expect from Meta:

- **One-shot.** The block is attached to the event that follows the click and is not repeated on later messages. Store it on the conversation when it arrives; Fiwano keeps no message history.
- **Creative links are temporary.** `image_url` and the URLs in `raw` are public signed Meta CDN links that need no token. Meta does not document their lifetime: fetch the image when the event arrives if you want to keep it.
- **Attribution can be incomplete.** Meta omits `raw.ctwa_clid` for ads placed in WhatsApp Status and may omit it for clicks from a web browser, after the ad was deleted, or when the user dismissed the ad context before writing. An absent click id does not mean an organic conversation.
- **Only ads and `ref` links carry attribution.** A message from the profile button, the link in an Instagram bio, a wa.me link or a QR code is an ordinary message without `referral`.
- **`raw.welcome_message.text` (WhatsApp)** is the greeting configured in the ad. WhatsApp shows it in the chat before the user writes; it is not sent through the API, so there is no outbound message and no echo for it.

Fiwano does not call Meta's Conversions API. To attribute a sale to a Click-to-WhatsApp ad, store `raw.ctwa_clid` and `raw.source_id` when the conversation starts and send the conversion event yourself within Meta's 7-day window (`action_source: business_messaging`, `messaging_channel: whatsapp`, `user_data.ctwa_clid` unhashed, `user_data.whatsapp_business_account_id`). On Instagram and Messenger use `raw.ad_id` for your own reporting; Meta has no click id for these channels.

#### 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`, `order` (catalog order), `system` (for example a number change), `edit` and `revoke` (a message edited or deleted in the WhatsApp Business app), `nfm_reply` (a WhatsApp Flow response), `poll_creation`, `poll_update`, `gif`, `group_invite`, and any other type WhatsApp reports |
| Instagram | `template` (a product or card shared from a catalog), `ephemeral` (a view-once photo or video — Meta does not deliver the content), `unsupported` (Instagram itself could not deliver the content) |
| Facebook Messenger | `template`, `location`, `appointment_booking`, `fallback` (a shared link that came without a URL), `unsupported` |

Any type not listed here arrives the same way, so an unfamiliar `unsupported_type` is still just unsupported content. Message reactions, message edits and deletions, and messages in WhatsApp groups are not delivered as webhook events. A Messenger link preview never becomes `unsupported`: the message text with the link is delivered as `text`, and a forwarded link without text arrives as `text` containing the URL.

#### 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`; a sticker is `image` with
`media.sticker: true`) 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`); a shared post or reel arrives as [`type: "share"`](#shares)
and other non-file attachments as `type: "unsupported"` with `unsupported_type` —
same as `message.received`. An echo of a reply carries [`reply_to`](#reply-to):
when an operator answers a specific customer message, `reply_to.message_id` is
that message's provider id.

**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 (all channels)

```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"}]
  }
}
```

Sent when a message you sent is not delivered **after** the send call returned:

- the send returned `queued` and Fiwano could not complete it — Meta kept
  rejecting it until the [retries](/documentation/sending-messages#delivery-and-retries)
  ran out, rejected it permanently on a retry, never confirmed it, or the channel
  was disconnected while the message waited;
- on WhatsApp, also when Meta accepted the message and reports later that it
  could not deliver it.

A send that returns `failed` right away produces no webhook — the response
already tells you. `error` is a short text reason, always present. `errors` is an
array with Meta's full error details — `code`, `title`, `message`, and
`error_subcode` / `error_data.details` when Meta gave them — present only when
Meta caused the failure. `message.failed` is final: no other status follows it for the
same `message_id`.

### 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 stored encrypted only while a delivery is being retried, and deleted once it succeeds or expires
- In the portal, the channel's **Delivery problems** page (Webhooks tab) lists the last 7 days of deliveries that are retrying, failed, or recovered after 3+ attempts, with your endpoint's error (payloads are never shown)

**Important:** Your endpoint **must respond with HTTP 2xx within 5 seconds**. Non-2xx responses or timeouts trigger the retries above.

**Respond first, then process.** Do slow work, such as generating an AI reply, after you return 2xx (on serverless platforms, with the platform's background-task mechanism). A slow response is retried and delivers the same event again — deduplicate `message.received` on `message_id`.

> **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). Results are cached for up to 5 minutes; 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)
```

Meta can later pause (`PAUSED`, low quality) or disable (`DISABLED`) an approved
template. A send then fails with `error_code` `132015` / `132016`; listing
templates with its default sync shows the current status.

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).

### Buttons

`BUTTONS` can hold `URL`, phone-number and `QUICK_REPLY` buttons. When a
recipient taps a quick-reply button, your webhook receives an ordinary
[`message.received` of `type: "text"`](/documentation/webhooks#button-taps)
whose `text` is the button label, with `reply_to.message_id` equal to the
`message_id` you got from `send-template` — so you know both which button was
chosen and which send it answers. Standalone interactive reply-button and list
messages (outside templates) cannot be sent yet, but taps on them are received
the same way.

---

## Capabilities

What each channel supports, the license tiers, and the platform limits.

### Channel Capabilities

| Feature | WhatsApp | Instagram | Facebook Messenger |
|---|---|---|---|
| Outbound text — max length | 4096 chars | 1000 chars | 2000 chars |
| Outbound media (Pro) | image, audio, video, document, sticker (WebP file) | image, audio, video, document | image, audio, video, document, sticker (Meta catalog `sticker_id`) |
| 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 (stickers as `image` + `media.sticker`) | image, audio, video, document | image, audio, video, document (stickers as `image` + `media.sticker`) |
| Delivery statuses | `sent` `delivered` `read` `failed` | `delivered` `read` `failed` | `delivered` `read` `failed` |
| 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 account 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`.

Every other public API operation (channels, subscriptions, templates, sender
profiles, media downloads, redirects) is limited to **20 requests per second per
API key**. This is a guardrail against runaway loops, not a quota: a normal
integration is nowhere near it, and one misbehaving key does not affect the other
keys of the same account. Exceeding it returns HTTP `429` with `Retry-After`. Fire
event-driven reads (media downloads, sender profiles) with a retry on `429` rather
than in one unbounded parallel burst — inbound media stays available for 60 minutes.

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:

- **Instagram & Facebook Messenger** — you can reply only within **24 hours** of
  the user's last message. These replies are **free** — Meta does not charge for
  them. There is no template workaround — wait for the user to message again.
- **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. Replies inside
  the window are **free for the first 1,000 a month per number**; Meta bills the
  replies after that, and every template. Without a payment method on the
  WhatsApp Business account, the free 1,000 still go out, and later replies are
  not delivered (error `131042`) — see
  [Messaging Costs](/documentation/messaging-costs).

### 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 | Facebook Messenger |
|---|---|---|---|
| Image | 5 MB (JPEG, PNG) | 8 MB (JPEG, PNG) | 8 MB (JPEG, PNG, GIF) |
| Sticker | 100 KB static / 500 KB animated (WebP, 512×512 px) | not available | no file — sent by Meta catalog `sticker_id` |
| Video | 16 MB (MP4, 3GPP) | 25 MB (MP4, OGG, AVI, MOV, WebM) | 25 MB |
| Audio | 16 MB (AAC, AMR, MP3, MP4, OGG) | 25 MB (AAC, M4A, WAV, MP4) | 25 MB |
| Document | 100 MB (PDF, Office, text) | 25 MB (PDF) | 25 MB |

These are Meta's limits and Meta may change them; encoding overhead can push a
file over the cap even when its size on disk looks safe. Meta does not list
accepted formats per type exhaustively, so handle the oversize or
unsupported-format rejection rather than relying on a fixed list.

#### Inbound media

- **Inbound media** (images, audio, video, documents) is stored for **60
  minutes**. Download it via `GET /api/v1/media/{media_id}` promptly after the
  webhook. 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).

---

## Errors

Fiwano reports a problem in one of two ways:

- **The request is refused** with an HTTP `4xx`/`5xx` status — see
  [HTTP errors](#http-errors). A `4xx` from a send endpoint means no message was
  sent.
- **The message fails.** The send endpoints answer `200` with `success: false`
  and `status: "failed"`, or a send that answered `queued` later ends in a
  [`message.failed`](/documentation/webhooks#event-types) webhook. When Meta
  caused it, the reason is in the [send error code](#send-error-codes).

### HTTP errors

The body has a `detail` field, usually a readable string:

```json
{ "detail": "Channel is inactive" }
```

A validation error (`422`) lists the failing fields in `detail`. Rejections you
may want to handle in code carry a structured `detail` with a stable `code`, a
`message`, and usually a `hint`:

| `detail.code` | Status | When |
|---|---|---|
| `text_too_long` | `400` | Text is over the channel's limit; `max_length` and `actual_length` are included |
| `invalid_recipient` | `400` | `recipient` is empty, has no digits, or is not a numeric PSID/IGSID on Messenger/Instagram; `reason` is `empty`, `no_digits` or `not_numeric` |
| `recipient_equals_sender` | `400` | A WhatsApp send is addressed to the channel's own number |
| `invalid_media_request` | `400` | A sticker uses the wrong field for the channel; `reason` is `media_url_required` (WhatsApp), `sticker_id_required` (Messenger) or `sticker_unsupported` (Instagram) |
| `no_free_slot` | `409` | Every subscription slot for this channel type is taken; `occupied_by` lists the channels holding them |

| Status | Meaning | What to do |
|---|---|---|
| `400` | The request cannot be carried out as sent — for example the channel is inactive or must be reconnected | Read `detail`; fix the request or [reconnect the channel](/documentation/channels#reconnecting-an-inactive-channel) |
| `401` | API key missing, malformed or revoked | Send a valid key — see [Authentication](/documentation#authentication) |
| `402` | No active subscription, or the feature needs Pro (media, templates) and the channel is on Starter | See [Subscriptions & Billing](/documentation/subscriptions) |
| `403` | The account is inactive, or the OAuth code belongs to another account | Contact support if the account should be active |
| `404` | Not found, or it belongs to another account | Check the ID |
| `409` | No free subscription slot (`no_free_slot`) | Reconnect a channel, release a slot or add a subscription — see [subscription slots](/documentation/channels#subscription-slots) |
| `410` | Inbound media has expired | Download within 60 minutes — see [Downloading inbound media](/documentation/webhooks#downloading-inbound-media) |
| `422` | A field is missing, has the wrong type or breaks a constraint | Read the field list in `detail` |
| `429` | Rate limit — per channel on sends, per API key on everything else | Retry after `Retry-After` — see [rate limits](/documentation/capabilities#rate-limits) |
| `502` | Meta failed on a sender-profile or template-management call (send endpoints never return it) | Retry later |
| `503` | Temporarily unavailable | Retry after `Retry-After` |

Do not rely on the body of a `5xx`; use the status and `Retry-After`. Codes per
endpoint are in the [API Reference](/documentation/api).

### Send error codes

When Meta rejects a message, its error code reaches you unchanged:

- in the send response, as `error_code` next to `status: "failed"`;
- in a `message.failed` webhook, as `errors[].code`, with `error_subcode` and
  `error_data.details` when Meta gave them.

`error` carries the readable reason: Meta's own text, plus a hint from Fiwano
where Meta's text does not explain the problem.

The *Retried* column applies to text and media sends: such a send answers
`queued` and Fiwano retries it — see
[Delivery and retries](/documentation/sending-messages#delivery-and-retries).
`send-template` never retries: there every error returns `failed`, with Meta's
code in `error_code`.

| Code | Meaning | Retried | What to do |
|---|---|---|---|
| `10` | Meta refuses the action. There are four unrelated causes | no | See [Error 10](#error-10) |
| `100` | Invalid parameter. Meta uses it for unrelated causes, including **a file above the [size cap](/documentation/capabilities#outbound-media-size)** and a PSID/IGSID that is not a user of this Page | no | Read `error`; check `media_url`, `media_type`, file size and `recipient` |
| `190` | Fiwano's access to the account was revoked or has expired. The channel is marked *Action required* ([`health`](/documentation/channels#channel-health) in the API) and the owner is emailed | no | [Reconnect the channel](/documentation/channels#reconnecting-an-inactive-channel) |
| `200` | WhatsApp: Meta does not allow this account to send. Not a token problem: the channel stays connected and keeps receiving | no | Check the WhatsApp Business account in Meta Business Settings |
| `368` | The account is temporarily blocked for policy violations | no | Resolve it in Meta Business Manager |
| `551` | Messenger/Instagram: this person is not receiving your messages — they blocked the Page, closed the chat, restricted business messages, or never messaged the Page | no | Nothing to fix on your side; do not resend automatically |
| `803` | The recipient does not exist or is unavailable | no | Check the recipient ID |
| `131008`, `131009` | A required parameter is missing, or a value is not valid for this channel | no | Fix the request |
| `131026` | WhatsApp: undeliverable — the number is not on WhatsApp, or the recipient has not accepted WhatsApp's terms or uses an old app version | no | Check the number; do not resend automatically |
| `131042` | A payment problem on the WhatsApp Business account in Meta — no payment method, a credit line over its limit, the account suspended. This is Meta's billing, not your Fiwano subscription | **yes** | Fix billing for the WhatsApp Business account — see [Meta's help article](https://www.facebook.com/business/help/2225184664363779) |
| `131047` | WhatsApp: more than 24 hours since the customer last wrote | no | Send an approved [template](/documentation/sending-messages#template-messages) |
| `131049`, `131050`, `130472` | WhatsApp marketing template not delivered: Meta's per-user marketing limit, the user stopped your marketing messages, or Meta held it back for an experiment | no | Do not resend automatically |
| `131051` | This message type is not supported on the channel | no | See [channel capabilities](/documentation/capabilities#channel-capabilities) |
| `131052` | Meta could not download `media_url` | no | The URL must answer `200` with a `Content-Type` matching `media_type`, and its signature must still be valid |
| `131053` | Meta could not process the media | **yes**, unless the file itself is wrong | Usually temporary. A WebP sent as `image`, or a sticker that breaks the [sticker rules](/documentation/sending-messages#stickers), fails at once with a hint |
| `131056` | WhatsApp: too many messages to the same recipient in a short time | **yes** | Slow down for that recipient |
| `131057` | WhatsApp Business account in maintenance mode, for example a throughput upgrade | **yes** | Nothing; it is temporary |
| `132000`, `132001`, `132012`, `132015`, `132016` | WhatsApp template problem: variables do not match the template (`132000`, `132012`), the template is missing or not approved in this language (`132001`), paused (`132015`) or disabled (`132016`) by Meta | no | Fix the variables, or check the template in WhatsApp Manager — see [WhatsApp Templates](/documentation/templates) |
| `133010` | The WhatsApp number is not registered on the WhatsApp Business Platform: the *WhatsApp Business App* connection was not completed | no | Reconnect choosing *WhatsApp Business App* and finish the step in the app — see [Reconnecting a channel](/documentation/channels#reconnecting-an-inactive-channel) |

Other codes reach you as Meta returns them, and Fiwano retries any code it does
not know to be permanent. Meta's full lists:
[WhatsApp](https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes),
[Messenger and Instagram](https://developers.facebook.com/docs/messenger-platform/error-codes).

#### Error `10`

Meta returns `10` for unrelated causes. The text in `error` tells them apart:

| `error` contains | Cause | What to do |
|---|---|---|
| *outside of allowed window* | Messenger/Instagram: more than 24 hours since the user last wrote | Wait for the user to write again; there is no template workaround — see [messaging windows](/documentation/capabilities#messaging-windows-24h) |
| *another app is controlling this thread* | Messenger/Instagram: Fiwano is not the app in control of the conversation — another connected app, Meta's inbox or Meta AI is | Make Fiwano the *Default routing app* — see [Prerequisites](/documentation/channels#prerequisites) |
| *Meta has temporarily restricted this Page* (Fiwano's hint, sub-code `1893063`) | Messenger/Instagram: Meta restricted the Page from sending for activity against its messaging policies. Some sends may still go through | Check the Page in Account Quality in Meta Business Suite. Reconnecting does not help; Meta lifts the restriction |
| anything else | Meta denies this action for the account | Read `error` and check the account in Meta Business Settings. Not a token problem: the channel stays connected |

---

## 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 subscription
is (re)attached.

**Naming:** the API calls it a *subscription*; the portal's Billing page calls the
same thing a *license*. One license = one subscription = one slot per channel
type. Trial, Paddle and Enterprise entitlements are all subscriptions here.

### 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 subscription 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: <next renewal>}`.
- **Paddle renewal being retried** —
  `{status: "active", source: "paddle", auto_renew: true, expires_at: <recently in the past>}`.
  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: <cutoff>}`.
  The customer cancelled in Paddle; service continues until `expires_at`, then the
  subscription ends and the channel stops sending and receiving.
- **Trial** —
  `{status: "active", source: "trial", tier: "pro", auto_renew: false, expires_at: <signup + 7 days>}`.
- **Enterprise** —
  `{status: "active", source: "enterprise", auto_renew: false, expires_at: <agreed term end>}`.
  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 subscription 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.<channel_type>.total > 0` as the signal that a new channel
of that type can be connected. A deactivated channel still occupies its slot
(see [subscription slots](/documentation/channels#subscription-slots)). 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). What each event carries and when it fires is the same as without n8n — see [Receiving Messages → Event Types](/documentation/webhooks#event-types).

| 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, Instagram, Facebook |
| `conversation.referral` (beta) | Instagram, Facebook |

In the node:

- **Track Echo Statuses** (in **Channel → Update** and **Exchange OAuth Code**) is the API's `echo_statuses` — see [message.echo](/documentation/webhooks#message-echo).
- Referral context (beta) is `{{ $json.data.referral }}`; `data.referral.text` and `data.referral.image_url` can go straight into an AI Agent prompt — see [Referral context](/documentation/webhooks#referral).

### 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` 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); `Authorization: Bearer <key>` is
    accepted when `X-API-Key` is absent. Keys are secret: call the API from your server,
    not from browser code. 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.<type>.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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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 channel of yours, setup updates that same

        channel row and returns its existing channel_id: an inactive channel is

        reactivated, an active one gets its credentials refreshed in place (this is

        how a channel that lost Meta access is repaired). A genuinely new eligible

        identity is preferred when both new and inactive assets are available.


        Refused with 402 when the account has no active subscription, and with 409

        (`detail.code` = `no_free_slot`, `detail.occupied_by` = channel ids) when

        every subscription slot of this type is already taken and none of the

        occupying channels can be reconnected through this flow.'
      operationId: create_setup_url_api_v1_channels_setup_url_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupUrlRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeCodeRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
    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:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectCreateRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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, document, and sticker

        (WhatsApp: WebP `media_url`; Facebook Messenger: `sticker_id`; Instagram:

        not available — `400 invalid_media_request`). 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:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMediaRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
  /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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
      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:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendTemplateRequest'
        required: true
      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'
      security:
      - ApiKeyAuth: []
      - BearerAuth: []
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.
    ChannelHealth:
      properties:
        status:
          type: string
          enum:
          - ok
          - action_required
          title: Status
          description: '''ok'', or ''action_required'' when Meta no longer lets Fiwano
            work with this account or number (access or a permission revoked, number
            removed or not connected to the WhatsApp Business Platform, account blocked
            by Meta). The channel is not deactivated, but messages may fail until
            the owner fixes the cause — in Meta, or by running the connect flow again
            for the same account. Clears automatically once Meta''s checks pass, and
            on reconnect.'
        reason:
          anyOf:
          - type: string
          - type: 'null'
          title: Reason
          description: 'Human-readable text, usually Meta''s own error as ''(#<code>)
            <text>''. Not a code: branch on `status`. Null when status is ''ok''.'
        since:
          anyOf:
          - type: string
          - type: 'null'
          title: Since
          description: ISO-8601 UTC timestamp when the problem was detected. Null
            when status is 'ok'.
      type: object
      required:
      - status
      title: ChannelHealth
      description: 'Meta-side state of a channel: whether Meta still lets Fiwano work
        with the

        account and number. Failures of individual messages are not reflected here
        —

        they come in the send response and the `message.failed` webhook.'
    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: False only after the channel was deactivated (DELETE, or its
            subscription ended). An active channel can still have a Meta-side problem
            — see `health`.
        health:
          $ref: '#/components/schemas/ChannelHealth'
          description: Meta-side state of the channel.
        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
      - health
      - 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, document, or sticker'
        media_url:
          anyOf:
          - type: string
            maxLength: 2048
          - type: 'null'
          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. Required for every media_type except a Facebook Messenger sticker.
        sticker_id:
          anyOf:
          - type: string
            maxLength: 32
          - type: 'null'
          title: Sticker Id
          description: Meta sticker id (digits) — Facebook Messenger only, with media_type
            'sticker' instead of media_url. A catalog sticker or 369239263222822 (thumbs
            up); reuse media.sticker_id from an inbound event.
        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
      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.


        `sticker` is channel-specific: WhatsApp takes a WebP `media_url`,

        Facebook Messenger takes a Meta catalog `sticker_id` instead of a URL,

        Instagram has no sticker message. Which field is required is decided

        against the channel in the send preflight (`400 invalid_media_request`);

        the schema only checks that exactly one of them is given.'
    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
          description: 'Readable reason for a failed send: Meta''s text, plus a Fiwano
            hint where Meta''s text does not explain the problem.'
        error_code:
          anyOf:
          - type: integer
          - type: 'null'
          title: Error Code
          description: Meta's error code, unchanged, when Meta rejected the send (https://fiwano.com/documentation/errors#send-error-codes).
            Null for 'sent'/'queued' and for a failure without a Meta error (network,
            Meta 5xx without a body).
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
          description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (Fiwano
            is completing the send in the background after a temporary Meta failure
            on text/media or a slow Meta response on any send; the outcome follows
            as webhooks), or ''failed'' (not sent and not retried; see error/error_code).'
      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
      description: API key in the format mip_live_xxx. Create one on the API Keys
        page in the portal.
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      description: 'The same API key sent as `Authorization: Bearer mip_live_xxx`.
        Used only when `X-API-Key` is absent.'
      scheme: bearer
```
