turlte API
Control a turtle on a shared canvas by calling an HTTP API. Three tiers unlock progressively more power.
Overview
Every device that calls the API gets its own turtle on the canvas, identified by its IP address. The turtle leaves a coloured trail as it moves. The more expressive the communication method you use, the more control you have.
| Tier | Method | What you unlock |
|---|---|---|
| 1 | GET /api |
Move the turtle. Trail drawn in default colour. |
| 2 | POST /api |
Set trail colour & width, give your turtle a name. |
| 3 | WebSocket /ws |
Full Logo-style control: move, turn, pen up/down, coin spray. 👑 crown appears. |
The canvas
The virtual coordinate space is 1000 × 600 units. The
origin (0, 0) is the top-left corner. x
increases to the right, y increases downward — standard
screen coordinates. The canvas scales to fill the viewer's browser window,
so coordinates are always consistent regardless of screen size.
New turtles spawn at a random position. When a turtle moves via
GET or POST, it automatically rotates to face
the direction of travel.
Colours are specified as a hue value from
0 to 255 using maximum saturation and
brightness (HSB colour model). Some reference points:
| hue | colour |
|---|---|
| 0 | red |
| 43 | orange |
| 64 | yellow |
| 85 | green |
| 128 | cyan |
| 170 | blue |
| 213 | magenta |
| 255 | red (full cycle) |
Tier 1 — GET simplest
https://turlte.nl/api?x=number&y=number
Send a GET request with x and y query parameters.
Your turtle appears at that position. Call it again with a new position
and a trail is drawn between the two points.
Parameters
| Name | Required | Type | Range | Description |
|---|---|---|---|---|
x | ✅ yes | number | 0 – 1000 | Horizontal position |
y | ✅ yes | number | 0 – 600 | Vertical position |
Try it in the browser
Paste this into your browser's address bar and press Enter:
https://turlte.nl/api?x=200&y=300
curl example
curl "https://turlte.nl/api?x=200&y=300"
JavaScript fetch example
const res = await fetch('https://turlte.nl/api?x=200&y=300');
const data = await res.json();
console.log(data);
Response
{
"ok": true,
"state": {
"id": "a3f8c2d1e4b9", // your anonymous ID (hashed from IP)
"name": "a3f8c2d1e4b9", // default name = your ID
"x": 200,
"y": 300,
"heading": 0,
"hue": 0, // red by default
"width": 2,
"penDown": true,
"tier": "get"
}
}
Tier 2 — POST more control
https://turlte.nl/api
Content-Type: application/json
Send a POST request with a JSON body. This unlocks trail colour, trail width, and the ability to name your turtle.
Request body fields
| Field | Required | Type | Range | Description |
|---|---|---|---|---|
x | at least one field required | number | 0 – 1000 | Horizontal position |
y | number | 0 – 600 | Vertical position | |
hue | integer | 0 – 255 | Trail colour (HSB hue) | |
width | integer | 1 – 16 | Trail stroke width in pixels | |
name | string | max 16 chars | Turtle display name |
name
to rename without moving; send only hue to change colour
for future trail segments without moving either.
curl example
curl -X POST https://turlte.nl/api \
-H "Content-Type: application/json" \
-d '{"x": 500, "y": 300, "hue": 170, "width": 4, "name": "Alice"}'
JavaScript fetch example
const res = await fetch('https://turlte.nl/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
x: 500,
y: 300,
hue: 170, // blue
width: 4,
name: 'Alice',
}),
});
const data = await res.json();
console.log(data);
Tier 3 — WebSocket full control
wss://turlte.nl/ws
Open a persistent WebSocket connection and send JSON commands. This unlocks Logo-style turtle movement: move forward by steps, turn by degrees, lift or lower the pen. A 👑 crown appears above your turtle on the canvas as long as the WebSocket connection is — or has ever been — active.
Connecting
const ws = new WebSocket('wss://turlte.nl/ws');
ws.onopen = () => console.log('connected');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onerror = (e) => console.error(e);
On connection, the server immediately sends a hello message
with your turtle's current state.
Sending commands
Every message must be valid JSON. After each message the server replies
with an ack containing your updated turtle state.
Command reference
| cmd | Extra fields | Effect |
|---|---|---|
move |
steps (number) |
Move forward steps pixels in the current heading direction. Negative steps move backward. |
turn |
degrees (number) |
Rotate the turtle. Positive = clockwise. 90 turns right, -90 turns left. |
penup |
— | Lift the pen. Movement no longer draws a trail. |
pendown |
— | Lower the pen. Movement draws a trail again. |
coinspray |
— | Emit a burst of 🪙 coin particles from the turtle's position. |
Any command can also include the POST-tier state fields
(name, hue, width, x, y)
— they are applied before the command is executed.
Heading convention
Heading follows the Logo convention:
0° points up, 90° points right,
180° points down, 270° points left.
Angles always increase clockwise.
Example session
const ws = new WebSocket('wss://turlte.nl/ws');
ws.onopen = () => {
// Name the turtle and set a colour
ws.send(JSON.stringify({ name: 'Bob', hue: 85, width: 3 }));
// Draw a square
for (let i = 0; i < 4; i++) {
ws.send(JSON.stringify({ cmd: 'move', steps: 100 }));
ws.send(JSON.stringify({ cmd: 'turn', degrees: 90 }));
}
// Celebrate
ws.send(JSON.stringify({ cmd: 'coinspray' }));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'ack') console.log('position:', msg.state.x, msg.state.y);
};
GET /api/turtles
https://turlte.nl/api/turtles
Returns all currently active turtles. Turtles that have been inactive for more than 5 minutes are removed automatically.
Query parameters (all optional)
| Parameter | Description | Example |
|---|---|---|
id |
Comma-separated list of turtle IDs to include. Returns only those turtles. | ?id=a3f8c2d1e4b9,bb12ef34ab56 |
tier |
Comma-separated list of tiers to include: get, post, ws. |
?tier=post,ws |
Examples
# All active turtles
curl https://turlte.nl/api/turtles
# Only turtles using WebSocket (have a crown)
curl "https://turlte.nl/api/turtles?tier=ws"
# A specific turtle by ID
curl "https://turlte.nl/api/turtles?id=a3f8c2d1e4b9"
Response
{
"ok": true,
"turtles": [
{
"id": "a3f8c2d1e4b9",
"name": "Alice",
"x": 500,
"y": 300,
"heading": 90,
"hue": 170,
"width": 4,
"penDown": true,
"tier": "post"
}
]
}
JavaScript example
// Poll the canvas every second and log all crown-turtles (WebSocket tier)
setInterval(async () => {
const res = await fetch('https://turlte.nl/api/turtles?tier=ws');
const data = await res.json();
console.log(data.turtles);
}, 1000);
GET /api/history
https://turlte.nl/api/history
Returns the complete stored history: every trail segment ever drawn, every turtle state snapshot, and the timestamp range. This is the data used by the canvas timeline slider.
/api/turtles when you only need current positions.
Response
{
"segments": [
{
"id": 1,
"ip": "a3f8c2d1e4b9", // turtle ID
"x0": 200, "y0": 300,
"x1": 350, "y1": 420,
"hue": 85,
"width": 3,
"created_at": 1712345678000 // Unix ms timestamp
}
],
"snapshots": [
{
"id": 1,
"ip": "a3f8c2d1e4b9",
"name": "Alice",
"x": 350, "y": 420, "heading": 90,
"hue": 85, "width": 3, "tier": "post",
"created_at": 1712345678000
}
],
"range": {
"min": 1712345000000,
"max": 1712345999000
}
}
Turtle state reference
All API responses include a state object:
| Field | Type | Description |
|---|---|---|
id | string | Your anonymous 12-character identifier (hashed from your IP — never exposed raw) |
name | string | Display name shown on the canvas (default: your id) |
x | number | Current x position (0 – 1000) |
y | number | Current y position (0 – 600) |
heading | number | Direction in degrees (0 = up, clockwise) |
hue | integer | Trail colour hue (0 – 255) |
width | integer | Trail stroke width (1 – 16 px) |
penDown | boolean | Whether movement draws a trail |
tier | string | "get" / "post" / "ws" — highest tier used so far |
Errors
All errors return HTTP 400 with a JSON body:
{
"ok": false,
"errors": [
{ "msg": "x is required and must be a number between 0 and 1000" }
]
}
Common causes:
- GET request missing
xory xoryoutside the canvas bounds- POST body is empty or contains no recognised fields
- Field values are out of range (e.g.
hue: 999)
WebSocket errors are returned as a message to the sending client:
{ "type": "error", "message": "Unrecognized message. Valid cmds: move, turn, penup, pendown, coinspray" }