textbee Logotextbee.dev
Plans from $9.99/mo.View Plans

Forward SMS to Telegram with a bot: every text your phone receives, in a chat

Published and updated

Create a Telegram bot with BotFather, find the chat ID it should post to, and run a relay that receives the textbee webhook and calls the Bot API sendMessage method. Every text the phone receives then shows up in that chat within seconds, with the sender number on the first line.

What you need

  • The textbee app on the Android phone that holds the SIM, with receiving turned on. Download the app.
  • A place to run the relay with a public HTTPS address: a small server, a container or a serverless function. textbee rejects private and loopback addresses.

Before you start

  1. Open @BotFather in Telegram, send /newbot, pick a name and a username, and copy the token it returns.
  2. Start a chat with the new bot (or add it to a group) and send it any message, so the bot is allowed to write there.
  3. Open https://api.telegram.org/bot<token>/getUpdates in a browser and copy the chat id from the message you just sent. Group ids start with a minus sign. getUpdates only works while the bot has no webhook of its own set.

The relay

It checks the HMAC-SHA256 signature in the X-Signature header, skips anything but received messages, and ignores a delivery it has already forwarded. When the destination is down it answers 502, so textbee delivers the text again later; an error a retry cannot fix, such as a deleted webhook, is logged instead. It needs Node.js 18 or newer and only the standard library, so there is nothing to install.

relay.mjs
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const SECRET = process.env.TEXTBEE_WEBHOOK_SECRET
if (!SECRET) throw new Error('Set TEXTBEE_WEBHOOK_SECRET')
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN
const CHAT_ID = process.env.TELEGRAM_CHAT_ID
const TELEGRAM_API = process.env.TELEGRAM_API_URL ?? 'https://api.telegram.org'
const MAX_BODY = 64 * 1024
const seen = new Set() // use your database in production

async function forward(event) {
  // No parse_mode, so the text is shown exactly as it arrived.
  const response = await fetch(`${TELEGRAM_API}/bot${BOT_TOKEN}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    signal: AbortSignal.timeout(20_000),
    body: JSON.stringify({ chat_id: CHAT_ID, text: `SMS from ${event.sender}\n\n${event.message}`.slice(0, 4096) }),
  })
  if (!response.ok) throw failure('Telegram', response.status)
}

// A 4xx other than 408 or 429 will not fix itself, so it is logged instead of retried.
function failure(name, status) {
  const error = new Error(`${name} answered ${status}`)
  error.permanent = status >= 400 && status < 500 && status !== 408 && status !== 429
  return error
}

function verified(rawBody, signature = '') {
  const expected = Buffer.from(createHmac('sha256', SECRET).update(rawBody).digest('hex'))
  const received = Buffer.from(signature)
  return received.length === expected.length && timingSafeEqual(received, expected)
}

createServer((request, response) => {
  const chunks = []
  let size = 0
  request.on('data', (chunk) => {
    size += chunk.length
    if (size <= MAX_BODY) chunks.push(chunk)
  })
  request.on('end', async () => {
    if (size > MAX_BODY) return response.writeHead(413).end()
    const rawBody = Buffer.concat(chunks)
    if (!verified(rawBody, request.headers['x-signature'])) return response.writeHead(401).end()

    const event = JSON.parse(rawBody.toString())
    if (event.webhookEvent !== 'MESSAGE_RECEIVED' || seen.has(event.idempotencyKey)) {
      return response.writeHead(200).end()
    }

    try {
      await forward(event)
      seen.add(event.idempotencyKey)
      response.writeHead(200).end()
    } catch (error) {
      console.error(error.message)
      // 502 makes textbee retry the delivery; a permanent failure is not worth retrying.
      response.writeHead(error.permanent ? 200 : 502).end()
    }
  })
}).listen(process.env.PORT ?? 3000)
Environment variableWhat it holds
TEXTBEE_WEBHOOK_SECRETThe signing secret you set on the webhook in the textbee dashboard, at least 20 characters.
TELEGRAM_BOT_TOKENToken from BotFather.
TELEGRAM_CHAT_IDChat or group id from getUpdates.
TELEGRAM_API_URLBot API base URL. Leave unset; tests point it at a mock.
PORTPort the relay listens on. Defaults to 3000.

Register the webhook in textbee

  1. Start the relay and note its public HTTPS address.
  2. In the textbee dashboard open Webhooks, choose Create Webhook, and enter that address as the Delivery URL.
  3. Generate a Signing Secret, select the MESSAGE_RECEIVED event, and set the same secret as TEXTBEE_WEBHOOK_SECRET on the relay.
  4. Text the phone from a different phone and check that the forward arrives.

What arrives

Webhook fieldWhere it goes
senderFirst line: "SMS from +12015550123"
messageMessage text, below a blank line

Limits to plan for

  • sendMessage accepts 1 to 4,096 characters of text. core.telegram.org, checked on September 23, 2026
  • A bot should send no more than one message a second to one chat, and no more than 20 messages a minute to a group. core.telegram.org, checked on September 23, 2026

Anyone who can read the chat can read the forwarded texts, including one time passcodes. Use a private chat or a group limited to the people who need them, and keep the bot token out of source control.

textbee delivers the webhook once the phone has uploaded the text, so a phone that was offline delivers its backlog when it reconnects. Texts received more than 48 hours before the upload are stored but not sent to webhooks. A 5xx answer or a timeout is retried, up to 10 attempts in all with growing gaps, and a subscription that keeps failing is paused. Received messages count toward the plan allowance the same as sent ones.

Frequently asked questions

Can I forward SMS to Telegram without an app on the phone?

The phone needs the textbee app, because that is what reads incoming texts and uploads them. After that the phone does nothing Telegram specific: the relay on your server talks to Telegram.

Can one bot forward to several chats?

Yes. Call sendMessage once per chat id, or post to one group that everyone is in. Keep an eye on the per chat and per group rate limits when the phone receives many texts at once.

Why does the relay not use Markdown formatting?

An SMS can contain characters that Telegram would read as formatting, and a malformed message is rejected. Sending plain text shows the message exactly as it arrived.

Sources

  1. core.telegram.org, checked on September 23, 2026
  2. core.telegram.org, checked on September 23, 2026

Read next