API / API reference
Decks & stacks
Grouping components, and card order.
data. Paginated collections add
links and meta.
Errors are never wrapped; see Errors.
/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()
{
"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
-
projectinteger · path required - Project id.
-
kindstring · query - One of: deck, stack
-
include_membersboolean · query - Include each group's components in order. Off by default because large decks make big payloads.
Response: Deck or stack
-
idinteger - Group id, used in the member and delete paths.
-
kindstring deck(holds cards) orstack(holds tokens).-
namestring - Group name. Matched case-insensitively within a project.
-
positioninteger - Ordering among the project's groups.
-
member_typestring - The component type this group accepts:
cardortoken. -
member_countinteger - How many components are in it. Only present when members were requested.
-
membersarray - Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
-
created_atstring - ISO 8601 timestamp.
-
updated_atstring - ISO 8601 timestamp.
Response: Deck or stack member
-
unique_idstring - The component. This is the id you send back to set membership; see the members endpoint.
-
component_idinteger - Our internal row id for that component, for reference. Not accepted on write.
-
namestring - Display name of that component, so you do not have to fetch it separately.
-
positioninteger - Its place in the group. 0 is the top of the deck.
-
countinteger - How many copies of the component this membership holds.
-
face_upboolean|null - Which side this entry starts on:
truefront up,falseface down.nullmeans it is dealt however the group is. Note this is not the same idea as a component'sfacing, which is the edge that is up. -
sourcestring|null - Which integration added it, using the same values as a component
source.
/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()
{
"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
-
projectinteger · path required - Project id.
Body parameters
-
kindstring required - deck (holds cards) or stack (holds tokens).
-
namestring required - Matched case-insensitively; an existing name returns that group with 200.
Response: Deck or stack
-
idinteger - Group id, used in the member and delete paths.
-
kindstring deck(holds cards) orstack(holds tokens).-
namestring - Group name. Matched case-insensitively within a project.
-
positioninteger - Ordering among the project's groups.
-
member_typestring - The component type this group accepts:
cardortoken. -
member_countinteger - How many components are in it. Only present when members were requested.
-
membersarray - Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
-
created_atstring - ISO 8601 timestamp.
-
updated_atstring - ISO 8601 timestamp.
/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()
{
"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
-
projectinteger · path required - Project id.
-
groupinteger · path required - Group id.
Body parameters
-
namestring - New name.
-
positioninteger - Ordering among the project's groups.
Response: Deck or stack
-
idinteger - Group id, used in the member and delete paths.
-
kindstring deck(holds cards) orstack(holds tokens).-
namestring - Group name. Matched case-insensitively within a project.
-
positioninteger - Ordering among the project's groups.
-
member_typestring - The component type this group accepts:
cardortoken. -
member_countinteger - How many components are in it. Only present when members were requested.
-
membersarray - Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
-
created_atstring - ISO 8601 timestamp.
-
updated_atstring - ISO 8601 timestamp.
/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()
{
"deleted": true,
"components_retained": true
}
Parameters
-
projectinteger · path required - Project id.
-
groupinteger · path required - Group id.
Response: Deletion result
-
deletedboolean|integer truefor a single delete, or the number removed for a bulk one.-
components_retainedboolean - Only on a group delete. Confirms removing the deck or stack left its components in the project.
/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()
{
"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
-
projectinteger · path required - Project id.
-
groupinteger · path required - Group id.
Body parameters
-
membersarray required - The group's full contents. ARRAY ORDER BECOMES CARD ORDER. Send [] to empty it without deleting components.
-
members[].unique_idstring required - Identifies the component. Your own stable id, looked up inside this project only.
-
members[].countinteger - How many copies of that component the group holds. Defaults to its quantity.
-
members[].face_upboolean - 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
-
idinteger - Group id, used in the member and delete paths.
-
kindstring deck(holds cards) orstack(holds tokens).-
namestring - Group name. Matched case-insensitively within a project.
-
positioninteger - Ordering among the project's groups.
-
member_typestring - The component type this group accepts:
cardortoken. -
member_countinteger - How many components are in it. Only present when members were requested.
-
membersarray - Its components IN ORDER. Only present when include_members was set. Each entry is described under Deck or stack member.
-
created_atstring - ISO 8601 timestamp.
-
updated_atstring - ISO 8601 timestamp.
Response: Deck or stack member
-
unique_idstring - The component. This is the id you send back to set membership; see the members endpoint.
-
component_idinteger - Our internal row id for that component, for reference. Not accepted on write.
-
namestring - Display name of that component, so you do not have to fetch it separately.
-
positioninteger - Its place in the group. 0 is the top of the deck.
-
countinteger - How many copies of the component this membership holds.
-
face_upboolean|null - Which side this entry starts on:
truefront up,falseface down.nullmeans it is dealt however the group is. Note this is not the same idea as a component'sfacing, which is the edge that is up. -
sourcestring|null - Which integration added it, using the same values as a component
source.