# Fiwano — API Documentation

Unified REST API for WhatsApp, Instagram and Facebook Messenger.

| | |
|---|---|
| **Base URL** | `/api/v1` |
| **Format** | JSON |
| **Auth** | `X-API-Key` header |
| **n8n nodes** | [`n8n-nodes-fiwano`](https://www.npmjs.com/package/n8n-nodes-fiwano) on npm |

---

## 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 | ✅ | ✅ | ✅ |
| 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 |
| Incoming webhooks — media (Starter) | `type: "unsupported"` + `upgrade_required: "pro"` | Same | Same |
| Delivery statuses | `sent` `delivered` `read` `failed` | `delivered` `read` | `delivered` `read` |
| Recipient format | Phone number without `+` (e.g. `1234567890`) | IGSID — from `data.from` in webhooks | PSID — from `data.from` in webhooks |
| 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](#sender-profile) | Via [profile endpoint](#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). Upgrade or manage subscriptions via the Billing page in the portal.

---

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

---

## Connecting Channels

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

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.

A `webhook_secret` is auto-generated when you first set a webhook URL. Use it to verify incoming webhook signatures.

### Option B: Via API (Programmatic)

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

**Step 1.** Register redirect URIs — whitelist the URL(s) where users will be redirected after OAuth:

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

**Step 2.** Request a setup URL (valid for 10 minutes):

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

Response contains `setup_url` — open it in a browser or popup for the user.

**Step 3.** User completes Meta OAuth — after approval, they are redirected to your `redirect_uri` with a one-time `code` parameter:

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

If the user cancels: `?error=access_denied`

**Step 4.** Exchange the code + configure webhook (within 5 minutes, single-use):

```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_secret": "your-optional-secret",
    "webhook_events": ["message.received", "message.delivered", "message.failed"]
  }'
```

Returns `channel_id`, channel details, and `webhook_secret` (auto-generated if not provided). All fields except `code` are optional — you can set them later via `PATCH /api/v1/channels/{id}`.

**Important:** By default, no events are delivered. You must explicitly set `webhook_events` to receive webhooks.

---

## Channels

### GET /api/v1/channels

List all channels for your account (both active and inactive).

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

Response:

```json
{
  "channels": [
    {
      "id": "a1b2c3d4e5f67890",
      "channel_type": "whatsapp",
      "name": "My Business",
      "is_active": true,
      "phone_number_id": "123456789",
      "phone_number": "+1234567890",
      "waba_id": "987654321",
      "webhook_url": "https://yourapp.com/webhooks",
      "has_webhook_secret": true,
      "connected_at": "2025-01-15T10:30:00",
      "token_expires_at": "2025-03-15T10:30:00",
      "subscription": {
        "status": "active",
        "source": "paddle",
        "tier": "pro",
        "expires_at": "2025-02-15T10:30:00",
        "auto_renew": true
      }
    }
  ],
  "total": 1
}
```

Response fields:

| Field | Description |
|---|---|
| `id` | Channel ID (use in all other API calls) |
| `channel_type` | `whatsapp`, `instagram`, or `facebook` |
| `name` | Display name (business name, username, or page name) |
| `is_active` | `true` if the channel can send/receive messages |
| `phone_number_id` | WhatsApp only — Meta's phone number ID |
| `phone_number` | WhatsApp only — human-readable phone number |
| `waba_id` | WhatsApp only — WhatsApp Business Account ID |
| `ig_account_id` | Instagram only — Instagram account ID |
| `ig_username` | Instagram only — Instagram username |
| `page_id` | Instagram/Facebook — linked Facebook Page ID |
| `webhook_url` | Where incoming messages are delivered |
| `has_webhook_secret` | Whether a webhook secret is configured |
| `webhook_events` | List of enabled event types (e.g. `["message.received", "message.delivered"]`) |
| `token_expires_at` | When the access token expires (auto-refreshed 7 days before) |
| `subscription` | Subscription/license state object — see below |

#### `subscription` object

The current billing state of the channel. Always present — for a channel that is not bound to any active subscription, `status` is `"none"` and the rest are null/false.

| Field | Type | Description |
|---|---|---|
| `status` | string | `active` — channel can send/receive. `expired` — billing period ended (or scheduled cancellation reached `expires_at`). `canceled` — subscription was canceled mid-period. `none` — no subscription bound; channel cannot send/receive messages. |
| `source` | string \| null | Where the entitlement came from: `trial` (auto-granted on signup), `paddle` (paid subscription via Paddle), `enterprise` (custom subscription provisioned by Fiwano staff, e.g. partner deal or invoice billing). `null` when `status` is `none`. |
| `tier` | string \| null | `starter` or `pro`. `pro` is required for media messages and WhatsApp template CRUD/send. `null` when `status` is `none`. |
| `expires_at` | string \| null | ISO-8601 UTC timestamp when the current billing period ends. `null` only when `status` is `none`. If `auto_renew` is `true`, this is the next renewal date; otherwise this is the cutoff after which the channel will stop working. |
| `auto_renew` | boolean | `true` only for an active Paddle subscription that will renew on `expires_at`. Becomes `false` as soon as the customer cancels in Paddle — the subscription stays usable until `expires_at`, then lapses. Always `false` for trial and Enterprise licenses (Enterprise renewals are handled out-of-band by Fiwano staff before `expires_at`). |

Common combinations:

- **Active Paddle subscription** — `{status: "active", source: "paddle", auto_renew: true, expires_at: <next renewal>}`
- **Paddle with cancellation scheduled** — `{status: "active", source: "paddle", auto_renew: false, expires_at: <cutoff>}`. Service continues until `expires_at`, then 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; reconnect a license to restore service.

### GET /api/v1/channels/{channel_id}

Get details of a specific channel. Same response format (including the `subscription` block) as the list endpoint.

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

### POST /api/v1/channels/setup-url

Generate an OAuth URL to connect a new channel. See [API connection flow](#option-b-via-api-programmatic) above.

| Field | Type | Required | Description |
|---|---|---|---|
| `channel_type` | string | Yes | `whatsapp`, `instagram`, or `facebook` |
| `redirect_uri` | string | Yes | Must be in your allowed redirect URIs |

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

Response:

```json
{
  "setup_url": "https://fiwano.com/setup/start?session=...",
  "session_id": "e7f8a1b2c3d45678",
  "expires_at": "2025-01-15T10:40:00"
}
```

The setup URL is valid for **10 minutes**.

### POST /api/v1/channels/exchange-code

Exchange a one-time completion code for channel data. Optionally configure webhook in the same call.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | Yes | One-time code from the OAuth redirect. Expires in 5 minutes, single-use. |
| `webhook_url` | string | No | HTTPS URL for incoming webhooks. If provided, webhook is configured automatically. |
| `webhook_secret` | string | No | Custom HMAC secret. If `webhook_url` is set and this is omitted, a secret is auto-generated. |
| `webhook_events` | string[] | No | List of event types to deliver. See [Event Types](#event-types) for available values per channel type. If omitted, no events are delivered until configured. |

WhatsApp response:

```json
{
  "channel_id": "a1b2c3d4e5f67890",
  "channel_type": "whatsapp",
  "name": "My Business",
  "phone_number_id": "123456789",
  "phone_number": "+1234567890",
  "webhook_url": "https://yourapp.com/webhooks",
  "webhook_secret": "a1b2c3d4e5f6...",
  "webhook_events": ["message.received", "message.delivered", "message.failed"]
}
```

Instagram response:

```json
{
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "name": "mybusiness",
  "ig_account_id": "17841400123456",
  "ig_username": "mybusiness",
  "webhook_url": "https://yourapp.com/webhooks",
  "webhook_secret": "a1b2c3d4e5f6...",
  "webhook_events": ["message.received", "message.delivered"]
}
```

### PATCH /api/v1/channels/{channel_id}

Update channel webhook settings.

| Field | Type | Required | Description |
|---|---|---|---|
| `webhook_url` | string | No | HTTPS URL for incoming webhooks |
| `webhook_secret` | string | No | Custom HMAC secret. If omitted and no secret exists, one is auto-generated. |
| `webhook_events` | string[] | No | List of event types to deliver. Only events valid for the channel type are accepted. Empty array disables all events. |

Example — enable only incoming messages and failures:

```bash
curl -X PATCH https://fiwano.com/api/v1/channels/a1b2c3d4e5f67890 \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhook_events": ["message.received", "message.failed"]}'
```

Response includes the current `webhook_events` list.

### DELETE /api/v1/channels/{channel_id}

Deactivate a channel (soft delete). The channel stops sending and receiving messages. Fiwano also attempts to unsubscribe the channel's Meta webhook resource when it is safe: WABA subscriptions are kept if another active WhatsApp channel uses the same WABA, and Page subscriptions are kept if another active Instagram/Facebook channel uses the same Page. You can reconnect the same Meta account later via a new OAuth flow.

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

---

## Sending Messages

### POST /api/v1/messages/send

Send a text message through a connected channel. Works for all channel types.

| Field | Type | Required | Description |
|---|---|---|---|
| `channel_id` | string | Yes | Channel to send from |
| `recipient` | string | Yes | See recipient format in channel capabilities table |
| `text` | string | Yes | Message text. Max length is platform-dependent: WhatsApp 4096, Facebook Messenger 2000, Instagram 1000. Longer messages are rejected by Meta — split on your side. |

WhatsApp example:

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

Instagram example:

```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": "b2c3d4e5f6789012", "recipient": "6543217890123456", "text": "Thanks for reaching out!"}'
```

Response:

```json
{
  "success": true,
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

The `message_id` (UUID) is used in all subsequent delivery status webhooks.

#### Text length limits & errors

Each platform enforces a hard maximum on outgoing text length. Fiwano validates the length **before** calling Meta and rejects oversize requests immediately:

| Platform | Max text length |
|---|---|
| WhatsApp | 4096 characters |
| Facebook Messenger | 2000 characters |
| Instagram | 1000 characters |

If `text` exceeds the limit, the API returns `400 Bad Request` with a structured error:

```json
{
  "detail": {
    "code": "text_too_long",
    "message": "Text exceeds instagram limit of 1000 characters (got 1234).",
    "actual_length": 1234,
    "max_length": 1000,
    "channel_type": "instagram",
    "hint": "Split the text on your side and send as multiple messages."
  }
}
```

Fiwano does **not** auto-split long messages — splitting is the caller's responsibility (preserves your chunking conventions, ordering and headers).

#### Behavior on transient vs permanent Meta errors

| Failure type | Examples | What Fiwano does |
|---|---|---|
| **Transient** (network, 5xx, rate limit) | Meta 500/503, timeouts | Response: `200 OK` with `status: "queued"`. Background retries up to 7 times over ~20 minutes. After 3 failed retries — early-warning email; after final expiry — failure email + Telegram alert. |
| **Permanent** (Meta error code 100) | Text too long, invalid recipient, malformed payload, bad parameter | Response: `200 OK` with `success: false`, `status: "failed"`, `error: "..."`. **Not enqueued for retry.** Immediate alert to admin and email to channel owner — fix the request and resend. |

If a message somehow reaches the retry queue and Meta later returns a permanent error, Fiwano aborts further retries on that entry and notifies the user immediately.

### POST /api/v1/messages/send-template

*WhatsApp only. Pro required.* Send a template message to initiate conversations outside the 24-hour window. Only templates with status `APPROVED` can be sent.

| Field | Type | Required | Description |
|---|---|---|---|
| `channel_id` | string | Yes | WhatsApp channel ID |
| `template_name` | string | Yes | Template name (e.g. `order_confirmation`) |
| `language` | string | Yes | Language code (e.g. `en_US`) |
| `recipient` | string | Yes | Phone number without `+` |
| `variables` | object | No | Variable values keyed by component type. Omit if no variables. |

Positional variables example:

```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 variables example:

```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": "welcome_message",
    "language": "en_US",
    "recipient": "1234567890",
    "variables": {
      "body": {"customer_name": "Pablo", "order_number": "ORD-123"}
    }
  }'
```

Template without variables — omit `variables` entirely.

### POST /api/v1/messages/send-media

*Pro required.* Meta fetches the file directly from `media_url` — Fiwano does not download or store it.

| Field | Type | Required | Description |
|---|---|---|---|
| `channel_id` | string | Yes | Channel to send from |
| `recipient` | string | Yes | Recipient identifier (see channel capabilities) |
| `media_type` | string | Yes | `image`, `audio`, `video`, or `document` |
| `media_url` | string | Yes | HTTPS URL, max 2048 chars. No credentials in URL, no private IPs. |
| `caption` | string | No | Caption, max 1024 chars (WhatsApp image/video/document) |
| `filename` | string | No | Filename, max 255 chars (WhatsApp document only) |

For audio messages, use `media_type: "audio"` on all supported channels. Fiwano sends it as the channel's normal audio attachment. WhatsApp Cloud API sends outbound audio as `type: "audio"` with an audio media object (`link` or uploaded media `id`); it does not expose a separate outbound `voice` flag.

**Use a signed URL for non-public content** — S3/GCS/R2 presigned, Azure SAS, or HMAC-signed URL on your own server. Set expiry ≥ 5 min. Public URLs are accessible to anyone who learns them.

```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, `error_code` (when present) identifies the Meta error:

```json
{ "success": false, "error_code": 131052, "error": "...Hint: ...", "status": "failed" }
```

| `error_code` | Meaning | What to do |
|---|---|---|
| `100` | Invalid parameter | Check `media_url`, `recipient`, `media_type`. |
| `131052` | Meta could not download from URL | Verify URL is reachable, returns 200, correct Content-Type, signed URL not expired. |
| `131053` | Upload error | Unsupported format/size, **or** Meta rate-limited your hosting provider's network. Try AWS S3, GCS, or Cloudflare R2 instead. |
| `131026` | Recipient invalid | Recipient is not a valid WhatsApp user, or messaging restricted. |
| `131047`, `131057` | Outside 24h window (WhatsApp) | Use an approved [template](#post-apiv1messagessend-template). |
| `190`, `200`, `10` | Token issue | Reconnect the channel. |

**Size limits** (per Meta): WhatsApp — image 5 MB, audio/video 16 MB, document 100 MB. Instagram & Messenger — 8–25 MB depending on type. On exceeding: `error_code: 131053`.

| HTTP Status | Meaning |
|---|---|
| `200` | Request accepted (check `success`) |
| `402` | Pro license required |
| `404` | Channel not found |
| `422` | Validation failed (URL/media_type/filename) |

---

## Media Files

Media files received from inbound webhooks are stored temporarily. Download them via the endpoint below. Files expire after **60 minutes** — after expiry, the URL returns `410 Gone`.

### GET /api/v1/media/{media_id}

Download a media file. The `media_id` comes from the `data.media.media_id` field in inbound webhook payloads (see [Incoming Webhooks — media](#messagereceived--media-pro)).

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

Response is a raw binary file with appropriate `Content-Type` header (e.g. `image/jpeg`, `audio/ogg`, `video/mp4`). If the original filename is known, it's included in `Content-Disposition`.

| HTTP Status | Meaning |
|---|---|
| `200` | File returned |
| `404` | File not found (wrong ID or belongs to another account) |
| `410` | File expired or unavailable (already cleaned up, or download from Meta had failed) |

> **Tip:** Download media files as soon as possible after receiving the webhook. Files are cleaned up after 60 minutes. If you need to keep them longer, save them in your own storage.

---

## WhatsApp Templates

WhatsApp requires pre-approved message templates to start conversations outside the 24-hour customer service window. Template list, sync, create, update, delete, portal test sends, and API send-template require a Pro license. Templates are submitted to Meta for review (typically < 24 hours). This section only applies to WhatsApp channels.

**Template lifecycle:** Create → `PENDING` (under Meta review) → `APPROVED` (can be sent) or `REJECTED` (fix and resubmit).

### GET /api/v1/channels/{channel_id}/templates

List templates for a WhatsApp channel. By default, syncs with Meta before returning.

| Query param | Default | Description |
|---|---|---|
| `sync` | `true` | Sync from Meta before returning. Set `false` for cached data (faster). |
| `status` | all | Filter: `APPROVED`, `PENDING`, `REJECTED` |

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

### GET /api/v1/channels/{channel_id}/templates/{template_id}

Get template details including components and variable definitions.

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

### POST /api/v1/channels/{channel_id}/templates

Create a new template (submitted to Meta for review, starts as `PENDING`).

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Lowercase alphanumeric + underscores, max 512 chars |
| `category` | string | Yes | `MARKETING`, `UTILITY`, or `AUTHENTICATION` |
| `language` | string | Yes | Language code (e.g. `en_US`, `ru`, `es`) |
| `components` | array | Yes | Template components. `BODY` required. `HEADER` (text only), `FOOTER`, `BUTTONS` optional. |
| `parameter_format` | string | No | `positional` (default: `{{1}}`, `{{2}}`) or `named` (`{{customer_name}}`) |

Example:

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

The `example` field is required by Meta for review.

### PUT /api/v1/channels/{channel_id}/templates/{template_id}

Update template components. All components are replaced entirely (no partial update).

| Field | Type | Required | Description |
|---|---|---|---|
| `components` | array | Yes | New components (replaces all existing) |
| `category` | string | No | New category (only for `REJECTED` or `PAUSED` templates) |

```bash
curl -X PUT https://fiwano.com/api/v1/channels/a1b2c3d4e5f67890/templates/tpl_abc123 \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, your order {{2}} has been updated.",
        "example": {"body_text": [["Pablo", "ORD-123"]]}
      }
    ]
  }'
```

> **Restrictions:** Approved templates can be edited max 10 times per 30 days, and only once per 24 hours. After editing, the template goes back to `PENDING` for re-review.

### DELETE /api/v1/channels/{channel_id}/templates/{template_id}

Delete a template.

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

| Query param | Default | Description |
|---|---|---|
| `all_languages` | `false` | Delete all language versions of this template |

> **Warning:** After deleting an `APPROVED` template, you cannot create a new template with the same name for 30 days (Meta restriction).

---

## Incoming Webhooks

When a message arrives on your connected channel, we deliver it to your channel's `webhook_url` as a `POST` request with a JSON body. Each delivery is signed with your channel's `webhook_secret`.

### Verifying Signatures

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.

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

Available events per channel type:

| Channel | Available events |
|---|---|
| WhatsApp | `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed` |
| Instagram | `message.received`, `message.delivered`, `message.read` |
| Facebook | `message.received`, `message.delivered`, `message.read` |

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

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

Sender name is **not** included in webhook payloads for Instagram and Facebook (`data.from_name` is `null`). To get sender profile data, use the dedicated [profile endpoint](#sender-profile) — `GET /api/v1/channels/{channel_id}/profile/{user_id}`.

For WhatsApp, `data.from_name` is always present in webhooks (Meta includes it in the payload). No separate API call needed.

---

## Sender Profile

Fetch sender profile data from Meta Graph API. Use this to get the name, profile picture, and other details of users who message your channel.

- **WhatsApp:** Not supported — sender name is included in every webhook payload (`data.from_name`).
- **Instagram:** Returns `username`, `name`, `profile_pic`, `follower_count`, `is_verified`.
- **Facebook:** Returns `first_name`, `last_name`, `profile_pic`.

Results are **cached for 5 minutes** on our side. The `cached` field in the response indicates whether the result was served from cache.

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

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

| Parameter | In | Description |
|---|---|---|
| `channel_id` | path | Channel ID |
| `user_id` | path | Sender identifier — IGSID (Instagram) or PSID (Facebook) from `data.from` in webhooks |

Instagram example:

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

Response (Instagram):

```json
{
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "user_id": "6543217890123456",
  "profile": {
    "username": "johndoe",
    "name": "John Doe",
    "profile_pic": "https://scontent.cdninstagram.com/...",
    "follower_count": 1500,
    "is_verified_user": false
  },
  "cached": false
}
```

Response (Facebook):

```json
{
  "channel_id": "c3f8a1b2e4d56789",
  "channel_type": "facebook",
  "user_id": "7890123456789012",
  "profile": {
    "first_name": "John",
    "last_name": "Doe",
    "profile_pic": "https://platform-lookaside.fbsbx.com/..."
  },
  "cached": true
}
```

If the profile is unavailable (private account, invalid ID):

```json
{
  "channel_id": "b2c3d4e5f6789012",
  "channel_type": "instagram",
  "user_id": "6543217890123456",
  "profile": null,
  "cached": false
}
```

| HTTP Status | Meaning |
|---|---|
| `200` | Profile returned (may be `null` if unavailable) |
| `400` | WhatsApp channel (not supported), inactive channel, or expired token |
| `404` | Channel not found |
| `502` | Meta API error (retry may help) |

---

## Redirect URIs

Manage allowed redirect URIs for your API key. Required for the programmatic channel connection flow.

### GET /api/v1/redirects

List allowed redirect URIs for the current API key.

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

### POST /api/v1/redirects

Add an allowed redirect URI.

| Field | Type | Required | Description |
|---|---|---|---|
| `uri_pattern` | string | Yes | HTTPS URL or wildcard pattern (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"}'
```

### DELETE /api/v1/redirects/{redirect_id}

Remove an allowed redirect URI.

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

---

## Errors & Rate Limits

### 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 — go to Billing |
| `404` | Not found | Resource doesn't exist or belongs to another account |
| `429` | Rate limit exceeded | Wait and retry. Limit: 100 req/min per API key. |
| `502` | Meta API error | Upstream failure. Check `detail`. Retry may help. |

### Error Format

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

### Rate Limits

100 requests per minute per API key. HTTP `429` when exceeded. Meta has its own per-channel limits (shown in Meta Business Manager, not controlled by Fiwano).

---

## 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/RomanBabakin/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 built-in HMAC signature verification |

### Action node — operations

| Resource | Operations |
|---|---|
| Message | Send Text, Send Template (WhatsApp), Send Media (image/audio/video/document) |
| Channel | Get Many, Get, Generate OAuth URL, Exchange OAuth Code, Update Webhook, Delete |
| Contact | Get Profile (Instagram, Facebook — returns name, profile picture, follower count) |
| Template | Get Many, Get, Create, 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 in — the trigger validates every webhook automatically.

### Example workflow

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

---

## Important Notes

**Token lifecycle** — Meta tokens expire ~60 days after connection. Fiwano automatically refreshes them 7 days before expiry. If auto-refresh fails (e.g. user revoked permissions in Meta), the channel goes inactive and needs reconnection via OAuth.

**Channel uniqueness** — Each Meta account can only be connected to one Fiwano user at a time. HTTP `400` with `"Already connected to another account"` if it's already connected by someone else.

**Billing** — New accounts get a 7-day free trial. After expiry, sending and webhook relay are blocked until you subscribe. Incoming messages are still saved and will be forwarded once the subscription is active.

**Inactive channels** — A channel goes inactive if deleted via API, if the token can't be refreshed, or if the user revokes Meta permissions. Reconnect via a new OAuth flow.

**WhatsApp 24-hour window** — You can send regular text messages only within 24 hours of the customer's last message. Outside this window, use approved templates via `/api/v1/messages/send-template`. This is a Meta policy.

**Instagram & Facebook 24-hour window** — You can reply only within 24 hours of the user's last message. No template workaround — wait for the user to message again.

**Media files** — Inbound media files (images, audio, video, documents) are stored temporarily for 60 minutes. Download them via `GET /api/v1/media/{media_id}` as soon as possible after receiving the webhook. Files are automatically cleaned up after expiry. Maximum file size: 10 MB.

**Pro license** — Sending and receiving media and using WhatsApp templates requires a Pro license ($19/mo). With a Starter license ($12/mo), media messages arrive as `type: "unsupported"` with `upgrade_required: "pro"`. Upgrade via the Billing page.
