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

# Message Encoding

> Understand SMS encoding types, character limits, segment calculations, and how to optimize message length to reduce costs.

export const SegmentCalculator = () => {
  const GSM_CHARSET_STRING = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "\n\r !\"#$%&'()*+,-./:;<=>?@[\\]^_{}|~" + "£¥èéùìòØøÅåÆæßÉ¡ÄÖÑÜ¿äöñüà";
  const GSM_CHARSET = new Set(GSM_CHARSET_STRING);
  const LIMITS = {
    gsm7: {
      single: 160,
      multi: 153
    },
    ascii7: {
      single: 160,
      multi: 153
    },
    ascii8: {
      single: 140,
      multi: 134
    },
    utf16: {
      single: 70,
      multi: 67
    }
  };
  const [message, setMessage] = useState('');
  const [encoding, setEncoding] = useState('gsm7');
  const [results, setResults] = useState({
    segments: 0,
    charCount: 0,
    charsPerSegment: 160,
    totalBytes: 0,
    encoding: 'gsm7'
  });
  const isGSMCharacter = char => GSM_CHARSET.has(char);
  const isASCII7Character = char => {
    const code = char.charCodeAt(0);
    return code >= 0 && code <= 127;
  };
  const isASCII8Character = char => {
    const code = char.charCodeAt(0);
    return code >= 0 && code <= 255;
  };
  const countASCII7Length = text => {
    let length = 0;
    for (const char of text) {
      if (isASCII7Character(char)) {
        length += 1;
      } else {
        return -1;
      }
    }
    return length;
  };
  const countASCII8Length = text => {
    let length = 0;
    for (const char of text) {
      if (isASCII8Character(char)) {
        length += 1;
      } else {
        return -1;
      }
    }
    return length;
  };
  const isEmoji = str => {
    const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA70}-\u{1FAFF}]/gu;
    return emojiRegex.test(str);
  };
  const hasEmojis = text => {
    const emojiRegex = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu;
    return emojiRegex.test(text);
  };
  const countGSMLength = text => {
    let length = 0;
    for (const char of text) {
      if (GSM_CHARSET.has(char)) {
        length += 1;
      } else {
        return -1;
      }
    }
    return length;
  };
  const calculateSegments = (text, enc) => {
    if (!text) {
      return {
        segments: 0,
        charCount: 0,
        charsPerSegment: 160,
        totalBytes: 0,
        encoding: enc
      };
    }
    let charCount;
    let limits;
    let bytesPerChar;
    const applyUTF16 = () => {
      if (hasEmojis(text)) {
        return {
          limits: {
            single: 35,
            multi: 33
          },
          bytesPerChar: 2
        };
      }
      return {
        limits: LIMITS.utf16,
        bytesPerChar: 2
      };
    };
    switch (enc) {
      case 'gsm7':
        charCount = countGSMLength(text);
        if (charCount === -1) {
          charCount = text.length;
          const utf16 = applyUTF16();
          limits = utf16.limits;
          bytesPerChar = utf16.bytesPerChar;
          enc = 'utf16';
        } else {
          limits = LIMITS.gsm7;
          bytesPerChar = 7 / 8;
        }
        break;
      case 'ascii7':
        charCount = countASCII7Length(text);
        if (charCount === -1) {
          charCount = text.length;
          const utf16 = applyUTF16();
          limits = utf16.limits;
          bytesPerChar = utf16.bytesPerChar;
          enc = 'utf16';
        } else {
          limits = LIMITS.ascii7;
          bytesPerChar = 7 / 8;
        }
        break;
      case 'ascii8':
        charCount = countASCII8Length(text);
        if (charCount === -1) {
          charCount = text.length;
          const utf16 = applyUTF16();
          limits = utf16.limits;
          bytesPerChar = utf16.bytesPerChar;
          enc = 'utf16';
        } else {
          limits = LIMITS.ascii8;
          bytesPerChar = 1;
        }
        break;
      case 'utf16':
        charCount = text.length;
        const utf16 = applyUTF16();
        limits = utf16.limits;
        bytesPerChar = utf16.bytesPerChar;
        break;
      default:
        charCount = text.length;
        limits = LIMITS.utf16;
        bytesPerChar = 2;
    }
    let segments;
    let charsPerSegment;
    if (charCount <= limits.single) {
      segments = charCount > 0 ? 1 : 0;
      charsPerSegment = limits.single;
    } else {
      segments = Math.ceil(charCount / limits.multi);
      charsPerSegment = limits.multi;
    }
    const totalBytes = Math.ceil(charCount * bytesPerChar);
    return {
      segments,
      charCount,
      charsPerSegment,
      totalBytes,
      encoding: enc
    };
  };
  const handleMessageChange = e => {
    const text = e.target.value;
    setMessage(text);
    const newResults = calculateSegments(text, encoding);
    setResults(newResults);
  };
  const handleEncodingChange = e => {
    const newEncoding = e.target.value;
    setEncoding(newEncoding);
    const newResults = calculateSegments(message, newEncoding);
    setResults(newResults);
  };
  const getEncodingDisplayName = enc => {
    const names = {
      gsm7: 'GSM 7-bit',
      ascii7: 'ASCII 7-bit',
      ascii8: 'ASCII 8-bit',
      utf16: 'UTF-16'
    };
    return names[enc] || enc;
  };
  const renderHighlightedText = () => {
    if (!message) return '\u00A0';
    const chars = Array.from(message);
    return chars.map((char, index) => {
      if (char === '\n') {
        return <br key={index} />;
      }
      if (isEmoji(char)) {
        return <span key={index} style={{
          backgroundColor: 'rgba(255, 71, 133, 0.6)',
          borderRadius: '3px',
          color: 'transparent'
        }}>{char}</span>;
      } else if (!isGSMCharacter(char) && char !== '\r') {
        return <span key={index} style={{
          backgroundColor: 'rgba(0, 227, 170, 0.6)',
          borderRadius: '3px',
          color: 'transparent'
        }}>{char}</span>;
      }
      return <span key={index} style={{
        color: 'transparent'
      }}>{char}</span>;
    });
  };
  const overhead = results.segments > 1 ? results.segments * 7 : 0;
  const totalSent = results.totalBytes + overhead;
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const checkDarkMode = () => {
      const isDarkMode = document.documentElement.classList.contains('dark');
      setIsDark(isDarkMode);
    };
    checkDarkMode();
    const observer = new MutationObserver(checkDarkMode);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => {
      observer.disconnect();
    };
  }, []);
  const colors = {
    bg: isDark ? '#1f2937' : '#f9fafb',
    bgInput: isDark ? '#374151' : '#fff',
    border: isDark ? '#4b5563' : '#e5e7eb',
    borderInput: isDark ? '#6b7280' : '#d1d5db',
    text: isDark ? '#f9fafb' : '#111827',
    textMuted: isDark ? '#9ca3af' : '#6b7280',
    textLabel: isDark ? '#d1d5db' : '#374151'
  };
  const containerStyle = {
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    maxWidth: '100%',
    margin: '0 auto'
  };
  const inputSectionStyle = {
    marginBottom: '24px'
  };
  const labelStyle = {
    display: 'block',
    marginBottom: '8px',
    fontWeight: '600',
    fontSize: '0.95rem',
    color: colors.textLabel
  };
  const textareaContainerStyle = {
    position: 'relative',
    marginBottom: '12px'
  };
  const textareaStyle = {
    width: '100%',
    minHeight: '120px',
    padding: '12px',
    fontSize: '1rem',
    fontFamily: 'inherit',
    border: `1px solid ${colors.borderInput}`,
    borderRadius: '8px',
    resize: 'vertical',
    backgroundColor: colors.bgInput,
    color: colors.text,
    boxSizing: 'border-box'
  };
  const encodingSelectStyle = {
    padding: '8px 12px',
    fontSize: '0.9rem',
    border: `1px solid ${colors.borderInput}`,
    borderRadius: '6px',
    backgroundColor: colors.bgInput,
    color: colors.text,
    cursor: 'pointer'
  };
  const resultsGridStyle = {
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gap: '12px',
    marginBottom: '24px'
  };
  const resultCardStyle = {
    backgroundColor: colors.bg,
    border: `1px solid ${colors.border}`,
    borderRadius: '8px',
    padding: '16px 12px',
    textAlign: 'center',
    minHeight: '80px',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center'
  };
  const resultLabelStyle = {
    fontSize: '0.75rem',
    color: colors.textMuted,
    marginBottom: '4px',
    textTransform: 'uppercase',
    letterSpacing: '0.05em'
  };
  const resultValueStyle = {
    fontSize: '1.25rem',
    fontWeight: '700',
    color: colors.text
  };
  const segmentBlocksStyle = {
    display: 'flex',
    flexWrap: 'wrap',
    gap: '8px',
    marginTop: '16px'
  };
  const segmentBlockStyle = (index, isPartial) => ({
    width: '40px',
    height: '40px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: isPartial ? isDark ? '#78350f' : '#fef3c7' : isDark ? '#064e3b' : '#d1fae5',
    border: `2px solid ${isPartial ? '#f59e0b' : '#10b981'}`,
    borderRadius: '8px',
    fontWeight: '600',
    fontSize: '0.9rem',
    color: isPartial ? isDark ? '#fcd34d' : '#92400e' : isDark ? '#6ee7b7' : '#065f46'
  });
  return <div style={containerStyle}>
      <div style={inputSectionStyle}>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: '8px'
  }}>
          <label style={{
    ...labelStyle,
    marginBottom: '0'
  }}>Enter your message:</label>
          <div style={{
    display: 'flex',
    gap: '16px'
  }}>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px',
    fontSize: '0.8rem',
    color: colors.textMuted
  }}>
              <span style={{
    width: '12px',
    height: '12px',
    borderRadius: '2px',
    background: 'linear-gradient(180deg, #00E3AA 0%, #00B588 100%)'
  }}></span>
              <span>Unicode</span>
            </div>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px',
    fontSize: '0.8rem',
    color: colors.textMuted
  }}>
              <span style={{
    width: '12px',
    height: '12px',
    borderRadius: '2px',
    background: 'linear-gradient(180deg, #FF4785 0%, #FF2E63 100%)'
  }}></span>
              <span>Emoji</span>
            </div>
          </div>
        </div>
        <div style={textareaContainerStyle}>
          <div style={{
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    padding: '12px',
    fontSize: '1rem',
    fontFamily: 'inherit',
    lineHeight: '1.5',
    whiteSpace: 'pre-wrap',
    wordWrap: 'break-word',
    pointerEvents: 'none',
    overflow: 'hidden',
    borderRadius: '8px',
    boxSizing: 'border-box'
  }}>
            {renderHighlightedText()}
          </div>
          <textarea value={message} onChange={handleMessageChange} placeholder="Type your message here to see how many segments it will use..." style={{
    ...textareaStyle,
    backgroundColor: 'transparent',
    position: 'relative',
    zIndex: 1,
    caretColor: colors.text
  }} />
        </div>

        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '12px'
  }}>
          <label style={{
    fontSize: '0.9rem',
    color: colors.textLabel,
    fontWeight: '500'
  }}>Encoding:</label>
          <select value={encoding} onChange={handleEncodingChange} style={encodingSelectStyle}>
            <option value="gsm7">GSM 7-bit</option>
            <option value="ascii7">ASCII 7-bit</option>
            <option value="ascii8">ASCII 8-bit</option>
            <option value="utf16">UTF-16 (Unicode)</option>
          </select>
        </div>
      </div>

      <div style={resultsGridStyle}>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Segments</div>
          <div style={resultValueStyle}>{results.segments}</div>
        </div>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Characters</div>
          <div style={resultValueStyle}>{results.charCount}</div>
        </div>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Encoding</div>
          <div style={resultValueStyle}>{getEncodingDisplayName(results.encoding)}</div>
        </div>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Chars / Segment</div>
          <div style={resultValueStyle}>{results.charsPerSegment}</div>
        </div>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Message Size</div>
          <div style={resultValueStyle}>{results.totalBytes} bytes</div>
        </div>
        <div style={resultCardStyle}>
          <div style={resultLabelStyle}>Total Sent</div>
          <div style={resultValueStyle}>{totalSent} bytes</div>
        </div>
      </div>

      {results.segments > 0 && <div>
          <label style={labelStyle}>Message Segments:</label>
          <div style={segmentBlocksStyle}>
            {Array.from({
    length: results.segments
  }, (_, i) => {
    const isLast = i === results.segments - 1;
    const segmentChars = results.segments === 1 ? results.charCount : isLast ? results.charCount - results.charsPerSegment * i : results.charsPerSegment;
    const isPartial = results.segments > 1 && isLast && segmentChars < results.charsPerSegment;
    return <div key={i} style={segmentBlockStyle(i, isPartial)} title={`Segment ${i + 1}: ${segmentChars} characters`}>
                  {i + 1}
                </div>;
  })}
          </div>
        </div>}

    </div>;
};

SMS messages are encoded into **segments** of 140 bytes each. You are billed per segment, so understanding encoding is key to controlling costs.

The encoding determines how many characters fit in each segment:

| Encoding    | Bits per char | Single segment | Multi-part segment |
| ----------- | ------------- | -------------- | ------------------ |
| GSM 7-bit   | 7             | 160 chars      | 153 chars          |
| ASCII 7-bit | 7             | 160 chars      | 153 chars          |
| ASCII 8-bit | 8             | 140 chars      | 134 chars          |
| UTF-16      | 16            | 70 chars       | 67 chars           |

<Warning>
  A single non-GSM-7 character (like an emoji or curly quote) switches the **entire message** to UTF-16, cutting capacity from 160 to 70 characters per segment. This can more than double your costs.
</Warning>

## Segment calculator

Use this interactive tool to check how your message will be encoded and segmented:

<SegmentCalculator />

## How segments work

Every SMS message is transmitted in units of **140 bytes**. When a message exceeds one segment, a **6-byte header** (User Data Header, or UDH) is added to each segment for reassembly, reducing the usable space.

```
Single segment:   140 bytes available → 160 GSM-7 chars or 70 UTF-16 chars
Multi-part:       134 bytes per segment → 153 GSM-7 chars or 67 UTF-16 chars
Maximum:          10 segments per message
```

### Segment calculation formula

To calculate the number of segments for a message:

<Tabs>
  <Tab title="GSM-7">
    ```
    Characters ≤ 160  →  1 segment
    Characters > 160  →  ⌈characters / 153⌉ segments

    Examples:
    - 100 chars = 1 segment
    - 160 chars = 1 segment
    - 161 chars = 2 segments (153 + 8)
    - 306 chars = 2 segments (153 + 153)
    - 307 chars = 3 segments (153 + 153 + 1)
    - 1530 chars = 10 segments (maximum)
    ```
  </Tab>

  <Tab title="UTF-16">
    ```
    Characters ≤ 70   →  1 segment
    Characters > 70   →  ⌈characters / 67⌉ segments

    Examples:
    - 50 chars = 1 segment
    - 70 chars = 1 segment
    - 71 chars = 2 segments (67 + 4)
    - 134 chars = 2 segments (67 + 67)
    - 135 chars = 3 segments (67 + 67 + 1)
    - 670 chars = 10 segments (maximum)
    ```
  </Tab>
</Tabs>

### Cost impact example

Consider a 200-character message:

| Scenario                                                                     | Encoding | Segments | Relative cost |
| ---------------------------------------------------------------------------- | -------- | -------- | ------------- |
| All GSM-7 characters                                                         | GSM-7    | 2        | 2×            |
| Contains one emoji 😀                                                        | UTF-16   | 3        | 3×            |
| Contains one curly quote "                                                   | UTF-16   | 3        | 3×            |
| With [smart encoding](/docs/messaging/messages/smart-encoding/index) enabled | GSM-7    | 2        | 2×            |

<Note>
  Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) to automatically replace common Unicode characters (like curly quotes and em dashes) with GSM-7 equivalents, reducing segment counts.
</Note>

***

## Encoding by sender type

| Sender type  | Default encoding | Fallback |
| ------------ | ---------------- | -------- |
| Long Code    | GSM 7-bit        | UTF-16   |
| Toll-Free    | GSM 7-bit        | UTF-16   |
| Short Code   | ASCII 7-bit      | UTF-16   |
| Alphanumeric | GSM 7-bit        | UTF-16   |

If your message contains characters outside the default encoding's character set, the fallback encoding is used automatically for the entire message.

<Note>MMS and RCS messages use **UTF-8** encoding by default and are not affected by these limits.</Note>

***

## GSM 7-bit character set

Telnyx uses a GSM 7-bit encoding optimized for maximum carrier compatibility. Only characters in this set will keep your message in the efficient GSM-7 encoding.

<Accordion title="Standard characters (1 character each)">
  **Letters:**

  ```
  A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  a b c d e f g h i j k l m n o p q r s t u v w x y z
  ```

  **Digits:**

  ```
  0 1 2 3 4 5 6 7 8 9
  ```

  **Symbols and punctuation:**

  ```
  ! " # $ % & ' ( ) * + , - . / : ; < = > ? @
  ```

  **Special characters:**

  | Character | Description          |
  | --------- | -------------------- |
  | `space`   | Space                |
  | `\n`      | Line feed            |
  | `\r`      | Carriage return      |
  | `_`       | Underscore           |
  | `£`       | Pound sign           |
  | `¥`       | Yen sign             |
  | `è`       | e grave              |
  | `é`       | e acute              |
  | `ù`       | u grave              |
  | `ì`       | i grave              |
  | `ò`       | o grave              |
  | `Ø`       | O with stroke        |
  | `ø`       | o with stroke        |
  | `Å`       | A with ring          |
  | `å`       | a with ring          |
  | `Æ`       | AE ligature          |
  | `æ`       | ae ligature          |
  | `ß`       | Sharp s              |
  | `É`       | E acute              |
  | `¡`       | Inverted exclamation |
  | `Ä`       | A umlaut             |
  | `Ö`       | O umlaut             |
  | `Ñ`       | N tilde              |
  | `Ü`       | U umlaut             |
  | `§`       | Section sign         |
  | `¿`       | Inverted question    |
  | `ä`       | a umlaut             |
  | `ö`       | o umlaut             |
  | `ñ`       | n tilde              |
  | `ü`       | u umlaut             |
  | `à`       | a grave              |
</Accordion>

<Accordion title="Extended characters (2 characters each)">
  These characters require an escape sequence and count as **2 characters** in segment calculations:

  | Character | Description          | Character count |
  | --------- | -------------------- | --------------- |
  | `~`       | Tilde                | 2               |
  | `^`       | Circumflex           | 2               |
  | `\|`      | Pipe / vertical bar  | 2               |
  | `\`       | Backslash            | 2               |
  | `{`       | Left curly bracket   | 2               |
  | `}`       | Right curly bracket  | 2               |
  | `[`       | Left square bracket  | 2               |
  | `]`       | Right square bracket | 2               |
  | `€`       | Euro sign            | 2               |

  <Warning>
    Extended characters are easy to overlook when estimating segment counts. A message with 155 standard characters and 3 pipe characters (`|`) uses 155 + (3 × 2) = 161 character slots, requiring **2 segments** instead of 1.
  </Warning>
</Accordion>

***

## Detecting encoding in your application

Before sending, you can check if a message will use GSM-7 or UTF-16 encoding to estimate costs. Here are helper functions for each language:

<CodeGroup>
  ```python Python theme={null}
  import re

  # GSM-7 basic character set
  GSM7_BASIC = set(
      "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ"
      " !\"#¤%&'()*+,-./0123456789:;<=>?"
      "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz"
      "äöñüà"
  )
  GSM7_EXTENDED = set("^{}\\[~]|€")

  def calculate_segments(text: str) -> dict:
      """Calculate encoding and segment count for an SMS message."""
      is_gsm7 = all(c in GSM7_BASIC or c in GSM7_EXTENDED for c in text)

      if is_gsm7:
          # Count extended chars as 2
          char_count = sum(2 if c in GSM7_EXTENDED else 1 for c in text)
          if char_count <= 160:
              segments = 1
          else:
              segments = -(-char_count // 153)  # ceiling division
          return {"encoding": "GSM-7", "char_count": char_count, "segments": segments}
      else:
          # UTF-16: emojis count as 2 chars (surrogate pairs)
          char_count = 0
          for c in text:
              char_count += 2 if ord(c) > 0xFFFF else 1
          if char_count <= 70:
              segments = 1
          else:
              segments = -(-char_count // 67)
          return {"encoding": "UTF-16", "char_count": char_count, "segments": segments}

  # Example usage
  result = calculate_segments("Hello, world!")
  print(f"Encoding: {result['encoding']}, Segments: {result['segments']}")
  # Output: Encoding: GSM-7, Segments: 1

  result = calculate_segments("Hello 😀")
  print(f"Encoding: {result['encoding']}, Segments: {result['segments']}")
  # Output: Encoding: UTF-16, Segments: 1
  ```

  ```javascript Node theme={null}
  const GSM7_BASIC = new Set(
    '@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ' +
    ' !"#¤%&\'()*+,-./0123456789:;<=>?' +
    '¡ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
    'ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz' +
    'äöñüà'
  );
  const GSM7_EXTENDED = new Set('^{}\\[~]|€');

  function calculateSegments(text) {
    const chars = [...text];
    const isGsm7 = chars.every(c => GSM7_BASIC.has(c) || GSM7_EXTENDED.has(c));

    if (isGsm7) {
      const charCount = chars.reduce(
        (sum, c) => sum + (GSM7_EXTENDED.has(c) ? 2 : 1), 0
      );
      const segments = charCount <= 160 ? 1 : Math.ceil(charCount / 153);
      return { encoding: 'GSM-7', charCount, segments };
    } else {
      const charCount = chars.reduce(
        (sum, c) => sum + (c.codePointAt(0) > 0xFFFF ? 2 : 1), 0
      );
      const segments = charCount <= 70 ? 1 : Math.ceil(charCount / 67);
      return { encoding: 'UTF-16', charCount, segments };
    }
  }

  // Example usage
  console.log(calculateSegments('Hello, world!'));
  // { encoding: 'GSM-7', charCount: 13, segments: 1 }

  console.log(calculateSegments('Hello 😀'));
  // { encoding: 'UTF-16', charCount: 8, segments: 1 }
  ```

  ```ruby Ruby theme={null}
  GSM7_BASIC = Set.new(
    "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ" \
    " !\"#¤%&'()*+,-./0123456789:;<=>?" \
    "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
    "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz" \
    "äöñüà"
  ).flat_map(&:chars).to_set
  GSM7_EXTENDED = Set.new("^{}\\[~]|€".chars)

  def calculate_segments(text)
    chars = text.chars
    is_gsm7 = chars.all? { |c| GSM7_BASIC.include?(c) || GSM7_EXTENDED.include?(c) }

    if is_gsm7
      char_count = chars.sum { |c| GSM7_EXTENDED.include?(c) ? 2 : 1 }
      segments = char_count <= 160 ? 1 : (char_count.to_f / 153).ceil
      { encoding: "GSM-7", char_count: char_count, segments: segments }
    else
      char_count = chars.sum { |c| c.ord > 0xFFFF ? 2 : 1 }
      segments = char_count <= 70 ? 1 : (char_count.to_f / 67).ceil
      { encoding: "UTF-16", char_count: char_count, segments: segments }
    end
  end

  puts calculate_segments("Hello, world!")
  # {:encoding=>"GSM-7", :char_count=>13, :segments=>1}
  ```

  ```go Go theme={null}
  package main

  import (
  	"fmt"
  	"math"
  	"unicode/utf8"
  )

  var gsm7Basic = map[rune]bool{
  	'@': true, '£': true, '$': true, '¥': true, 'è': true, 'é': true,
  	'ù': true, 'ì': true, 'ò': true, 'Ç': true, '\n': true, 'Ø': true,
  	'ø': true, '\r': true, 'Å': true, 'å': true, 'Δ': true, '_': true,
  	'Φ': true, 'Γ': true, 'Λ': true, 'Ω': true, 'Π': true, 'Ψ': true,
  	'Σ': true, 'Θ': true, 'Ξ': true, ' ': true, 'Æ': true, 'æ': true,
  	'ß': true, 'É': true, '¡': true, 'Ä': true, 'Ö': true, 'Ñ': true,
  	'Ü': true, '§': true, '¿': true, 'ä': true, 'ö': true, 'ñ': true,
  	'ü': true, 'à': true,
  }

  var gsm7Extended = map[rune]bool{
  	'^': true, '{': true, '}': true, '\\': true,
  	'[': true, '~': true, ']': true, '|': true, '€': true,
  }

  func init() {
  	// Add printable ASCII
  	for c := '!'; c <= '~'; c++ {
  		gsm7Basic[c] = true
  	}
  }

  type SegmentResult struct {
  	Encoding  string
  	CharCount int
  	Segments  int
  }

  func CalculateSegments(text string) SegmentResult {
  	isGSM7 := true
  	for _, r := range text {
  		if !gsm7Basic[r] && !gsm7Extended[r] {
  			isGSM7 = false
  			break
  		}
  	}

  	if isGSM7 {
  		charCount := 0
  		for _, r := range text {
  			if gsm7Extended[r] {
  				charCount += 2
  			} else {
  				charCount++
  			}
  		}
  		segments := 1
  		if charCount > 160 {
  			segments = int(math.Ceil(float64(charCount) / 153))
  		}
  		return SegmentResult{"GSM-7", charCount, segments}
  	}

  	charCount := 0
  	for _, r := range text {
  		if r > 0xFFFF {
  			charCount += 2
  		} else {
  			charCount++
  		}
  	}
  	_ = utf8.RuneCountInString(text) // ensure valid UTF-8
  	segments := 1
  	if charCount > 70 {
  		segments = int(math.Ceil(float64(charCount) / 67))
  	}
  	return SegmentResult{"UTF-16", charCount, segments}
  }

  func main() {
  	fmt.Println(CalculateSegments("Hello, world!"))
  	// {GSM-7 13 1}
  }
  ```

  ```java Java theme={null}
  import java.util.Set;

  public class SmsEncoding {

      private static final Set<Character> GSM7_EXTENDED =
          Set.of('^', '{', '}', '\\', '[', '~', ']', '|', '€');

      private static final String GSM7_CHARS =
          "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ"
          + " !\"#¤%&'()*+,-./0123456789:;<=>?"
          + "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ"
          + "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz"
          + "äöñüà^{}\\[~]|€";

      private static final Set<Character> GSM7_BASIC = new java.util.HashSet<>();
      static {
          for (char c : GSM7_CHARS.toCharArray()) {
              GSM7_BASIC.add(c);
          }
      }

      public record SegmentResult(String encoding, int charCount, int segments) {}

      public static SegmentResult calculateSegments(String text) {
          boolean isGsm7 = text.chars()
              .mapToObj(c -> (char) c)
              .allMatch(c -> GSM7_BASIC.contains(c) || GSM7_EXTENDED.contains(c));

          if (isGsm7) {
              int charCount = text.chars()
                  .map(c -> GSM7_EXTENDED.contains((char) c) ? 2 : 1)
                  .sum();
              int segments = charCount <= 160 ? 1 : (int) Math.ceil(charCount / 153.0);
              return new SegmentResult("GSM-7", charCount, segments);
          } else {
              int charCount = text.codePoints()
                  .map(cp -> cp > 0xFFFF ? 2 : 1)
                  .sum();
              int segments = charCount <= 70 ? 1 : (int) Math.ceil(charCount / 67.0);
              return new SegmentResult("UTF-16", charCount, segments);
          }
      }

      public static void main(String[] args) {
          System.out.println(calculateSegments("Hello, world!"));
          // SegmentResult[encoding=GSM-7, charCount=13, segments=1]
      }
  }
  ```

  ```csharp .NET theme={null}
  using System;
  using System.Collections.Generic;
  using System.Linq;

  public record SegmentResult(string Encoding, int CharCount, int Segments);

  public static class SmsEncoding
  {
      private static readonly HashSet<char> Gsm7Extended =
          new("^{}\\[~]|€");

      private static readonly HashSet<char> Gsm7Basic = new(
          "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ"
          + " !\"#¤%&'()*+,-./0123456789:;<=>?"
          + "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ"
          + "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz"
          + "äöñüà"
      );

      public static SegmentResult CalculateSegments(string text)
      {
          bool isGsm7 = text.All(c => Gsm7Basic.Contains(c) || Gsm7Extended.Contains(c));

          if (isGsm7)
          {
              int charCount = text.Sum(c => Gsm7Extended.Contains(c) ? 2 : 1);
              int segments = charCount <= 160 ? 1 : (int)Math.Ceiling(charCount / 153.0);
              return new SegmentResult("GSM-7", charCount, segments);
          }
          else
          {
              int charCount = 0;
              for (int i = 0; i < text.Length; i++)
              {
                  charCount += char.IsHighSurrogate(text[i]) ? 2 : 1;
                  if (char.IsHighSurrogate(text[i])) i++; // skip low surrogate
              }
              int segments = charCount <= 70 ? 1 : (int)Math.Ceiling(charCount / 67.0);
              return new SegmentResult("UTF-16", charCount, segments);
          }
      }
  }

  // Usage:
  // var result = SmsEncoding.CalculateSegments("Hello, world!");
  // Console.WriteLine($"{result.Encoding}, {result.Segments} segment(s)");
  ```

  ```php PHP theme={null}
  <?php

  function calculateSegments(string $text): array
  {
      $gsm7Extended = str_split('^{}\\[~]|€');
      $gsm7Basic = str_split(
          "@£\$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ ÆæßÉ"
          . " !\"#¤%&'()*+,-./0123456789:;<=>?"
          . "¡ABCDEFGHIJKLMNOPQRSTUVWXYZ"
          . "ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyz"
          . "äöñüà"
      );
      $basicSet = array_flip($gsm7Basic);
      $extSet = array_flip($gsm7Extended);

      $chars = mb_str_split($text);
      $isGsm7 = true;
      foreach ($chars as $c) {
          if (!isset($basicSet[$c]) && !isset($extSet[$c])) {
              $isGsm7 = false;
              break;
          }
      }

      if ($isGsm7) {
          $charCount = array_sum(array_map(
              fn($c) => isset($extSet[$c]) ? 2 : 1, $chars
          ));
          $segments = $charCount <= 160 ? 1 : (int) ceil($charCount / 153);
          return ['encoding' => 'GSM-7', 'char_count' => $charCount, 'segments' => $segments];
      }

      $charCount = 0;
      foreach ($chars as $c) {
          $charCount += mb_ord($c) > 0xFFFF ? 2 : 1;
      }
      $segments = $charCount <= 70 ? 1 : (int) ceil($charCount / 67);
      return ['encoding' => 'UTF-16', 'char_count' => $charCount, 'segments' => $segments];
  }

  // Usage
  $result = calculateSegments("Hello, world!");
  echo "{$result['encoding']}, {$result['segments']} segment(s)\n";
  // GSM-7, 1 segment(s)
  ```
</CodeGroup>

***

## Common encoding issues

<AccordionGroup>
  <Accordion title="Message unexpectedly uses UTF-16 (too many segments)">
    **Symptom:** Your message uses more segments than expected.

    **Cause:** A non-GSM-7 character is present, forcing the entire message to UTF-16. Common culprits:

    | Character                   | Source                            | GSM-7?                      |
    | --------------------------- | --------------------------------- | --------------------------- |
    | `"` `"` (curly quotes)      | Word processors, mobile keyboards | ❌                           |
    | `'` `'` (curly apostrophes) | Auto-correct, CMS platforms       | ❌                           |
    | `—` (em dash)               | Word processors                   | ❌                           |
    | `…` (ellipsis)              | Mobile keyboards                  | ❌                           |
    | `€` (euro sign)             | Manual entry                      | ✅ (extended, costs 2 chars) |

    **Fix:**

    1. Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) to auto-replace these characters
    2. Or manually replace them with GSM-7 equivalents before sending
  </Accordion>

  <Accordion title="Emojis dramatically increase segment count">
    **Symptom:** Adding a single emoji doubles or triples the number of segments.

    **Cause:** Emojis force UTF-16 encoding (70 chars/segment instead of 160). Additionally, most emojis use **surrogate pairs** and count as 2 UTF-16 characters.

    **Example:**

    ```
    "Thanks for your order!"        → GSM-7, 1 segment (22 chars)
    "Thanks for your order! 🎉"     → UTF-16, 1 segment (25 chars)
    "Thanks for your order! ... 🎉" → UTF-16, 2 segments (71+ chars)
    ```

    **Fix:** If cost is a concern, avoid emojis in SMS. Use emojis freely in MMS/RCS where encoding isn't a factor.
  </Accordion>

  <Accordion title="Extended GSM-7 characters cause unexpected segment splits">
    **Symptom:** A 155-character message that looks like it should fit in one segment actually requires two.

    **Cause:** Characters like `[`, `]`, `{`, `}`, `|`, `\`, `^`, `~`, and `€` are in the GSM-7 extended set and count as **2 characters** each.

    **Example:**

    ```
    "Price: $100 [USD]" → 18 visible chars but 20 GSM-7 chars ([ and ] each cost 2)
    ```

    **Fix:** Account for extended characters when calculating message length. Use the segment calculator above or the SDK helpers in this guide.
  </Accordion>

  <Accordion title="Copy-pasted text from Word/Google Docs causes issues">
    **Symptom:** Text that looks like normal ASCII actually contains Unicode characters.

    **Cause:** Word processors often replace straight quotes with curly quotes, hyphens with em dashes, and three periods with an ellipsis character. These are invisible differences that force UTF-16.

    **Fix:**

    1. Enable [smart encoding](/docs/messaging/messages/smart-encoding/index) — this handles the most common substitutions automatically
    2. Sanitize text before sending by replacing known problem characters
    3. Use the `encoding` parameter set to `gsm7` to get a `400` error if non-GSM-7 characters are present (fail-fast approach)
  </Accordion>

  <Accordion title="Messages truncated or split incorrectly on recipient's phone">
    **Symptom:** The recipient sees a message split in unexpected places, or parts arrive out of order.

    **Cause:** Multi-part messages are reassembled by the recipient's device using the UDH (User Data Header). Some older devices or carriers may not support reassembly for messages over a certain number of segments.

    **Fix:**

    * Keep messages under 3-4 segments for maximum compatibility
    * Telnyx supports up to 10 segments, but recipient device support varies
    * Consider using MMS for longer content
  </Accordion>

  <Accordion title="Non-Latin scripts (Chinese, Arabic, Cyrillic) use too many segments">
    **Symptom:** Messages in non-Latin scripts use significantly more segments than English messages of similar visible length.

    **Cause:** Non-Latin characters have no GSM-7 equivalents, so the entire message uses UTF-16 encoding (70 characters per segment). Smart encoding cannot help here.

    **Fix:**

    * This is expected behavior — plan for higher segment counts when messaging in non-Latin scripts
    * Keep messages concise
    * Consider MMS for longer non-Latin content
  </Accordion>
</AccordionGroup>

***

## Best practices

<Steps>
  <Step title="Enable smart encoding">
    Turn on [smart encoding](/docs/messaging/messages/smart-encoding/index) on your messaging profile to automatically handle Unicode-to-GSM-7 substitutions. This is the single biggest cost-saving measure.
  </Step>

  <Step title="Validate before sending">
    Use the encoding detection helpers above to check segment counts before sending. Alert your application when messages will be unexpectedly expensive.
  </Step>

  <Step title="Sanitize input text">
    If you accept user-generated content, sanitize it before sending. Strip or replace invisible Unicode characters, curly quotes, and other common problem characters.
  </Step>

  <Step title="Keep messages concise">
    Stay under 160 characters (GSM-7) or 70 characters (UTF-16) to avoid multi-part message overhead. Each additional segment adds 7 characters of UDH overhead.
  </Step>

  <Step title="Use the right channel">
    For messages that need emojis, rich formatting, or non-Latin scripts, consider [MMS](/docs/messaging/messages/configuration-and-usage/index) or [RCS](/docs/messaging/messages/send-an-rcs-message/index) instead of SMS.
  </Step>
</Steps>

***

## Related resources

<CardGroup cols={2}>
  <Card title="Smart Encoding" icon="wand-magic-sparkles" href="/docs/messaging/messages/smart-encoding/index">
    Automatically replace Unicode characters with GSM-7 equivalents to reduce costs.
  </Card>

  <Card title="Send Your First Message" icon="paper-plane" href="/docs/messaging/messages/send-message/index">
    Get started with the Telnyx Messaging API.
  </Card>

  <Card title="Messages API Reference" icon="code" href="/api-reference/messages/send-a-message">
    API reference for sending messages with encoding options.
  </Card>

  <Card title="Messaging Profiles" icon="sliders" href="/api-reference/messaging-profiles/update-a-messaging-profile">
    Configure smart encoding and other profile settings.
  </Card>
</CardGroup>
