API / API reference

Decks & stacks

Grouping components, and card order.

Single resources and collections come back wrapped in data. Paginated collections add links and meta. Errors are never wrapped; see Errors.
GET /api/v1/projects/{project}/groups Scope components:read

List decks and stacks

Every deck and stack in the project. A deck holds cards, a stack holds tokens.

Contents are left out unless you ask for them, because a project with several large decks would return a very big payload on a plain listing. Add ?include_members=1 when you actually need the components and their order.

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

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

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

data = res.json()
Response 200
{
    "data": [
        {
            "id": 1,
            "kind": "deck",
            "name": "Weapons",
            "position": 1,
            "member_type": "card",
            "member_count": 2,
            "members": [
                {
                    "unique_id": "ext-card-001",
                    "component_id": 1,
                    "name": "Rusty Dagger",
                    "position": 0,
                    "count": 3,
                    "face_up": null,
                    "source": "import"
                },
                {
                    "unique_id": "ext-card-002",
                    "component_id": 2,
                    "name": "Iron Shield",
                    "position": 1,
                    "count": 1,
                    "face_up": null,
                    "source": "import"
                }
            ],
            "created_at": "2026-08-01T16:20:39+00:00",
            "updated_at": "2026-08-01T16:20:39+00:00"
        }
    ]
}

Parameters

project integer · path required
Project id.
kind string · query
One of: deck, stack
include_members boolean · query
Include each group's components in order. Off by default because large decks make big payloads.

Response: Deck or stack

id integer
Group id, used in the member and delete paths.
kind string
deck (holds cards) or stack (holds tokens).
name string
Group name. Matched case-insensitively within a project.
position integer
Ordering among the project's groups.
member_type string
The component type this group accepts: card or token.
member_count integer
How many components are in it. Only present when members were requested.
members array
Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.

Response: Deck or stack member

unique_id string
The component. This is the id you send back to set membership; see the members endpoint.
component_id integer
Our internal row id for that component, for reference. Not accepted on write.
name string
Display name of that component, so you do not have to fetch it separately.
position integer
Its place in the group. 0 is the top of the deck.
count integer
How many copies of the component this membership holds.
face_up boolean|null
Which side this entry starts on: true front up, false face down. null means it is dealt however the group is. Note this is not the same idea as a component's facing, which is the edge that is up.
source string|null
Which integration added it, using the same values as a component source.
POST /api/v1/projects/{project}/groups Scope components:write

Create a deck or stack

kind is deck (holds cards) or stack (holds tokens). Names are matched case-insensitively within a project, so creating one that already exists returns the existing group with 200 instead of making a duplicate.

curl -X POST "https://dustinsdesignerden.com/api/v1/projects/1/groups" \
  -H "Authorization: Bearer $DDD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "deck",
    "name": "Treasure"
}'
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/groups', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "kind": "deck",
      "name": "Treasure"
  }),
});

const data = await res.json();
$response = Http::withToken($token)->post('https://dustinsdesignerden.com/api/v1/projects/1/groups', [
    'kind' => 'deck',
    'name' => 'Treasure',
]);

$data = $response->json();
res = requests.post(
    'https://dustinsdesignerden.com/api/v1/projects/1/groups',
    headers={'Authorization': f'Bearer {token}'},
    json={
    'kind': 'deck',
    'name': 'Treasure'
},
)

data = res.json()
Response 201
{
    "data": {
        "id": 2,
        "kind": "deck",
        "name": "Treasure",
        "position": 2,
        "member_type": "card",
        "created_at": "2026-08-01T16:20:39+00:00",
        "updated_at": "2026-08-01T16:20:39+00:00"
    }
}

Parameters

project integer · path required
Project id.

Body parameters

kind string required
deck (holds cards) or stack (holds tokens).
name string required
Matched case-insensitively; an existing name returns that group with 200.

Response: Deck or stack

id integer
Group id, used in the member and delete paths.
kind string
deck (holds cards) or stack (holds tokens).
name string
Group name. Matched case-insensitively within a project.
position integer
Ordering among the project's groups.
member_type string
The component type this group accepts: card or token.
member_count integer
How many components are in it. Only present when members were requested.
members array
Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
PATCH /api/v1/projects/{project}/groups/{group} Scope components:write

Rename or reorder a deck or stack

Renames a deck or stack, or moves it in the project's ordering. The contents are untouched: use the members endpoint for those.

kind cannot be changed. It decides which component type the group may hold, so turning a deck into a stack would strand every card already in it. Names are matched case-insensitively within a project, so renaming onto a name already in use is rejected.

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

const data = await res.json();
$response = Http::withToken($token)->patch('https://dustinsdesignerden.com/api/v1/projects/1/groups/1', [
    'name' => 'Starter Weapons',
]);

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

data = res.json()
Response 200
{
    "data": {
        "id": 1,
        "kind": "deck",
        "name": "Starter Weapons",
        "position": 1,
        "member_type": "card",
        "created_at": "2026-08-01T16:20:39+00:00",
        "updated_at": "2026-08-01T16:20:39+00:00"
    }
}

Parameters

project integer · path required
Project id.
group integer · path required
Group id.

Body parameters

name string
New name.
position integer
Ordering among the project's groups.

Response: Deck or stack

id integer
Group id, used in the member and delete paths.
kind string
deck (holds cards) or stack (holds tokens).
name string
Group name. Matched case-insensitively within a project.
position integer
Ordering among the project's groups.
member_type string
The component type this group accepts: card or token.
member_count integer
How many components are in it. Only present when members were requested.
members array
Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
DELETE /api/v1/projects/{project}/groups/{group} Scope components:write

Delete a deck or stack

Deletes the deck or stack itself. The components that were in it are NOT deleted, they stay in the project as loose components, which is why the response confirms components_retained.

Use this when a grouping was a mistake. To empty a deck but keep it, send an empty members array instead.

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

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

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

data = res.json()
Response 200
{
    "deleted": true,
    "components_retained": true
}

Parameters

project integer · path required
Project id.
group integer · path required
Group id.

Response: Deletion result

deleted boolean|integer
true for a single delete, or the number removed for a bulk one.
components_retained boolean
Only on a group delete. Confirms removing the deck or stack left its components in the project.
PUT /api/v1/projects/{project}/groups/{group}/members Scope components:write

Set a deck or stack's contents and card order

Replaces the membership wholesale. Array order becomes card order, so this is how you control the order of a deck.

Identify each member by its unique_id — the only id this API accepts for a member, so you can push a deck using only your own ids. Ids are looked up inside this project only. Responses also return our internal component_id for reference; it is not accepted here. Optional per-member count (how many copies) and face_up (true front up, false face down; omit it to let the entry be dealt however the group is).

Send an empty array to empty the deck without deleting any component.

Two rules are enforced, the same ones the in-app Deck & Stack Builder enforces: a deck holds only cards and a stack only tokens, and every member must share one shape/mask. A mixed deck is rejected with 400 rather than silently dropping cards, because it renders wrong on the tabletop.

curl -X PUT "https://dustinsdesignerden.com/api/v1/projects/1/groups/1/members" \
  -H "Authorization: Bearer $DDD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "members": [
        {
            "unique_id": "ext-card-002",
            "count": 1,
            "face_up": false
        },
        {
            "unique_id": "ext-card-001",
            "count": 3
        }
    ]
}'
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/groups/1/members', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "members": [
          {
              "unique_id": "ext-card-002",
              "count": 1,
              "face_up": false
          },
          {
              "unique_id": "ext-card-001",
              "count": 3
          }
      ]
  }),
});

const data = await res.json();
$response = Http::withToken($token)->put('https://dustinsdesignerden.com/api/v1/projects/1/groups/1/members', [
    'members' => [
        [
            'unique_id' => 'ext-card-002',
            'count' => 1,
            'face_up' => false,
        ],
        [
            'unique_id' => 'ext-card-001',
            'count' => 3,
        ],
    ],
]);

$data = $response->json();
res = requests.put(
    'https://dustinsdesignerden.com/api/v1/projects/1/groups/1/members',
    headers={'Authorization': f'Bearer {token}'},
    json={
    'members': [
        {
            'unique_id': 'ext-card-002',
            'count': 1,
            'face_up': False
        },
        {
            'unique_id': 'ext-card-001',
            'count': 3
        }
    ]
},
)

data = res.json()
Response 200
{
    "data": {
        "id": 1,
        "kind": "deck",
        "name": "Starter Weapons",
        "position": 1,
        "member_type": "card",
        "member_count": 2,
        "members": [
            {
                "unique_id": "ext-card-002",
                "component_id": 2,
                "name": "Iron Shield",
                "position": 0,
                "count": 1,
                "face_up": false,
                "source": "import"
            },
            {
                "unique_id": "ext-card-001",
                "component_id": 1,
                "name": "Rusty Dagger",
                "position": 1,
                "count": 3,
                "face_up": null,
                "source": "import"
            }
        ],
        "created_at": "2026-08-01T16:20:39+00:00",
        "updated_at": "2026-08-01T16:20:39+00:00"
    }
}

Parameters

project integer · path required
Project id.
group integer · path required
Group id.

Body parameters

members array required
The group's full contents. ARRAY ORDER BECOMES CARD ORDER. Send [] to empty it without deleting components.
members[].unique_id string required
Identifies the component. Your own stable id, looked up inside this project only.
members[].count integer
How many copies of that component the group holds. Defaults to its quantity.
members[].face_up boolean
Which side this entry starts on: true front up, false face down. Omit to let it be dealt however the group is. (Note this is NOT facing, which on a component means the edge that is up.)

Response: Deck or stack

id integer
Group id, used in the member and delete paths.
kind string
deck (holds cards) or stack (holds tokens).
name string
Group name. Matched case-insensitively within a project.
position integer
Ordering among the project's groups.
member_type string
The component type this group accepts: card or token.
member_count integer
How many components are in it. Only present when members were requested.
members array
Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
created_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.

Response: Deck or stack member

unique_id string
The component. This is the id you send back to set membership; see the members endpoint.
component_id integer
Our internal row id for that component, for reference. Not accepted on write.
name string
Display name of that component, so you do not have to fetch it separately.
position integer
Its place in the group. 0 is the top of the deck.
count integer
How many copies of the component this membership holds.
face_up boolean|null
Which side this entry starts on: true front up, false face down. null means it is dealt however the group is. Note this is not the same idea as a component's facing, which is the edge that is up.
source string|null
Which integration added it, using the same values as a component source.