API / API reference

Projects

Create, read and update projects.

Anything that answers with a resource wraps it in data — a project, a component, a deck or stack, a stored file, a webhook — whether one or a list of them. Paginated lists add links and meta. Anything that answers with a result is that result at the top level, with no data: the account and shape lookups, an upload, a newly created webhook, the counts from a component write, and every delete confirmation. Each endpoint below shows exactly which it is. Errors are never wrapped; see Errors.
GET /api/v1/projects Scope projects:read

List the projects this token can reach

Every project this designer owns, newest first, paginated. Each entry carries component_count, so you can show a picker without a second call per project.

This is normally your first call after /me: a designer picks which of their games your tool should work on, and the id you get back is the {project} in almost every other path on this page.

curl "https://dev.dustinsdesignerden.com/api/v1/projects" \
  -H "Authorization: Bearer $DDD_TOKEN"
const res = await fetch('https://dev.dustinsdesignerden.com/api/v1/projects', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

const data = await res.json();
$response = Http::withToken($token)->get('https://dev.dustinsdesignerden.com/api/v1/projects');

$data = $response->json();
res = requests.get(
    'https://dev.dustinsdesignerden.com/api/v1/projects',
    headers={'Authorization': f'Bearer {token}'},
)

data = res.json()
Response 200
{
    "data": [
        {
            "id": 1,
            "name": "Harvest Moon",
            "short_description": "A farming game.",
            "description": null,
            "is_locked": false,
            "player_count_min": 2,
            "player_count_max": 4,
            "playtime_minutes": 45,
            "min_age": null,
            "genre": "Worker Placement",
            "theme": null,
            "primary_mechanic": null,
            "edition": null,
            "logo_url": null,
            "backdrop_url": null,
            "website_url": null,
            "bgg_id": null,
            "component_count": 0,
            "created_at": "2026-01-01T12:00:00+00:00",
            "updated_at": "2026-01-01T12:00:00+00:00"
        }
    ],
    "links": {
        "first": "http://dustinsdesignerden.test/api/v1/projects?page=1",
        "last": "http://dustinsdesignerden.test/api/v1/projects?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "« Previous",
                "page": null,
                "active": false
            },
            {
                "url": "http://dustinsdesignerden.test/api/v1/projects?page=1",
                "label": "1",
                "page": 1,
                "active": true
            },
            {
                "url": null,
                "label": "Next »",
                "page": null,
                "active": false
            }
        ],
        "path": "http://dustinsdesignerden.test/api/v1/projects",
        "per_page": 50,
        "to": 1,
        "total": 1
    }
}

Parameters

per_page integer · query
Defaults to 50
page integer · query

Response: Pagination

data array
ONE PAGE of results, never the whole collection. At most meta.per_page rows, which is 50 unless you ask for more. A value that is absent from data is absent from THIS PAGE, which is not the same as absent from the project.
links object
first, last, prev and next URLs. next is the whole pagination contract: follow it until it is null and you have every row. Read data once and stop, and you have the first 50 rows and no warning that there were more.
meta object
current_page, last_page, per_page, from, to, total and path. Check total against what you have collected to know you are done.

Response: Project

id integer
Designer Den's id for the project. Use it in every project-scoped path.
name string
Project name.
short_description string|null
One-line summary.
description string|null
Long description.
is_locked boolean
A locked project is read-only. Every write returns 409 while this is true.
player_count_min integer|null
Minimum players.
player_count_max integer|null
Maximum players.
playtime_minutes integer|null
Typical play length.
min_age integer|null
Recommended minimum age.
genre string|null
Free text.
theme string|null
Free text.
primary_mechanic string|null
Free text.
edition string|null
Free text.
logo_url string|null
Project logo. Set in the app, not through this API.
backdrop_url string|null
Project backdrop. Set in the app.
website_url string|null
Project website.
bgg_id string|null
BoardGameGeek id.
component_count integer
How many components the project has.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
POST /api/v1/projects Scope projects:write

Create a project

Subject to the designer's plan limit. A free account is capped, and exceeding it returns 403 plan_limit_reached. Surface that message rather than swallowing it.

curl -X POST "https://dev.dustinsdesignerden.com/api/v1/projects" \
  -H "Authorization: Bearer $DDD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Second Game",
    "short_description": "A quick filler."
}'
const res = await fetch('https://dev.dustinsdesignerden.com/api/v1/projects', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "name": "Second Game",
      "short_description": "A quick filler."
  }),
});

const data = await res.json();
$response = Http::withToken($token)->post('https://dev.dustinsdesignerden.com/api/v1/projects', [
    'name' => 'Second Game',
    'short_description' => 'A quick filler.',
]);

$data = $response->json();
res = requests.post(
    'https://dev.dustinsdesignerden.com/api/v1/projects',
    headers={'Authorization': f'Bearer {token}'},
    json={
    'name': 'Second Game',
    'short_description': 'A quick filler.'
},
)

data = res.json()
Response 201
{
    "data": {
        "id": 2,
        "name": "Second Game",
        "short_description": "A quick filler.",
        "description": null,
        "is_locked": false,
        "player_count_min": null,
        "player_count_max": null,
        "playtime_minutes": null,
        "min_age": null,
        "genre": null,
        "theme": null,
        "primary_mechanic": null,
        "edition": null,
        "logo_url": null,
        "backdrop_url": null,
        "website_url": null,
        "bgg_id": null,
        "component_count": 0,
        "created_at": "2026-01-01T12:00:00+00:00",
        "updated_at": "2026-01-01T12:00:00+00:00"
    }
}

Body parameters

name string required
Project name. Max 255 characters.
short_description string
One-line summary. Max 500.
description string
Long description. Max 20000.
player_count_min integer
1 to 99.
player_count_max integer
1 to 99.
playtime_minutes integer
Typical play length.
min_age integer
0 to 99.
genre string
Free text.
theme string
Free text.
primary_mechanic string
Free text.
edition string
Free text.
website_url url
Project website.
bgg_id string
BoardGameGeek id.

Response: Project

id integer
Designer Den's id for the project. Use it in every project-scoped path.
name string
Project name.
short_description string|null
One-line summary.
description string|null
Long description.
is_locked boolean
A locked project is read-only. Every write returns 409 while this is true.
player_count_min integer|null
Minimum players.
player_count_max integer|null
Maximum players.
playtime_minutes integer|null
Typical play length.
min_age integer|null
Recommended minimum age.
genre string|null
Free text.
theme string|null
Free text.
primary_mechanic string|null
Free text.
edition string|null
Free text.
logo_url string|null
Project logo. Set in the app, not through this API.
backdrop_url string|null
Project backdrop. Set in the app.
website_url string|null
Project website.
bgg_id string|null
BoardGameGeek id.
component_count integer
How many components the project has.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
GET /api/v1/projects/{project} Scope projects:read

Read one project

One project, with its component count. Use it to refresh something you already know about, for instance to check is_locked before you start writing.

A locked project rejects every write with 409 project_locked, so looking first lets you tell the designer why, instead of failing halfway through a sync.

curl "https://dev.dustinsdesignerden.com/api/v1/projects/1" \
  -H "Authorization: Bearer $DDD_TOKEN"
const res = await fetch('https://dev.dustinsdesignerden.com/api/v1/projects/1', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

const data = await res.json();
$response = Http::withToken($token)->get('https://dev.dustinsdesignerden.com/api/v1/projects/1');

$data = $response->json();
res = requests.get(
    'https://dev.dustinsdesignerden.com/api/v1/projects/1',
    headers={'Authorization': f'Bearer {token}'},
)

data = res.json()
Response 200
{
    "data": {
        "id": 1,
        "name": "Harvest Moon",
        "short_description": "A farming game.",
        "description": null,
        "is_locked": false,
        "player_count_min": 2,
        "player_count_max": 4,
        "playtime_minutes": 45,
        "min_age": null,
        "genre": "Worker Placement",
        "theme": null,
        "primary_mechanic": null,
        "edition": null,
        "logo_url": null,
        "backdrop_url": null,
        "website_url": null,
        "bgg_id": null,
        "component_count": 0,
        "created_at": "2026-01-01T12:00:00+00:00",
        "updated_at": "2026-01-01T12:00:00+00:00"
    }
}

Parameters

project integer · path required
Project id.

Response: Project

id integer
Designer Den's id for the project. Use it in every project-scoped path.
name string
Project name.
short_description string|null
One-line summary.
description string|null
Long description.
is_locked boolean
A locked project is read-only. Every write returns 409 while this is true.
player_count_min integer|null
Minimum players.
player_count_max integer|null
Maximum players.
playtime_minutes integer|null
Typical play length.
min_age integer|null
Recommended minimum age.
genre string|null
Free text.
theme string|null
Free text.
primary_mechanic string|null
Free text.
edition string|null
Free text.
logo_url string|null
Project logo. Set in the app, not through this API.
backdrop_url string|null
Project backdrop. Set in the app.
website_url string|null
Project website.
bgg_id string|null
BoardGameGeek id.
component_count integer
How many components the project has.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
PATCH /api/v1/projects/{project} Scope projects:write

Update a project's details

Updates the project's own details, such as its name or description. Send only the fields you want changed; anything you leave out is untouched.

This never affects components. Deliberately narrow: it cannot alter ownership, lock state or billing. If at least one field really changes, a project.updated webhook fires naming the fields that were written.

curl -X PATCH "https://dev.dustinsdesignerden.com/api/v1/projects/1" \
  -H "Authorization: Bearer $DDD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "genre": "Engine Builder",
    "playtime_minutes": 60
}'
const res = await fetch('https://dev.dustinsdesignerden.com/api/v1/projects/1', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "genre": "Engine Builder",
      "playtime_minutes": 60
  }),
});

const data = await res.json();
$response = Http::withToken($token)->patch('https://dev.dustinsdesignerden.com/api/v1/projects/1', [
    'genre' => 'Engine Builder',
    'playtime_minutes' => 60,
]);

$data = $response->json();
res = requests.patch(
    'https://dev.dustinsdesignerden.com/api/v1/projects/1',
    headers={'Authorization': f'Bearer {token}'},
    json={
    'genre': 'Engine Builder',
    'playtime_minutes': 60
},
)

data = res.json()
Response 200
{
    "data": {
        "id": 1,
        "name": "Harvest Moon",
        "short_description": "A farming game.",
        "description": null,
        "is_locked": false,
        "player_count_min": 2,
        "player_count_max": 4,
        "playtime_minutes": 60,
        "min_age": null,
        "genre": "Engine Builder",
        "theme": null,
        "primary_mechanic": null,
        "edition": null,
        "logo_url": null,
        "backdrop_url": null,
        "website_url": null,
        "bgg_id": null,
        "component_count": 0,
        "created_at": "2026-01-01T12:00:00+00:00",
        "updated_at": "2026-01-01T12:00:00+00:00"
    }
}

Parameters

project integer · path required
Project id.

Body parameters

name string
Project name. Max 255 characters.
short_description string
One-line summary. Max 500.
description string
Long description. Max 20000.
player_count_min integer
1 to 99.
player_count_max integer
1 to 99.
playtime_minutes integer
Typical play length.
min_age integer
0 to 99.
genre string
Free text.
theme string
Free text.
primary_mechanic string
Free text.
edition string
Free text.
website_url url
Project website.
bgg_id string
BoardGameGeek id.

Response: Project

id integer
Designer Den's id for the project. Use it in every project-scoped path.
name string
Project name.
short_description string|null
One-line summary.
description string|null
Long description.
is_locked boolean
A locked project is read-only. Every write returns 409 while this is true.
player_count_min integer|null
Minimum players.
player_count_max integer|null
Maximum players.
playtime_minutes integer|null
Typical play length.
min_age integer|null
Recommended minimum age.
genre string|null
Free text.
theme string|null
Free text.
primary_mechanic string|null
Free text.
edition string|null
Free text.
logo_url string|null
Project logo. Set in the app, not through this API.
backdrop_url string|null
Project backdrop. Set in the app.
website_url string|null
Project website.
bgg_id string|null
BoardGameGeek id.
component_count integer
How many components the project has.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.