Skip to main content

Vestaboard Note Arrays

Multiple Vestaboard Notes can be enabled for the Local API independently and arranged together to display a single large message spanning all of them.

Overview

Each Vestaboard Note has a 3×15 character grid. By positioning several Notes side-by-side or stacked, you can create larger display surfaces:

ArrangementVestaboard NotesEffective grid
2 side-by-side23 × 30
4 side-by-side43 × 60
2 stacked26 × 15
4 stacked412 × 15
2×2 grid46 × 30

There is no central endpoint that broadcasts to multiple Vestaboard Notes at once. Instead, you manage the layout yourself — a local script or server slices the full message into per-board regions and sends each slice to the appropriate Vestaboard Note.

Enabling the Local API on Each Vestaboard Note

Each Vestaboard Note must be enabled individually using its own enablement token. Repeat the enablement step for every Vestaboard Note in your array, saving each API key alongside the IP address of that board.

See Endpoints for the full enablement flow.

The X-Vestaboard-Local-Api-Key header used in the code examples below is the apiKey value returned by the enablement endpoint — not the one-time enablement token. The key does not expire. See Authentication for the full enablement flow.

Finding Each Vestaboard Note's IP Address

Each Vestaboard Note announces itself on your local network via mDNS. The first Vestaboard Note you connect responds to vestaboard.local, the second to vestaboard-2.local, and so on. You can verify connectivity with:

ping vestaboard.local
ping vestaboard-2.local

If mDNS is unavailable on your network, check your router's DHCP table or look up each board's IP in the Vestaboard app under device settings. Assigning static DHCP leases by MAC address (visible in the app under Advanced Settings) keeps the mapping stable across reboots.

Character Codes

Each cell in the message array is an integer representing a character. 0 is blank (black background). Letters, digits, punctuation, and color tiles each have their own code.

See the Character Code Reference for the full table.

How Many Vestaboard Notes Can You Use?

Any number of Vestaboard Notes can form an array — the slicing logic generalizes to 3, 5, 6, or any count. The overview table shows common configurations, but you are not limited to 2 or 4. Simply define one entry per Vestaboard Note with its IP, API key, and offset into the full message.

Slicing and Sending the Message

For a 4-Vestaboard-Note side-by-side array (3×60 total), split the full character array into four 3×15 slices and send each to the corresponding Vestaboard Note.

// Full message for a 4-Vestaboard-Note side-by-side array (3 rows × 60 columns)
const fullMessage = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];

// One entry per Vestaboard Note: its IP address and its starting column in the full message
const notes = [
{ host: "http://192.168.1.101:7000", apiKey: "KEY_1", colOffset: 0 },
{ host: "http://192.168.1.102:7000", apiKey: "KEY_2", colOffset: 15 },
{ host: "http://192.168.1.103:7000", apiKey: "KEY_3", colOffset: 30 },
{ host: "http://192.168.1.104:7000", apiKey: "KEY_4", colOffset: 45 },
];

const NOTE_COLS = 15;

async function sendToNoteArray(fullMessage, notes) {
await Promise.all(
notes.map(({ host, apiKey, colOffset }) => {
const slice = fullMessage.map((row) =>
row.slice(colOffset, colOffset + NOTE_COLS)
);
return fetch(`${host}/local-api/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Vestaboard-Local-Api-Key": apiKey,
},
body: JSON.stringify(slice),
});
})
);
}

await sendToNoteArray(fullMessage, notes);

Stacked Arrangement

For Vestaboard Notes stacked vertically (e.g. 4 stacked = 12×15), split by row offset instead of column offset.

// Full message for 4 stacked Vestaboard Notes (12 rows × 15 columns)
const fullMessage = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];

const notes = [
{ host: "http://192.168.1.101:7000", apiKey: "KEY_1", rowOffset: 0 },
{ host: "http://192.168.1.102:7000", apiKey: "KEY_2", rowOffset: 3 },
{ host: "http://192.168.1.103:7000", apiKey: "KEY_3", rowOffset: 6 },
{ host: "http://192.168.1.104:7000", apiKey: "KEY_4", rowOffset: 9 },
];

const NOTE_ROWS = 3;

async function sendToStackedNotes(fullMessage, notes) {
await Promise.all(
notes.map(({ host, apiKey, rowOffset }) => {
const slice = fullMessage.slice(rowOffset, rowOffset + NOTE_ROWS);
return fetch(`${host}/local-api/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Vestaboard-Local-Api-Key": apiKey,
},
body: JSON.stringify(slice),
});
})
);
}

await sendToStackedNotes(fullMessage, notes);

2×2 Grid Arrangement

For a 2×2 grid (6 rows × 30 columns), each Vestaboard Note gets a slice defined by both a row offset and a column offset.

┌──────────────┬──────────────┐
│ Board 1 │ Board 2 │
│ rows 0–2 │ rows 0–2 │
│ cols 0–14 │ cols 15–29 │
├──────────────┼──────────────┤
│ Board 3 │ Board 4 │
│ rows 3–5 │ rows 3–5 │
│ cols 0–14 │ cols 15–29 │
└──────────────┴──────────────┘
// Full message for a 2×2 grid (6 rows × 30 columns)
const fullMessage = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];

// Each Vestaboard Note has both a row offset and a column offset
const notes = [
{ host: "http://192.168.1.101:7000", apiKey: "KEY_1", rowOffset: 0, colOffset: 0 },
{ host: "http://192.168.1.102:7000", apiKey: "KEY_2", rowOffset: 0, colOffset: 15 },
{ host: "http://192.168.1.103:7000", apiKey: "KEY_3", rowOffset: 3, colOffset: 0 },
{ host: "http://192.168.1.104:7000", apiKey: "KEY_4", rowOffset: 3, colOffset: 15 },
];

const NOTE_ROWS = 3;
const NOTE_COLS = 15;

async function sendToGridNotes(fullMessage, notes) {
await Promise.all(
notes.map(({ host, apiKey, rowOffset, colOffset }) => {
const slice = fullMessage
.slice(rowOffset, rowOffset + NOTE_ROWS)
.map((row) => row.slice(colOffset, colOffset + NOTE_COLS));
return fetch(`${host}/local-api/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Vestaboard-Local-Api-Key": apiKey,
},
body: JSON.stringify(slice),
});
})
);
}

await sendToGridNotes(fullMessage, notes);

Tips

  • Discover IP addresses — each Vestaboard Note's IP appears in your router's DHCP table or in the Vestaboard app under device settings. Using static DHCP leases (assigned by MAC address) keeps the mapping stable.
  • Send in parallel — use Promise.all (JavaScript) or a thread pool (Python) so all Vestaboard Notes flip at the same time and the display looks synchronized.
  • Check firmware — all Vestaboard Notes in an array should be on the same firmware version to ensure consistent timing behavior.