Posting this because it is undocumented and I had to decode it from engine source, so hopefully it saves someone the afternoon.
If you write .tscn files outside the Godot editor (map generators, level converters, build scripts), a TileMapLayer stores its painted cells in a single tile_map_data PackedByteArray. Layout, little-endian:
Bytes 0-1: uint16 format version. It is 0 in every release from 4.3 through current master, since the enum still has one entry.
Then one 12-byte record per painted cell, in any order:
+0 int16 cell x (negative coords are two's complement, so -1 is FF FF)
+2 int16 cell y
+4 uint16 source id
+6 uint16 atlas coords x
+8 uint16 atlas coords y
+10 uint16 alternative tile
Total length is always 2 + 12n. The engine checks (len - 2) % 12 == 0 and throws "Corrupted tile map data" otherwise, which is how I found that rule.
Things that cost me time:
Empty cells are never stored. There is no -1 sentinel, so an empty layer is an empty array rather than a grid of blanks.
Flip flags ride in the alternative field, OR-ed on: FLIP_H 0x1000, FLIP_V 0x2000, TRANSPOSE 0x4000.
Both text encodings parse. Since 4.2 the parser accepts PackedByteArray(0, 0, 1, ...) and PackedByteArray("base64..."). Godot's own saver writes decimal while every array is under 64 bytes, then switches the whole file to format=4 with base64. Writing format=3 with base64 is legal and loads in 4.3 through master; the editor just normalises it on the next save.
Worked example, six cells, source 0, alternative 0, at (0,0) (1,0) (2,0) (0,1) (1,1) and (-1,-1):
00 00 version 0
00 00 00 00 00 00 00 00 00 00 00 00 (0,0) atlas 0,0
01 00 00 00 00 00 01 00 00 00 00 00 (1,0) atlas 1,0
02 00 00 00 00 00 02 00 00 00 00 00 (2,0) atlas 2,0
00 00 01 00 00 00 00 00 01 00 00 00 (0,1) atlas 0,1
01 00 01 00 00 00 01 00 01 00 00 00 (1,1) atlas 1,1
FF FF FF FF 00 00 03 00 00 00 00 00 (-1,-1) atlas 3,0
Version trap: TileMapLayer only exists in 4.3 and later, so a scene using it will not open in 4.2. If you need older support, the deprecated TileMap node holds the same 12 bytes reinterpreted as three int32s per cell in layer_0/tile_data, with a node-level format = 2 selecting that decoding.
One more: the per-tile physics polygons in the companion .tres use tile-CENTER origin, not top-left. A 32px tile's full-square collision is PackedVector2Array(-16, -16, 16, -16, 16, 16, -16, 16). I had that one wrong for a while and the shapes come out offset by half a tile.
All of the above verified by writing files by hand and opening them in 4.7.1.


