DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
World desk9 min

FIX 4.4 in C# From Scratch: Build, Checksum and Parse Orders Without a Library

A byte-aware C# walkthrough of the FIX 4.4 TagValue envelope: BodyLength and CheckSum rules, a builder, a strict parser, and why envelope parsing is not order validation.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A FIX 4.4 TagValue message is a sequence of tag=value fields, each ended by the SOH byte (0x01). You can build and parse that envelope in C# with byte-level code and no FIX engine. Write BeginString (8), BodyLength (9) and MsgType (35) first, compute BodyLength from the bytes that follow it, and append CheckSum (10) computed over everything before it. That gives you a small, checkable framing layer. It does not make an order valid. A valid New Order Single (35=D) depends on the field rules in the FIX 4.4 application specification, which this article does not reproduce in full, and a successful envelope parse only proves framing and checksum.

Start from the FIX 4.4 errata release

The FIX Trading Community identifies FIX 4.4 Specification with Errata 20030618 as the current FIX 4.4 specification and recommends that implementers use that errata release. The download is a ZIP archive containing seven volumes and release notes. FIX 4.4 also appears among the legacy application-message versions the standards organization lists as supported.

As an Amazon Associate I earn from qualifying purchases.

Keep two sources open while you work. The BodyLength rule quoted below comes from the FIX Session Layer technical standard (June 2020), while message-level field requirements such as the New Order Single definition come from the FIX 4.4 application volumes in the errata download.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Three layers of a FIX message

Most confusion in from-scratch FIX code comes from mixing three separate concerns. Keep them apart in your code:

  • Message envelope: tags 8, 9 and 35 open the message, and tag 10 closes it. This is what the code below builds and checks.
  • Application schema: the MsgType decides which fields a message must and may carry. A 35=D order is validated against its own definition, not against the envelope rules.
  • Stream and session framing: a socket delivers bytes, not messages. Finding message boundaries in that byte stream is a separate problem, covered later in this article.

Byte rules for BodyLength and CheckSum

The Session Layer standard states the BodyLength rule this way:

The length must be calculated by counting the number of octets in the message following the end of field delimiter (<SOH>) of BodyLength(9), up to and including the end of field delimiter (<SOH>) of the field immediately preceding the CheckSum(10) field.

FIX Trading Community, FIX Session Layer technical standard (June 2020)

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

In practice, the calculation runs in this order:

  1. Build the body as bytes: MsgType (35) first, then every other field, each written as tag=value followed by SOH.
  2. Set BodyLength to the number of bytes in that buffer. Use the array length, not string.Length, which counts UTF-16 code units rather than octets.
  3. Write the header as 8=FIX.4.4 SOH 9= the BodyLength digits SOH, then the body bytes.
  4. Compute CheckSum over every byte written so far, from the first byte of 8= through the SOH that precedes 10=. Add the byte values, take the result modulo 256, and write it as three decimal digits. Confirm this arithmetic against the CheckSum definition in the errata volumes before relying on it in production; this article does not reproduce that passage.
  5. Append 10=, the three CheckSum digits and a final SOH.

Use one byte-oriented encoding throughout. The code below uses strict US-ASCII rather than Encoding.ASCII, because the default ASCII encoder replaces unsupported characters with ?. That silent substitution would change both the length and the checksum, and you would not see an error.

Build the message in C#

This builder takes the body fields as an ordered list, writes them as bytes, and generates tags 8, 9 and 10 itself. It rejects any value that contains SOH, because a stray delimiter would corrupt the field structure.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

public static class FixCodec
{
    public const byte Soh = 0x01;

    // Throws on any byte above 0x7F instead of substituting '?'.
    private static readonly Encoding Strict = Encoding.GetEncoding(
        "us-ascii",
        EncoderFallback.ExceptionFallback,
        DecoderFallback.ExceptionFallback);

    public static byte[] Build(string beginString, IReadOnlyList<KeyValuePair<int, string>> body)
    {
        if (body.Count == 0 || body[0].Key != 35)
            throw new ArgumentException("The first body field must be MsgType (35).");

        using var bodyStream = new MemoryStream();
        foreach (var field in body)
        {
            if (field.Key == 8 || field.Key == 9 || field.Key == 10)
                throw new ArgumentException("Tags 8, 9 and 10 are generated by Build.");
            if (field.Value.IndexOf((char)Soh) >= 0)
                throw new ArgumentException("Tag " + field.Key + " contains SOH.");
            WriteAscii(bodyStream, field.Key + "=" + field.Value);
            bodyStream.WriteByte(Soh);
        }
        byte[] bodyBytes = bodyStream.ToArray();

        using var pre = new MemoryStream();
        WriteAscii(pre, "8=" + beginString);
        pre.WriteByte(Soh);
        WriteAscii(pre, "9=" + bodyBytes.Length.ToString(CultureInfo.InvariantCulture));
        pre.WriteByte(Soh);
        pre.Write(bodyBytes, 0, bodyBytes.Length);
        byte[] preBytes = pre.ToArray();

        int checksum = ComputeChecksum(preBytes, preBytes.Length);

        using var msg = new MemoryStream();
        msg.Write(preBytes, 0, preBytes.Length);
        WriteAscii(msg, "10=" + checksum.ToString("D3", CultureInfo.InvariantCulture));
        msg.WriteByte(Soh);
        return msg.ToArray();
    }

    private static int ComputeChecksum(byte[] data, int count)
    {
        int sum = 0;
        for (int i = 0; i < count; i++) sum += data[i];
        return sum % 256;
    }

    private static void WriteAscii(Stream stream, string text)
    {
        byte[] bytes = Strict.GetBytes(text);
        stream.Write(bytes, 0, bytes.Length);
    }
}

A sample call uses session header fields and illustrative order fields. It is not a validated New Order Single: the required and conditional fields for 35=D must be checked against the FIX 4.4 definition.

var fields = new List<KeyValuePair<int, string>>
{
    new KeyValuePair<int, string>(35, "D"),
    new KeyValuePair<int, string>(49, "SENDER"),
    new KeyValuePair<int, string>(56, "TARGET"),
    new KeyValuePair<int, string>(34, "1"),
    new KeyValuePair<int, string>(52, "20260101-12:00:00.000"),
    new KeyValuePair<int, string>(11, "ORDER-0001"),
    new KeyValuePair<int, string>(55, "MSFT"),
    new KeyValuePair<int, string>(54, "1"),
    new KeyValuePair<int, string>(38, "100"),
    new KeyValuePair<int, string>(40, "1"),
    new KeyValuePair<int, string>(60, "20260101-12:00:00.000"),
};

byte[] raw = FixCodec.Build("FIX.4.4", fields);

// Display only: make SOH visible. Never feed this text back into Parse.
Console.WriteLine(Encoding.ASCII.GetString(raw).Replace((char)1, '|'));

When you display a message with | in place of SOH, that substitution exists only in the text you print. Keep the original byte array for every calculation and transmission.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Parse a complete message

The parser works on a byte array that already holds one whole message. It checks the envelope before it trusts any field. Run these checks in order and stop at the first failure:

  1. The message starts with tag 8 and its BeginString is terminated by SOH.
  2. Tag 9 is the second field and holds a non-negative decimal length.
  3. BodyLength places the start of the trailer inside the buffer, and the byte before that offset is SOH.
  4. The bytes at that offset read 10=, followed by three digits and SOH, and the SOH is the last byte of the buffer.
  5. The recomputed checksum over the bytes before the trailer matches the declared value.
  6. Tag 35 is the first field after BodyLength.
public static IReadOnlyList<KeyValuePair<int, string>> Parse(byte[] msg)
{
    int firstSoh = Array.IndexOf(msg, Soh);
    if (firstSoh < 0) throw new FormatException("BeginString is not terminated by SOH.");
    string begin = Strict.GetString(msg, 0, firstSoh);
    if (!begin.StartsWith("8=", StringComparison.Ordinal))
        throw new FormatException("Message must start with tag 8.");

    int lenStart = firstSoh + 1;
    int lenEnd = Array.IndexOf(msg, Soh, lenStart);
    if (lenEnd < 0) throw new FormatException("BodyLength is not terminated by SOH.");
    string lenField = Strict.GetString(msg, lenStart, lenEnd - lenStart);
    if (!lenField.StartsWith("9=", StringComparison.Ordinal) ||
        !int.TryParse(lenField.Substring(2), NumberStyles.None, CultureInfo.InvariantCulture, out int bodyLength))
        throw new FormatException("Tag 9 must follow tag 8 and hold a decimal length.");

    int bodyStart = lenEnd + 1;
    long expectedTrailer = (long)bodyStart + bodyLength;
    if (bodyLength < 0 || expectedTrailer + 7 > msg.Length)
        throw new FormatException("BodyLength points past the end of the buffer.");
    int checksumStart = (int)expectedTrailer;

    if (msg[checksumStart - 1] != Soh)
        throw new FormatException("The field before tag 10 must end with SOH.");
    if (msg[checksumStart] != (byte)'1' || msg[checksumStart + 1] != (byte)'0' || msg[checksumStart + 2] != (byte)'=')
        throw new FormatException("Tag 10 is not where BodyLength predicts.");
    if (msg[checksumStart + 6] != Soh || checksumStart + 7 != msg.Length)
        throw new FormatException("CheckSum must be 10=nnn plus SOH, ending the message.");

    string csText = Strict.GetString(msg, checksumStart + 3, 3);
    if (!int.TryParse(csText, NumberStyles.None, CultureInfo.InvariantCulture, out int declared) || declared > 255)
        throw new FormatException("CheckSum must be three decimal digits, 000 to 255.");
    if (ComputeChecksum(msg, checksumStart) != declared)
        throw new FormatException("CheckSum does not match the message bytes.");

    var body = SplitFields(msg, bodyStart, checksumStart);
    if (body.Count == 0 || body[0].Key != 35)
        throw new FormatException("Tag 35 must be the first field after BodyLength.");

    var result = new List<KeyValuePair<int, string>>
    {
        new KeyValuePair<int, string>(8, begin.Substring(2)),
        new KeyValuePair<int, string>(9, lenField.Substring(2)),
    };
    result.AddRange(body);
    result.Add(new KeyValuePair<int, string>(10, csText));
    return result;
}

private static List<KeyValuePair<int, string>> SplitFields(byte[] msg, int start, int end)
{
    var result = new List<KeyValuePair<int, string>>();
    int pos = start;
    while (pos < end)
    {
        int soh = Array.IndexOf(msg, Soh, pos, end - pos);
        if (soh < 0) throw new FormatException("Field is not terminated by SOH.");
        string field = Strict.GetString(msg, pos, soh - pos);
        int eq = field.IndexOf('=');
        if (eq <= 0 || !int.TryParse(field.Substring(0, eq), NumberStyles.None, CultureInfo.InvariantCulture, out int tag))
            throw new FormatException("Malformed field: " + field);
        result.Add(new KeyValuePair<int, string>(tag, field.Substring(eq + 1)));
        pos = soh + 1;
    }
    return result;
}

Calling FixCodec.Parse(raw) on the output of the builder returns the fields in wire order, including tags 8, 9 and 10. Each check above is a separate failure mode, so a malformed or tampered message fails with a specific exception rather than producing partial output. Treat the code as a starting point and add tests built from your own fixtures before using it on live traffic.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Validating an order

The parser above accepts any well-formed envelope, whatever its MsgType. It does not know whether a 35=D message has the fields it needs. Build that check from the New Order Single definition in the FIX 4.4 application specification:

  • List the fields the definition marks as required for 35=D, and check each one is present exactly once.
  • Implement any conditional rules, where the requiredness of one field depends on the value of another. Some of these are easy to miss, so take each one directly from the specification rather than from memory.
  • Check value domains, such as allowed enumerations, against the same definition.

Keep this validator separate from Parse, so a framing error and an invalid order produce different errors.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Framing a live stream

A socket delivers arbitrary chunks of bytes. A chunk can hold half a message, one message, or several messages back to back. Parse assumes the buffer already holds one complete message, so a receiver needs a separate step to find those boundaries. Using BodyLength, the logic is:

  1. Append each received chunk to a buffer.
  2. Find the first two SOH bytes. If they are not both present, wait for more bytes.
  3. Read the tag 9 value. If tag 9 is not the second field, treat the stream as out of sync and apply your session recovery policy.
  4. Compute the end of the message as the body start plus BodyLength plus the seven-byte trailer 10=nnn and SOH. If the buffer is shorter than that, wait.
  5. Copy exactly that many bytes out, pass them to Parse, remove them from the buffer, and repeat from step 2.

This sketch covers only the BodyLength path. The FIX Session Layer standard defines the wider rules for session-level stream handling, including recovery after a framing error. Read it before you depend on a particular recovery behavior.

Concern Complete buffer (Parse) Continuous stream
Where the length comes from Tag 9 in the bytes you already hold Tag 9 read from the first bytes received, before the rest of the message has arrived
Message boundary End of the buffer Body start plus BodyLength plus the seven-byte trailer
Partial input Not applicable; malformed input is rejected Wait for more bytes; never parse a truncated slice
Bytes after the trailer Rejected by the parser above Start the next message at the byte after the trailer’s SOH
Framing source BodyLength in tag 9 BodyLength in tag 9, or SOFH if your counterparty wraps messages in SOFH

SOFH is a separate framing standard that supplies a message length and encoding type for messages in a continuous stream. It is not part of every raw FIX 4.4 TagValue message. Check with your counterparty whether it uses SOFH before you add any SOFH handling.

Where a from-scratch parser stops

This parser is well suited to learning the encoding, building controlled examples, and testing a counterparty’s sample messages. A production connection usually needs more than envelope handling. Logon, heartbeats, sequence-number tracking and resend requests are session-layer concerns that this code does not cover. Teams that move to a live connection often adopt a FIX engine or implementation support at that point. No specific product or service is recommended or verified here, so compare options against the session requirements of your counterparty.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.