# 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** — From sign-up to your first sent and received message in five minutes.
4. **Channels** — Connect, manage and reconnect WhatsApp, Instagram and Messenger channels.
5. **Sending Messages** — Send text, media and WhatsApp template messages.
6. **Receiving Messages** — Receive messages and statuses, download media, look up sender profiles.
7. **WhatsApp Templates** — Create, manage and review WhatsApp message templates.
8. **Capabilities** — Channel capabilities, license tiers, rate limits, messaging windows and media limits.
9. **Subscriptions & Billing** — Read a channel's subscription state and the billing lifecycle.
10. **n8n Integration** — Verified n8n community node — nodes, operations and events.
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](/in/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](/in/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](/in/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](/in/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](/in/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](/in/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](/in/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](/in/documentation/capabilities#messaging-windows-24h).

Read [WhatsApp Templates](/in/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](/in/documentation/n8n) — same channels,
  same events, drag-and-drop.
- **Media and templates** — [Sending Messages](/in/documentation/sending-messages).
- **Limits, windows and tiers** — [Capabilities](/in/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).

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

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

**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](/in/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](/in/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](/in/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](/in/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. 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 that is currently active under a different Fiwano account cannot
  be reconnected (`"already connected to another account"`).

---

## 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](/in/documentation/capabilities#channel-capabilities). Full
request/response schemas are in the [API Reference](/documentation/api); this page
is the task guide.

### 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](/in/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](/in/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](/in/documentation/capabilities#messaging-windows-24h)). Only `APPROVED`
templates can be sent — to create and manage them, see
[WhatsApp Templates](/in/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](/in/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](/in/documentation/channels)); by default no events are enabled. If the channel has a [`webhook_secret`](/in/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.

### 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](/in/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`
- **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](/in/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](/in/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`](/in/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](/in/documentation/webhooks#sender-profile) | Via [profile endpoint](/in/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](/in/documentation/subscriptions). For how this flat fee relates to Meta's own per-message charges, see [Messaging Costs Explained](/in/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](/in/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.

---

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

HMAC-SHA256 signature verification is built into the trigger node. To enable it, set the channel's `webhook_secret` in the trigger's **Webhook Secret** field — the node then verifies every webhook against that secret and rejects mismatches with HTTP 401. The secret is the one you configured per-channel (via **Exchange OAuth Code** or **Update Webhook**); n8n stores your copy in the node, it is not fetched automatically. Leave the field empty to skip verification (not recommended in production).

### Example workflow

A ready-to-import demo workflow is included in the [GitHub repository](https://github.com/fiwano-com/n8n-nodes-fiwano) covering the core operations: echo bot with profile enrichment, channel management, template CRUD, text and template messaging.

---

## 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/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 be HTTPS (or http://localhost
        for dev).

        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: Delete Channel
      description: 'Deactivate a channel (soft delete).


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

        and history are preserved so it can be reconnected later via a new OAuth

        flow. 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 a channel on behalf of a user (programmatic
        flow).


        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.'
      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\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:
    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. Fields vary by channel type. null if
            profile is unavailable.
        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.
    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.'
    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: []
```
