# Fiwano — API Documentation

Unified REST API for WhatsApp, Instagram and Facebook Messenger.

| | |
|---|---|
| **Base URL** | `https://fiwano.com/api/v1` |
| **Format** | JSON |
| **Auth** | `X-API-Key` header |

---

## Contents

The complete Fiwano API documentation as a single file. The sections below appear in this order.

1. **Authentication** — API keys and the X-API-Key header.
2. **Errors** — Error format, HTTP status codes and the Meta send error codes returned by the message endpoints.
3. **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, verify HMAC signatures, process media and track 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. **Subscriptions & Billing** — Read each channel's subscription state, understand trial and paid billing lifecycles, and handle grace periods, expiration and channel reassignment.
10. **n8n Integration** — Use the verified Fiwano n8n community node to receive and send WhatsApp, Instagram DM and Messenger messages in automated and AI workflows.
11. **API Reference (OpenAPI)** — The complete machine-readable contract for the public /api/v1 API, generated from the live service.

---

## Authentication

All API requests require an API key in the `X-API-Key` header.

Create a key from the **API Keys** page in the portal. The full key is shown **only once** — save it securely. Lost keys cannot be recovered; revoke and create a new one.

```bash
curl https://fiwano.com/api/v1/channels \
  -H "X-API-Key: YOUR_API_KEY"
```

All keys start with `mip_live_`. Keys are hashed on our side.

---

## Errors

Every error response has a `detail` field. Most domain errors use a human-readable string:

```json
{ "detail": "Human-readable error description" }
```

Schema validation (`422`) uses a list of field errors. Some domain validation errors
instead use a structured `detail` object with a `code` field — for example
`text_too_long` when sending overlong text, or `recipient_equals_sender` (`400`) when a
WhatsApp send is addressed to the channel's own number. The per-endpoint shapes are in
the [API Reference](/documentation/api).

### HTTP status codes

| Code | Meaning | What to do |
|---|---|---|
| `200` | Success | — |
| `201` | Created | — |
| `400` | Bad request | Check the `detail` field |
| `401` | Unauthorized | Check your `X-API-Key` header |
| `402` | Payment required | Trial ended or subscription inactive — see [Subscriptions & Billing](/documentation/subscriptions) |
| `404` | Not found | Resource doesn't exist or belongs to another account |
| `422` | Validation error | Check required fields, types, and field constraints in `detail` |
| `429` | Rate limit exceeded | Back off and retry after `Retry-After` — see [rate limits](/documentation/capabilities#rate-limits) |
| `502` | Meta API error | Upstream failure. Check `detail`. Retry may help. |
| `503` | Temporarily overloaded | Transient load shedding. Retry after `Retry-After`. |

### Send error codes

The three send endpoints answer `200` even when the send fails — the outcome is
in `success`, `status`, and `error_code`. See
[Delivery and retries](/documentation/sending-messages#delivery-and-retries).

`error_code` is Meta's error code, passed through unchanged. It is present only
when the failure came from Meta; a rejection by Fiwano itself uses an HTTP
status code from the table above instead. `error` always carries a
human-readable description, and for media sends a hint about the likely cause.

| `error_code` | Meaning | Retried by Fiwano | What to do |
|---|---|---|---|
| `10`, `200` | Permission denied for this action | no | Reconnect the channel |
| `100` | Invalid parameter — Meta reuses this for several unrelated causes, including **a file above the size cap** | no | Read `error` for the specific cause; check `media_url`, `media_type`, recipient format, and [file size](/documentation/capabilities#outbound-media-size) |
| `190` | Access token expired or revoked | no | Reconnect the channel |
| `368` | Account temporarily blocked for policy violations | no | Resolve in Meta Business Manager |
| `803` | Object does not exist or is unavailable | no | Check the recipient identifier |
| `131008` | Required parameter missing | no | Fix the request payload |
| `131009` | Parameter value invalid for this channel | no | Fix the request payload |
| `131026` | Recipient is not reachable on this platform | no | Verify the recipient |
| `131047`, `131057` | 24h re-engagement window closed | no | Send an approved WhatsApp template — see [messaging windows](/documentation/capabilities#messaging-windows-24h) |
| `131051` | Unsupported message type for this channel | no | Check [channel capabilities](/documentation/capabilities#channel-capabilities) |
| `131052` | Meta could not download `media_url` | no | Verify the URL returns `200`, `Content-Type` matches `media_type`, and the signature has not expired |
| `131053` | Meta could not process the media | **yes** | Often transient; check format and size if it persists |
| `131056` | Pair rate limit between this sender and recipient | **yes** | Slow down messages to that recipient |

Codes outside this table are passed through as Meta returns them. Anything not
recognised as permanent is treated as transient and retried.

### Unverified send outcomes

Rarely, Meta accepts a send but its response never reaches Fiwano — a lost
connection or a timeout during the reply. The message may or may not have been
delivered, and Meta offers no way to ask afterwards.

Fiwano does **not** retry these: an automatic retry would risk delivering the
same message twice. The response is `success: false`, `status: "failed"`, with
an `error` that states the outcome is unverified and carries no `error_code`.

**Check the conversation before resending.** The same caution applies if your
own HTTP client times out — see
[Response time](/documentation/sending-messages#response-time).

---

## Quickstart

Fiwano puts WhatsApp, Instagram and Messenger behind one REST API. This is the
core loop in **four steps** — authenticate, connect a channel, receive a message,
reply — plus an optional fifth for messaging outside the 24-hour window. Every
request uses the base URL `https://fiwano.com` and carries your key in the
`X-API-Key` header.

### 1. Get and verify your API key

Every new account gets a **7-day free trial with full functionality** — every
channel type, media and templates, no card required. Open **API Keys** in the
[portal](https://fiwano.com) and create a key: the full key is shown **once**,
starts with `mip_live_`, and is stored only as a hash — lost keys can't be
recovered, so revoke and recreate if needed. Keep it in an environment variable
(e.g. `FIWANO_API_KEY`); never hardcode or commit it.

**Verify the key works** by listing channels:

```bash
curl https://fiwano.com/api/v1/channels -H "X-API-Key: $FIWANO_API_KEY"
```

A valid key on a fresh account (no channels yet) returns **`200`** with an empty
list — this is the success signal that you're authenticated and ready for step 2:

```json
{ "channels": [], "total": 0 }
```

An invalid or missing key returns `401`. Error shapes and status codes:
[Errors](/documentation/errors).

### 2. Connect a channel

There are **two ways to connect** — pick the one that matches who owns the
account, both covered in [Channels](/documentation/channels):

- **Your own channel** — connect it in the [portal](https://fiwano.com)
  (Channels → Connect), no code. Best when you operate the accounts yourself.
  The prerequisites (the asset must belong to a Meta Business Portfolio, and you
  must be its admin) are spelled out there.
- **Your end-users' channels** — an embedded OAuth flow you drive from your app:
  whitelist a `redirect_uri`, create a setup URL, the user completes Meta login
  inside it, and you exchange the returned one-time `code` for a `channel_id`.

OpenAPI operations for this step:

- Manage channels: `GET /api/v1/channels`, `GET /api/v1/channels/{channel_id}`, `PATCH /api/v1/channels/{channel_id}`, `DELETE /api/v1/channels/{channel_id}`
- Embedded connect flow: `POST /api/v1/channels/setup-url`, `POST /api/v1/channels/exchange-code`
- Redirect-URI whitelist: `GET /api/v1/redirects`, `POST /api/v1/redirects`, `DELETE /api/v1/redirects/{redirect_id}`

**Success:** you have a `channel_id` (the embedded flow returns it straight from
`exchange-code`), and `GET /api/v1/channels` now lists the channel with
`"is_active": true` — it can send and receive. That `channel_id` is what you pass
to every send and receive call from here on.

### 3. Receive a message

Replying to inbound is Fiwano's core use case, so set up receiving **before**
sending. Two parts:

**1. Enable events on the channel.** Delivery is opt-in — **by default no events
are delivered**. Set `webhook_events` (and a `webhook_url`) on the channel, and
enable only the events you actually handle (start with `message.received`). The
per-channel event list and how to configure it are in
[Channels](/documentation/channels).

**2. Handle the webhook.** Fiwano POSTs each enabled event to your `webhook_url`.
Your endpoint **must verify the `X-Webhook-Signature`** (HMAC-SHA256 with the
channel's `webhook_secret`) and **respond HTTP 2xx within ~5 seconds** —
otherwise Fiwano retries with backoff and emails you. Payload shapes and the
signature check are in [Receiving Messages](/documentation/webhooks).

An inbound `message.received` carries the two identifiers you need to reply
(marked below):

```jsonc
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",   // ← which of your channels received it
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",              // ← who sent it — reply to this
    "from_name": "John Doe",
    "type": "text",
    "text": "Hello!"
  }
}
```

- **`channel_id`** (top level) — the channel the message arrived on.
- **`data.from`** — the sender's id: phone number (WhatsApp), IGSID (Instagram),
  or PSID (Facebook). This is exactly what you pass back as `recipient`.

From a handler you'll often also call:

- `PATCH /api/v1/channels/{channel_id}` — set or update `webhook_events` / `webhook_url`
- `GET /api/v1/media/{media_id}` — download received media
- `GET /api/v1/channels/{channel_id}/profile/{user_id}` — look up the sender's profile

**Success:** message your connected channel from a real device; your endpoint
receives a `message.received` webhook with a valid signature and returns 2xx.
You're now receiving.

### 4. Reply to it

With receiving in place, send outbound. The everyday case is a **free-form reply
within the 24-hour window** after a user messages you — plain text or media, no
approval needed. This is the core move: answer the sender by feeding the **same**
identifiers straight back — the webhook's `channel_id` as `channel_id`, and
`data.from` as `recipient`:

```bash
curl -X POST https://fiwano.com/api/v1/messages/send \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Thanks for your message!"}'
```

- Send: `POST /api/v1/messages/send` (text), `POST /api/v1/messages/send-media` (media)

**Success:** the call returns an accepted message with a `message_id`; if you
enabled delivery events in step 3, you'll then receive `message.sent` /
`message.delivered` webhooks tracking it. Read
[Sending Messages](/documentation/sending-messages) for media and more.

That covers the core loop — connect, receive, reply. Step 5 is optional.

### 5. WhatsApp templates — messaging outside the 24-hour window (optional)

Free-form messages only reach a user **inside** the 24-hour window. To start a
conversation, or to reply after the window has closed, WhatsApp requires a
pre-approved **template** (WhatsApp only). Skip this step if you only ever reply
within the window — see the 24-hour window in
[Capabilities](/documentation/capabilities#messaging-windows-24h).

Read [WhatsApp Templates](/documentation/templates) for the create/review
lifecycle, then send the approved template.

- Send a template: `POST /api/v1/messages/send-template`
- Manage templates: `GET /api/v1/channels/{channel_id}/templates`, `POST /api/v1/channels/{channel_id}/templates`, `GET|PUT|DELETE /api/v1/channels/{channel_id}/templates/{template_id}`

### Next steps

- **No code?** Use the verified [n8n node](/documentation/n8n) — same channels,
  same events, drag-and-drop.
- **Media and templates** — [Sending Messages](/documentation/sending-messages).
- **Limits, windows and tiers** — [Capabilities](/documentation/capabilities).
- **The full machine-readable contract** — [API Reference](/documentation/api).
</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.

For the exact request/response schema of every channel endpoint (fields, types,
status codes), see the **[API Reference](/documentation/api)**. This page is the
task-level guide; it does not repeat the field tables. 

### Prerequisites

Before connecting any channel — WhatsApp, Instagram or Facebook Messenger — make
sure both conditions below are met. They apply equally to the Portal flow and the
API flow; if either is 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.

### Option A: Via Portal (self-service)

Use this to connect **your own** channels, no code required.

1. Go to **Channels → Connect Channel** in the portal.
2. Select the channel type (WhatsApp, Instagram, or Facebook Messenger).
3. Complete the Meta OAuth flow in the popup window.
4. Configure the **Webhook URL** and select **Webhook Events** in channel settings.
5. By default, no events are enabled — select which events to forward to your endpoint.

Saving a webhook URL in the Portal does **not** create a `webhook_secret`. Set one
explicitly so incoming deliveries are signed — see [Webhook secret](#webhook-secret).
The URL must be an absolute HTTPS URL reachable from Fiwano. Explicit ports from
1 to 65535 are supported; embedded credentials and URL fragments are not.

### Option B: Via API (programmatic)

Use this when your application connects channels **on behalf of your end users**.

**Step 1 — Whitelist your redirect URI.** For security, the user can only be
redirected back to a URL you have pre-registered for your API key. Register the
URL(s) where users land after OAuth (wildcards are allowed, e.g.
`https://*.example.com/callback`):

```bash
curl -X POST https://fiwano.com/api/v1/redirects \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"uri_pattern": "https://yourapp.com/callback"}'
```

Redirect URI patterns must use HTTPS and cannot target localhost or a loopback
address. Explicit ports from 1 to 65535 are supported, including non-standard
HTTPS ports such as `https://yourapp.com:4426/callback`. Exact URIs are safest
and recommended. When a wildcard is necessary, it may appear in the path/query
or as one complete left-most hostname label (`*.example.com`), but cannot replace
the whole hostname, part of a label, or the port. Embedded credentials, URL
fragments, and the reserved query keys `code`, `status`, `channel_type`, and
`error` are rejected.

To associate a setup flow with your authenticated tenant or administrator,
generate a high-entropy, single-use opaque nonce, store it server-side with that
context, and put only the nonce in the redirect URI. Register a narrowly scoped
pattern such as `https://yourapp.com/callback?state=*`, then request the setup URL
with `https://yourapp.com/callback?state=BASE64URL_NONCE`. Fiwano preserves
`state` and appends its own parameters, for example
`?state=BASE64URL_NONCE&code=...&status=success&channel_type=whatsapp`. Use a
URL-safe value and do not place tenant/user identifiers or other sensitive data
directly in the URI.

You manage these with `GET /api/v1/redirects` and `DELETE /api/v1/redirects/{id}`.

**Step 2 — Request a setup URL.** Pass one of your whitelisted redirect URIs. The
URL is valid until the `expires_at` returned in the response — open it in a
browser or popup for the user:

```bash
curl -X POST https://fiwano.com/api/v1/channels/setup-url \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel_type": "whatsapp", "redirect_uri": "https://yourapp.com/callback"}'
```

The same endpoint also reconnects channels; there is no separate reconnect API.
If the Meta identity already belongs to one of your inactive channels, Fiwano
reactivates that row and `exchange-code` returns the existing `channel_id`. When
both a genuinely new asset and an inactive asset are available, the new asset
is preferred.

**Step 3 — User completes Meta OAuth.** After approval, the user is redirected to
your `redirect_uri` with a one-time `code` parameter:

```
https://yourapp.com/callback?code=abc123...
```

On failure, the redirect instead carries two query params — branch your logic on
`error` only:

| Query param | How to use it |
|---|---|
| `error` | Machine-readable code. **Branch on this.** `access_denied` — the user cancelled the Meta dialog. `setup_failed` — setup could not complete (e.g. no Instagram Business account was accessible with the permissions granted). |
| `message` | URL-encoded, human-readable English explanation, safe to display to the user. **Free-form and may change — never parse or branch on its text.** |

Example failure redirect:

```
https://yourapp.com/callback?error=setup_failed&message=We%20couldn%27t%20access%20any%20Instagram%20Business%20account...
```

**Step 4 — Exchange the code.** Within 5 minutes (single-use), exchange the code
for the channel. You can configure the webhook in the same call:

```bash
curl -X POST https://fiwano.com/api/v1/channels/exchange-code \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "abc123...",
    "webhook_url": "https://yourapp.com/webhooks/meta",
    "webhook_events": ["message.received", "message.delivered", "message.failed"]
  }'
```

The response returns your `channel_id` (store it — every other call uses it). When
you set `webhook_url` and pass no `webhook_secret`, Fiwano **auto-generates** one and
returns it here. It is returned **only in this response** — `GET` never shows it
again — so store it to verify webhook signatures ([Webhook secret](#webhook-secret)).
All fields except `code` are optional and can be set later via
`PATCH /api/v1/channels/{id}`.

### Webhook events

Webhook delivery is **opt-in per channel**: by default **no events are delivered**. You
choose what you receive by setting `webhook_events` — in the connect call, in the Portal,
or later via `PATCH /api/v1/channels/{id}`. Until you do, your endpoint gets nothing.

The available events depend on the channel type — WhatsApp exposes more
(`message.sent`, `message.failed`) than Instagram and Facebook. The full list with
payloads is on the **[Webhooks](/documentation/webhooks#event-types)** page. An event you
list that isn't valid for the channel type is simply ignored, not an error.

**Your endpoint owns the other half of this contract.** Once events are enabled, Fiwano
POSTs each one to your `webhook_url`, and your endpoint **must respond with HTTP 2xx
within ~5 seconds**. A non-2xx response or a timeout counts as a failed delivery: Fiwano
**retries with backoff and emails you** — a warning after the 3rd failed attempt and an
alert when retries are exhausted. So enable **only the events you actually handle**, and
   return 2xx as soon as you've accepted the payload (do slower work afterwards). Successfully
   delivered webhook payloads are not retained for relay; failures are stored encrypted for retries. Full behavior:
**[Webhooks → Retry Policy](/documentation/webhooks#retry-policy)**.

### Webhook secret

The `webhook_secret` is the HMAC key Fiwano uses to **sign webhook deliveries**, so
your endpoint can confirm a request genuinely came from Fiwano and was not altered in
transit. When a channel has a secret, every delivery carries an
`X-Webhook-Signature: sha256=<hmac>` header — see **[Webhooks](/documentation/webhooks)**
for the verification snippet. A channel with no secret receives **unsigned** deliveries.

How a secret first appears differs by how you connect — and this is the one place the
Portal and the API deliberately behave differently:

- **Portal (Option A):** a new channel has **no secret**, and saving a webhook URL
  does not create one. Set it yourself in channel settings: click **Generate random**
  for a random 64-character secret, or type your own and **Save** (minimum 16
  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` in that same call.

**Reading it back.** The value is only returned the moment it is set or changed — in
the Portal's one-time reveal, or in the `exchange-code` and
`PATCH /api/v1/channels/{id}` responses. `GET /api/v1/channels` and
`GET /api/v1/channels/{id}` never return it; they only report
`has_webhook_secret: true | false`. **Store the value when it is shown** — if you
lose it, your only option is to set a new one.

**Rotating it.** Set a new secret any time by passing a new `webhook_secret` to
`PATCH /api/v1/channels/{id}`, or with the Portal's **Generate random** / **Save**
actions. Updating only `webhook_url`/`webhook_events` leaves the secret untouched. A
change takes effect on the **very next delivery** — there is no overlap window, so
switch your verifier to the new secret at the same moment, or signatures will mismatch.

**Constraints and recommendations.**

- Use a high-entropy random string of **16–64 characters** (the Portal enforces the
  16-character minimum; the field stores up to 64). Auto-generated secrets are
  64-character hex — prefer those unless you have a reason to bring your own.
- The secret is **per channel** — each channel has its own, independent of the rest.
- Reconnecting an inactive channel **keeps** its existing secret (see
  [Reconnecting an inactive channel](#reconnecting-an-inactive-channel) below).
- Treat it like a password: store it in a secret manager, never commit it, and
  verify signatures using a constant-time comparison (as in the Webhooks example).

### Managing channels

| Task | Endpoint |
|---|---|
| List all channels (active and inactive), each with its current subscription state | `GET /api/v1/channels` |
| Inspect one channel | `GET /api/v1/channels/{id}` |
| Update webhook URL / secret / events, or the subscription binding | `PATCH /api/v1/channels/{id}` |
| Deactivate a channel | `DELETE /api/v1/channels/{id}` |

Each channel carries a `subscription` block describing its billing state — see
**[Subscriptions & Billing](/documentation/subscriptions)** for what the
combinations mean. Full field lists live in the **[API Reference](/documentation/api)**.

**Deactivation is a soft delete.** `DELETE` stops the channel from sending and
receiving, but does not erase it — its `channel_id` and history are preserved so
you can reconnect later. The channel also remains owned by the same Fiwano
account: deactivation does not release its WhatsApp number, Instagram account or
Facebook Page for connection to another Fiwano account. If the channel must move
between accounts, contact `contact@fiwano.com`.

Fiwano also unsubscribes the channel's Meta webhook
resource only when it is safe to: a WABA subscription is kept if another active
WhatsApp channel uses the same WABA, and a Page subscription is kept if another
active Instagram/Facebook channel uses the same Page.

### Subscription slots

Each subscription grants **one slot per channel type** — one WhatsApp, one
Instagram, one Facebook. A slot stays occupied while a channel is bound to it,
**including a deactivated channel**: the binding is what lets you reconnect that
channel later without buying another subscription.

`GET /api/v1/subscriptions` shows which channel sits in each slot and how many
slots are free; each channel reports its own `subscription.id` in return.

Send `subscription_id` to `PATCH /api/v1/channels/{channel_id}` to change that. A
subscription ID moves the channel there — no downtime, and it does not have to be
deactivated first, but a move to a Starter subscription stops media and template
sending immediately. An empty string releases the slot, and that is allowed only
for a channel already deactivated with `DELETE /api/v1/channels/{channel_id}`, so
a slot is never freed as a side effect of a settings update.

**Releasing a slot is effectively permanent.** Once another channel takes the
freed slot, the released one can no longer be reconnected until a slot is free
again. It is not erased and its Meta identity stays owned by your Fiwano account —
but treat the release as retiring that channel, not pausing it.

Replacing a channel when you have a single subscription:

```text
GET    /api/v1/subscriptions           → find the subscription and its occupied slot
DELETE /api/v1/channels/{old_id}       → deactivate the channel you are replacing
PATCH  /api/v1/channels/{old_id}       → {"subscription_id": ""} frees the slot
POST   /api/v1/channels/setup-url      → user connects the new Meta account
POST   /api/v1/channels/exchange-code  → new channel takes the free slot
```

### Reconnecting an inactive channel

A channel goes inactive when it is deactivated (`DELETE /api/v1/channels/{id}`) or
when its Meta connection can no longer be maintained (for example, the account
owner revoked access in Meta). To bring it back, run the **same connection flow
again for the same Meta account** (same WhatsApp number, Instagram account, or
Facebook Page):

- The existing channel is **reactivated in place** — its `channel_id`, webhook
  URL/secret/events and history are preserved. No new channel is created and your
  stored `channel_id` mapping stays valid.
- Reconnecting requires an **active license**: the channel must still hold one, or
  you must have a free license slot. Otherwise the flow is refused — attach a
  license in Billing first.
- A Meta account owned by a different Fiwano account cannot be connected, even
  when that channel is inactive. If it is your channel, contact
  `contact@fiwano.com` to request an ownership release.

---

## Sending Messages

Fiwano has three send endpoints — plain text, media, and WhatsApp templates. All
take a `channel_id` and a `recipient`. The `recipient` format depends on the
channel (phone number for WhatsApp, IGSID for Instagram, PSID for Facebook) — see
the recipient row in [Capabilities](/documentation/capabilities#channel-capabilities). Full
request/response schemas are in the [API Reference](/documentation/api); this page
is the task guide.

### Send WhatsApp messages with the API

For a normal WhatsApp reply inside the 24-hour customer service window, use the
plain text endpoint below with a WhatsApp `channel_id` and the recipient's phone
number. You authenticate with your Fiwano `X-API-Key`; you do not need a separate
Meta or WhatsApp API key in your application.

Outside the 24-hour window, WhatsApp requires an approved template message. That
uses `/api/v1/messages/send-template` and is described in
[Template messages](#template-messages). Instagram DM and Facebook Messenger use
the same text endpoint for ordinary replies, with IGSID or PSID as `recipient`.

### Text messages

`POST /api/v1/messages/send` — works on all channel types.

```bash
curl -X POST https://fiwano.com/api/v1/messages/send \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel_id": "a1b2c3d4e5f67890", "recipient": "1234567890", "text": "Hello! Your order is ready."}'
```

The response carries a `message_id` (a Fiwano UUID) that every later delivery-status
webhook references. Text has a per-platform length cap (WhatsApp 4096, Facebook
2000, Instagram 1000) — oversize text is rejected with `400 text_too_long` before
Meta is called. Fiwano does **not** auto-split; split on your side to preserve your
own chunking and ordering. The `text` value must contain at least one non-whitespace
character; empty or whitespace-only values are rejected with `422` before Meta is
called. Leading and trailing whitespace in otherwise valid text is preserved. See
[Capabilities](/documentation/capabilities).

### Media messages

`POST /api/v1/messages/send-media` — **Pro license required.** Meta fetches the file
directly from `media_url`; Fiwano never downloads or stores it. Pass `media_type`
(`image`, `audio`, `video`, `document`) and an HTTPS `media_url`.

**Use a signed URL for non-public content** — S3/GCS/R2 presigned, Azure SAS, or an
HMAC-signed URL on your own server, with expiry ≥ 20 min so background retries can
still fetch it. A public URL is reachable
by anyone who learns it.

```bash
curl -X POST https://fiwano.com/api/v1/messages/send-media \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "recipient": "1234567890",
    "media_type": "image",
    "media_url": "https://my-bucket.s3.amazonaws.com/photo.jpg?X-Amz-Signature=...&X-Amz-Expires=1800",
    "caption": "Your order photo"
  }'
```

Always check `success` and `status`. Permanent failures include a Meta
`error_code` — for example `131052` when Meta cannot download the URL. Failures
that can recover, including `131053` processing/fetcher failures and `131056`
pair rate limits, return `queued` and use the same durable retry schedule as text.
File-size caps are in
[Capabilities](/documentation/capabilities#outbound-media-size) and the full
error-code table is in [Errors](/documentation/errors#send-error-codes).

### Response time

`POST /messages/send-media` is **synchronous and can be slow**. Fiwano never
downloads your file: we hand Meta the `media_url` and **Meta fetches it inside
your request**. The wait is therefore proportional to the file size and to how
fast your own hosting serves it. A 12-second call for a large file is normal;
Fiwano gives up on Meta after **30 seconds**.

Text and template sends are not affected — they carry no file and typically
complete in well under a second.

**Set your HTTP client timeout to at least 35 seconds** for `send-media`. Some
environments cap this for you and cannot wait that long — AWS API Gateway stops
at 29 seconds, and serverless functions often default to 10–15 seconds.

> **If your client times out, the message may still have been sent.** Meta can
> accept it after you stopped waiting. Resending then delivers it twice.
> Retry only after confirming the message is absent from the conversation.

To keep media sends fast, serve `media_url` from storage close to your users
(S3/GCS/R2 with a CDN) and keep files well under the
[size caps](/documentation/capabilities#outbound-media-size).

### Template messages

`POST /api/v1/messages/send-template` — **WhatsApp only, Pro required.** Use a
pre-approved template to start a conversation outside the 24-hour window (see
[Capabilities](/documentation/capabilities#messaging-windows-24h)). Only `APPROVED`
templates can be sent — to create and manage them, see
[WhatsApp Templates](/documentation/templates).

Provide variable values keyed by component. **Positional** templates (`{{1}}`,
`{{2}}`) take arrays:

```bash
curl -X POST https://fiwano.com/api/v1/messages/send-template \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "template_name": "order_confirmation",
    "language": "en_US",
    "recipient": "1234567890",
    "variables": {
      "header": ["Summer Sale"],
      "body": ["Pablo", "ORD-123", "25%"],
      "buttons": [{"index": 0, "value": "promo25"}]
    }
  }'
```

**Named** templates (`{{customer_name}}`) take objects:

```bash
  -d '{
    "channel_id": "a1b2c3d4e5f67890",
    "template_name": "welcome_message",
    "language": "en_US",
    "recipient": "1234567890",
    "variables": {"body": {"customer_name": "Pablo", "order_number": "ORD-123"}}
  }'
```

Omit `variables` entirely if the template has none.

Template sends return the same response shape as text and media sends. The
`message_id` is a Fiwano UUID; keep it to correlate later delivery webhooks:

```json
{
  "success": true,
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "error": null,
  "error_code": null,
  "status": "sent"
}
```

### Delivery and retries

All three send endpoints return `200` with the common `success`, `message_id`,
`error`, `error_code`, and `status` fields, because what happens after Meta
accepts the request matters:

- **`sent`** — Meta accepted it. Track the rest via delivery-status webhooks
  (`message.delivered` / `read` / `failed`) — see
  [Receiving Messages](/documentation/webhooks#delivery-status-tracking).
- **`queued`** — a transient Meta failure (network, 5xx, rate limit). Fiwano
  retries in the background (up to 7 times over ~20 min). You get an early-warning
  email after 3 failed retries and a final email if they're exhausted. Only
  `send` and `send-media` can return `queued`.
- **`failed`** (`success: false`) — the request will not be retried. For `send`
  and `send-media`, this means a permanent error (bad recipient, oversize text
  or media, malformed payload), and the channel owner is emailed. `send-template`
  does not retry automatically, so any Meta send error is returned as `failed`;
  the caller can decide whether and when to resend.
  `failed` is also used for the rare case where Meta's response was lost and the
  outcome cannot be confirmed — see
  [unverified send outcomes](/documentation/errors#unverified-send-outcomes).

So `200` does not by itself mean "delivered" — always read `success` and `status`.

---

## Receiving Messages

When a user messages your connected channel, Fiwano delivers the message — and later its delivery statuses — to your channel's `webhook_url` as a `POST` request. This page covers verifying webhooks, the payload formats per channel, downloading inbound media, and looking up a sender's profile.

Set `webhook_url` and choose which `webhook_events` to receive when you connect a channel (see [Channels](/documentation/channels)); by default no events are enabled. If the channel has a [`webhook_secret`](/documentation/channels#webhook-secret), each delivery is signed so you can verify it came from Fiwano — strongly recommended. Until you set a secret, deliveries are sent unsigned.

### WhatsApp webhook setup and payload

For WhatsApp, enable `message.received` on the channel and point `webhook_url` at
your public HTTPS endpoint. Fiwano receives the original Meta webhook from the
WhatsApp Cloud API, resolves the connected channel, normalizes the payload, signs
it if you configured a `webhook_secret`, and delivers it to you.

The useful difference from wiring Meta directly is that the webhook envelope is
the same shape across all three channels:

- WhatsApp senders arrive as phone numbers in `data.from`.
- Instagram senders arrive as IGSID values in `data.from`.
- Facebook Messenger senders arrive as PSID values in `data.from`.

The top-level fields (`event`, `channel_id`, `channel_type`, `timestamp`, `data`)
stay stable, so one receiver can handle WhatsApp webhooks, Instagram webhooks and
Messenger webhooks without three separate Meta parsers.

### Verifying Signatures

When the channel has a `webhook_secret`, every webhook request includes an `X-Webhook-Signature` header:

```
X-Webhook-Signature: sha256=<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.sent` | Your message was accepted by Meta | WhatsApp |
| `message.delivered` | Message delivered to recipient's device | WhatsApp, Instagram, Facebook |
| `message.read` | Message read by recipient * | WhatsApp, Instagram, Facebook |
| `message.failed` | Message delivery failed | WhatsApp |

\* `message.read` for WhatsApp depends on recipient's privacy settings — if read receipts are disabled, the `read` status will never arrive. Treat `delivered` as a terminal success state.


### Delivery Status Tracking

When you send text, media, or a WhatsApp template through any send endpoint, you
receive a `message_id` (UUID). All subsequent status webhooks reference this same
UUID; Meta's provider ID remains internal to Fiwano.

- `message_id` is always present in all status events — it's a UUID generated by Fiwano, not a Meta internal ID.
- Status progression: `sent → delivered → read`. Each status implies all previous ones.
- `data.recipient` is the user identifier: phone number (WhatsApp), IGSID (Instagram), or PSID (Facebook).
- All channels use the exact same webhook format.
- **Read cascading:** when a user reads a conversation, Fiwano sends a separate `message.read` webhook for *each* unread message — not just the latest one.

### Payload Format

All payloads share the same top-level structure:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": { ... }
}
```

#### message.received (WhatsApp)

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "text",
    "text": "Hello!"
  }
}
```

`data.from` — sender's phone number without `+`. Use directly as `recipient` when replying.

#### message.received (Instagram)

```json
{
  "event": "message.received",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "mid.xxx",
    "from": "6543217890123456",
    "from_name": null,
    "type": "text",
    "text": "Hi there!"
  }
}
```

`data.from` — IGSID. Use as `recipient` when replying. `from_name` is always `null` (Meta does not include sender name in IG webhooks).

#### message.received (Facebook Messenger)

```json
{
  "event": "message.received",
  "channel_id": "c3f8a1b2e4d56789",
  "channel_type": "facebook",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "mid.xxx",
    "from": "7890123456789012",
    "from_name": null,
    "type": "text",
    "text": "Hello from Messenger!"
  }
}
```

`data.from` — PSID. Use as `recipient` when replying. `from_name` is always `null` (Meta does not include sender name in FB webhooks).

#### message.received — media (Pro)

With a **Pro** license, media messages include the file content. The media file is downloaded from Meta and stored temporarily. Use the `download_url` to fetch the file before it expires.

WhatsApp image example:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "image",
    "caption": "Check this photo",
    "media": {
      "media_id": "m1b2c3d4e5f67890",
      "mime_type": "image/jpeg",
      "file_size": 245760,
      "filename": null,
      "sha256": "abc123...",
      "duration_ms": null,
      "download_url": "https://fiwano.com/api/v1/media/m1b2c3d4e5f67890",
      "expires_at": "2025-01-15T11:30:00Z"
    }
  }
}
```

WhatsApp voice message example:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "audio",
    "media": {
      "media_id": "m2b3c4d5e6f78901",
      "voice": true,
      "mime_type": "audio/ogg; codecs=opus",
      "file_size": 12345,
      "filename": null,
      "sha256": "def456...",
      "duration_ms": 5200,
      "download_url": "https://fiwano.com/api/v1/media/m2b3c4d5e6f78901",
      "expires_at": "2025-01-15T11:30:00Z"
    }
  }
}
```

Instagram and Facebook Messenger deliver the same `data.media` block; only `data.from` differs (IGSID or PSID instead of a phone number).

`data.type` is the message type on every channel and always comes from one fixed set: `text`, `image`, `audio`, `video`, `document`, `sticker` (WhatsApp only), or `unsupported`. Route on it. The four media types are exactly the values accepted as the outbound `media_type`, so an inbound media event can be forwarded without a mapping table — except `sticker`, which is inbound-only and has to be re-encoded to go out as `image`.

Instagram and Facebook Messenger also use attachments for things that are not a file, such as a shared post or a location pin. Those arrive as `type: "unsupported"` with no `data.media` block — see **unsupported type** below.

When Meta includes text together with media, Fiwano exposes that accompanying text as `data.caption` on the media event. Plain text messages continue to use `data.text`. This rule is the same across WhatsApp, Instagram and Facebook Messenger.

**Multiple attachments:** every attachment is delivered as its own `message.received` webhook and its own HTTP `POST`; Fiwano never sends an array of webhook events. All files from the source message are prepared before the first event is delivered, then the events are sent in Meta's attachment order. The first event keeps Meta's message ID and carries the caption, if present. Later events use deterministic IDs with `.2`, `.3`, and so on, and omit the caption:

```text
mid.xxx       image + caption
mid.xxx.2     image
mid.xxx.3     video
```

Treat inbound `message_id` as an opaque idempotency key; do not parse the suffix or pass the ID to Meta. Delivery retries remain independent per event, so a failing client endpoint can still observe a later part before a retried earlier part. A failed media download does not suppress the other attachments: its event has `media.download_url: null` and `media.error`.

Fiwano preserves Meta's original file format and does not transcode media. `media.mime_type` describes the downloaded file bytes, not the message semantics. For example, Facebook Messenger voice-style clips commonly download as OGG/Opus (`audio/ogg`), while Instagram audio messages can download as audio-only MP4 served with `video/mp4`. In both cases the message type is still `data.type: "audio"`.

For inbound WhatsApp only, Meta provides a reliable voice-message flag. Fiwano exposes it as `media.voice: true` when present. Instagram and Facebook Messenger do not expose an equivalent reliable voice flag through the webhook payload, so `media.voice` is omitted for those channels.

The `download_url` is authenticated; fetch it with your `X-API-Key`. Do not pass it directly as an outbound `media_url` because Meta will not send your API key header — re-host the bytes behind a public or signed HTTPS URL first.

**Media payload fields:**

| Field | Type | Description |
|---|---|---|
| `media_id` | string | Media file ID — use in `GET /api/v1/media/{media_id}` to download |
| `voice` | bool | Present only for WhatsApp voice messages (`true`). Omitted for IG/FB because Meta does not provide a reliable voice flag there. |
| `mime_type` | string | MIME type (e.g. `image/jpeg`, `audio/ogg; codecs=opus`) |
| `file_size` | int | File size in bytes |
| `filename` | string\|null | Original filename (documents only) |
| `sha256` | string\|null | SHA-256 hash from Meta (WhatsApp only) |
| `duration_ms` | int\|null | Duration in milliseconds (audio/video only) |
| `download_url` | string\|null | Authenticated download URL. `null` if download from Meta failed. |
| `error` | string | Present only when download failed — describes the error |
| `expires_at` | string | ISO 8601 timestamp — file is deleted after this time |

> **Note:** Treat voice messages as audio messages. `data.type: "audio"` is the stable cross-channel value for routing and forwarding. `media.voice` is an optional WhatsApp-only hint for UI/UX.

#### Downloading inbound media

Fetch the file from `data.media.download_url` (which is `GET /api/v1/media/{media_id}`) with your `X-API-Key`:

```bash
curl https://fiwano.com/api/v1/media/m1b2c3d4e5f67890 \
  -H "X-API-Key: YOUR_API_KEY" \
  --output photo.jpg
```

The response is the raw file bytes with the original `Content-Type` (and a `Content-Disposition` filename when known). Files expire about 60 minutes after Fiwano retrieves them from Meta; `media.expires_at` is authoritative. Download promptly and re-host anything you need to keep; after expiry the URL returns `410 Gone`. Sizes are in [Capabilities](/documentation/capabilities#media-limits); status codes in the [API Reference](/documentation/api).

#### message.received — unsupported type (all channels)

A message arrives as `type: "unsupported"` when Fiwano cannot give you the content as a file. `unsupported_type` says what it was, and there is no `data.media` block. There are two reasons, and `upgrade_required` tells them apart.

**Media on a Starter license.** The file exists but your tier does not include it. `unsupported_type` is the media type Pro would have delivered, and `upgrade_required` names the tier that unlocks it:

```json
{
  "event": "message.received",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "message_id": "wamid.xxx",
    "from": "1234567890",
    "from_name": "John Doe",
    "type": "unsupported",
    "unsupported_type": "image",
    "upgrade_required": "pro"
  }
}
```

Upgrade via the Billing page in the portal to receive full media content.

**Content that is not a file.** No tier delivers these, so `upgrade_required` is absent. `unsupported_type` carries Meta's own name for the content:

| Channel | `unsupported_type` values |
|---|---|
| WhatsApp | `location`, `contacts`, and other non-media message types |
| Instagram, Facebook Messenger | `share` and `ig_reel` (a shared post or reel), `story_mention`, `location`, `fallback` (a shared link), `template`, `unsupported` |

Any type not listed here arrives the same way, so an unfamiliar `unsupported_type` is still just unsupported content. Message reactions are ignored and are not delivered as webhook events.

#### message.delivered / message.read (all channels)

```json
{
  "event": "message.read",
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "timestamp": "2025-01-15T10:30:10Z",
  "data": {
    "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "read",
    "recipient": "6543217890123456"
  }
}
```

Same format for all channels and all statuses (`sent`, `delivered`, `read`). `message_id` is the UUID from the send response.

#### message.failed (WhatsApp)

```json
{
  "event": "message.failed",
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "timestamp": "2025-01-15T10:30:05Z",
  "data": {
    "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "recipient": "1234567890",
    "status": "failed",
    "error": "Message undeliverable",
    "errors": [{"code": 131047, "title": "Message undeliverable"}]
  }
}
```

### Retry Policy

If your webhook URL returns a non-2xx status or is unreachable, the system retries automatically:

- **7 retry attempts** with exponential backoff: 30s, 1m, 2m, 2m, 2m, 2m, 2m (~12 minutes total)
- **20-minute hard deadline** — after which the delivery is marked as permanently failed
- **Email warning** sent after the 3rd failed attempt (retries still in progress)
- **Email alert** sent when all retries are exhausted (permanent failure)
- Payloads are encrypted at rest during retry and cleared after delivery or expiry

**Important:** Your endpoint **must respond with HTTP 2xx within 5 seconds**. Non-2xx responses or timeouts trigger the retry queue. Fiwano does not retain successfully delivered webhook payloads for relay; after a failed initial delivery, the encrypted payload is stored temporarily for automatic retries.

> **Tip:** Only enable the webhook events you actually handle. Unhandled events that receive non-2xx responses will fill your retry queue unnecessarily.

### Sender Profile

WhatsApp includes the sender's name inline in every webhook (`data.from_name`) — no extra call needed. Instagram and Facebook do **not** (`data.from_name` is always `null`); to get a name or avatar, call the profile endpoint:

```
GET /api/v1/channels/{channel_id}/profile/{user_id}
```

Pass the `data.from` value (IGSID for Instagram, PSID for Facebook) as `user_id`. It returns:

- **Instagram** — `username`, `name`, `profile_pic`, `follower_count`, `is_verified_user`
- **Facebook** — display name in `first_name`; `last_name` and `profile_pic` when available from Meta

WhatsApp is not supported (the name is already in the webhook). Successful results are cached for 5 minutes; unavailable results are cached briefly so a newly indexed conversation can be retried soon. The response's `cached` flag tells you if it was a cache hit. Full request/response and status codes are in the [API Reference](/documentation/api).

> **Tip:** call this once when you first see a new `data.from`, then cache the result on your side — no need to call it on every message.

---

## WhatsApp Templates

WhatsApp requires **pre-approved templates** to start a conversation outside the
24-hour window (see [Capabilities](/documentation/capabilities#messaging-windows-24h)).
Templates are WhatsApp-only and require a **Pro license**. This page is about
managing them; to *send* an approved template see
[Sending → Template messages](/documentation/sending-messages#template-messages).

### Lifecycle

```
Create → PENDING (Meta review, ~24h) → APPROVED (sendable)
                                      → REJECTED (fix & resubmit)
```

Templates belong to the channel's WhatsApp Business Account (WABA). Manage them
through these endpoints — full request/response schemas are in the
[API Reference](/documentation/api):

| Task | Endpoint |
|---|---|
| List (filter by status; syncs from Meta by default) | `GET /api/v1/channels/{id}/templates` |
| Get one (components + variable definitions) | `GET /api/v1/channels/{id}/templates/{template_id}` |
| Create (→ submitted to Meta, starts `PENDING`) | `POST /api/v1/channels/{id}/templates` |
| Update components | `PUT /api/v1/channels/{id}/templates/{template_id}` |
| Delete | `DELETE /api/v1/channels/{id}/templates/{template_id}` |

### Creating a template

A template is a `name` + `category` (`MARKETING`, `UTILITY`, or `AUTHENTICATION`)
+ `language` + `components`. `BODY` is required; `HEADER` (text only), `FOOTER` and
`BUTTONS` are optional. Variables are `{{1}}, {{2}}` (positional) or `{{name}}`
(named) — Meta requires `example` values for review.

```bash
curl -X POST https://fiwano.com/api/v1/channels/a1b2c3d4e5f67890/templates \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "order_confirmation",
    "category": "UTILITY",
    "language": "en_US",
    "components": [
      {"type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed.",
       "example": {"body_text": [["Pablo", "ORD-123"]]}}
    ],
    "parameter_format": "positional"
  }'
```

### Rules to know

- **Editing an approved template** re-submits it for review (back to `PENDING`)
  and is rate-limited by Meta: **max 10 edits per 30 days, 1 per 24 hours**. You
  can't change the category of an approved template.
- **Deleting an approved template** locks its **name for 30 days** — you can't
  recreate a template with the same name until then (Meta restriction).
- **Creating** is capped at ~100 templates per WABA per hour.

Once a template is `APPROVED`, send it with
[`POST /api/v1/messages/send-template`](/documentation/sending-messages#template-messages).

---

## Capabilities

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

### Channel Capabilities

All three channels are connected the same way (OAuth). The table below shows what each channel supports.

| Feature | WhatsApp | Instagram | Facebook Messenger |
|---|---|---|---|
| Outbound text — max length | 4096 chars | 1000 chars | 2000 chars |
| Outbound media (Pro) | image, audio, video, document | image, audio, video, document | image, audio, video, document |
| Template messages (Pro) | ✅ Required outside 24h window | ❌ Not supported | ❌ Not supported |
| Incoming webhooks — text | ✅ `type: "text"` | ✅ `type: "text"` | ✅ `type: "text"` |
| Incoming webhooks — media (Pro) | image, audio, video, document, sticker | image, audio, video, document | image, audio, video, document |
| Delivery statuses | `sent` `delivered` `read` `failed` | `delivered` `read` | `delivered` `read` |
| Recipient format | Phone number without `+`  | IGSID | PSID — |
| 24h window workaround | Use approved templates | None — wait for user to message | None — wait for user to message |
| Channel identifier | `phone_number_id` | `ig_account_id` | `page_id` |
| Sender profile | `data.from_name` (from Meta contacts) | Via [profile endpoint](/documentation/webhooks#sender-profile) | Via [profile endpoint](/documentation/webhooks#sender-profile) |

> **Note:** Each Meta account (phone number, Instagram account, or Facebook Page) can only be connected to one Fiwano user at a time.

### License Tiers

Fiwano offers two license tiers. Each connected channel requires an active license.

| Tier | Monthly | Capabilities |
|---|---|---|
| **Starter** | $12 | Unlimited inbound and outbound text messages, delivery statuses |
| **Pro** | $19 | Everything in Starter **+** inbound media with files, outbound media via HTTPS URL (signed URLs supported), WhatsApp template management and sending |

New accounts start with a 7-day free trial (Pro tier). For the billing lifecycle and how a channel's subscription state is reported, see [Subscriptions & Billing](/documentation/subscriptions). For how this flat fee relates to Meta's own per-message charges, see [Messaging Costs Explained](/documentation/messaging-costs).

### Rate limits

Message sends are limited to **10 accepted send attempts per second per channel**
across all API keys. The limit is shared by text, media, and template sends, so
creating another key does not increase one channel's allowance while one key can
drive many channels independently. Exceeding it returns HTTP `429` with
`Retry-After`. Other public API operations do not share a product-wide RPS cap.
During exceptional outbound saturation, a send can briefly return HTTP `503` +
`Retry-After`; honor the header and retry. Meta also enforces its own channel and
recipient limits (shown in Meta Business Manager, not controlled by Fiwano).

### Messaging windows (24h)

Meta restricts when you can message a user outside an open conversation:

- **WhatsApp** — you can send regular text only within **24 hours** of the
  customer's last message. Outside the window, use an approved template via
  `POST /api/v1/messages/send-template`. This is a Meta policy.
- **Instagram & Facebook Messenger** — you can reply only within **24 hours** of
  the user's last message. There is no template workaround — wait for the user to
  message again.

### Media limits

#### Outbound file size

Meta downloads your `media_url` and enforces its own per-platform caps. Fiwano
does not re-check the file, so an oversize file is rejected by Meta with
`error_code` `100` and the message is **not** retried — see
[Errors](/documentation/errors#send-error-codes).

| Media type | WhatsApp | Instagram |
|---|---|---|
| Image | 5 MB (JPEG, PNG) | 8 MB (JPEG, PNG) |
| Video | 16 MB (MP4, 3GPP) | 25 MB (MP4, OGG, AVI, MOV, WebM) |
| Audio | 16 MB (AAC, AMR, MP3, MP4, OGG) | 25 MB (AAC, M4A, WAV, MP4) |
| Document | 100 MB (PDF, Office, text) | 25 MB (PDF) |

Meta does not publish per-type caps for the Facebook Messenger Send API. Treat
the Instagram figures as a safe working assumption for Messenger and handle the
oversize rejection rather than relying on a fixed number.

These are Meta's limits and Meta may change them. Note that encoding overhead
can push a file over the cap even when its size on disk looks safe.

#### Inbound file size

- **Inbound media** (images, audio, video, documents) is stored temporarily for
  **60 minutes**. Download it via `GET /api/v1/media/{media_id}` promptly after
  the webhook; files are cleaned up automatically after expiry. Maximum file
  size: **10 MB**.
- **Pro license required** for sending/receiving media and using WhatsApp
  templates. With a Starter license, inbound media arrives as
  `type: "unsupported"` with `upgrade_required: "pro"`. See
  [Subscriptions & Billing](/documentation/subscriptions).

---

## Subscriptions & Billing

Every channel returned by `GET /api/v1/channels` carries a `subscription` object
describing its current billing state. This page explains what those states mean
and how they change over a channel's lifecycle. For the field types, see the
**[API Reference](/documentation/api)**.

A channel can **send and receive messages only while its subscription is
`active`.** When it is not, send/receive calls are rejected until a license is
(re)attached.

### The subscription object

```json
"subscription": {
  "status": "active",
  "source": "paddle",
  "tier": "pro",
  "expires_at": "2025-02-15T10:30:00",
  "auto_renew": true
}
```

- **`status`** — `active`, `expired`, `canceled`, or `none` (no license bound;
  the channel cannot send/receive).
- **`source`** — where the entitlement came from: `trial` (auto-granted on
  signup), `paddle` (paid subscription), or `enterprise` (custom subscription
  provisioned by Fiwano staff, e.g. a partner deal or invoice billing). `null`
  when `status` is `none`.
- **`tier`** — `starter` or `pro`. `pro` is required for media messages and
  WhatsApp template CRUD/send. `null` when `status` is `none`.
- **`expires_at`** — ISO-8601 UTC timestamp when the current period ends. If
  `auto_renew` is `true`, this is the next renewal date; otherwise it is the
  cutoff after which the channel stops working.
- **`auto_renew`** — `true` only for an active Paddle subscription that will renew
  at `expires_at`. Always `false` for trial and Enterprise.

### What the combinations mean

- **Active Paddle subscription** —
  `{status: "active", source: "paddle", auto_renew: true, expires_at: <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
  channel becomes orphaned.
- **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 license to restore service.

> **Tip.** Treat `status` as the single source of truth for whether a channel can
> operate. Do not infer it yourself from `expires_at` — during the Paddle grace
> window an `active` channel can legitimately have an `expires_at` in the past.

### Checking available slots

Use `GET /api/v1/subscriptions` when an external service needs to decide whether
it can start a new channel connection flow. The endpoint is read-only and returns
all subscriptions plus aggregate slot availability:

```bash
curl -H "X-API-Key: $FIWANO_API_KEY" \
  https://fiwano.com/api/v1/subscriptions
```

```json
{
  "available_slots": {
    "whatsapp": { "total": 0, "starter": 0, "pro": 0 },
    "instagram": { "total": 1, "starter": 0, "pro": 1 },
    "facebook": { "total": 1, "starter": 0, "pro": 1 }
  },
  "subscriptions": [
    {
      "id": "a1b2c3d4e5f67890",
      "status": "active",
      "source": "trial",
      "tier": "pro",
      "auto_renew": false,
      "assigned_channels": {
        "whatsapp": {
          "channel_id": "1111222233334444",
          "channel_type": "whatsapp",
          "name": "Acme Support",
          "is_active": false
        },
        "instagram": null,
        "facebook": null
      }
    }
  ]
}
```

Use `available_slots.<channel_type>.total > 0` as the signal that a new channel
of that type can be connected. An inactive channel can still occupy a slot
because Fiwano preserves the binding for reconnect. In `available_slots`, `total`
is the sum of the currently free `starter` and `pro` slots for that channel type.
In each subscription, `assigned_channels` shows which channel is assigned to the
subscription for each type; `null` means no channel is assigned there. The reverse
mapping is on the channel itself: `subscription.id` in `GET /api/v1/channels`.

To move a channel to a different subscription, or to release a slot so another
channel of the same type can take it, use `subscription_id` in
`PATCH /api/v1/channels/{channel_id}` — see
[subscription slots](/documentation/channels#subscription-slots).

---

## n8n Integration

Fiwano is a **verified n8n community node** — listed on [n8n.io/integrations/fiwano/](https://n8n.io/integrations/fiwano/). Use it to build WhatsApp, Instagram and Facebook Messenger automations, AI agent workflows, and chatbots.

### Install

#### From the n8n editor (recommended)

1. Open the nodes panel with **+** or **N**
2. Search for **Fiwano**
3. Select **Fiwano** under **More from the community**
4. Click **Install**

On n8n Cloud, installation may need to be enabled by the instance owner in the Cloud Admin Panel first.

#### Manual fallback (npm)

Use only when in-app installation is unavailable in your environment (e.g. restricted self-hosted setup).

```bash
mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes
npm install n8n-nodes-fiwano
# Restart n8n
```

For self-hosted Docker: build this package into a custom n8n image — see the [GitHub repository](https://github.com/fiwano-com/n8n-nodes-fiwano) for details.

### Nodes

| Node | Type | Description |
|---|---|---|
| **Fiwano** | Action | Send messages, manage channels, WhatsApp templates, contact profile enrichment, redirect URIs |
| **Fiwano Trigger** | Webhook Trigger | Receive incoming messages and delivery status webhooks with optional HMAC signature verification |

### Action node — operations

| Resource | Operations |
|---|---|
| Message | Send Text, Send Template (WhatsApp), Send Media (image/audio/video/document) |
| Media | Download (fetch a received media file; expires 60 min after the webhook) |
| Channel | Get Many, Get, Generate OAuth URL, Exchange OAuth Code, Update (webhook settings and subscription binding), Deactivate |
| Subscription | Get Many (subscriptions, the channel assigned to each slot, and free slots per channel type and tier) |
| Contact | Get Profile (Instagram & Facebook — returns name/username and profile picture; Instagram also follower count) |
| Template | Get Many, Get, Create, Update, Delete (WhatsApp only) |
| Redirect URI | Get Many, Add, Delete |

**Deactivate is a soft delete.** It stops a channel sending and receiving but keeps its ID, history and its subscription slot, so the same Meta account can be reconnected later. To free the slot for a different channel, deactivate it and then send an empty **Subscription ID** in **Update** — see [subscription slots](/documentation/channels#subscription-slots).

### Trigger node — events

Starts your workflow for any of these events (filter by event type in node settings):

| Event | Channels |
|---|---|
| `message.received` | WhatsApp, Instagram, Facebook |
| `message.delivered` | WhatsApp, Instagram, Facebook |
| `message.read` | WhatsApp, Instagram, Facebook |
| `message.sent` | WhatsApp |
| `message.failed` | WhatsApp |

### Common workflow patterns

The Fiwano node is intentionally small: it gives n8n a reliable transport layer
for WhatsApp, Instagram DM and Facebook Messenger, then leaves the workflow logic
to n8n and the tools you connect around it.

| Pattern | How to build it |
|---|---|
| WhatsApp AI agent or chatbot | `Fiwano Trigger` receives `message.received` → your AI/model/tool nodes decide the answer → `Fiwano` sends the reply. Use WhatsApp templates only when you need to start or reopen a conversation outside the 24-hour window. |
| n8n WhatsApp trigger | Use `Fiwano Trigger` with **Specific Channel** for one WhatsApp number, or **All Active Channels** when one workflow should handle every connected channel. |
| Instagram DM automation | Use the same trigger/action pair on an Instagram channel. Keep the workflow focused on inbound support, opt-in lead qualification and customer replies; do not build cold-DM scraping or spam automation. |
| Facebook Messenger workflow | Use `channel_type: "facebook"` branches when a Messenger Page needs different copy or routing. It is lower-volume than WhatsApp, but useful when customers already start on Messenger. |
| One workflow for all Meta channels | Use **All Active Channels**, then branch on `channel_type` (`whatsapp`, `instagram`, `facebook`) only where the channel rules differ. |

### When to use Fiwano with n8n

Use it when you want n8n to own the automation — AI logic, routing, CRM updates,
memory, approvals, escalation — and you only need a clean way to receive and send
messages on Meta's official channels.

Do not use it as a bulk cold-outreach engine. WhatsApp, Instagram and Messenger
all have messaging-window and opt-in rules; Fiwano follows the official APIs and
does not bypass Meta policy.

### Webhook auto-setup

The trigger can wire its own webhook onto your channels, so you don't have to call **Update** by hand. Pick a **Webhook Auto-Setup** mode and attach a Fiwano API credential. The auto modes (**All Active Channels** / **Specific Channel**) need it to call the API — if it's missing, activation fails with a clear error. In **Manual** the credential is optional, used only to read a default webhook secret:

| Mode | What happens on activation | On deactivation |
|---|---|---|
| **All Active Channels** | Points every active channel that **isn't already wired elsewhere** (WhatsApp + Instagram + Facebook) at this trigger — one workflow handles all three. Channels already pointing at another URL are **left untouched**. | Clears the webhook on the channels that still point at this trigger. |
| **Specific Channel** | Points one **Channel ID** at this trigger — **takes it over** even if it already has a webhook. | Clears that channel's webhook (only if it still points here). |
| **Manual** *(default)* | Nothing — you set `webhook_url` yourself via **Exchange OAuth Code** / **Update**. No credential needed. | Nothing. |

The trigger's selected **Event Types** are registered as the channel's `webhook_events` (events that don't apply to a channel type are ignored — e.g. `message.sent`/`message.failed` on Instagram). Channels start with no events enabled, so auto-setup turns them on for you.

**Webhook secret.** Set a **Webhook Secret** to verify incoming signatures (HMAC-SHA256; mismatches are rejected with HTTP 401) and, in auto-setup, to register on your channels. You can set it in two places: the trigger's own **Webhook Secret** field, or — to reuse one secret everywhere — the **Webhook Secret** field on the Fiwano API credential. The trigger's field wins; if it's empty, the credential's secret is used. That same credential secret also backs the **Exchange OAuth Code** and **Update** operations when you leave their secret empty. Leave both empty to skip verification (not recommended in production).

**When it runs:** only on workflow **activation / deactivation** (and when n8n restarts active workflows) — **never per message**, so it adds no overhead to message handling. A few points to keep in mind:

- **Deactivating removes the webhook** from the channels that point at this trigger. This only clears the webhook URL — it does **not** delete the channel or existing data. While deactivated, new inbound webhook events are neither relayed nor stored; reactivate to resume delivery.
- **All Active Channels skips channels silently.** A channel already pointing at another URL is left alone and the workflow still activates without an error. So if one channel isn't responding, check whether its webhook points somewhere else — clear it or use **Specific Channel** to take it over.
- **Clean up before removing.** Deactivate the workflow (don't just delete it, and don't remove the credential first) so the trigger can clear the webhook. If cleanup can't run, a channel keeps pointing at an inactive n8n URL — Fiwano then logs delivery failures and emails you until you clear it (via **Update** or the portal).
- Connect a **new channel** after activating? Re-activate the workflow (toggle off/on) so the trigger wires it.
- Two **All Active Channels** workflows won't fight over a channel — whichever claims an unwired channel first owns it; the other leaves it alone. To move a channel deliberately, clear its webhook or use **Specific Channel**.
- Your n8n must be **publicly reachable** — Fiwano delivers webhooks over the internet to the URL the trigger registers.

### Example workflows

Two ready-to-import workflows are in the [GitHub repository](https://github.com/fiwano-com/n8n-nodes-fiwano/tree/main/workflows). Use them in order:

1. **Connect a Channel** — generate a Meta setup link per channel and capture the connected `channel_id` automatically via a webhook callback. (You can also connect channels in the [Fiwano portal](https://fiwano.com).)
2. **Universal Auto-Responder** — one trigger answers **every** message across WhatsApp, Instagram and Facebook: echoes text and replies to attachments with file details. The unified ping-pong pattern — **needs at least one connected channel** (step 1).

Import them from the editor (**Workflows → Import from File…**) or the CLI.

---

## API Reference (OpenAPI)

The complete machine-readable contract for the public `/api/v1` API, generated from the live service (also at https://fiwano.com/api/v1/openapi.json and https://fiwano.com/api/v1/openapi.yaml).

```yaml
openapi: 3.1.0
info:
  title: Fiwano API
  description: 'Unified REST API for WhatsApp, Instagram and Facebook Messenger.


    Authenticate every request with the `X-API-Key` header (keys start with `mip_live_`,
    created on the API Keys page in the portal). Base URL: `https://fiwano.com`.


    Human-readable guides: https://fiwano.com/documentation'
  version: 2.0.0
servers:
- url: https://fiwano.com
paths:
  /api/v1/subscriptions:
    get:
      tags:
      - api
      summary: List Subscriptions
      description: 'List subscriptions and free channel slots for the authenticated
        user.


        Use this endpoint before starting a channel setup flow to check whether a

        new WhatsApp, Instagram, or Facebook channel can be connected right now.

        A positive `available_slots.<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'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels:
    get:
      tags:
      - api
      summary: List Channels
      description: 'List all channels for the authenticated user.


        Returns both active and inactive channels. Each channel includes its current

        `subscription` (billing) state and the channel-type-specific identifiers

        (WhatsApp: phone_number_id/waba_id; Instagram: ig_account_id/ig_username;

        Instagram & Facebook: page_id).'
      operationId: list_channels_api_v1_channels_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelListResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/{channel_id}:
    get:
      tags:
      - api
      summary: Get Channel
      description: 'Get one channel by ID.


        Same shape as the list endpoint — a single channel object including its

        `subscription` (billing) state. Returns 404 if the channel does not belong

        to the authenticated account.'
      operationId: get_channel_api_v1_channels__channel_id__get
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    patch:
      tags:
      - api
      summary: Update Channel
      description: 'Update channel webhook settings, or move the channel between subscriptions.


        Set webhook_url to receive incoming messages. Must use HTTPS and cannot target
        loopback.

        Optionally provide a custom webhook_secret for HMAC verification.


        `subscription_id` binds the channel to another subscription, or — when sent
        as

        an empty string — releases its subscription slot so a different channel of
        the

        same type can be connected. Releasing a slot requires the channel to be

        deactivated first via DELETE /api/v1/channels/{channel_id}; this is deliberate,

        so a slot is never freed as a side effect of an unrelated settings update.


        Fields left out (or sent as null) are not changed, and the whole request is

        applied atomically: if the subscription change is rejected, the webhook settings

        are not updated either.'
      operationId: update_channel_api_v1_channels__channel_id__patch
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelUpdateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api
      summary: Deactivate channel
      description: 'Deactivate a channel (soft delete).


        The channel stops sending and receiving, but is not erased — its channel_id,

        history and ownership claim are preserved so the same Fiwano account can

        reconnect it later via a new OAuth flow. Another Fiwano account cannot claim

        the Meta identity after deactivation; ownership release requires support.

        Fiwano unsubscribes the channel''s Meta webhook resource only when safe:

        a WABA subscription is kept if another active WhatsApp channel uses the same

        WABA, and a Page subscription is kept if another active Instagram/Facebook

        channel uses the same Page.'
      operationId: delete_channel_api_v1_channels__channel_id__delete
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/setup-url:
    post:
      tags:
      - api
      summary: Create Setup Url
      description: 'Generate a URL for connecting or reconnecting a channel on behalf
        of a user.


        The client opens the returned `setup_url` in a popup or browser. After the

        user completes Meta OAuth (or WhatsApp Embedded Signup), they are redirected

        to `redirect_uri` with a one-time `code`. Exchange that code via

        POST /api/v1/channels/exchange-code to obtain the channel_id.


        `redirect_uri` must already be whitelisted for this API key (add it via

        POST /api/v1/redirects), otherwise the request is rejected. The setup URL
        is

        valid until the `expires_at` returned in the response.


        There is intentionally no separate API reconnect endpoint. When Meta returns

        the identity of an existing inactive channel, setup reactivates that same

        channel row and returns its existing channel_id. A genuinely new eligible

        identity is preferred when both new and inactive assets are available.'
      operationId: create_setup_url_api_v1_channels_setup_url_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupUrlRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetupUrlResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/exchange-code:
    post:
      tags:
      - api
      summary: Exchange Code
      description: 'Exchange a one-time completion code for channel data.


        After the user completes the OAuth flow, the redirect_uri receives a `code`
        parameter.

        This endpoint exchanges that code for the channel_id and basic details.


        The code is one-time use and expires after 5 minutes.'
      operationId: exchange_code_api_v1_channels_exchange_code_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeCodeRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExchangeCodeResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/{channel_id}/profile/{user_id}:
    get:
      tags:
      - api
      summary: Get Sender Profile
      description: "Fetch sender profile from Meta Graph API.\n\nReturns profile data\
        \ for a user who messaged your channel.\nSuccessful results are cached for\
        \ 5 minutes to reduce Meta API calls.\nUnavailable results are cached briefly\
        \ so newly indexed conversations recover quickly.\n\n- Instagram: username,\
        \ name, profile_pic, follower_count, is_verified_user\n- Facebook: display\
        \ name in first_name; last_name and profile_pic when available\n- WhatsApp:\
        \ not supported (name comes inline in webhooks via data.from_name)\n\nArgs:\n\
        \    channel_id: Channel ID\n    user_id: Sender identifier (IGSID for Instagram,\
        \ PSID for Facebook)\n\nReturns:\n    SenderProfileResponse with profile data\n\
        \nRaises:\n    HTTPException 404: Channel not found or WhatsApp (not supported)\n\
        \    HTTPException 400: Channel inactive or missing token\n    HTTPException\
        \ 502: Meta API error"
      operationId: get_sender_profile_api_v1_channels__channel_id__profile__user_id__get
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: user_id
        in: path
        required: true
        schema:
          type: string
          title: User Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SenderProfileResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/redirects:
    get:
      tags:
      - api
      summary: List Redirects
      description: 'List the redirect URIs whitelisted for this API key.


        These gate the programmatic channel-connection flow — the `redirect_uri` in

        POST /api/v1/channels/setup-url must match one of these patterns.'
      operationId: list_redirects_api_v1_redirects_get
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectListResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    post:
      tags:
      - api
      summary: Add Redirect
      description: 'Add an allowed redirect URI.


        Must be HTTPS. Explicit ports are supported. Exact URIs are preferred;

        constrained wildcard patterns like https://*.example.com/* remain supported.'
      operationId: add_redirect_api_v1_redirects_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectCreateRequest'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectCreateResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/redirects/{redirect_id}:
    delete:
      tags:
      - api
      summary: Remove Redirect
      description: Remove an allowed redirect URI.
      operationId: remove_redirect_api_v1_redirects__redirect_id__delete
      parameters:
      - name: redirect_id
        in: path
        required: true
        schema:
          type: string
          title: Redirect Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send:
    post:
      tags:
      - api
      summary: Send Message
      description: 'Send a text message through a connected channel.


        Recipient format is a phone number without `+` for WhatsApp, an IGSID for

        Instagram, and a PSID for Messenger. Text must be non-blank and stay within

        the platform limit. Transient failures are queued for background retry;

        permanent errors return `success: false` and are not retried. A WhatsApp

        send addressed to the channel''s own number is rejected before reaching Meta

        with `400` and a `recipient_equals_sender` detail.'
      operationId: send_message_api_v1_messages_send_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/media/{media_id}:
    get:
      tags:
      - api
      summary: Download Media
      description: 'Download a media file previously received via webhook.


        Requires a Pro license. Files are temporary and expire after

        the configured TTL (default 60 minutes).


        Response: raw file bytes with appropriate Content-Type header.'
      operationId: download_media_api_v1_media__media_id__get
      parameters:
      - name: media_id
        in: path
        required: true
        schema:
          type: string
          title: Media Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send-media:
    post:
      tags:
      - api
      summary: Send Media Message
      description: 'Send a media message through a connected Pro channel.


        Meta fetches `media_url` directly; Fiwano does not store the outbound file.

        Use a signed URL for non-public content and keep it valid for the retry

        window. Supported types are image, audio, video, and document. Transient

        failures are queued; permanent payload or policy errors are not retried.'
      operationId: send_media_message_api_v1_messages_send_media_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMediaRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/{channel_id}/templates:
    get:
      tags:
      - api
      summary: List Templates
      description: 'List message templates for a WhatsApp channel.


        By default, syncs templates from Meta before returning (sync=true).

        Set sync=false to return cached data only (faster, but may be stale).


        Templates are tied to the WhatsApp Business Account (WABA).

        Only WhatsApp channels support templates.'
      operationId: list_templates_api_v1_channels__channel_id__templates_get
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: sync
        in: query
        required: false
        schema:
          type: boolean
          description: Sync templates from Meta before returning. Set false for faster
            cached data (may be stale).
          default: true
          title: Sync
        description: Sync templates from Meta before returning. Set false for faster
          cached data (may be stale).
      - name: status
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Filter by status: APPROVED, PENDING, or REJECTED.'
          title: Status
        description: 'Filter by status: APPROVED, PENDING, or REJECTED.'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateListResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    post:
      tags:
      - api
      summary: Create Template
      description: 'Create a new WhatsApp message template.


        The template is submitted to Meta for review (status=PENDING).

        Review typically takes up to 24 hours.


        Template name must be lowercase alphanumeric with underscores.

        BODY component is required. HEADER (TEXT only), FOOTER, and BUTTONS are optional.


        Variables use {{1}}, {{2}} syntax (positional) or {{name}} (named).

        Example values are required for Meta review.


        Rate limit: 100 templates created per WABA per hour.'
      operationId: create_template_api_v1_channels__channel_id__templates_post
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateCreateRequest'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/channels/{channel_id}/templates/{template_id}:
    get:
      tags:
      - api
      summary: Get Template
      description: 'Get a specific template by its ID.


        Returns cached template data including variable definitions

        with example values for each variable.'
      operationId: get_template_api_v1_channels__channel_id__templates__template_id__get
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    put:
      tags:
      - api
      summary: Update Template
      description: 'Update a template''s components.


        All components are replaced entirely — partial update is not supported by
        Meta.


        Restrictions:

        - Only APPROVED, REJECTED, or PAUSED templates can be edited

        - Approved templates: max 10 edits per 30 days, 1 per 24 hours

        - Cannot change category of an approved template


        After editing an approved template, it goes back to PENDING for re-review.'
      operationId: update_template_api_v1_channels__channel_id__templates__template_id__put
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateUpdateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateOut'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
    delete:
      tags:
      - api
      summary: Delete Template
      description: 'Delete a message template.


        By default, deletes only the specific language version.

        Set all_languages=true to delete ALL language versions of this template.


        WARNING: After deleting an approved template, you cannot create a template

        with the same name for 30 days.'
      operationId: delete_template_api_v1_channels__channel_id__templates__template_id__delete
      parameters:
      - name: channel_id
        in: path
        required: true
        schema:
          type: string
          title: Channel Id
      - name: template_id
        in: path
        required: true
        schema:
          type: string
          title: Template Id
      - name: all_languages
        in: query
        required: false
        schema:
          type: boolean
          description: Delete all language versions of this template, not just this
            one.
          default: false
          title: All Languages
        description: Delete all language versions of this template, not just this
          one.
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
  /api/v1/messages/send-template:
    post:
      tags:
      - api
      summary: Send Template Message
      description: 'Send an approved WhatsApp template without automatic retry.


        Variables must match the cached template definition. Positional variables

        use arrays such as `{"body": ["Pablo", "ORD-123"]}`; named variables use

        objects such as `{"body": {"customer_name": "Pablo"}}`.'
      operationId: send_template_message_api_v1_messages_send_template_post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendTemplateRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    AvailableSlotsByTier:
      properties:
        total:
          type: integer
          title: Total
          description: Total free active slots for this channel type.
        starter:
          type: integer
          title: Starter
          description: Free Starter subscription slots for this channel type.
        pro:
          type: integer
          title: Pro
          description: Free Pro subscription slots for this channel type.
      type: object
      required:
      - total
      - starter
      - pro
      title: AvailableSlotsByTier
      description: Free active subscription slots for one channel type.
    ChannelListResponse:
      properties:
        channels:
          items:
            $ref: '#/components/schemas/ChannelOut'
          type: array
          title: Channels
        total:
          type: integer
          title: Total
      type: object
      required:
      - channels
      - total
      title: ChannelListResponse
      description: List of channels.
    ChannelOut:
      properties:
        id:
          type: string
          title: Id
          description: Channel ID. Use this value in every other channel/message API
            call.
        channel_type:
          type: string
          title: Channel Type
          description: 'Channel type: ''whatsapp'', ''instagram'', or ''facebook''.'
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
          description: 'Display name: business name (WhatsApp), username (Instagram),
            or Page name (Facebook).'
        is_active:
          type: boolean
          title: Is Active
          description: True if the channel can currently send and receive messages.
        phone_number_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number Id
          description: WhatsApp only — Meta's phone number ID.
        phone_number:
          anyOf:
          - type: string
          - type: 'null'
          title: Phone Number
          description: WhatsApp only — human-readable phone number.
        waba_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Waba Id
          description: WhatsApp only — WhatsApp Business Account (WABA) ID.
        quality_rating:
          anyOf:
          - type: string
          - type: 'null'
          title: Quality Rating
          description: WhatsApp only — Meta's current quality rating for the number,
            when available.
        ig_account_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Account Id
          description: Instagram only — Instagram account ID.
        ig_username:
          anyOf:
          - type: string
          - type: 'null'
          title: Ig Username
          description: Instagram only — Instagram username.
        page_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Page Id
          description: Instagram/Facebook — linked Facebook Page ID.
        webhook_url:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Url
          description: Where incoming messages and delivery statuses are delivered.
        has_webhook_secret:
          type: boolean
          title: Has Webhook Secret
          description: Whether a webhook secret is configured for HMAC signature verification.
          default: false
        webhook_events:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          title: Webhook Events
          description: Enabled event types, e.g. ["message.received", "message.delivered"].
        connected_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Connected At
          description: ISO-8601 UTC timestamp when the channel was connected.
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
          description: ISO-8601 UTC timestamp when the channel record was first created.
        subscription:
          $ref: '#/components/schemas/SubscriptionInfo'
          description: Current subscription/billing state. The channel can send/receive
            only while status='active'.
      type: object
      required:
      - id
      - channel_type
      - is_active
      - subscription
      title: ChannelOut
      description: 'A connected channel — one WhatsApp number, Instagram account,
        or Facebook

        Page. Channel-type-specific fields are populated only for the relevant type

        (e.g. `phone_number_id`/`waba_id` for WhatsApp, `ig_username` for Instagram);

        the rest are null.'
    ChannelUpdateRequest:
      properties:
        webhook_url:
          anyOf:
          - type: string
            maxLength: 500
          - type: 'null'
          title: Webhook Url
          description: HTTPS URL for incoming webhook delivery. Explicit ports are
            supported.
        webhook_secret:
          anyOf:
          - type: string
          - type: 'null'
          title: Webhook Secret
          description: Set a specific HMAC secret (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.
        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
        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
          - 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 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.
      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
      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
          title: Recipient
          description: Recipient identifier
        media_type:
          type: string
          title: Media Type
          description: 'Media type: image, audio, video, or document'
        media_url:
          type: string
          maxLength: 2048
          title: Media Url
          description: HTTPS URL of the media file. For non-public content, use a
            signed URL (S3 presigned, GCS Signed, R2 signed, or HMAC). Max length
            2048.
        caption:
          anyOf:
          - type: string
            maxLength: 1024
          - type: 'null'
          title: Caption
          description: Caption (WhatsApp image/video/document)
        filename:
          anyOf:
          - type: string
            maxLength: 255
          - type: 'null'
          title: Filename
          description: Filename (WhatsApp document only)
      type: object
      required:
      - channel_id
      - recipient
      - media_type
      - media_url
      title: SendMediaRequest
      description: 'Request to send a media message. Requires a Pro license.


        Meta fetches the file directly from `media_url`. For non-public

        content, use a signed URL — presigned S3, GCS Signed URL, Cloudflare

        R2 signed URL, or HMAC-signed URL on your own server. Public URLs

        are accessible to anyone who learns them; only use them for

        non-sensitive content.


        Use image, audio, video, or document across all supported channels.

        Provider-specific attachment names are handled internally.'
    SendMessageRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Channel ID to send from
        recipient:
          type: string
          title: Recipient
          description: Recipient identifier (phone number for WhatsApp, IGSID for
            Instagram, PSID for Facebook)
        text:
          type: string
          minLength: 1
          title: Text
          description: Message text containing at least one non-whitespace character
      type: object
      required:
      - channel_id
      - recipient
      - text
      title: SendMessageRequest
      description: Request to send a message. Only text messages are supported.
    SendMessageResponse:
      properties:
        success:
          type: boolean
          title: Success
        message_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Message Id
        error:
          anyOf:
          - type: string
          - type: 'null'
          title: Error
        error_code:
          anyOf:
          - type: integer
          - type: 'null'
          title: Error Code
          description: Meta error code (when failure originates at Meta). See documentation
            for common send error codes.
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
          description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (transient
            text/media failure — retried in the background), or ''failed'' (terminal
            for this request; see error/error_code). Template sends are never queued.'
      type: object
      required:
      - success
      title: SendMessageResponse
      description: Response after sending a message.
    SendTemplateRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: WhatsApp channel ID to send from
        template_name:
          type: string
          title: Template Name
          description: Template name (e.g., 'order_confirmation')
        language:
          type: string
          title: Language
          description: Template language code (e.g., 'en_US')
        recipient:
          type: string
          title: Recipient
          description: Recipient phone number (without +, e.g., '1234567890')
        variables:
          anyOf:
          - type: object
          - type: 'null'
          title: Variables
          description: 'Variable values keyed by component type. Positional: {"header":
            ["Sale"], "body": ["Pablo", "ORD-123"], "buttons": [{"index": 0, "value":
            "promo"}]}. Named: {"body": {"customer_name": "Pablo", "order_number":
            "ORD-123"}}. Omit if template has no variables.'
      type: object
      required:
      - channel_id
      - template_name
      - language
      - recipient
      title: SendTemplateRequest
      description: "Request to send a WhatsApp template message.\n\nVariables must\
        \ match the template's parameter definitions.\nOnly APPROVED templates can\
        \ be sent.\n\nFor positional templates, provide variables as arrays:\n   \
        \ {\"body\": [\"Pablo\", \"ORD-123\"]}\n\nFor named templates, provide variables\
        \ as objects:\n    {\"body\": {\"customer_name\": \"Pablo\", \"order_number\"\
        : \"ORD-123\"}}"
    SenderProfileResponse:
      properties:
        channel_id:
          type: string
          title: Channel Id
        channel_type:
          type: string
          title: Channel Type
        user_id:
          type: string
          title: User Id
        profile:
          anyOf:
          - type: object
          - type: 'null'
          title: Profile
          description: 'Profile data from Meta, normalized and minimized by channel
            type. null if profile is unavailable. Instagram: username, name, profile_pic,
            follower_count, is_verified_user. Facebook: display name in first_name;
            last_name and profile_pic when available from Meta. WhatsApp is not supported.'
          examples:
          - follower_count: 46
            is_verified_user: false
            name: Roman Babakin
            profile_pic: https://scontent.cdninstagram.com/v/t51.2885-19/...
            username: winnerzzz
          - first_name: Roman
            last_name: Babakin
            profile_pic: https://platform-lookaside.fbsbx.com/platform/profilepic/...
        cached:
          type: boolean
          title: Cached
          description: Whether the result was served from cache
          default: false
      type: object
      required:
      - channel_id
      - channel_type
      - user_id
      title: SenderProfileResponse
      description: Sender profile from Meta Graph API.
    SetupUrlRequest:
      properties:
        channel_type:
          type: string
          pattern: ^(whatsapp|instagram|facebook)$
          title: Channel Type
          description: 'Channel type: whatsapp, instagram, or facebook'
        redirect_uri:
          type: string
          maxLength: 500
          title: Redirect Uri
          description: Exact HTTPS URL to redirect after OAuth completion. Explicit
            ports are supported. Must match allowed_redirects.
      type: object
      required:
      - channel_type
      - redirect_uri
      title: SetupUrlRequest
      description: Request to generate channel setup URL.
    SetupUrlResponse:
      properties:
        setup_url:
          type: string
          title: Setup Url
          description: URL to open in popup/browser for channel setup
        session_id:
          type: string
          title: Session Id
        expires_at:
          type: string
          title: Expires At
      type: object
      required:
      - setup_url
      - session_id
      - expires_at
      title: SetupUrlResponse
      description: Response with channel setup URL.
    SubscriptionAssignedChannel:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: Fiwano channel ID assigned to this subscription.
        channel_type:
          type: string
          title: Channel Type
          description: 'Channel type: whatsapp, instagram, or facebook.'
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
          description: Human-readable channel name when available.
        is_active:
          type: boolean
          title: Is Active
          description: Whether the bound channel is currently active.
      type: object
      required:
      - channel_id
      - channel_type
      - is_active
      title: SubscriptionAssignedChannel
      description: 'Channel currently assigned to this subscription for one channel
        type.


        An inactive channel still occupies its slot while it remains bound to an

        active subscription. This preserves the owner''s reconnect path and matches

        the billing gate used by channel setup.'
    SubscriptionInfo:
      properties:
        id:
          anyOf:
          - type: string
          - type: 'null'
          title: Id
          description: Subscription ID this channel is bound to — the same value as
            `subscriptions[].id` in GET /api/v1/subscriptions. Pass it back in PATCH
            /api/v1/channels/{channel_id} to move the channel between subscriptions.
            Null when status='none'.
        status:
          type: string
          title: Status
          description: 'Subscription status: ''active'', ''expired'', ''canceled'',
            or ''none'' (no license bound — channel cannot send/receive messages).'
        source:
          anyOf:
          - type: string
          - type: 'null'
          title: Source
          description: 'Origin of the license: ''trial'' (auto-created on signup),
            ''paddle'' (paid subscription), or ''enterprise'' (admin-granted custom
            subscription). Null when status=''none''.'
        tier:
          anyOf:
          - type: string
          - type: 'null'
          title: Tier
          description: 'License tier: ''starter'' or ''pro''. Null when status=''none''.'
        expires_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Expires At
          description: ISO-8601 UTC timestamp when the current billing period / license
            ends. Null when status='none' or for perpetual enterprise licenses.
        auto_renew:
          type: boolean
          title: Auto Renew
          description: True only for an active Paddle subscription that is set to
            renew automatically at `expires_at`. False for trial, enterprise, or any
            Paddle subscription where the user has scheduled/performed a cancellation.
          default: false
      type: object
      required:
      - status
      title: SubscriptionInfo
      description: 'Subscription/license state for a channel.


        Always present on ChannelOut. When the channel is not bound to any license

        (orphaned after expiry/cancellation, or briefly between connect and

        auto-assign), `status` is `"none"` and all other fields are null/false.


        `expires_at` is `null` when no license is bound; in normal operation

        every active license (trial / Paddle / Enterprise) carries an explicit

        expiration date. (The schema still permits `NULL` for legacy admin-

        granted rows — clients should treat that as "no announced end date".)


        `auto_renew` is `true` only for an active Paddle subscription with no

        scheduled cancellation. Once the user cancels in Paddle (or the

        subscription enters a non-renewing state) it flips to `false` and the

        license will lapse at `expires_at` unless resumed.'
    SubscriptionOut:
      properties:
        id:
          type: string
          title: Id
          description: Fiwano subscription ID.
        status:
          type: string
          title: Status
          description: 'Subscription status: active, expired, or canceled.'
        source:
          type: string
          title: Source
          description: 'Origin: trial, paddle, or enterprise.'
        tier:
          type: string
          title: Tier
          description: 'Subscription tier: starter or pro.'
        starts_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Starts At
          description: ISO-8601 UTC timestamp when the subscription started.
        expires_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Expires At
          description: ISO-8601 UTC timestamp when the current period ends. Treat
            status as the source of truth for operability; Paddle grace can leave
            an active subscription with expires_at in the past.
        auto_renew:
          type: boolean
          title: Auto Renew
          description: True only for an active Paddle subscription set to renew automatically.
          default: false
        assigned_channels:
          additionalProperties:
            anyOf:
            - $ref: '#/components/schemas/SubscriptionAssignedChannel'
            - type: 'null'
          type: object
          title: Assigned Channels
          description: Channels assigned to this subscription by channel type. Values
            are null when no channel is assigned for that type. Use top-level available_slots
            for current connection availability.
      type: object
      required:
      - id
      - status
      - source
      - tier
      - assigned_channels
      title: SubscriptionOut
      description: 'A Fiwano subscription/license and its channel-slot availability.


        Public API uses the product term "subscription"; internally this maps to a

        License row. One subscription grants one slot for each channel type.'
    SubscriptionsResponse:
      properties:
        subscriptions:
          items:
            $ref: '#/components/schemas/SubscriptionOut'
          type: array
          title: Subscriptions
        total:
          type: integer
          title: Total
          description: Total number of subscriptions returned.
        active_total:
          type: integer
          title: Active Total
          description: Number of currently active subscriptions.
        available_slots:
          additionalProperties:
            $ref: '#/components/schemas/AvailableSlotsByTier'
          type: object
          title: Available Slots
          description: Free active subscription slots by channel type, split by tier.
            `total` is the sum of Starter and Pro free slots.
      type: object
      required:
      - subscriptions
      - total
      - active_total
      - available_slots
      title: SubscriptionsResponse
      description: Subscriptions and aggregate slot availability for the authenticated
        user.
    TemplateComponentInput:
      properties:
        type:
          type: string
          title: Type
          description: 'Component type: HEADER, BODY, FOOTER, BUTTONS'
        format:
          anyOf:
          - type: string
          - type: 'null'
          title: Format
          description: 'Header format: TEXT (media not yet supported)'
        text:
          anyOf:
          - type: string
          - type: 'null'
          title: Text
          description: Component text. Use {{1}}, {{2}} for positional or {{name}}
            for named variables
        example:
          anyOf:
          - type: object
          - type: 'null'
          title: Example
          description: Example values for variables (required by Meta for review)
        buttons:
          anyOf:
          - items:
              type: object
            type: array
          - type: 'null'
          title: Buttons
          description: Button definitions (for BUTTONS component)
      type: object
      required:
      - type
      title: TemplateComponentInput
      description: 'A single template component for creation/update.


        Components define the structure of a WhatsApp message template.'
    TemplateCreateRequest:
      properties:
        name:
          type: string
          maxLength: 512
          pattern: ^[a-z0-9_]+$
          title: Name
          description: Template name. Lowercase alphanumeric and underscores only.
            Max 512 chars.
        category:
          type: string
          pattern: ^(MARKETING|UTILITY|AUTHENTICATION)$
          title: Category
          description: 'Template category: MARKETING, UTILITY, or AUTHENTICATION'
        language:
          type: string
          title: Language
          description: Language code (e.g., en_US, ru, es)
        components:
          items:
            $ref: '#/components/schemas/TemplateComponentInput'
          type: array
          title: Components
          description: Template components (HEADER, BODY, FOOTER, BUTTONS). BODY is
            required.
        parameter_format:
          type: string
          pattern: ^(positional|named)$
          title: Parameter Format
          description: 'Variable format: ''positional'' for {{1}}, {{2}} or ''named''
            for {{customer_name}}'
          default: positional
      type: object
      required:
      - name
      - category
      - language
      - components
      title: TemplateCreateRequest
      description: 'Request to create a WhatsApp message template.


        The template will be submitted to Meta for review (status=PENDING).

        Review typically takes up to 24 hours.'
    TemplateListResponse:
      properties:
        templates:
          items:
            $ref: '#/components/schemas/TemplateOut'
          type: array
          title: Templates
        total:
          type: integer
          title: Total
        synced:
          type: boolean
          title: Synced
          default: false
      type: object
      required:
      - templates
      - total
      title: TemplateListResponse
      description: List of templates.
    TemplateOut:
      properties:
        id:
          type: string
          title: Id
        meta_template_id:
          type: string
          title: Meta Template Id
        name:
          type: string
          title: Name
        language:
          type: string
          title: Language
        category:
          type: string
          title: Category
        status:
          type: string
          title: Status
        components:
          items: {}
          type: array
          title: Components
        parameter_format:
          type: string
          title: Parameter Format
          default: positional
        variables:
          anyOf:
          - $ref: '#/components/schemas/TemplateVariablesSummary'
          - type: 'null'
        synced_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Synced At
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
      type: object
      required:
      - id
      - meta_template_id
      - name
      - language
      - category
      - status
      - components
      title: TemplateOut
      description: Template data returned by API.
    TemplateUpdateRequest:
      properties:
        components:
          items:
            $ref: '#/components/schemas/TemplateComponentInput'
          type: array
          title: Components
          description: New components (replaces all existing)
        category:
          anyOf:
          - type: string
          - type: 'null'
          title: Category
          description: New category (only for REJECTED or PAUSED templates)
      type: object
      required:
      - components
      title: TemplateUpdateRequest
      description: 'Request to update a template''s components.


        All components are replaced entirely (partial update not supported by Meta).

        Approved templates: max 10 edits per 30 days, 1 per 24 hours.'
    TemplateVariableInfo:
      properties:
        position:
          type: integer
          title: Position
        name:
          anyOf:
          - type: string
          - type: 'null'
          title: Name
        example:
          anyOf:
          - type: string
          - type: 'null'
          title: Example
      type: object
      required:
      - position
      title: TemplateVariableInfo
      description: Variable info for display/documentation.
    TemplateVariablesSummary:
      properties:
        total_count:
          type: integer
          title: Total Count
          default: 0
        parameter_format:
          type: string
          title: Parameter Format
          default: positional
        header:
          anyOf:
          - items:
              $ref: '#/components/schemas/TemplateVariableInfo'
            type: array
          - type: 'null'
          title: Header
        body:
          anyOf:
          - items:
              $ref: '#/components/schemas/TemplateVariableInfo'
            type: array
          - type: 'null'
          title: Body
        buttons:
          anyOf:
          - items:
              type: object
            type: array
          - type: 'null'
          title: Buttons
      type: object
      title: TemplateVariablesSummary
      description: Summary of all variables in a template.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
      type: object
      required:
      - loc
      - msg
      - type
      title: ValidationError
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key in the format mip_live_xxx. Create one on the API Keys
        page in the portal.
security:
- ApiKeyAuth: []
```
