Skip to content

Incoming message

4 min read

Event incoming_message fires when a customer messages a chat device linked to your webhook, or when mail is accepted for a linked email sender address.

Header:

X-Kirisan-Event: incoming_message

Unified envelope

WhatsApp, Telegram, WABA, and Email share the same top-level shape:

FieldTypeMeaning
eventstringAlways incoming_message
devicestringChat device token/number, or email receiving address
channelstringwhatsapp, telegram, waba, or email
senderobjectWho sent the message — see below
messageobjectMessage body — see below
metadataobjectChannel-specific extra fields
timestampintegerUnix seconds
productionbooleantrue for live traffic; false for Webhooks → Test

sender

FieldMeaning
senderReply target — WhatsApp/WABA phone, Telegram chat id, or email From address
nameDisplay name after normalization (use for {{name}})
detailsChannel-specific bag — see per-channel sections

message

FieldMeaning
typetext, image, video, document, location, callback, button_reply, list_reply, and other media types
textPrimary line — body, caption, or callback data (use for {{message}})
fileOptional — url, filename, filetype, mime_type, file_id
locationOptional — latitude, longitude, coordinate
buttonOptional — id and text for button/list replies

More {{…}} shortcuts: Resources → Variables → Incoming. Dashboard tour: Variables.


WhatsApp example

{
  "event": "incoming_message",
  "device": "your-whatsapp-device-token",
  "channel": "whatsapp",
  "sender": {
    "sender": "6281234567890",
    "name": "Ada",
    "details": {
      "type": "personal",
      "sender": "6281234567890",
      "pushname": "Ada",
      "from_me": false
    }
  },
  "message": {
    "type": "text",
    "text": "Hi, I need help with my order."
  },
  "metadata": {
    "inboxid": 0,
    "isgroup": false,
    "isforwarded": false
  },
  "timestamp": 1735689600,
  "production": true
}

WhatsApp sender.details may also include senderlid, member, memberlid, and type: "group" for group chats.

Media messages set message.type to image, video, audio, or file and populate message.file.url.


Telegram example

{
  "event": "incoming_message",
  "device": "your-telegram-bot-token",
  "channel": "telegram",
  "sender": {
    "sender": "-1001234567890",
    "name": "Alex",
    "details": {
      "type": "group",
      "chat_id": "-1001234567890",
      "user_id": "555444333",
      "username": "alex_test",
      "first_name": "Alex",
      "last_name": "",
      "from_me": false
    }
  },
  "message": {
    "type": "text",
    "text": "Hello from the group."
  },
  "metadata": {
    "update_id": 100002,
    "message_id": "1001",
    "chat_type": "supergroup"
  },
  "timestamp": 1735689600,
  "production": true
}

For callback queries (inline button taps), message.type is callback and message.text holds the callback data. metadata.callback_query_id may be present.

In groups, sender.sender is the chat id (reply target), not the user id.


WABA example

WABA uses the same envelope as WhatsApp and Telegram:

{
  "event": "incoming_message",
  "device": "your-waba-device-token",
  "channel": "waba",
  "sender": {
    "sender": "6281234567890",
    "name": "Ada",
    "details": {
      "type": "personal",
      "sender": "6281234567890",
      "from_me": false
    }
  },
  "message": {
    "type": "text",
    "text": "Hello — WABA text message."
  },
  "metadata": {
    "message_id": "wamid.HBgLNjI4...",
    "waba_type": "text"
  },
  "timestamp": 1735689600,
  "production": true
}

Interactive replies use message.type button_reply or list_reply with a message.button object. metadata may include interactive_type, button_reply_id, or list_reply_id.


Email example

Email uses the same envelope. device is the receiving address (your registered sender). sender.sender is the From address.

{
  "event": "incoming_message",
  "device": "support@example.com",
  "channel": "email",
  "sender": {
    "sender": "alice@example.com",
    "name": "Alice",
    "details": {
      "type": "personal",
      "from": "alice@example.com",
      "to": "support@example.com",
      "from_me": false
    }
  },
  "message": {
    "type": "text",
    "text": "Hi, I need help with my order."
  },
  "metadata": {
    "inbound_id": 42,
    "subject": "Order question",
    "status": "accepted_inbox",
    "to": "support@example.com"
  },
  "timestamp": 1735689600,
  "production": true
}

metadata.subject holds the email subject. metadata.status is accepted_inbox or accepted_spam. When attachments are stored, the first file appears in message.file and the full list in metadata.attachments.


Attachments and downloading files

When the channel plan includes attachments and storage quota is active, Kirisan saves inbound media to your Files library and puts a downloadable link on the webhook payload.

message.file fieldMeaning
urlShort-lived presigned GET (Cloudflare R2). Valid for about 24 hours from delivery — download promptly.
filenameOriginal display name (for example invoice.pdf)
filetypeCoarse kind: image, video, audio, or file
mime_typeMIME type when known (for example application/pdf)
file_idKirisan Files row id after store (string). If storage/plan does not allow saving, Telegram may only expose the provider file id and no hosted url.

Without active storage (or an attachment-capable plan), WhatsApp may still include a provider/CDN url; Telegram often has metadata only and no downloadable Kirisan URL.

Example payload (PDF document)

{
  "event": "incoming_message",
  "device": "your-telegram-device-token",
  "channel": "telegram",
  "sender": {
    "sender": "357496317",
    "name": "Ada",
    "details": {
      "type": "personal",
      "chat_id": "357496317",
      "user_id": "357496317",
      "username": "ada",
      "first_name": "Ada",
      "last_name": "",
      "from_me": false
    }
  },
  "message": {
    "type": "document",
    "text": "Here is the invoice",
    "file": {
      "url": "https://….r2.cloudflarestorage.com/…/object-key?X-Amz-Algorithm=AWS4-HMAC-SHA256&…",
      "filename": "invoice.pdf",
      "filetype": "file",
      "mime_type": "application/pdf",
      "file_id": "1842"
    }
  },
  "metadata": {
    "update_id": 609668173,
    "message_id": "110",
    "chat_type": "private"
  },
  "timestamp": 1735689600,
  "production": true
}

Email with several attachments: use message.file for the first file, and metadata.attachments[] (each with url, filename, mime_type, file_id) for the full list.

Download the attachment

Treat message.file.url as a normal HTTPS GET. No Kirisan API token is required — the signature is in the query string. Save the response body using filename (or a name you choose).

cURL

# $URL = message.file.url from the webhook JSON
# $NAME = message.file.filename (fallback: attachment.bin)
curl -fsSL -o "$NAME" "$URL"

Node.js

import { writeFile } from 'node:fs/promises';

async function downloadIncomingFile(payload) {
  const file = payload?.message?.file;
  const url = String(file?.url || '').trim();
  if (!url) throw new Error('No message.file.url — check plan attachments and storage quota');

  const name = String(file.filename || 'attachment.bin').replace(/[/\]/g, '_');
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);

  const buf = Buffer.from(await res.arrayBuffer());
  await writeFile(name, buf);
  return { name, bytes: buf.length, mime: file.mime_type, fileId: file.file_id };
}

Python

import pathlib
import requests

def download_incoming_file(payload: dict) -> pathlib.Path:
    file = (payload.get("message") or {}).get("file") or {}
    url = (file.get("url") or "").strip()
    if not url:
        raise ValueError("No message.file.url — check plan attachments and storage quota")

    name = (file.get("filename") or "attachment.bin").replace("/", "_").replace("\", "_")
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    path = pathlib.Path(name)
    path.write_bytes(r.content)
    return path

If the GET returns 403 or 404, the presigned URL likely expired — keep a copy when you first receive the webhook, or open the file again from Resources → Files in the dashboard.