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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

You can build and parse a FIX 4.4 TagValue envelope in C# with a few dozen lines of byte-level code and no FIX engine. The envelope needs three things computed correctly: BodyLength (tag 9), CheckSum (tag 10), and the SOH delimiter that separates every field. An order is a different matter. A valid order is an application message, such as NewOrderSingle (35=D), and its required fields come from the FIX 4.4 application definition, not from the envelope. This guide builds the envelope, shows how to parse a complete message, and marks clearly where the envelope stops and the order definition starts.

What the envelope must contain

A FIX 4.4 TagValue message is a sequence of tag=value fields, each ended by the SOH control character (byte 0x01). Three fields open every message in this order: BeginString (tag 8), BodyLength (tag 9), and MsgType (tag 35). CheckSum (tag 10) is always the last field. The FIX Trading Community’s FIX 4.4 Specification with Errata 20030618 is the release the organization recommends implementers use, and it is distributed as a ZIP of seven volumes plus release notes. The header and trailer rules are in that package, so keep it open while you work.

For FIX 4.4, the BeginString value is FIX.4.4. The message types you will meet most often on a session include Heartbeat (35=0), Logon (35=A), and NewOrderSingle (35=D). Session-level messages also carry standard header fields such as SenderCompID (49), TargetCompID (56), MsgSeqNum (34), and SendingTime (52). Confirm their requirements in the header section of the specification rather than copying them from a sample.

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

BodyLength and SOH: the byte rules

BodyLength is the number of bytes between the SOH that ends tag 9 and the SOH that ends the field immediately before tag 10. The FIX Session Layer technical standard (FIX Trading Community, June 2020) states the 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.”

Three practical consequences follow:

  • Count bytes, not characters. A .NET string has a length in UTF-16 code units. If a value contains a character outside ASCII, the UTF-8 or other encoded byte count changes. Restrict FIX field values to ASCII and reject anything else before you encode.
  • Use SOH, never a pipe, in the real bytes. Many samples print | so people can read them. That display character must be replaced by 0x01 before any length or checksum is calculated.
  • The trailer is not part of BodyLength. The 10= field and its SOH sit outside the counted range.

CheckSum: serialize first, then sum

The CheckSum field covers the bytes that come before it. The safe order of operations is: build the BeginString, BodyLength, and body bytes exactly as they will be sent; sum those bytes; then append the trailer. The FIX standard defines the checksum as the byte sum taken modulo 256, written as a three-digit field. Verify that definition in the FIX 4.4 specification text before relying on it in production, because the arithmetic is the part most often implemented wrong.

Because the trailer is always 10=, three digits, and SOH, it occupies exactly seven bytes at the end of a complete message. The parser below uses that fixed length.

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

Building a message in C#

The builder below works entirely on byte lists. It writes the body first so BodyLength is known, prepends the header, then computes CheckSum over everything that precedes tag 10. The sample sends a Heartbeat (35=0) because it needs no application fields beyond the header. It is an envelope demonstration, not an order.

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

public static class FixTagValue
{
    private const byte Soh = 0x01;

    public static byte[] Build(string msgType,
        IReadOnlyList<(int Tag, string Value)> bodyFields)
    {
        // Body: MsgType first, then the remaining fields, each ended by SOH.
        var body = new List<byte>();
        AppendField(body, 35, msgType);
        foreach (var (tag, value) in bodyFields)
            AppendField(body, tag, value);

        // BeginString and BodyLength. BodyLength is the body byte count,
        // which runs from after the SOH of tag 9 through the SOH before tag 10.
        var preTrailer = new List<byte>();
        AppendField(preTrailer, 8, "FIX.4.4");
        AppendField(preTrailer, 9, body.Count.ToString(CultureInfo.InvariantCulture));
        preTrailer.AddRange(body);

        // CheckSum: byte sum of everything before tag 10, modulo 256, three digits.
        int sum = 0;
        foreach (byte b in preTrailer)
            sum += b;
        string checksum = (sum % 256).ToString("D3", CultureInfo.InvariantCulture);

        AppendField(preTrailer, 10, checksum);
        return preTrailer.ToArray();
    }

    private static void AppendField(List<byte> buffer, int tag, string value)
    {
        string text = tag.ToString(CultureInfo.InvariantCulture) + "=" + value;
        foreach (char c in text)
        {
            if (c > 0x7F)
                throw new ArgumentException("FIX field values must be ASCII.");
            buffer.Add((byte)c);
        }
        buffer.Add(Soh);
    }
}

A call such as FixTagValue.Build("0", new List<(int Tag, string Value)> { (49, "SENDER"), (56, "TARGET"), (34, "1"), (52, "20261009-12:00:00.000") }) produces a complete byte array. Write it to the socket or log it with Encoding.ASCII.GetString for inspection. Replace 0x01 with | only in the displayed copy.

Parsing a complete message

The parser assumes it already holds one complete message. It checks the envelope before it trusts any field. The order of checks matters: confirm the start fields, confirm the trailer position, compare the declared BodyLength with the real byte range, then verify CheckSum before handing fields to application code.

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

public sealed record FixField(int Tag, string Value);

public static class FixParser
{
    private const byte Soh = 0x01;
    private const int TrailerLength = 7; // "10=" + three digits + SOH

    public static List<FixField> Parse(byte[] msg)
    {
        int pos = 0;
        var begin = ReadField(msg, msg.Length, ref pos);
        if (begin.Tag != 8 || begin.Value != "FIX.4.4")
            throw new FormatException("BeginString must be FIX.4.4.");

        var length = ReadField(msg, msg.Length, ref pos);
        if (length.Tag != 9 ||
            !int.TryParse(length.Value, NumberStyles.None,
                          CultureInfo.InvariantCulture, out int declared))
            throw new FormatException("BodyLength (tag 9) is missing or invalid.");

        int bodyStart = pos;
        int trailerStart = msg.Length - TrailerLength;
        if (trailerStart < bodyStart)
            throw new FormatException("Message is too short to hold a trailer.");

        if (msg[trailerStart] != (byte)'1' || msg[trailerStart + 1] != (byte)'0' ||
            msg[trailerStart + 2] != (byte)'=' || msg[msg.Length - 1] != Soh)
            throw new FormatException("CheckSum trailer is not at the end of the message.");

        if (trailerStart - bodyStart != declared)
            throw new FormatException("BodyLength does not match the bytes received.");

        string received = Encoding.ASCII.GetString(msg, trailerStart + 3, 3);
        int sum = 0;
        for (int i = 0; i < trailerStart; i++)
            sum += msg[i];
        string computed = (sum % 256).ToString("D3", CultureInfo.InvariantCulture);
        if (computed != received)
            throw new FormatException("CheckSum mismatch: expected " + computed + ", got " + received + ".");

        var fields = new List<FixField>();
        int p = bodyStart;
        while (p < trailerStart)
            fields.Add(ReadField(msg, trailerStart, ref p));

        if (fields.Count == 0 || fields[0].Tag != 35)
            throw new FormatException("MsgType (tag 35) must be the first body field.");

        return fields;
    }

    private static FixField ReadField(byte[] msg, int limit, ref int pos)
    {
        int eq = Array.IndexOf(msg, (byte)'=', pos, limit - pos);
        if (eq < 0)
            throw new FormatException("Field has no '=' separator.");

        int end = Array.IndexOf(msg, Soh, eq + 1, limit - (eq + 1));
        if (end < 0)
            throw new FormatException("Field is not terminated by SOH.");

        string tagText = Encoding.ASCII.GetString(msg, pos, eq - pos);
        if (!int.TryParse(tagText, NumberStyles.None,
                          CultureInfo.InvariantCulture, out int tag))
            throw new FormatException("Tag is not numeric: " + tagText);

        string value = Encoding.ASCII.GetString(msg, eq + 1, end - eq - 1);
        pos = end + 1;
        return new FixField(tag, value);
    }
}

The parser returns fields in wire order and keeps repeated tags, which matters because repeating groups reuse tags. It does not map fields to a schema. Reading a value such as 35 tells you the MsgType, but it does not tell you whether the message is a legal NewOrderSingle.

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

Common failure causes

  • CheckSum mismatch on a message you built yourself. The usual cause is computing the sum over a string that still contains | instead of 0x01, or summing after the trailer was appended.
  • BodyLength mismatch only with non-English text. The length was counted in characters rather than bytes. Keep values ASCII.
  • Parser fails on messages from a counterparty. Check whether the counterparty uses a different line ending, extra whitespace, or a field outside your expected header order before assuming the checksum rule is wrong.
  • Trailer not found. The message was truncated or split across socket reads. Confirm the byte count before parsing.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Stream framing is a separate problem

The parser above assumes you already have one whole message. A TCP socket delivers a continuous byte stream, so a message can arrive in pieces or several messages can arrive together. Reading that stream needs a boundary policy. For raw TagValue messages, the BodyLength field gives the body size, so a receiver can:

  1. Accumulate bytes in a buffer until at least the first SOH-terminated fields (tags 8 and 9) are present.
  2. Read the declared BodyLength and compute the total frame size as the body size plus the seven-byte trailer, counted from the start of the body.
  3. Wait until the buffer holds that many bytes, then slice out exactly one message and pass it to Parse.
  4. Leave any extra bytes in the buffer for the next message.
  5. If the buffer does not start with 8=, discard bytes until a valid BeginString appears. Define that resynchronization rule explicitly and test it against your counterparty.

SOFH is a different FIX framing standard. It supplies its own length and encoding type for message boundaries and is not part of every raw FIX 4.4 TagValue message. The FIX Session Layer technical standard covers session-level behavior. This article does not reproduce all of its stream-boundary rules, so read that document before building a production receiver.

Three layers, three sources of truth

Layer What it governs Where the rule comes from What this article covers
Message envelope BeginString (8), BodyLength (9), MsgType (35) at the start; CheckSum (10) at the end FIX 4.4 Specification with Errata 20030618, header and trailer sections Full build and parse
Application schema Fields and their requiredness for each MsgType, such as NewOrderSingle (35=D) FIX 4.4 application message definitions in the same errata package Explained, not implemented; the field table was not reproduced here
Session and stream framing Message boundaries on a socket, sequencing, and session behavior; SOFH is one framing standard FIX Session Layer technical standard (June 2020) and the SOFH description from FIX Trading Community Outlined only

What a valid order requires

The envelope code proves only that a message is well formed at the byte level. A valid order must also satisfy the NewOrderSingle definition in FIX 4.4: the required fields for that MsgType, their allowed values, and any conditional rules that apply to the order type and side. Those rules are in the application message section of the errata package. Build your order field list from that table, then run the envelope builder over it. Do not infer required fields from a sample you found elsewhere, because a sample that works for one counterparty may omit fields another one rejects.

When a from-scratch parser is the wrong tool

A custom parser is well suited to learning the encoding, writing test fixtures, and handling controlled message examples. A live trading connection usually needs more: sequence number recovery, resend requests, heartbeat and test-request handling, logon and logout sequencing, and persistence of session state. A FIX engine or implementation support service provides those functions. Evaluate them against your counterparty’s requirements. No engine was compared or benchmarked for this article.

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

Before any code touches a live counterparty, compare its logs byte for byte with your builder’s output for the same message type, then run both sides in a test environment.

The Bottom Line

“”

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.