API / API reference
Components
The primary write path.
data. Paginated collections add
links and meta.
Errors are never wrapped; see Errors.
/api/v1/projects/{project}/components
Scope
components:read
List components
Every component in the project, paginated, grouped by type and then in the designer's own ordering.
Read this before writing, to see what already exists and to pick up the unique_id values of rows you did not create yourself. Narrow it to one type with ?type=card.
curl "https://dustinsdesignerden.com/api/v1/projects/1/components?type=card" \
-H "Authorization: Bearer $DDD_TOKEN"
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/components?type=card', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
$response = Http::withToken($token)->get('https://dustinsdesignerden.com/api/v1/projects/1/components?type=card');
$data = $response->json();
res = requests.get(
'https://dustinsdesignerden.com/api/v1/projects/1/components?type=card',
headers={'Authorization': f'Bearer {token}'},
)
data = res.json()
{
"data": [
{
"id": 1,
"unique_id": "ext-card-001",
"type": "card",
"name": "Rusty Dagger",
"book_name": null,
"quantity": 3,
"description": null,
"notes": null,
"size": null,
"color": null,
"color_back": null,
"shape": "circle",
"card_mask_type": "rectangle_rounded",
"material": null,
"image_url": "https://cdn.example.com/dagger.png",
"back_image_url": null,
"image_rotation": 0,
"facing": null,
"phys_width_in": 2.5,
"phys_height_in": 3.5,
"weight_lbs": null,
"cost_per_unit": null,
"page_name": null,
"sort_order": 0,
"flippable": false,
"playtest_scale_x": 1,
"playtest_scale_y": 1,
"die_faces": null,
"die_face_labels": null,
"dice_style": null,
"spinner_sections": null,
"deck_names": [
"Weapons"
],
"stack_names": [],
"source": "api:personal",
"created_at": "2026-08-01T16:20:39+00:00",
"updated_at": "2026-08-01T16:20:39+00:00"
},
{
"id": 2,
"unique_id": "ext-card-002",
"type": "card",
"name": "Iron Shield",
"book_name": null,
"quantity": 1,
"description": null,
"notes": null,
"size": null,
"color": null,
"color_back": null,
"shape": "circle",
"card_mask_type": "rectangle_rounded",
"material": null,
"image_url": null,
"back_image_url": null,
"image_rotation": 0,
"facing": null,
"phys_width_in": null,
"phys_height_in": null,
"weight_lbs": null,
"cost_per_unit": null,
"page_name": null,
"sort_order": 0,
"flippable": false,
"playtest_scale_x": 1,
"playtest_scale_y": 1,
"die_faces": null,
"die_face_labels": null,
"dice_style": null,
"spinner_sections": null,
"deck_names": [
"Weapons"
],
"stack_names": [],
"source": "api:personal",
"created_at": "2026-08-01T16:20:39+00:00",
"updated_at": "2026-08-01T16:20:39+00:00"
}
],
"links": {
"first": "http://dustinsdesignerden.test/api/v1/projects/1/components?page=1",
"last": "http://dustinsdesignerden.test/api/v1/projects/1/components?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/1/components?page=1",
"label": "1",
"page": 1,
"active": true
},
{
"url": null,
"label": "Next »",
"page": null,
"active": false
}
],
"path": "http://dustinsdesignerden.test/api/v1/projects/1/components",
"per_page": 50,
"to": 2,
"total": 2
}
}
Parameters
-
projectinteger · path required - Project id.
-
typestring · query - Accepts the legacy types too (other, piece, cube), so you can list older rows in order to migrate them. You cannot create those. One of: card, token, dice, board, rulebook, spinner, coin, bag, counter, timer, packaging, other, piece, cube
-
per_pageinteger · query - Defaults to 50
Response: Pagination
-
dataarray - The page of results.
-
linksobject - first, last, prev and next URLs.
-
metaobject - current_page, last_page, per_page, from, to, total and path.
Response: Component
-
idinteger - Our internal row id, returned for reference. Everything you send us is keyed on
unique_idinstead. -
unique_idstring - Your id if you sent one, otherwise one we generated. Never null. This is what later writes match on.
-
typestring - Component type. See the type list under Components.
-
namestring - Display name. For a rulebook page this is its page_name.
-
book_namestring|null - Rulebook pages only: which book the page belongs to.
-
quantityinteger - How many exist.
-
descriptionstring|null - Free text.
-
notesstring|null - Private free text.
-
sizestring|null - Die type for dice (d6, d20). A legacy label on other types.
-
colorstring|null - Front colour, used when there is no artwork.
-
color_backstring|null - Back-face colour.
-
shapestring|null - Token shape.
-
card_mask_typestring|null - Card shape, reported under the same name GET /masks lists it as, so a value you send comes back as itself. A plain rectangle is
rectangle. -
materialstring|null - Free text.
-
image_urlstring|null - Front artwork.
-
back_image_urlstring|null - Back artwork.
-
image_rotationinteger - Artwork rotation: 0, 90, 180 or 270.
-
facinginteger|null - Which edge is up: 0, 90, 180 or 270. Null means it has never been set and the tabletop default applies.
-
phys_width_innumber|null - Real width in inches.
-
phys_height_innumber|null - Real height in inches.
-
weight_lbsnumber|null - Per-unit weight.
-
cost_per_unitnumber|null - Per-unit cost.
-
page_namestring|null - Rulebook pages only: the page title.
-
sort_orderinteger|null - Ordering within its type. Page order for a rulebook.
-
flippableboolean - Whether it can be flipped on the tabletop.
-
playtest_scale_xnumber|null - Fallback width scale used when no physical size is set.
-
playtest_scale_ynumber|null - Fallback height scale.
-
die_facesarray|null - Dice: resolved face image URLs.
-
die_face_labelsarray|null - Dice: face labels.
-
dice_stylestring|null - Dice: standard or custom.
-
spinner_sectionsarray|null - Spinner: its sections.
-
deck_namesarray - Names of the decks this card is in. Empty for a card in no deck, and for every non-card type. Set it with the
deck_namesfield on a write. -
stack_namesarray - Names of the stacks this token is in. Empty for a token in no stack, and for every non-token type.
-
sourcestring|null - Which integration owns this row:
api:{client},dextrous, or null. Null means hand-made OR imported from The Game Crafter, which tracks its own rows separately. Your deletes only ever reach rows matching your own value, so null rows are never yours to remove. -
created_atstring - ISO 8601 timestamp.
-
updated_atstring - ISO 8601 timestamp.
/api/v1/projects/{project}/components
Scope
components:write
Create or update a single component
Creates or updates ONE component. It is a batch of one and behaves identically to the batch endpoint, so anything true there is true here.
Matching is on unique_id: send your own stable id, and the first call creates while every later call with that id updates in place. Reach for this when a single thing changed and a whole batch would be noise. For a real sync, prefer the batch endpoint, which is one transaction and one webhook instead of many.
curl -X POST "https://dustinsdesignerden.com/api/v1/projects/1/components" \
-H "Authorization: Bearer $DDD_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "token",
"component": {
"unique_id": "ext-tok-001",
"name": "Gold Coin",
"shape": "circle"
}
}'
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/components', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"type": "token",
"component": {
"unique_id": "ext-tok-001",
"name": "Gold Coin",
"shape": "circle"
}
}),
});
const data = await res.json();
$response = Http::withToken($token)->post('https://dustinsdesignerden.com/api/v1/projects/1/components', [
'type' => 'token',
'component' => [
'unique_id' => 'ext-tok-001',
'name' => 'Gold Coin',
'shape' => 'circle',
],
]);
$data = $response->json();
res = requests.post(
'https://dustinsdesignerden.com/api/v1/projects/1/components',
headers={'Authorization': f'Bearer {token}'},
json={
'type': 'token',
'component': {
'unique_id': 'ext-tok-001',
'name': 'Gold Coin',
'shape': 'circle'
}
},
)
data = res.json()
{
"created": 1,
"updated": 0,
"deleted": 0,
"errors": [],
"kept_artwork": [],
"components": [
{
"id": 3,
"name": "Gold Coin",
"type": "token"
}
],
"internalizing": false
}
Parameters
-
projectinteger · path required - Project id.
Body parameters
-
typestring required - Component type.
-
componentobject required - A single component row. Fields below.
-
match_by_nameboolean - As above.
-
internalize_imagesboolean - As above.
Response: Write result
-
createdinteger - Rows that did not exist and were inserted.
-
updatedinteger - Rows matched by unique_id (or name) and updated in place.
-
deletedinteger - Rows removed because remove_missing was set and the batch omitted them.
-
errorsarray - Per-row problems. Rows that succeeded still applied, so always read this even on a 200.
-
kept_artworkarray - Names of components the batch matched but sent no artwork for, so they kept the artwork they already had. Usually means you forgot an image_url.
-
componentsarray - The components created or updated, with id, name and type.
-
internalizingboolean - True if internalize_images was set and a background copy of external artwork actually started.
Component row fields (31)
-
unique_idstring - Your own stable id for this component. The sync key: send it and later calls update in place instead of creating duplicates.
-
namestring required - Display name. Required for every type EXCEPT rulebook, which uses book_name + page_name. Without a unique_id, rows are matched on this instead.
-
quantityinteger - How many exist. Defaults to 1. Ignored for a rulebook, where a row is one page.
-
sizestring - DICE: the die type (d2, d3, d4, d6, d8, d10, d12, d20). Every other type: a legacy free-text label that is no longer collected in the app, so use phys_width_in / phys_height_in instead.
-
descriptionstring - Free text shown to the designer.
-
notesstring - Private free text.
-
image_urlurl - Front artwork. Any public URL, or one returned by the asset endpoints.
-
back_image_urlurl - Back face, shown when the component is flipped.
-
card_mask_typestring - Shape for a card. A
valuefrom GET /masks, or one of the designer's custom mask names. Sendrectangleto reset it to a plain rectangle. -
shapestring - Shape for a token. Same value space as card_mask_type.
-
mask_imageboolean - Trace the artwork's own transparency instead of applying a geometric shape. Set this when you upload die-cut art on a transparent background (a meeple, a standee, an acrylic bit); without it the piece is cropped to its
shape, which is a circle by default.colorthen tints the silhouette, so send a fully transparent colour if you want the art untinted. -
colorhex - Front colour, e.g. #ffffff. Used when there is no artwork.
-
color_backhex - Back-face colour.
-
deck_namesarray|string - Cards only. The decks this card belongs to, as an array of names (a pipe-delimited string also works). Decks are created if they do not exist. This sets WHICH decks a card is in; for the ORDER of cards inside one, use the group members endpoint.
-
stack_namesarray|string - Tokens only. The stacks this token belongs to, same shape as deck_names.
-
phys_width_innumber - Real width in inches; drives tabletop scale. A poker card is 2.5.
-
phys_height_innumber - Real height in inches. A poker card is 3.5.
-
image_rotationinteger - Artwork rotation: 0, 90, 180 or 270.
-
facinginteger - Which edge is up: 0, 90, 180 or 270.
-
playtest_scale_xnumber - Fallback width scale when no physical size is set.
-
playtest_scale_ynumber - Fallback height scale.
-
sort_orderinteger - Ordering within its type.
-
die_facesarray|string - Dice only. Face image URLs, one per face, in face order. An array is the normal form; a pipe-delimited string also works. This is the same field a component reports back as
die_faces. The older namedie_face_urlsstill works. -
die_face_labelsstring - Dice only. Pipe-delimited face labels, or a JSON array.
-
dice_stylestring - Dice only. standard or custom.
-
spinner_sectionsjson - Spinner only. Array of sections.
-
book_namestring - Rulebook only, and REQUIRED there. Which book the page belongs to.
-
page_namestring - Rulebook only, and REQUIRED there. The page title. A rulebook row has no
name; its name is taken from this. -
materialstring - Free text, e.g. cardstock.
-
weight_lbsnumber - Per-unit weight.
-
cost_per_unitnumber - Per-unit cost.
Which of these a type honours differs. See fields per component type.
/api/v1/projects/{project}/components
Scope
components:write
Delete every component this app created
Never a full wipe. Scoped to this app's own rows; hand-made and other apps' components are untouched. Optional type narrows it further.
curl -X DELETE "https://dustinsdesignerden.com/api/v1/projects/1/components" \
-H "Authorization: Bearer $DDD_TOKEN"
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/components', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
$response = Http::withToken($token)->delete('https://dustinsdesignerden.com/api/v1/projects/1/components');
$data = $response->json();
res = requests.delete(
'https://dustinsdesignerden.com/api/v1/projects/1/components',
headers={'Authorization': f'Bearer {token}'},
)
data = res.json()
{
"deleted": 2
}
Parameters
-
projectinteger · path required - Project id.
-
typestring · query - One of: card, token, dice, board, rulebook, spinner, coin, bag, counter, timer, packaging, other, piece, cube
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}/components/batch
Scope
components:write
Create or update components in bulk (the primary write path)
Send up to 500 rows. unique_id is the sync key: the first call creates, later calls with the same ids update in place.
Optional flags: match_by_name (match on name when ids differ), remove_missing (delete rows THIS app created that the batch omitted), internalize_images (copy external artwork into the designer's storage, which spends their quota, off by default).
The whole batch is one transaction; per-row problems come back in errors while valid rows still apply.
curl -X POST "https://dustinsdesignerden.com/api/v1/projects/1/components/batch" \
-H "Authorization: Bearer $DDD_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "card",
"remove_missing": false,
"components": [
{
"unique_id": "ext-card-001",
"name": "Rusty Dagger",
"quantity": 3,
"image_url": "https://cdn.example.com/dagger.png",
"card_mask_type": "rectangle_rounded",
"deck_names": [
"Weapons"
],
"phys_width_in": 2.5,
"phys_height_in": 3.5
},
{
"unique_id": "ext-card-002",
"name": "Iron Shield",
"card_mask_type": "rectangle_rounded",
"deck_names": [
"Weapons"
]
}
]
}'
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/components/batch', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"type": "card",
"remove_missing": false,
"components": [
{
"unique_id": "ext-card-001",
"name": "Rusty Dagger",
"quantity": 3,
"image_url": "https://cdn.example.com/dagger.png",
"card_mask_type": "rectangle_rounded",
"deck_names": [
"Weapons"
],
"phys_width_in": 2.5,
"phys_height_in": 3.5
},
{
"unique_id": "ext-card-002",
"name": "Iron Shield",
"card_mask_type": "rectangle_rounded",
"deck_names": [
"Weapons"
]
}
]
}),
});
const data = await res.json();
$response = Http::withToken($token)->post('https://dustinsdesignerden.com/api/v1/projects/1/components/batch', [
'type' => 'card',
'remove_missing' => false,
'components' => [
[
'unique_id' => 'ext-card-001',
'name' => 'Rusty Dagger',
'quantity' => 3,
'image_url' => 'https://cdn.example.com/dagger.png',
'card_mask_type' => 'rectangle_rounded',
'deck_names' => [
'Weapons',
],
'phys_width_in' => 2.5,
'phys_height_in' => 3.5,
],
[
'unique_id' => 'ext-card-002',
'name' => 'Iron Shield',
'card_mask_type' => 'rectangle_rounded',
'deck_names' => [
'Weapons',
],
],
],
]);
$data = $response->json();
res = requests.post(
'https://dustinsdesignerden.com/api/v1/projects/1/components/batch',
headers={'Authorization': f'Bearer {token}'},
json={
'type': 'card',
'remove_missing': False,
'components': [
{
'unique_id': 'ext-card-001',
'name': 'Rusty Dagger',
'quantity': 3,
'image_url': 'https://cdn.example.com/dagger.png',
'card_mask_type': 'rectangle_rounded',
'deck_names': [
'Weapons'
],
'phys_width_in': 2.5,
'phys_height_in': 3.5
},
{
'unique_id': 'ext-card-002',
'name': 'Iron Shield',
'card_mask_type': 'rectangle_rounded',
'deck_names': [
'Weapons'
]
}
]
},
)
data = res.json()
{
"created": 2,
"updated": 0,
"deleted": 0,
"errors": [],
"kept_artwork": [],
"components": [
{
"id": 1,
"name": "Rusty Dagger",
"type": "card"
},
{
"id": 2,
"name": "Iron Shield",
"type": "card"
}
],
"internalizing": false
}
Parameters
-
projectinteger · path required - Project id.
Body parameters
-
typestring required - One component type per call. See the type list above.
-
componentsarray required - Up to 500 component rows. Fields below.
-
match_by_nameboolean - Overwrite a same-named component even when your unique_id differs. Useful for a first import over hand-made rows.
-
remove_missingboolean - Delete components THIS app created that the batch left out. Reconciles the whole feed in one call. Defaults to false.
-
internalize_imagesboolean - Copy external artwork into the designer's storage. Spends their quota, so it defaults to false.
Response: Write result
-
createdinteger - Rows that did not exist and were inserted.
-
updatedinteger - Rows matched by unique_id (or name) and updated in place.
-
deletedinteger - Rows removed because remove_missing was set and the batch omitted them.
-
errorsarray - Per-row problems. Rows that succeeded still applied, so always read this even on a 200.
-
kept_artworkarray - Names of components the batch matched but sent no artwork for, so they kept the artwork they already had. Usually means you forgot an image_url.
-
componentsarray - The components created or updated, with id, name and type.
-
internalizingboolean - True if internalize_images was set and a background copy of external artwork actually started.
Component row fields (31)
-
unique_idstring - Your own stable id for this component. The sync key: send it and later calls update in place instead of creating duplicates.
-
namestring required - Display name. Required for every type EXCEPT rulebook, which uses book_name + page_name. Without a unique_id, rows are matched on this instead.
-
quantityinteger - How many exist. Defaults to 1. Ignored for a rulebook, where a row is one page.
-
sizestring - DICE: the die type (d2, d3, d4, d6, d8, d10, d12, d20). Every other type: a legacy free-text label that is no longer collected in the app, so use phys_width_in / phys_height_in instead.
-
descriptionstring - Free text shown to the designer.
-
notesstring - Private free text.
-
image_urlurl - Front artwork. Any public URL, or one returned by the asset endpoints.
-
back_image_urlurl - Back face, shown when the component is flipped.
-
card_mask_typestring - Shape for a card. A
valuefrom GET /masks, or one of the designer's custom mask names. Sendrectangleto reset it to a plain rectangle. -
shapestring - Shape for a token. Same value space as card_mask_type.
-
mask_imageboolean - Trace the artwork's own transparency instead of applying a geometric shape. Set this when you upload die-cut art on a transparent background (a meeple, a standee, an acrylic bit); without it the piece is cropped to its
shape, which is a circle by default.colorthen tints the silhouette, so send a fully transparent colour if you want the art untinted. -
colorhex - Front colour, e.g. #ffffff. Used when there is no artwork.
-
color_backhex - Back-face colour.
-
deck_namesarray|string - Cards only. The decks this card belongs to, as an array of names (a pipe-delimited string also works). Decks are created if they do not exist. This sets WHICH decks a card is in; for the ORDER of cards inside one, use the group members endpoint.
-
stack_namesarray|string - Tokens only. The stacks this token belongs to, same shape as deck_names.
-
phys_width_innumber - Real width in inches; drives tabletop scale. A poker card is 2.5.
-
phys_height_innumber - Real height in inches. A poker card is 3.5.
-
image_rotationinteger - Artwork rotation: 0, 90, 180 or 270.
-
facinginteger - Which edge is up: 0, 90, 180 or 270.
-
playtest_scale_xnumber - Fallback width scale when no physical size is set.
-
playtest_scale_ynumber - Fallback height scale.
-
sort_orderinteger - Ordering within its type.
-
die_facesarray|string - Dice only. Face image URLs, one per face, in face order. An array is the normal form; a pipe-delimited string also works. This is the same field a component reports back as
die_faces. The older namedie_face_urlsstill works. -
die_face_labelsstring - Dice only. Pipe-delimited face labels, or a JSON array.
-
dice_stylestring - Dice only. standard or custom.
-
spinner_sectionsjson - Spinner only. Array of sections.
-
book_namestring - Rulebook only, and REQUIRED there. Which book the page belongs to.
-
page_namestring - Rulebook only, and REQUIRED there. The page title. A rulebook row has no
name; its name is taken from this. -
materialstring - Free text, e.g. cardstock.
-
weight_lbsnumber - Per-unit weight.
-
cost_per_unitnumber - Per-unit cost.
Which of these a type honours differs. See fields per component type.
/api/v1/projects/{project}/components/{uniqueId}
Scope
components:write
Delete one component
Deletes one component, addressed by its unique_id.
Only rows your own app created. A component the designer made by hand, or one another integration created, returns 403 forbidden. Deletes are scoped by the same source stamp your writes carry, so one tool can never remove another's work.
curl -X DELETE "https://dustinsdesignerden.com/api/v1/projects/1/components/ext-card-002" \
-H "Authorization: Bearer $DDD_TOKEN"
const res = await fetch('https://dustinsdesignerden.com/api/v1/projects/1/components/ext-card-002', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
$response = Http::withToken($token)->delete('https://dustinsdesignerden.com/api/v1/projects/1/components/ext-card-002');
$data = $response->json();
res = requests.delete(
'https://dustinsdesignerden.com/api/v1/projects/1/components/ext-card-002',
headers={'Authorization': f'Bearer {token}'},
)
data = res.json()
{
"deleted": true
}
Parameters
-
projectinteger · path required - Project id.
-
uniqueIdstring · path required - The component's
unique_id. This endpoint takes the unique_id only, never our numericid.
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.