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

Forward SMS to another number automatically

Published and updated

Run a relay that receives the textbee webhook for each incoming text and sends it on to the other number with POST /gateway/send-sms. The forward goes out from the phone that received the text, prefixed with the original sender, so the person on the other number knows who wrote it.

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. Create an API key in the textbee dashboard. The relay uses it to send the forwards.
  2. Decide the number that receives the forwards and write it in E.164 format, for example +12015550187.
  3. Check that the SIM plan covers the extra outgoing texts. Each forward is a normal SMS from your number.

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 API_KEY = process.env.TEXTBEE_API_KEY
const BASE_URL = process.env.TEXTBEE_BASE_URL ?? 'https://api.textbee.dev/api/v1'
const FORWARD_TO = process.env.FORWARD_TO
const MAX_BODY = 64 * 1024
const seen = new Set() // use your database in production

// Compare digits only, since the phone can report a number in national format.
function lastDigits(number) {
  return String(number).replace(/\D/g, '').slice(-9)
}

async function forward(event) {
  // Never forward the forwarding number's own texts, or two relays can loop.
  if (lastDigits(event.sender) === lastDigits(FORWARD_TO)) return

  const response = await fetch(`${BASE_URL}/gateway/send-sms`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
    signal: AbortSignal.timeout(20_000),
    body: JSON.stringify({
      deviceId: event.deviceId, // send from the phone that received the text
      recipients: [FORWARD_TO],
      message: `From ${event.sender}: ${event.message}`,
    }),
  })
  if (!response.ok) throw failure('textbee', 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.
TEXTBEE_API_KEYAPI key from the textbee dashboard.
FORWARD_TONumber that receives the forwards, in E.164 format.
TEXTBEE_BASE_URLAPI 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
senderStart of the text: "From +12015550123:"
messageRest of the text

Limits to plan for

  • 3GPP TS 23.038 defines the GSM 7 bit and UCS2 encodings: 160 GSM-7 characters fit one SMS, and each part of a longer message carries fewer. portal.3gpp.org, checked on September 5, 2026

Each forwarded text counts twice toward the plan allowance: once when it is received and once when it is sent on. The "From" prefix makes a long text longer, so a message near 160 characters becomes two segments.

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

Why not use the forwarding setting on the phone?

Most Android phones have no setting that forwards texts to another number, and call forwarding covers calls only. The relay does the forwarding on the server, so it keeps working with the phone locked in a drawer.

Can I forward to several numbers?

Yes. Put several numbers in the recipients array. Every recipient is a separate SMS from your SIM and a separate message on the plan.

Can the other person reply to the original sender?

A reply goes to your textbee number, not to the original sender. To pass replies back, the relay would need to track which conversation each reply belongs to.

What stops a forwarding loop?

The relay skips texts that come from the forwarding number itself. Without that check, two phones that forward to each other would bounce the same message forever.

Sources

  1. portal.3gpp.org, checked on September 5, 2026

Read next