# 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 and HTTP status codes.
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 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 uses the same shape — a `detail` field with a human-readable description:

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

Some validation errors carry a structured `detail` object instead (for example `text_too_long` when sending overlong text); 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 |
| `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`. |

---

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

### 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 literal
loopback address.

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). Incoming
messages are saved on our side even if relay fails. Full delivery and retry 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 | `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.

### 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. 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 ≥ 5 min. 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=600",
    "caption": "Your order photo"
  }'
```

Always check `success`. On failure the response includes a Meta `error_code` —
e.g. `131052` (Meta couldn't download the URL) or `131053` (unsupported
format/size, or Meta rate-limited your host's network — try a major cloud
provider). File-size caps and the full error-code table live in
[Capabilities](/documentation/capabilities) and the [API Reference](/documentation/api).

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

### Delivery and retries

`send` and `send-media` return `200` with a `status` field, 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.
- **`failed`** (`success: false`) — a permanent error (bad recipient, oversize
  text, malformed payload). **Not retried** — fix the request and resend. The
  channel owner is emailed.

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 a message via `/api/v1/messages/send`, you receive a `message_id` (UUID). All subsequent status webhooks reference this same UUID.

- `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 use the same normalized `data.media` block. The sender is still `data.from` (IGSID for Instagram, PSID for Facebook), and `data.type` follows the Meta attachment type such as `"image"` or `"audio"`. Use `data.type` as the primary message type and as the outbound `media_type` when echoing or forwarding media.

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 60 minutes after the webhook** — 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)

With a **Starter** license, media messages are delivered with `type: "unsupported"` and an `upgrade_required` field hinting which license is needed:

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

The `upgrade_required` field tells you which license tier is needed. Upgrade via the Billing page in the portal to receive full media content.

> **Note:** Messages with truly unsupported types (e.g. `location`, `contacts`, `reaction`) still arrive as `type: "unsupported"` without `upgrade_required`, regardless of your license tier.

#### 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. Incoming messages from Meta are always saved on our side, even if relay fails.

> **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** — `first_name`, `last_name`, `profile_pic`

WhatsApp is not supported (the name is already in the webhook). Results are cached for 5 minutes — the response's `cached` flag tells you if it was a cache hit. Full request/response and status codes are in the [API Reference](/documentation/api).

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

---

## WhatsApp Templates

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

### Lifecycle

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

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

Each API key is allowed **~600 requests/minute (sustained)** with short bursts
above that. Exceeding it returns HTTP `429` with a `Retry-After` header telling
you how many seconds to wait; successful responses carry `X-RateLimit-Limit` and
`X-RateLimit-Remaining` so you can pace yourself. Under exceptional aggregate
load the platform may briefly shed requests with HTTP `503` + `Retry-After` —
treat both `429` and `503` the same way: honor `Retry-After` and retry. This is a
generous guardrail, not a hard product cap — if you need more sustained
throughput, [contact us](mailto:contact@fiwano.com). Meta has its own per-channel
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

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

---

## 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, Delete |
| 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 |

### 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 Webhook** 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 Webhook**. 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 Webhook** 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, messages, or any data. While deactivated, Fiwano still stores inbound messages but doesn't relay them; reactivate to resume.
- **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 Webhook** 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.


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

        Optionally provide a custom webhook_secret for HMAC verification.'
      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.\nResults are cached for 5 minutes\
        \ to reduce Meta API calls.\n\n- Instagram: username, name, profile_pic, follower_count,\
        \ is_verified_user\n- Facebook: first_name, last_name, profile_pic\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. Supports wildcard patterns like https://*.example.com/*'
      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.


        Only text messages are supported here (use /api/v1/messages/send-media for

        media and /api/v1/messages/send-template for WhatsApp templates).


        Recipient format by channel:

        - WhatsApp: phone number without + (e.g. "1234567890")

        - Instagram: Instagram-scoped user ID (IGSID)

        - Facebook Messenger: Page-scoped user ID (PSID)


        `text` has a per-platform hard limit (WhatsApp 4096, Facebook 2000,

        Instagram 1000); oversize text is rejected with 400 `text_too_long` before

        Meta is called. On a transient Meta failure the response is 200 with

        `status: "queued"` (retried in the background); on a permanent error it is

        200 with `success: false`, `status: "failed"`.'
      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 channel.


        Requires a Pro license. Meta fetches the file directly from `media_url`

        — Fiwano does not download or store it. For non-public content, use a

        signed URL (S3 presigned, GCS Signed, R2 signed, or HMAC-signed URL).


        Supported media types per channel:

        - WhatsApp: image, audio, video, document

        - Instagram: image, audio, video, document

        - Facebook Messenger: image, audio, video, document


        On failure, returns 200 with `success=false` and `error_code` (Meta

        code, when applicable). See public API docs for the error code table.'
      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 a WhatsApp template message.\n\nUse this to initiate conversations\
        \ outside the 24-hour messaging window.\nOnly APPROVED templates can be sent.\n\
        \n## Variables\n\nEach template may have variables in HEADER, BODY, and BUTTON\
        \ components.\nAll variables must be provided — there are no defaults.\n\n\
        **Positional format** (for templates created with {{1}}, {{2}}):\n```json\n\
        {\n    \"variables\": {\n        \"header\": [\"Summer Sale\"],\n        \"\
        body\": [\"Pablo\", \"ORD-123\", \"25%\"],\n        \"buttons\": [{\"index\"\
        : 0, \"value\": \"promo25\"}]\n    }\n}\n```\n\n**Named format** (for templates\
        \ created with {{customer_name}}):\n```json\n{\n    \"variables\": {\n   \
        \     \"body\": {\"customer_name\": \"Pablo\", \"order_number\": \"ORD-123\"\
        }\n    }\n}\n```\n\nOmit `variables` entirely if the template has no variables."
      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/SendTemplateResponse'
        '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
          - type: 'null'
          title: Webhook Url
          description: HTTPS URL for incoming webhook delivery
        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.
      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
      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
          - type: 'null'
          title: Webhook Url
          description: HTTPS URL for incoming webhook delivery. 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
          title: Uri Pattern
          description: 'HTTPS URL pattern. Supports wildcards: 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
          title: Text
          description: Message text
      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 media error codes.
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
          description: 'Delivery state: ''sent'' (accepted by Meta), ''queued'' (transient
            failure — retried in the background), or ''failed'' (permanent error;
            see error/error_code).'
      type: object
      required:
      - success
      title: SendMessageResponse
      description: Response after sending a message.
    SendTemplateRequest:
      properties:
        channel_id:
          type: string
          title: Channel Id
          description: WhatsApp channel ID to send from
        template_name:
          type: string
          title: Template Name
          description: Template name (e.g., 'order_confirmation')
        language:
          type: string
          title: Language
          description: Template language code (e.g., 'en_US')
        recipient:
          type: string
          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\"}}"
    SendTemplateResponse:
      properties:
        success:
          type: boolean
          title: Success
        message_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Message Id
        error:
          anyOf:
          - type: string
          - type: 'null'
          title: Error
      type: object
      required:
      - success
      title: SendTemplateResponse
      description: Response after sending a template message.
    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, passed through as-is. Fields vary
            by channel type. null if profile is unavailable. Instagram: username,
            name, profile_pic, follower_count, is_verified_user. Facebook: first_name,
            last_name, profile_pic. 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
          title: Redirect Uri
          description: URL to redirect after OAuth completion. Must be in 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:
        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: []
```
