Get in Touch

Have a question about the platform, need help with your integration, or want to discuss partnership opportunities and enterprise pricing? Drop us an email — we’ll do our best to get back to you within 3–4 hours.

contact@fiwano.com
Documentation menu

Working with an AI agent? Download the full documentation as a Markdown file to use as context.

Download full .md

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); by default no events are enabled. If the channel has a 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.)

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:

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

message.received (WhatsApp)

{
  "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)

{
  "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)

{
  "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:

{
  "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:

{
  "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:

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; status codes in the API Reference.

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:

{
  "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)

{
  "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)

{
  "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:

  • Instagramusername, name, profile_pic, follower_count, is_verified
  • Facebookfirst_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.

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.

Fiwano API Documentation