Version: 1.0 Date: April 2026 Status: Specification — Revision D
For the encrypted envelope (TagoTiP/S), see TagoTiPs.md.
TagoTiP is a lightweight, human-readable protocol designed for sending and receiving IoT data to TagoIO. It provides a compact alternative to HTTP/JSON for resource-constrained embedded devices.
TagoTiP is transport-agnostic. It can be carried over UDP, TCP, HTTP(S), MQTT, or any other transport. This specification defines only the message format and parsing rules — not transport-specific behavior such as ports, connection management, or delivery guarantees.
Note: For encryption without TLS, TagoTiP frames can be wrapped in a TagoTiP/S crypto envelope. See TagoTiPs.md.
MUST, SHOULD, MAY, MUST NOT follow RFC 2119 definitionsPUSH, PULL, PING), status codes (OK, PONG, CMD, ERR), and boolean values (true, false) are case-sensitive and MUST use the exact casing shown in this specificationU+0000 / 0x00) MUST NOT appear anywhere in a TagoTiP frame. This rule is maintained for protocol hygiene and C-string safety.*, ?, !, <, >, ., -, =, $, or spaces (per TagoIO restrictions). Serial numbers MAY contain hyphens (-) in addition to alphanumeric characters and underscores.METHOD/N syntax (e.g., PUSH/2) to indicate a newer version while maintaining backward compatibility.PUSH|AUTH|SERIAL|BODY)Credentials are scoped to an Account/Profile (not to an individual device). A single profile may contain multiple devices, and the same credentials are used to authenticate traffic for any device that belongs to that profile.
at + 32 hex chars (34 chars total, e.g., ate2bd319014b24e0a8aca9f00aea4c0d0)4deedd7bab8817ec)Authorization Hash derivation:
Token: ate2bd319014b24e0a8aca9f00aea4c0d0
Input: e2bd319014b24e0a8aca9f00aea4c0d0 (strip "at" prefix)
Hash: SHA-256 of input (UTF-8 bytes)
Result: first 8 bytes as 16 hex chars
The server uses the Authorization Hash to resolve the Account/Profile, then routes the message to the device identified by the SERIAL field in the frame header.
Note: TagoTiP/S uses additional credentials (Device Hash, Encryption Key) for the crypto envelope. See TagoTiPs.md.
The following guidance is non-normative and intended to help implementers.
\n (0x0A) terminates each frame\n is received. Clients SHOULD reuse connections.\n terminator is OPTIONAL.The \n byte (0x0A) MUST NOT appear inside frame field values. On stream transports (TCP), it terminates the frame. On message transports (UDP, MQTT, HTTP), it is unnecessary but harmless if present.
Normative clarification: The ABNF grammar defines frames with a trailing
LFfor the canonical wire format. On message-boundary transports (UDP, MQTT, HTTP body), the trailingLFis OPTIONAL — receivers on these transports MUST accept frames both with and without a trailingLF. On stream transports (TCP), the trailingLFis REQUIRED as the frame delimiter.
CMD Delivery (Non-Normative): On connection-oriented transports (TCP), the server MAY send CMD frames at any time. On pub/sub transports (MQTT), the server MAY publish to device-specific topics. On request-response transports (HTTP, UDP), CMD frames are delivered as responses to client requests — clients SHOULD use periodic PING to poll for pending commands.
Each TagoTiP frame addresses exactly one device. To send data for multiple devices, the client sends multiple frames (one per device).
Note: The full frame structure described here applies to plaintext TagoTiP. When transmitted inside a TagoTiP/S envelope, a compact "headless" variant is used instead — see TagoTiPs.md §4.
Every uplink TagoTiP frame follows a pipe-delimited structure:
Without sequence counter:
METHOD|AUTH|SERIAL|BODY\n
METHOD|AUTH|SERIAL\n ← PING (no body)
With sequence counter:
METHOD|!N|AUTH|SERIAL|BODY\n
METHOD|!N|AUTH|SERIAL\n ← PING (no body)
METHOD!N! prefix + decimal integer (e.g., !42)AUTHSERIALBODY| (byte 0x7C)! prefix distinguishes the optional counter field from the AUTH field (hex characters 0-9, a-f never start with !)Examples:
PUSH|4deedd7bab8817ec|sensor-01|[temperature:=32]
PUSH|!42|4deedd7bab8817ec|sensor-01|[temperature:=32]
PING|4deedd7bab8817ec|sensor-01
PING|!5|4deedd7bab8817ec|sensor-01
All server-to-client communication uses the ACK frame format. This is a simplified frame — no AUTH field:
ACK|!N|STATUS|DETAIL\n ← correlated response (echoes uplink counter)
ACK|!N|STATUS\n ← correlated response (no detail)
ACK|STATUS|DETAIL\n ← unsolicited or no-counter client
ACK|STATUS\n
!N!N.STATUSDETAILThe server does not need to authenticate itself to the client. When the uplink frame includes a sequence counter (!N), the server echoes the same value in the ACK response (see §9.5). ACK frames without !N are either responses to requests that had no counter, or unsolicited server-initiated messages (e.g., CMD). This allows clients to correlate responses to requests on pipelined connections.
See §9 for the full ACK specification including status codes for responses and commands.
The Authorization Hash identifies the Account/Profile. The SERIAL field identifies the target device.
The server MUST:
SERIAL belongs to that Account/Profile.SERIAL does not belong to the profile (ACK|ERR|device_not_found).For passthrough payloads (>x, >b), the SERIAL field still identifies the target device. The payload parser receives the raw data associated with that device (see §6.5).
Escaping is supported inside string values (VALCHAR) and metadata values (METAVALCHAR). Unit strings (UNITCHAR) do not support escape sequences — they are plain text terminated by structural characters.
Rule: A backslash (\) escapes the next byte, producing the literal character. This applies to any reserved/structural character, including:
|, [, ], ;, ,, {, }, #, @, ^, \, and n (newline escape).
\n0x0A MUST NOT appear on stream transports)\\\|\[[\]]\;;\,,\{{\}}\##\@@\^^On the wire, \n is the two-byte sequence 0x5C 0x6E (backslash + lowercase n), which parsers decode to U+000A in the application-layer value.
A real newline byte (0x0A) always terminates a frame on stream transports and MUST NOT appear in values.
Note: When splitting frame fields by |, parsers MUST respect \| (backslash followed by pipe) as an escape sequence, not a field delimiter. The same applies to all structural characters within their respective contexts (e.g., \; inside variable lists, \, inside metadata blocks, \} inside metadata blocks).
To ensure predictable memory usage on embedded clients and consistent server behavior:
\n terminator on stream transports) with ACK|ERR|payload_too_large.Implementations MAY support larger limits, but clients SHOULD target this limit for maximum compatibility.
To enable safe fixed-buffer pre-allocation in C and other memory-constrained implementations, the following limits apply to identifiers (names and keys):
var-name)serial)group)meta-key)unit)The following limits apply to element counts:
[] block (var-list, pull-list){} block (meta-list)All limits above are normative: a frame that exceeds any of these limits MUST be rejected by the server with ACK|ERR|invalid_payload.
Number values, boolean values, location coordinates, and timestamp values are inherently bounded by their format definitions (§6.3.1) and do not require separate length limits.
String values and metadata values are bounded by the frame size limit (§4.5) and by application-level platform limits. The protocol does not define per-value byte limits.
All byte lengths are measured as UTF-8 encoded bytes. Since identifier fields (VARNAMECHAR, SERIALCHAR) are ASCII-only, the byte count equals the character count.
Implementations MAY enforce lower limits and SHOULD document their supported maximums.
PUSHPULLPINGFor real-time subscriptions to variable changes, use a transport that natively supports pub/sub (e.g., MQTT).
All downlink communication uses the ACK frame. The STATUS field determines the purpose:
OKACK|OK|3, ACK|!1|OK|3PONGACK|PONG, ACK|!2|PONGCMDACK|CMD|reboot (unsolicited, no counter)ERRACK|ERR|invalid_token, ACK|!5|ERR|invalid_payloadSee §9 for the full ACK specification.
PUSH|AUTH|SERIAL|BODY
PUSH|!N|AUTH|SERIAL|BODY
Where BODY is either a structured variable block or a passthrough payload:
PUSH|AUTH|SERIAL|@=LOC@TIMESTAMP^GROUP{META}[variables] ← structured
PUSH|AUTH|SERIAL|>xHEXDATA ← passthrough (hex)
PUSH|AUTH|SERIAL|>bBASE64DATA ← passthrough (base64)
Optional body-level modifiers may appear before the variable block. They set defaults that cascade to all variables in the body:
PUSH|AUTH|SERIAL|@=LOCATION @TIMESTAMP ^GROUP {METADATA} [variables]
Spaces shown for readability only — not present in actual frames.
@=LOCATION@=@TIMESTAMP@^GROUP^{METADATA}{}[variables][]Body-level modifiers MUST appear in the order shown (@=LOCATION, @TIMESTAMP, ^GROUP, {METADATA}) when present. Each modifier MAY be omitted, but those present MUST follow this order. If the same modifier type appears more than once, the frame MUST be rejected with invalid_payload.
Variables are separated by semicolons (;) inside the brackets. Each variable follows this structure:
NAME OPERATOR VALUE #UNIT @=LOCATION @TIMESTAMP ^GROUP {METADATA}
All suffixes are optional and MUST appear in the order shown when present.
The variable list inside [] MUST contain at least one variable. Empty blocks ([]) MUST be rejected with invalid_payload.
Metadata blocks MUST contain at least one key-value pair. Empty metadata blocks ({}) MUST be rejected with invalid_payload.
The same variable name MAY appear multiple times within a single variable block. Each occurrence is treated as a separate data point (useful for batch uploads — see §11.7).
PUSH frames are atomic: if any variable in the block fails validation (malformed operator, invalid value, illegal suffix combination), the server MUST reject the entire frame with ACK|ERR|invalid_payload. No partial acceptance.
:=temperature:=32.5=status=running?=true or falseactive?=true@=lat,lng or lat,lng,altposition@=39.74,-104.99Number values MUST match the pattern -?(0|[1-9][0-9]*)(\.[0-9]+)? — an optional minus sign, one or more digits, and an optional decimal fraction. Scientific notation, leading zeros (except in 0 and 0.x forms), and special values (NaN, Infinity) are not valid.
String values MUST contain at least one character. Empty values (e.g., status=) are not valid.
Boolean values MUST be the exact lowercase strings true or false.
Location coordinates follow the same numeric format as the number type. Validation of coordinate ranges (e.g., latitude −90 to 90, longitude −180 to 180) is application-level and not enforced by the protocol parser.
#temperature:=32#F@=lat,lng[,alt])speed:=10@=39.74,-104.99@temperature:=32@1694567890000^temperature:=32^reading_001{},temperature:=32{source=dht22,quality=high}The @=location suffix attaches geographic coordinates to a variable with a non-location value type. The coordinate format is the same as the location operator value: lat,lng or lat,lng,alt. This enables sending a value and its location in a single variable.
The #unit and @=location suffixes MUST NOT be used with the location operator (@=) — the server MUST reject the frame with invalid_payload. Altitude in a location triple is always in meters.
Parsers disambiguate the @= location suffix from the @ timestamp suffix by checking the character after @: if =, parse as location; if digit, parse as timestamp; otherwise, reject with invalid_payload.
Metadata keys follow the same character rules as variable names (lowercase alphanumeric and underscore). Metadata keys do not support escape sequences — they are restricted to the identifier charset ([a-z0-9_]), which contains no structural characters. Metadata values follow the same encoding rules as string values (printable UTF-8, with escaping for structural characters).
With all optional suffixes:
temperature:=32.5#C@=39.74,-104.99@1694567890000^reading_001{source=dht22,quality=high}
Body-level modifiers cascade to all variables in the body:
PUSH|4deedd7bab8817ec|sensor-01|@=39.74,-104.99@1694567890000^batch_42{firmware=2.1}[temp:=32#C;humidity:=65#%]
Both temp and humidity inherit:
{lat: 39.74, lng: -104.99}1694567890000batch_42firmware=2.1Variable-level modifiers override body-level for location, timestamp, and group:
PUSH|4deedd7bab8817ec|sensor-01|@=39.74,-104.99@1694567890000[temp:=32@=39.75,-105.00@1694567891000;humidity:=65]
Here temp uses its own location and timestamp, while humidity uses the body-level values. Variables using the @= operator carry their own location as the value itself; body-level @=LOCATION has no effect on them.
PUSH|4deedd7bab8817ec|sensor-01|@=39.74,-104.99[speed:=10;position@=40.00,-105.50]
Here speed inherits the body-level location {lat: 39.74, lng: -104.99}, but position uses its own value {lat: 40.00, lng: -105.50} — body-level @= does not override @= operator variables.
For metadata, variable-level merges with body-level (variable wins on key conflicts):
PUSH|4deedd7bab8817ec|sensor-01|{firmware=2.1}[temp:=32{source=dht22};humidity:=65]
temp has metadata: {firmware: "2.1", source: "dht22"}humidity has metadata: {firmware: "2.1"}When a device needs to send raw data instead of structured variables, the BODY begins with > followed by an encoding flag:
>xPUSH|AUTH|SERIAL|>xDEADBEEF01020304>bPUSH|AUTH|SERIAL|>b3q2+7wECAwQ=The > prefix signals passthrough mode: the server authenticates the frame (validates AUTH), identifies the target device (by SERIAL), but does NOT parse the BODY as variables. The data is delivered to the device's payload parser:
>x → the hex string is decoded and delivered as a raw byte buffer. The hex string MUST have an even number of characters (each byte is two hex digits); odd-length hex MUST be rejected with invalid_payload.>b → the base64 string is delivered as a text string (the payload parser is responsible for decoding if needed)Passthrough mode (>x, >b) is uplink-only.
The payload parser receives the raw data and can process it however needed. If the decoded data happens to be a TagoTiP frame (e.g., a device that encodes TagoTiP text as hex), a TagoTiP parser helper function is available in the payload parser environment to convert it to structured JSON objects.
PUSH|4deedd7bab8817ec|sensor-01|>xDEADBEEF01020304
PUSH|4deedd7bab8817ec|sensor-01|>b3q2+7wECAwQ=
The effective maximum passthrough data size depends on the frame budget remaining after method, auth, serial, and >x/>b prefix fields.
PULL|AUTH|SERIAL|[VAR_NAME;VAR_NAME;...]
Retrieves the last stored value of one or more variables from the specified device. Variable names are enclosed in brackets ([]) and separated by semicolons (;), matching the PUSH body syntax. Even a single variable MUST be bracket-wrapped.
The server MUST return the found variables in bracket-wrapped standard syntax:
ACK|OK|[VARIABLE OPERATOR VALUE #UNIT @=LOCATION @TIMESTAMP ^GROUP {METADATA};...]
The response always uses bracket-wrapped variable syntax, matching the PUSH body format. Only found variables are included — variables that do not exist or have no stored values are silently omitted. If none of the requested variables are found, the server MUST respond with ACK|ERR|variable_not_found.
The server does not echo the serial in ACK responses (see §9).
Examples:
→ PULL|4deedd7bab8817ec|weather-denver|[temperature]
← ACK|OK|[temperature:=32#F@1694567890000]
→ PULL|4deedd7bab8817ec|weather-denver|[temperature;humidity;pressure]
← ACK|OK|[temperature:=32#F@1694567890000;humidity:=65#%@1694567890000]
→ PULL|4deedd7bab8817ec|drone-07|[speed]
← ACK|OK|[speed:=10#km/h@=39.74,-104.99@1694567890000]
In the second example, pressure was requested but not found — it is silently omitted from the response. The third example shows a response with the @= location suffix attached to a numeric value.
PING|AUTH|SERIAL
No body field. The SERIAL field identifies the device performing the keepalive.
ACK|PONG
All downlink communication uses the ACK frame:
ACK|!N|STATUS|DETAIL
ACK|!N|STATUS
ACK|STATUS|DETAIL
ACK|STATUS
!N! prefix + decimal integer)STATUSDETAILACK frames never include a device serial number. A device may have multiple associated serials (e.g., after hardware replacement), so the server does not echo a serial in responses. The client already knows which device it addressed in the uplink request.
When !N is present, it appears between ACK and STATUS (e.g., ACK|!1|OK|3). The status codes themselves are unchanged:
OK>x, >b) — in the latter case, the count reflects data points produced by the payload parser. For PULL: bracket-wrapped variable list in standard syntax (see §7.2). `ACKPONGCMDERRinvalid_tokeninvalid_methodinvalid_payloadinvalid_seqdevice_not_foundvariable_not_foundrate_limitedauth_failedunsupported_versionpayload_too_largeserver_errorWithout sequence counter (unsolicited or no-counter client):
ACK|OK|2
ACK|OK|[temperature:=32#F@1694567890000]
ACK|PONG
ACK|CMD|reboot
ACK|CMD|ota=https://example.com/v2.1.bin
ACK|ERR|invalid_token
ACK|ERR|invalid_payload
ACK|ERR|auth_failed
With sequence counter (correlated responses):
ACK|!1|OK|2
ACK|!2|OK|[temperature:=32#F@1694567890000]
ACK|!3|PONG
ACK|CMD|reboot ← unsolicited, no counter
ACK|!5|ERR|invalid_token
ACK|!6|ERR|invalid_seq
ACK|!7|ERR|invalid_payload
rate_limited SHOULD implement exponential backoff.invalid_token SHOULD NOT be retried without re-provisioning.server_error MAY be retried after a delay.When the uplink frame includes a sequence counter (!N), the server MUST echo the same !N value in the ACK response. This allows clients to correlate responses to their originating requests on pipelined connections.
Rules:
!N when the uplink included it!N in unsolicited messages (CMD pushed without a request)!N to distinguish solicited responses from unsolicited CMDs! prefix disambiguates the counter from STATUS — status codes are alphabetic (OK, PONG, CMD, ERR) and never start with !TagoTiP supports an optional, monotonically increasing sequence counter. When used, the counter provides:
The counter also serves as a nonce component in TagoTiP/S. See TagoTiPs.md.
1 is RECOMMENDED0xFFFFFFFF, device MUST re-provision or reset with the serverWhen the server is configured to enforce sequence counters, it MUST maintain the last-seen counter value per device (identified by the SERIAL field).
When the server has no previously recorded counter for a device (first message ever, or after a server-side reset), the server MUST accept any valid counter value and store it as the new last-seen value.
The server SHOULD accept a message only if its counter is strictly greater than the last-seen value. The server MAY allow a configurable acceptance window to tolerate minor reordering. The counter MAY be reset by server-side policy (e.g., after an idle timeout or manual reset by the device owner).
Sequence counter validation (when enabled) applies to every uplink frame, including PING. The server MUST update the last-seen counter value regardless of method.
When the uplink frame includes a sequence counter, the server echoes it in the ACK response for correlation purposes (see §9.5). Sequence counter enforcement (monotonic validation) applies only to client→server messages.
In TagoTiP, the sequence counter is included in the frame header with a ! prefix followed by the decimal integer:
PUSH|!42|4deedd7bab8817ec|sensor-01|[temperature:=32]
PING|!5|4deedd7bab8817ec|sensor-01
PUSH|4deedd7bab8817ec|weather-denver|[temperature:=32;humidity:=65]
PUSH|!1|4deedd7bab8817ec|weather-denver|[temperature:=32;humidity:=65]
PUSH|4deedd7bab8817ec|sensor-0A1F|[temperature:=32.5#C;status=online;active?=true]
Negative number example:
PUSH|4deedd7bab8817ec|sensor-0A1F|[temperature:=-15.3#C]
Location as value (using @= operator):
PUSH|4deedd7bab8817ec|drone-07|[position@=39.74,-104.99,305]
Location attached to a non-location value (using @= suffix):
PUSH|4deedd7bab8817ec|drone-07|[speed:=10#km/h@=39.74,-104.99,305]
PUSH|4deedd7bab8817ec|sensor-01|[temperature:=32{source=dht22,quality=high}]
PUSH|4deedd7bab8817ec|sensor-01|@=39.74,-104.99@1694567890000^batch_42{firmware=2.1}[temperature:=32#C;humidity:=65#%]
PUSH|4deedd7bab8817ec|datalogger-7|[temp:=32@1694567890000;temp:=33@1694567900000;temp:=31@1694567910000]
PUSH|4deedd7bab8817ec|sensor-01|>xDEADBEEF01020304
PUSH|4deedd7bab8817ec|sensor-01|>b3q2+7wECAwQ=
PULL|4deedd7bab8817ec|weather-denver|[temperature]
PULL|!7|4deedd7bab8817ec|weather-denver|[temperature]
PING|4deedd7bab8817ec|sensor-01
→ PING|4deedd7bab8817ec|weather-denver
← ACK|PONG
→ PUSH|4deedd7bab8817ec|weather-denver|[temperature:=32#F;humidity:=65#%;active?=true]
← ACK|OK|3
→ PULL|4deedd7bab8817ec|weather-denver|[temperature]
← ACK|OK|[temperature:=32#F@1694567890000]
← ACK|CMD|reboot
→ PUSH|4deedd7bab8817ec|weather-denver|[invalid=broken
← ACK|ERR|invalid_payload
→ PING|!1|4deedd7bab8817ec|weather-denver
← ACK|!1|PONG
→ PUSH|!2|4deedd7bab8817ec|weather-denver|[temperature:=32#F]
← ACK|!2|OK|1
→ PUSH|!3|4deedd7bab8817ec|weather-denver|[humidity:=65#%]
← ACK|!3|OK|1
→ PUSH|!2|4deedd7bab8817ec|weather-denver|[pressure:=1013#hPa]
← ACK|!2|ERR|invalid_seq
\n for TCP, end of datagram for UDP, end of HTTP body, etc.)| into fields (respecting \| escape sequences)ACK, check if field 2 starts with ! — if yes, parse as [ACK, SEQ, STATUS[, DETAIL]]; otherwise parse as [ACK, STATUS[, DETAIL]]! — if yes, parse as [METHOD, SEQ, AUTH, SERIAL[, BODY]]; otherwise parse as [METHOD, AUTH, SERIAL[, BODY]]! and validate against the last-seen counter (when counter enforcement is enabled; see §10.2)Field-count matrix (after |-splitting, respecting escapes):
!N!NMETHOD | SEQ | AUTH | SERIAL | BODY (5 fields)METHOD | AUTH | SERIAL | BODY (4 fields)METHOD | SEQ | AUTH | SERIAL | BODY (5 fields)METHOD | AUTH | SERIAL | BODY (4 fields)[VARNAME;...]METHOD | SEQ | AUTH | SERIAL (4 fields)METHOD | AUTH | SERIAL (3 fields)ACK | SEQ | STATUS | DETAIL (4 fields)ACK | STATUS | DETAIL (3 fields)For ACK without !N: minimum 2 fields (ACK|STATUS), maximum 3 (ACK|STATUS|DETAIL).
For ACK with !N: minimum 3 fields (ACK|!N|STATUS), maximum 4 (ACK|!N|STATUS|DETAIL).
>, this is a passthrough: read encoding flag (x or b), deliver the data to the payload parser without further parsing[ — everything before [ is body-level modifiers, everything inside [] is variables@=LOCATION, @TIMESTAMP, ^GROUP, {METADATA} (MUST appear in this order when present; reject duplicates with invalid_payload). After @, check: if = follows → location; if digit follows → timestamp; otherwise reject with invalid_payload.; into individual variables (respecting \; escape):=, ?=, @=, or =)#, @, ^, {, ;, or ] (respecting escapes)# found, read until @, ^, {, ;, or ]. MUST NOT appear with @= operator.@= found, read coordinates (two or three comma-separated numbers) until @, ^, {, ;, or ]. MUST NOT appear with @= operator.@ found (not followed by =), read digits until ^, {, ;, or ]^ found, read until {, ;, or ]{ found, read until } and parse key-value pairs by , (respecting \, and \} escapes). Each metadata pair is split on the first =; subsequent = characters are part of the value.The BODY is a bracket-wrapped list of variable names: [var1;var2;...]. Strip the enclosing [ and ], then split by ; to obtain individual variable names, each matching 1*VARNAMECHAR. A single variable is valid (e.g., [temperature]).
The parser MUST check for multi-character operators first:
:= → Number?= → Boolean@= → Location= → StringThe same data point expressed across formats:
HTTP/JSON (~487 bytes with headers):
{
"variable": "temperature",
"value": 32,
"unit": "F",
"group": "batch_42",
"time": "1694567890000",
"location": {"lat": 39.74, "lng": -104.99},
"metadata": {"source": "dht22"}
}
TagoTiP (~103 bytes):
PUSH|4deedd7bab8817ec|sensor-01|@1694567890000^batch_42[temperature:=32#F@=39.74,-104.99{source=dht22}]
TagoTiP/S (~110 bytes):
Headless inner frame (81 bytes):
sensor-01|@1694567890000^batch_42[temperature:=32#F@=39.74,-104.99{source=dht22}]
(removed "PUSH|4deedd7bab8817ec|" = 22 bytes)
Envelope: 1 (flags) + 4 (counter) + 8 (auth hash) + 8 (device hash) + 81 (ciphertext) + 8 (auth tag) = 110 bytes
TagoTiP sizes exclude transport-layer overhead (TCP/IP headers). The HTTP/JSON body alone is ~180 bytes; the ~487 figure includes typical HTTP request headers. TagoTiP/S adds encryption overhead (29-37 bytes depending on cipher suite) but removes the method and auth hash fields from the inner frame.
; Core rules (ALPHA, DIGIT, HEXDIG, LF, etc.) per RFC 5234, Appendix B.
; === Uplink Frames (Client → Server) ===
; LF is REQUIRED on stream transports (TCP); OPTIONAL on message transports (UDP, MQTT, HTTP)
frame = push-frame / pull-frame / ping-frame
push-frame = "PUSH" "|" [seq "|"] auth "|" serial "|" push-body LF
pull-frame = "PULL" "|" [seq "|"] auth "|" serial "|" pull-body LF
ping-frame = "PING" "|" [seq "|"] auth "|" serial LF
; Future: METHOD "/" 1*DIGIT
seq = "!" counter-value ; Optional sequence counter
counter-value = "0" / (%x31-39 *DIGIT) ; No leading zeros
auth = 16HEXDIG ; Authorization Hash (8 bytes as hex)
serial = 1*100SERIALCHAR ; Device serial number (max 100 bytes)
; PUSH
push-body = passthrough-body / structured-body
passthrough-body = ">x" 1*(2HEXDIG) ; Hex-encoded passthrough (byte pairs)
/ ">b" 1*BASE64CHAR ; Base64-encoded passthrough
structured-body = [body-mods] "[" var-list "]"
body-mods = ["@=" loc-value] ["@" timestamp] ["^" group] ["{" meta-list "}"]
var-list = variable *99(";" variable) ; max 100 variables
variable = var-name ":=" num-value [common-suffixes]
/ var-name "=" str-value [common-suffixes]
/ var-name "?=" bool-value [common-suffixes]
/ var-name "@=" loc-value [loc-suffixes]
var-name = 1*100VARNAMECHAR ; max 100 bytes
num-value = ["-"] int-part ["." 1*DIGIT]
int-part = "0" / (%x31-39 *DIGIT) ; 0, or non-zero digit followed by any digits
str-value = 1*VALCHAR
bool-value = "true" / "false"
loc-value = coordinate "," coordinate ["," coordinate]
coordinate = ["-"] int-part ["." 1*DIGIT]
common-suffixes = ["#" unit] ["@=" loc-value] ["@" timestamp] ["^" group] ["{" meta-list "}"]
loc-suffixes = ["@" timestamp] ["^" group] ["{" meta-list "}"]
; no #unit or @=location for @= operator (§6.3.2)
unit = 1*25UNITCHAR ; max 25 bytes
timestamp = 1*DIGIT ; UNIX ms
group = 1*100VARNAMECHAR ; max 100 bytes
meta-list = meta-pair *31("," meta-pair) ; max 32 metadata pairs
meta-pair = meta-key "=" meta-value
meta-key = 1*100VARNAMECHAR ; max 100 bytes
meta-value = 1*METAVALCHAR
; PULL
pull-body = "[" pull-list "]"
pull-list = var-name *99(";" var-name) ; max 100 variables
pull-response = "[" var-list "]" ; Bracket-wrapped, same as PUSH body
; === Downlink Frames (Server → Client) ===
ack-frame = "ACK" "|" [seq "|"] ack-status ["|" ack-detail] LF
; seq starts with "!" — unambiguous vs. ack-status (alphabetic)
ack-status = "OK" / "PONG" / "CMD" / "ERR"
ack-detail = 1*DIGIT ; PUSH OK: count of accepted data points
/ pull-response ; PULL OK: bracket-wrapped variable list
/ 1*DETAILCHAR ; CMD detail or ERR error code
; Character classes below cover the ASCII subset only.
; Non-ASCII UTF-8 sequences (RFC 3629) are also valid in VALCHAR,
; METAVALCHAR, and UNITCHAR positions when the implementation supports UTF-8.
; === Character classes ===
VARNAMECHAR = %x61-7A / DIGIT / "_" ; lowercase a-z, digits, underscore
SERIALCHAR = ALPHA / DIGIT / "-" / "_" ; serial numbers (hyphens allowed)
VALCHAR = %x20-22 / %x24-3A / %x3C-3F / %x41-5A / %x5F-60 / %x61-7A / %x7E
/ "\" ("|" / "[" / "]" / ";" / "," / "{" / "}" / "#" / "@" / "^" / "n" / "\")
; printable ASCII excluding # ; @ [ \ ] ^ { | }
; with escape sequences for structural characters
METAVALCHAR = %x20-22 / %x24-2B / %x2D-3A / %x3C-3F / %x41-5A / %x5F-60 / %x61-7A / %x7E
/ "\" ("|" / "[" / "]" / ";" / "," / "{" / "}" / "#" / "@" / "^" / "n" / "\")
; like VALCHAR but also excludes unescaped ","
UNITCHAR = %x20-22 / %x24-3A / %x3C-3F / %x41-5A / %x5F-60 / %x61-7A / %x7E
; printable ASCII excluding # ; @ [ \ ] ^ { | }
; No escape sequences — units are plain text
DETAILCHAR = %x21-7B / %x7D-7E ; VCHAR excluding "|"
BASE64CHAR = ALPHA / DIGIT / "+" / "/" / "="
; Padding position enforced by decoder
Note: The character classes above precisely exclude structural delimiters from their base ranges. VALCHAR allows unescaped , (used literally in string values), while METAVALCHAR excludes it (since , separates metadata pairs). The variable production is split by operator type to enforce type-specific value formats (§6.3) and the rules that #unit and @=location MUST NOT be used with the @= operator (§6.3.2). See §12.2 for detailed parsing rules.
|!!42)[];[],{}:==?=@=#@^{}>x>b\n\|, ;, ], }, n, \, etc.For encryption-based security without TLS, see TagoTiPs.md (TagoTiP/S).
This specification is open source, published under the Apache License 2.0.
Anyone is free to implement TagoTiP — clients, servers, libraries, gateways, or any other component — for any purpose, including commercial use, without requiring permission from TagoIO Inc. The Apache 2.0 license includes an express patent grant to all implementers.
The names "TagoTiP", "TagoTiP/S", and "TagoIO" are trademarks of TagoIO Inc. See NOTICE for trademark details.
Copyright 2026 TagoIO Inc.