Skip to content

Use case

Two-way conversations and SMS chatbots.

Connect your own bot or AI assistant to answer people who texted first. SMSKit carries the messages.

Why teams switch

  • Customer replies sit on a phone nobody checks.
  • Your support team and your own bot need the conversation in one place.

How SMSKit fits

The signed sms:received webhook works on every plan, including Free’s one evaluation webhook. The dashboard inbox and messages API, which give a bot its history, require Standard. API replies use the same SIM when they pass the event’s deviceId and simSlot. Dashboard replies use the same SIM when staff pick that phone and SIM; otherwise automatic routing chooses any enabled SIM.

The conversation loop

  1. A customer texts your normal business number.
  2. SMSKit delivers the message to the dashboard inbox (Standard) and signed sms:received webhook (every plan).
  3. Your staff or your own bot writes the answer.
  4. The answer goes out from the same phone and SIM when the bot names them or staff pick them in the reply box.

Choose your conversation route

  • Alphanumeric sender ID: Customers cannot reply to a sender name. Source
  • Short code: Carrier approval and setup fees; customer reply charges depend on the carrier and market. Source
  • Twilio or another aggregator: Inbound and outbound message charges, with longer answers billed as several segments. Local two-way number availability depends on the market. Source
  • WhatsApp Business API: Meta business verification to scale, approved templates outside the 24-hour reply window, and template charges that depend on category; customers need WhatsApp and data. Source
  • GSM modem: Hardware to buy and a server to run.
  • SMSKit: Your existing phone and SIM, the number customers already text, and a flat price per phone with unlimited messages. Carrier charges and limits still apply.

Provider rules checked 13 October 2026. Availability and charges vary by market.

Your carrier rates and limits apply. Your business handles STOP and opt-outs. If you send customer messages to an AI provider, that provider becomes part of your data processing and your business is responsible for it.

Connect your SMS chatbot

Pass the untouched request bytes and headers to the handler. Set SMSKIT_WEBHOOK_SECRET and SMSKIT_API_KEY, then replace ask_bot() with your own bot. Reject unknown SIM slots instead of falling back to automatic routing. This compact handler verifies and sends inline. In production, split verification from reply work: verify on receipt, durably queue the verified event and return 2xx, then run the bot and send in a worker. Reuse eventId as the reply idempotency key while the original job is retained. Delivery retries can still duplicate an SMS.

import hashlib, hmac, json, os, time
from urllib.request import Request, urlopen

STOP_WORDS = {"STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"}
opted_out = set()  # In production, save this to durable storage.

def ask_bot(message):
    """Placeholder for the business's own bot."""
    raise NotImplementedError("Connect your own bot here")

def receive_sms(headers, raw_body):
    """Verify an incoming event and send its answer through the event's phone and SIM."""
    ts = headers.get("X-SMSKit-Timestamp", "")
    signature = headers.get("X-SMSKit-Signature-256", "")
    if not ts.isascii() or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        raise ValueError("Invalid webhook timestamp")
    expected = hmac.new(
        os.environ["SMSKIT_WEBHOOK_SECRET"].encode(),
        ts.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise ValueError("Invalid webhook signature")
    event = json.loads(raw_body)
    if event["event"] != "sms:received":
        return
    sender = event["sender"]
    if event["messageBody"].strip().upper() in STOP_WORDS:
        opted_out.add(sender)  # Record the opt-out and send nothing.
        return
    if sender in opted_out:
        return
    if not event.get("deviceId") or type(event.get("simSlot")) is not int or event["simSlot"] not in (0, 1):
        raise ValueError("Missing reply phone or SIM")
    payload = {
        "recipient": sender,
        "message": ask_bot(event["messageBody"]),
        "devices": [{"deviceId": event["deviceId"], "simSlots": [event["simSlot"]]}],
        "idempotencyKey": event["eventId"]
    }
    request = Request("https://api.smskit.cloud/api/v1/sms/send",
        data=json.dumps(payload).encode(), method="POST",
        headers={"X-Api-Key": os.environ["SMSKIT_API_KEY"], "Content-Type": "application/json"})
    with urlopen(request, timeout=15) as response:
        return json.load(response)
Read the SMS chatbot recipe

What your bot receives

Each text sent to your number arrives as a signed sms:received webhook. Use sender and messageBody to read the message. Pass deviceId and simSlot back to answer on the same SIM, and reuse eventId as the reply idempotency key.

sms:received
json
{
  "event": "sms:received",
  "deliveryId": "whd_2N5Q8S1V4X7Z0B3D6F9H",
  "eventId": "evt_5T8W1X4Y7Z0A3B6C9D2E",
  "incomingMessageId": "inc_9C2F5J8M1P4S7V0Y3B6E",
  "deviceId": "dvc_3K7J2H5F8M1N4Q9R6V0W",
  "simSlot": 0,
  "sender": "+254712345678",
  "messageBody": "Where is my order?",
  "receivedAt": "2026-06-23T10:05:00Z",
  "timestamp": "2026-06-23T10:05:02Z"
}

Opt-outs are your bot's job

Your bot handles opt-outs. SMSKit does not read or act on STOP. Both samples treat STOP, STOPALL, UNSUBSCRIBE, CANCEL, END and QUIT as an opt-out. They record the sender in an opt-out list and send no reply. A later message from that sender gets no reply either. The samples keep the list in memory, so save it to durable storage.

Reply errors to handle

A failed send returns a JSON body with an error message. Check the status code before you retry.

  • 400 Validation error. The request is invalid, for example an empty message. Fix the request. Do not retry it unchanged.
    JSON
    { "error": "Message is required." }
  • 401 Missing or wrong API key. The X-Api-Key header is missing or not valid. Check SMSKIT_API_KEY. Do not retry until it is fixed.
    JSON
    { "error": "Authentication required.", "statusCode": 401 }
  • 422 Plan restriction. Your plan's daily send limit is reached. Free allows 50 messages a day. Stop sending and keep the reply queued until the limit resets, or upgrade the plan.
    JSON
    { "error": "Daily send limit of 50 messages reached for plan 'Free'. Upgrade your plan to send more.", "statusCode": 422 }