## Authentication

All API endpoints require session authentication. You must be logged in via the web interface, or send the session cookie with your requests.

For programmatic access, authenticate first:

```bash
# Login and save cookies
curl -c cookies.txt -X POST https://your-yando-instance/accounts/login/ \
  -d "login=user@example.com&password=yourpassword" \
  -H "Referer: https://your-yando-instance/"
```

Then include `-b cookies.txt` in subsequent requests.

## Rate Limiting

API endpoints are rate-limited per user. If you exceed the rate limit, you'll receive a `429 Too Many Requests` response.

| Scope | Limit |
|-------|-------|
| General API requests | 100 / hour |
| Board create | 30 / hour |
| Board delete | 10 / hour |
| Board import | 20 / hour |
| Project create | 20 / hour |
| Project delete | 10 / hour |

The specific limits are separate allowances — creating a board does not use up your general 100/hour.

**Keeping well under the limit.** Adding objects counts against the general allowance, so creating them one request at a time is what usually exhausts it. Use the bulk object creation endpoint below (`POST /api/boards/{board_id}/objects/bulk/`) to add many objects in a single request, and target zones by **name** rather than fetching the board to look up a zone ID. Together these take a typical "create a board and fill it with 16 notes" workflow from around 34 requests down to 3.

Limits reset on a rolling one-hour window. If your account has a legitimate need for higher throughput — a demo environment or an internal integration — an administrator can exempt it from rate limits.

---

## POST /api/boards/import/

Create a new board from exported JSON data.

### Request

Accepts two content types:

**JSON body** (`application/json`):

```bash
curl -b cookies.txt -X POST https://your-yando-instance/api/boards/import/ \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <token>" \
  -d @board-export.json
```

**File upload** (`multipart/form-data`):

```bash
curl -b cookies.txt -X POST https://your-yando-instance/api/boards/import/ \
  -H "X-CSRFToken: <token>" \
  -F "json_file=@board-export.json"
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `template` | string | Template name or UUID to override the JSON template |
| `title` | string | Override the board title from the JSON |
| `description` | string | Override the board description |

### Response (201 Created)

```json
{
  "id": "a1b2c3d4-...",
  "title": "My Board",
  "description": "Board description",
  "template": "Business Model Canvas",
  "object_count": 15,
  "url": "/boards/a1b2c3d4-.../"
}
```

### Errors

| Status | Description |
|--------|-------------|
| 400 | Invalid JSON, validation error, or missing required fields |
| 403 | Authentication required |

---

## POST /api/boards/{board_id}/objects/bulk/

Create many objects on a board in a single request.

Use this instead of calling `POST /api/boards/{board_id}/objects/` in a loop. Each request counts against your rate limit, so filling a 16-note board costs 1 request this way instead of 16.

### Request

```bash
curl -b cookies.txt -X POST \
  https://your-yando-instance/api/boards/{board_id}/objects/bulk/ \
  -H "Content-Type: application/json" \
  -d '{
    "objects": [
      {
        "object_type": "sticky_note",
        "zone": "Strengths",
        "data": {"text": "Strong brand recognition", "color": "#7BC950"}
      },
      {
        "object_type": "sticky_note",
        "zone": "Weaknesses",
        "data": {"text": "Thin margins", "color": "#FF6B6B"}
      }
    ]
  }'
```

### Object Fields

| Field | Required | Description |
|-------|----------|-------------|
| `object_type` | Yes | `sticky_note`, `text_box`, `shape`, `line`, or `image` |
| `data` | Yes | Type-specific properties — `text` and `color` for a sticky note |
| `zone` | No | Zone **name** (case-insensitive) or UUID on the board's template |
| `zone_id` | No | Zone UUID. Takes precedence over `zone` |
| `x`, `y` | No | Position. Derived from the zone when omitted |
| `width`, `height` | No | Size. Defaults to 200×150 for sticky notes, 300×100 for text boxes |
| `rotation` | No | Degrees (default 0) |
| `opacity` | No | 0.0–1.0 (default 1.0) |

**Targeting zones by name.** Passing `zone: "Strengths"` saves you fetching the board first to find its UUID. An unknown name returns a 400 listing the zones that do exist.

**Automatic placement.** When you give a zone and omit `x`/`y`, the server positions each object inside that zone, spacing successive objects so they don't overlap. Supply explicit coordinates to override this.

**Limits.** Between 1 and 200 objects per request. Send larger imports in chunks.

### Response (201 Created)

```json
{
  "count": 2,
  "created": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "object_type": "sticky_note",
      "x": 40.0,
      "y": 30.0,
      "width": 200.0,
      "height": 150.0,
      "data": {"text": "Strong brand recognition", "color": "#7BC950"},
      "zone_name": "Strengths",
      "version": 1
    }
  ]
}
```

Objects are returned in the order you sent them. Collaborators with the board open see them appear live.

### Errors

The request is **atomic** — if any object is invalid, none are created:

| Status | Description |
|--------|-------------|
| 400 | Empty list, more than 200 objects, unknown zone name, or an invalid entry |
| 403 | You do not have editor access to this board |
| 404 | Board not found |

Validation errors name the offending entry, for example:

```json
{"error": "Object at index 1: Zone 'Sxrengths' does not exist on this board's template. Available zones: Strengths, Weaknesses, Opportunities, Threats"}
```

---

## POST /api/boards/{board_id}/resize/

Resize a board's canvas dimensions.

### Request

```bash
curl -b cookies.txt -X POST \
  https://your-yando-instance/api/boards/{board_id}/resize/ \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <token>" \
  -d '{"width": 4000, "height": 2400}'
```

### Response (200 OK)

```json
{
  "id": "a1b2c3d4-...",
  "canvas_width": 4000,
  "canvas_height": 2400
}
```

---

## PATCH /api/boards/{board_id}/update/

Update a board's title or description, or add it to, move it between, or remove it from a project.

```bash
curl -b cookies.txt -X PATCH \
  https://your-yando-instance/api/boards/{board_id}/update/ \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <token>" \
  -d '{"project_id": "7f090fb7-..."}'
```

| Field | Description |
|-------|-------------|
| `title`, `description` | Owner or editor |
| `project_id` | Project UUID to add/move the board to, or `null` to remove it. Board owner only; you must be an owner or editor of the target project |

When `project_id` is sent, the response includes how access changed:

```json
{
  "id": "a1b2c3d4-...",
  "title": "Roadmap",
  "project": {"id": "7f090fb7-...", "title": "Q3 Planning", "color": "#E3F2FD"},
  "access_changes": {"gained_access": 2, "lost_access": 0}
}
```

---

## Templates

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/templates/` | Built-in, your own, and other users' shared templates. Each has a `scope` (`built_in`, `mine`, `public`) and `owner` |
| GET | `/api/templates/{id}/` | Zones and scaffold objects |
| POST | `/api/templates/create/` | Create a custom template (Pro) |
| PATCH | `/api/templates/{id}/update/` | Update metadata, zones, or sharing (`is_public`) — your templates only |
| POST | `/api/templates/{id}/clone/` | Copy a built-in, own, or shared template (the copy records `cloned_from`) |
| GET / POST | `/api/templates/{id}/objects/` | List or add scaffold objects |
| PATCH / DELETE | `/api/templates/{id}/objects/{object_id}/` | Change (partial) or remove a scaffold object |

Boards can be created from any template you can see (`template` accepts a name or UUID). Names match your own templates first, then built-ins, then shared ones. A template shared by someone else is used through **your own copy** — made on first use, reused afterwards — so the owner's later changes never affect your boards.

**Sharing.** `{"is_public": true}` shares a template with everyone on the instance; `false` stops sharing (existing copies are unaffected). Sharing is refused with a 400 while the layout has errors (e.g. a zone off the canvas).

### Zones

Each zone needs `name`, `x`, `y`, `width`, `height` (minimum 50×50, inside the canvas). Optional: `description` (the zone's prompt — write it as the question the zone answers), `category` (one of `finance`, `customer`, `problem`, `idea`, `solution`, `value`, `action`, `metrics`, `team`, `timeline`, `neutral`; sets the zone's colour and is exported with its meaning), `icon` (a Lucide icon name overriding the category's icon), `zone_shape` (`rectangle`, `circle`, `pie-slice`, `rect-sector`), `start_angle` / `end_angle` (required for pie-slice and rect-sector; degrees, 0 = east, clockwise, −90 = top), `background_color` / `border_color` (`#RRGGBB`; a zone with a category gets that category's colour unless you set one, and a colour sent without a category makes an "Other colour" zone that exports as neutral), `border_width`, `order`, `enable_grid_positioning`, `grid_columns`, `grid_padding`.

**Updating zones.** `zones` in an update is the complete list the template should have. Each entry is matched to an existing zone by `id`, then by unique case-insensitive name, and updated in place — boards built from the template keep their objects in that zone. Unmatched entries are created; existing zones missing from the list are deleted. Optional fields you leave out keep their current values. The response reports what happened:

```json
"zone_changes": {
  "updated": ["Strengths", "Weaknesses"],
  "created": ["Opportunities"],
  "deleted": ["Threats"],
  "unzoned_board_objects": 4
}
```

To shrink the canvas, send the adjusted zones in the same request — otherwise a resize that would leave zones off-canvas is rejected.

**Templates in use need confirmation.** A template's zones and canvas are the layout of every board built from it. While boards use the template, a request that changes `zones`, `canvas_width` or `canvas_height` is refused with **409** and `"code": "TEMPLATE_HAS_BOARDS"`, listing the `boards` (`id`, `title`, `owner`); repeat it with `"apply_to_boards": true` once the user has agreed. Metadata (`name`, `description`) never needs it. To change one board only, use the board's layout (below).

Boards follow a confirmed change: each board's canvas grows to hold the template's canvas and zones (it never shrinks), and boards open at the time redraw the new zones straight away.

### One board's zones: `/api/boards/{board_id}/layout/`

`GET` returns the board's zones and canvas: `{board_id, canvas_width, canvas_height, template: {id, name, own_layout, based_on}, zones}`. `own_layout: true` means the board has its own copy of the layout.

`PATCH` with `{"zones": [...], "canvas_width"?, "canvas_height"?}` (zones as for template updates — the complete list, matched by `id` or unique name) changes **only this board**: the first change gives it its own copy of its template (zone ids you read before still work), so the template and its other boards are untouched. Without a canvas size the canvas grows to hold the zones. The board's objects follow their zones: a moved zone takes them along; a resized one keeps them in place, pulled back inside if they'd fall outside. Owners and editors of the board. The response adds `own_layout_created`, `zone_changes` and `objects_moved`.

`POST /api/boards/{board_id}/layout/save-as-template/` with `{"name"?}` (board owner) turns the board's own layout into an ordinary template. Own layouts are left out of template lists, can't be used for new boards and can't be shared until saved as a template.

### Scaffold Objects

Scaffold objects are copied onto every new board created from the template. Send one object, or several at once (all-or-nothing, up to 100):

```bash
curl -b cookies.txt -X POST \
  https://your-yando-instance/api/templates/{id}/objects/ \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: <token>" \
  -d '{"objects": [
        {"object_type": "text_box", "x": 200, "y": 80, "width": 5600, "height": 180, "locked": true,
         "data": {"text": "Value Proposition Canvas", "fontSize": 120, "fontWeight": "bold", "textAlign": "center"}},
        {"object_type": "line", "x": 400, "y": 700, "width": 2400, "height": 2400, "locked": true,
         "data": {"points": [[0, 0], [2400, 2400]], "stroke": "#90A4AE", "strokeWidth": 8, "arrowEnd": true}}
      ]}'
```

Common fields: `object_type`, `x`, `y`, `width`, `height`, `data`, and optionally `rotation` (degrees clockwise about the top-left corner), `opacity` (0–1), `locked`, `z_index`, `zone_id`. A `PATCH` changes only the fields sent; `data` is merged key by key. Only object types that boards can display are accepted — see **Object Types** below.

---

## GET /api/boards/{board_id}/snapshots/

List all snapshots for a board, ordered by creation date (newest first).

### Request

```bash
curl -b cookies.txt \
  https://your-yando-instance/api/boards/{board_id}/snapshots/
```

### Response (200 OK)

```json
[
  {
    "id": "s1n2a3p4-...",
    "created_at": "2026-02-21T08:00:00Z",
    "object_count": 24,
    "board_version": 15,
    "change_summary": {
      "added": {"sticky_note": 2},
      "edited": {"text_box": 1},
      "deleted": {}
    }
  }
]
```

---

## GET /api/boards/{board_id}/snapshots/{snapshot_id}/

Retrieve a single snapshot's full data.

### Request

```bash
curl -b cookies.txt \
  https://your-yando-instance/api/boards/{board_id}/snapshots/{snapshot_id}/
```

### Response (200 OK)

Returns the full snapshot including `snapshot_data` containing the complete board state at that point in time.

---

## POST /api/projects/{project_id}/pages/import/

Import markdown files as project pages.

### Request

Upload one or more `.md` or `.markdown` files:

```bash
curl -b cookies.txt -X POST \
  https://your-yando-instance/api/projects/{project_id}/pages/import/ \
  -H "X-CSRFToken: <token>" \
  -F "files=@notes.md" \
  -F "files=@planning.md"
```

### Response (201 Created)

```json
{
  "pages_created": 2,
  "pages": [
    {
      "id": "p1a2g3e4-...",
      "title": "Notes",
      "url": "/projects/.../pages/p1a2g3e4-.../"
    }
  ],
  "errors": []
}
```

### Status Codes

| Status | Description |
|--------|-------------|
| 201 | All files imported successfully |
| 207 | Partial success - some files imported, some had errors |
| 400 | No files provided or all files failed |
| 403 | Not an owner or editor of the project |
| 404 | Project not found |

---

## JSON Export Schema Reference

When exporting a board to JSON, the structure is:

```json
{
  "board": {
    "title": "Board Title",
    "description": "Optional description",
    "template": "Template Name or null",
    "canvas_width": 3200,
    "canvas_height": 1800
  },
  "zones": {
    "Zone Name": [
      {
        "type": "sticky_note",
        "x": 100,
        "y": 200,
        "width": 200,
        "height": 150,
        "rotation": 0,
        "data": {
          "text": "Content here",
          "color": "#FFEB3B"
        }
      }
    ]
  },
  "unzoned_objects": [
    {
      "type": "text_box",
      "x": 500,
      "y": 100,
      "width": 300,
      "height": 50,
      "data": {
        "text": "Unzoned text"
      }
    }
  ]
}
```

### Object Types

| Type | Data Fields |
|------|-------------|
| `sticky_note` | `text`, `color` |
| `text_box` | `text`, `fontSize`, `fontFamily`, `fontWeight` (`normal`, `bold`), `textAlign` (`left`, `center`, `right`), `color` (`#RRGGBB` or `auto` to follow the theme) |
| `shape` | `shapeType` (`rectangle`, `circle`, `triangle`, `line` — a horizontal bar), `fill`, `stroke`, `strokeWidth` |
| `line` | `points` (2–200 `[x, y]` pairs relative to the object's `x`/`y`), `stroke`, `strokeWidth`, `dashed`, `arrowStart`, `arrowEnd` |

All objects also carry `rotation`, `opacity` and `locked`.
