> ## Documentation Index
> Fetch the complete documentation index at: https://developers.phonovation.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SMS Message Lab

> Calculate SMS encoding, character units, and multipart segments as you type.

export const SmsMessageLab = () => {
  const gsmBasic = new Set(Array.from("@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\u001bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ§¿abcdefghijklmnopqrstuvwxyzäöñüà"));
  const gsmExtended = new Set(Array.from("\f^{}\\[~]|€"));
  const compatibilityReplacements = {
    "‘": "'",
    "’": "'",
    "‚": "'",
    "‛": "'",
    "“": '"',
    "”": '"',
    "„": '"',
    "‟": '"',
    "–": "-",
    "—": "-",
    "…": ".",
    "•": "*"
  };
  const examples = [{
    label: "GSM-7",
    value: "Reminder: your appointment is tomorrow at 10:30."
  }, {
    label: "Extended characters",
    value: "Use code {SAVE20} | Offer ends Friday"
  }, {
    label: "Unicode",
    value: "Your delivery is on the way 🚚"
  }];
  const visibleCharacter = character => {
    if (character === " ") return "·";
    if (character === "\n") return "↵";
    if (character === "\r") return "↵";
    if (character === "\t") return "→";
    if (character === "\f") return "␌";
    if (character === "\u001b") return "ESC";
    return character;
  };
  const characterName = character => {
    if (character === " ") return "Space";
    if (character === "\n") return "Line feed";
    if (character === "\r") return "Carriage return";
    if (character === "\t") return "Tab";
    if (character === "\f") return "Form feed";
    if (character === "\u001b") return "Escape";
    return character;
  };
  const analyseSmsMessage = source => {
    const baseTokens = Array.from(source).map((original, index) => {
      const character = compatibilityReplacements[original] ?? original;
      const originalGsmKind = gsmBasic.has(original) ? "basic" : gsmExtended.has(original) ? "extended" : "unsupported";
      const gsmKind = gsmBasic.has(character) ? "basic" : gsmExtended.has(character) ? "extended" : "unsupported";
      return {
        index,
        original,
        character,
        gsmKind,
        originalGsmKind,
        replaced: original !== character
      };
    });
    const isGsm = baseTokens.every(token => token.gsmKind !== "unsupported");
    const sourceUsesUnicode = baseTokens.some(token => token.originalGsmKind === "unsupported");
    const encoding = isGsm ? "GSM-7" : "UTF-16";
    const singleLimit = isGsm ? 160 : 70;
    const multipartLimit = isGsm ? 153 : 67;
    const tokens = baseTokens.map(token => {
      const units = isGsm ? token.gsmKind === "extended" ? 2 : 1 : token.character.length;
      const kind = token.replaced ? "replacement" : token.gsmKind === "unsupported" ? "unicode" : isGsm ? token.gsmKind : "unicode-compatible";
      return {
        ...token,
        units,
        kind
      };
    });
    const units = tokens.reduce((total, token) => total + token.units, 0);
    const partLimit = units > singleLimit ? multipartLimit : singleLimit;
    const segments = [];
    for (const token of tokens) {
      let segment = segments.at(-1);
      if (!segment || segment.units + token.units > partLimit) {
        segment = {
          units: 0,
          tokens: []
        };
        segments.push(segment);
      }
      segment.tokens.push(token);
      segment.units += token.units;
    }
    const lastSegment = segments.at(-1);
    const normalized = tokens.map(token => token.character).join("");
    return {
      encoding,
      units,
      characters: Array.from(normalized).length,
      parts: segments.length,
      segments,
      partLimit,
      remaining: lastSegment ? partLimit - lastSegment.units : singleLimit,
      normalized,
      replacements: tokens.filter(token => token.replaced),
      normalizedToGsm: sourceUsesUnicode && isGsm,
      multipart: segments.length > 1
    };
  };
  const renderMetric = ({label, value, detail}) => <div className="sms-lab__metric">
      <span>{label}</span>
      <strong>{value}</strong>
      <small>{detail}</small>
    </div>;
  const renderCharacterToken = token => {
    const description = token.replaced ? `${characterName(token.original)} is sent as ${characterName(token.character)}` : `${characterName(token.character)} uses ${token.units} ${token.units === 1 ? "unit" : "units"}`;
    return <span key={token.index} className={`sms-lab__character sms-lab__character--${token.kind}`} title={description} aria-label={description}>
        <span>{visibleCharacter(token.character)}</span>
        {token.units > 1 ? <small>{token.units}</small> : null}
      </span>;
  };
  const renderLegendItem = ({kind, label, detail}) => <div className="sms-lab__legend-item">
      <span className={`sms-lab__legend-swatch sms-lab__character--${kind}`} aria-hidden="true" />
      <div>
        <strong>{label}</strong>
        <small>{detail}</small>
      </div>
    </div>;
  const [message, setMessage] = useState("");
  const analysis = useMemo(() => analyseSmsMessage(message), [message]);
  return <section className="sms-lab not-prose">
      <header className="sms-lab__header">
        <div>
          <span className="phono-kicker">SMS length calculator</span>
          <h2>See exactly how your message will split</h2>
          <p>
            Type or paste a message to inspect its encoding, character units,
            and billable SMS parts in real time.
          </p>
        </div>
      </header>

      <div className="sms-lab__workspace">
        <div className="sms-lab__composer">
          <div className="sms-lab__section-heading">
            <label htmlFor="sms-message-lab-input">Message content</label>
            {message ? <button type="button" onClick={() => setMessage("")}>
                Clear
              </button> : null}
          </div>
          <textarea id="sms-message-lab-input" value={message} placeholder="Type your SMS message here…" onChange={event => setMessage(event.target.value)} />
          <div className="sms-lab__composer-footer">
            <span>
              {message.length.toLocaleString()}{" "}
              {message.length === 1 ? "character" : "characters"}
            </span>
            <span>No message is sent from this page</span>
          </div>

          <div className="sms-lab__examples" aria-label="Example messages">
            <span>Try an example</span>
            <div>
              {examples.map(example => <button key={example.label} type="button" onClick={() => setMessage(example.value)}>
                  {example.label}
                </button>)}
            </div>
          </div>
        </div>

        <aside className="sms-lab__summary" aria-live="polite">
          <div className="sms-lab__metrics">
            {renderMetric({
    label: "Encoding",
    value: analysis.encoding,
    detail: analysis.encoding === "GSM-7" ? "7-bit alphabet" : "Unicode message"
  })}
            {renderMetric({
    label: "SMS parts",
    value: analysis.parts,
    detail: analysis.parts === 0 ? "Start typing" : analysis.multipart ? "Multipart message" : "Single message"
  })}
            {renderMetric({
    label: "Units used",
    value: analysis.units,
    detail: `${analysis.characters} text ${analysis.characters === 1 ? "character" : "characters"}`
  })}
            {renderMetric({
    label: "Remaining",
    value: analysis.remaining,
    detail: `of ${analysis.partLimit} in this part`
  })}
          </div>

          {analysis.multipart ? <div className="sms-lab__multipart-note">
              <strong>This message uses {analysis.parts} SMS parts.</strong>
              <span>Each part can be billed separately.</span>
            </div> : null}
        </aside>
      </div>

      {analysis.replacements.length ? <div className="sms-lab__normalization" aria-live="polite">
          <div>
            <strong>
              {analysis.normalizedToGsm ? "Unicode detected — try this GSM-7 alternative" : "Suggested handset-compatible alternative"}
            </strong>
            <span>
              {analysis.normalizedToGsm ? "GSM-7 allows more characters per SMS part and may reduce credit usage." : "The API replaces typographic characters before sending; this message still requires UTF-16."}
            </span>
          </div>
          <code>{analysis.normalized}</code>
        </div> : null}

      <section className="sms-lab__detail" aria-live="polite">
        <div className="sms-lab__detail-heading">
          <div>
            <h3>Detailed view</h3>
            <p>
              Each tile is one visible character. A small number shows when it
              consumes more than one unit.
            </p>
          </div>
          {analysis.parts ? <span>
              {analysis.parts} {analysis.parts === 1 ? "part" : "parts"}
            </span> : null}
        </div>

        {analysis.parts ? <div className="sms-lab__segments">
            {analysis.segments.map((segment, segmentIndex) => <article className="sms-lab__segment" key={segmentIndex}>
                <div className="sms-lab__segment-heading">
                  <strong>Part {segmentIndex + 1}</strong>
                  <span>
                    {segment.units} / {analysis.partLimit} units
                  </span>
                </div>
                <div className="sms-lab__segment-track" aria-hidden="true">
                  <span style={{
    width: `${segment.units / analysis.partLimit * 100}%`
  }} />
                </div>
                <div className="sms-lab__characters">
                  {analysis.multipart ? <span className="sms-lab__character sms-lab__character--udh" title="User Data Header for multipart SMS">
                      UDH
                    </span> : null}
                  {segment.tokens.map(token => renderCharacterToken(token))}
                </div>
              </article>)}
          </div> : <div className="sms-lab__empty">
            <span aria-hidden="true">Aa</span>
            <div>
              <strong>Your message map will appear here</strong>
              <p>
                Start typing above to see every character and segment boundary.
              </p>
            </div>
          </div>}
      </section>

      <section className="sms-lab__legend" aria-label="Character legend">
        <h3>Legend</h3>
        <div className="sms-lab__legend-grid">
          {renderLegendItem({
    kind: "basic",
    label: "GSM-7",
    detail: "Uses one unit"
  })}
          {renderLegendItem({
    kind: "extended",
    label: "GSM-7 extended",
    detail: "Uses two units"
  })}
          {renderLegendItem({
    kind: "replacement",
    label: "Replaced",
    detail: "Changed for handset compatibility"
  })}
          {renderLegendItem({
    kind: "unicode-compatible",
    label: "Unicode-compatible",
    detail: "GSM character in a Unicode message"
  })}
          {renderLegendItem({
    kind: "unicode",
    label: "Forces Unicode",
    detail: "Not available in GSM-7"
  })}
          {renderLegendItem({
    kind: "udh",
    label: "UDH",
    detail: "Multipart segment header"
  })}
        </div>
      </section>
    </section>;
};

Use the lab to see how Phonovation encodes and divides your message before you send it. Everything runs in your browser, and the lab never submits an API request.

<SmsMessageLab />

## Rules represented by the lab

* Message length from 1 to 2,000 characters with at least one visible character.
* GSM-7 limits of 160 units for one part and 153 units per concatenated part.
* UTF-16 limits of 70 characters for one part and 67 characters per concatenated part.
* GSM-7 extended characters count as two units.
* Typographic quote, dash, ellipsis, and bullet replacements from the OpenAPI contract.

<Note>
  Each SMS part may be billed separately. The lab calculates message composition only; it does not check account permissions, credits, Sender ID provisioning, recipient deliverability, or consent.
</Note>

<Card title="Sending SMS" icon="paper-plane" href="/phonovation-api/sending-sms" cta="Read the campaign guide" arrow="true">
  Add the calculated text to a campaign and review Sender IDs, recipients, scheduling, summaries, and delivery behavior.
</Card>
