API / API reference

Components

The primary write path.

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}/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()
Response 200
{
    "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

project integer · path required
Project id.
type string · 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_page integer · query
Defaults to 50

Response: Pagination

data array
The page of results.
links object
first, last, prev and next URLs.
meta object
current_page, last_page, per_page, from, to, total and path.

Response: Component

id integer
Our internal row id, returned for reference. Everything you send us is keyed on unique_id instead.
unique_id string
Your id if you sent one, otherwise one we generated. Never null. This is what later writes match on.
type string
Component type. See the type list under Components.
name string
Display name. For a rulebook page this is its page_name.
book_name string|null
Rulebook pages only: which book the page belongs to.
quantity integer
How many exist.
description string|null
Free text.
notes string|null
Private free text.
size string|null
Die type for dice (d6, d20). A legacy label on other types.
color string|null
Front colour, used when there is no artwork.
color_back string|null
Back-face colour.
shape string|null
Token shape.
card_mask_type string|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.
material string|null
Free text.
image_url string|null
Front artwork.
back_image_url string|null
Back artwork.
image_rotation integer
Artwork rotation: 0, 90, 180 or 270.
facing integer|null
Which edge is up: 0, 90, 180 or 270. Null means it has never been set and the tabletop default applies.
phys_width_in number|null
Real width in inches.
phys_height_in number|null
Real height in inches.
weight_lbs number|null
Per-unit weight.
cost_per_unit number|null
Per-unit cost.
page_name string|null
Rulebook pages only: the page title.
sort_order integer|null
Ordering within its type. Page order for a rulebook.
flippable boolean
Whether it can be flipped on the tabletop.
playtest_scale_x number|null
Fallback width scale used when no physical size is set.
playtest_scale_y number|null
Fallback height scale.
die_faces array|null
Dice: resolved face image URLs.
die_face_labels array|null
Dice: face labels.
dice_style string|null
Dice: standard or custom.
spinner_sections array|null
Spinner: its sections.
deck_names array
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_names field on a write.
stack_names array
Names of the stacks this token is in. Empty for a token in no stack, and for every non-token type.
source string|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_at string
ISO 8601 timestamp.
updated_at string
ISO 8601 timestamp.
POST /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()
Response 201
{
    "created": 1,
    "updated": 0,
    "deleted": 0,
    "errors": [],
    "kept_artwork": [],
    "components": [
        {
            "id": 3,
            "name": "Gold Coin",
            "type": "token"
        }
    ],
    "internalizing": false
}

Parameters

project integer · path required
Project id.

Body parameters

type string required
Component type.
component object required
A single component row. Fields below.
match_by_name boolean
As above.
internalize_images boolean
As above.

Response: Write result

created integer
Rows that did not exist and were inserted.
updated integer
Rows matched by unique_id (or name) and updated in place.
deleted integer
Rows removed because remove_missing was set and the batch omitted them.
errors array
Per-row problems. Rows that succeeded still applied, so always read this even on a 200.
kept_artwork array
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.
components array
The components created or updated, with id, name and type.
internalizing boolean
True if internalize_images was set and a background copy of external artwork actually started.
Component row fields (31)
unique_id string
Your own stable id for this component. The sync key: send it and later calls update in place instead of creating duplicates.
name string 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.
quantity integer
How many exist. Defaults to 1. Ignored for a rulebook, where a row is one page.
size string
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.
description string
Free text shown to the designer.
notes string
Private free text.
image_url url
Front artwork. Any public URL, or one returned by the asset endpoints.
back_image_url url
Back face, shown when the component is flipped.
card_mask_type string
Shape for a card. A value from GET /masks, or one of the designer's custom mask names. Send rectangle to reset it to a plain rectangle.
shape string
Shape for a token. Same value space as card_mask_type.
mask_image boolean
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. color then tints the silhouette, so send a fully transparent colour if you want the art untinted.
color hex
Front colour, e.g. #ffffff. Used when there is no artwork.
color_back hex
Back-face colour.
deck_names array|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_names array|string
Tokens only. The stacks this token belongs to, same shape as deck_names.
phys_width_in number
Real width in inches; drives tabletop scale. A poker card is 2.5.
phys_height_in number
Real height in inches. A poker card is 3.5.
image_rotation integer
Artwork rotation: 0, 90, 180 or 270.
facing integer
Which edge is up: 0, 90, 180 or 270.
playtest_scale_x number
Fallback width scale when no physical size is set.
playtest_scale_y number
Fallback height scale.
sort_order integer
Ordering within its type.
die_faces array|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 name die_face_urls still works.
die_face_labels string
Dice only. Pipe-delimited face labels, or a JSON array.
dice_style string
Dice only. standard or custom.
spinner_sections json
Spinner only. Array of sections.
book_name string
Rulebook only, and REQUIRED there. Which book the page belongs to.
page_name string
Rulebook only, and REQUIRED there. The page title. A rulebook row has no name; its name is taken from this.
material string
Free text, e.g. cardstock.
weight_lbs number
Per-unit weight.
cost_per_unit number
Per-unit cost.

Which of these a type honours differs. See fields per component type.

DELETE /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()
Response 200
{
    "deleted": 2
}

Parameters

project integer · path required
Project id.
type string · query
One of: card, token, dice, board, rulebook, spinner, coin, bag, counter, timer, packaging, other, piece, cube

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.
POST /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()
Response 201
{
    "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

project integer · path required
Project id.

Body parameters

type string required
One component type per call. See the type list above.
components array required
Up to 500 component rows. Fields below.
match_by_name boolean
Overwrite a same-named component even when your unique_id differs. Useful for a first import over hand-made rows.
remove_missing boolean
Delete components THIS app created that the batch left out. Reconciles the whole feed in one call. Defaults to false.
internalize_images boolean
Copy external artwork into the designer's storage. Spends their quota, so it defaults to false.

Response: Write result

created integer
Rows that did not exist and were inserted.
updated integer
Rows matched by unique_id (or name) and updated in place.
deleted integer
Rows removed because remove_missing was set and the batch omitted them.
errors array
Per-row problems. Rows that succeeded still applied, so always read this even on a 200.
kept_artwork array
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.
components array
The components created or updated, with id, name and type.
internalizing boolean
True if internalize_images was set and a background copy of external artwork actually started.
Component row fields (31)
unique_id string
Your own stable id for this component. The sync key: send it and later calls update in place instead of creating duplicates.
name string 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.
quantity integer
How many exist. Defaults to 1. Ignored for a rulebook, where a row is one page.
size string
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.
description string
Free text shown to the designer.
notes string
Private free text.
image_url url
Front artwork. Any public URL, or one returned by the asset endpoints.
back_image_url url
Back face, shown when the component is flipped.
card_mask_type string
Shape for a card. A value from GET /masks, or one of the designer's custom mask names. Send rectangle to reset it to a plain rectangle.
shape string
Shape for a token. Same value space as card_mask_type.
mask_image boolean
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. color then tints the silhouette, so send a fully transparent colour if you want the art untinted.
color hex
Front colour, e.g. #ffffff. Used when there is no artwork.
color_back hex
Back-face colour.
deck_names array|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_names array|string
Tokens only. The stacks this token belongs to, same shape as deck_names.
phys_width_in number
Real width in inches; drives tabletop scale. A poker card is 2.5.
phys_height_in number
Real height in inches. A poker card is 3.5.
image_rotation integer
Artwork rotation: 0, 90, 180 or 270.
facing integer
Which edge is up: 0, 90, 180 or 270.
playtest_scale_x number
Fallback width scale when no physical size is set.
playtest_scale_y number
Fallback height scale.
sort_order integer
Ordering within its type.
die_faces array|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 name die_face_urls still works.
die_face_labels string
Dice only. Pipe-delimited face labels, or a JSON array.
dice_style string
Dice only. standard or custom.
spinner_sections json
Spinner only. Array of sections.
book_name string
Rulebook only, and REQUIRED there. Which book the page belongs to.
page_name string
Rulebook only, and REQUIRED there. The page title. A rulebook row has no name; its name is taken from this.
material string
Free text, e.g. cardstock.
weight_lbs number
Per-unit weight.
cost_per_unit number
Per-unit cost.

Which of these a type honours differs. See fields per component type.

DELETE /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()
Response 200
{
    "deleted": true
}

Parameters

project integer · path required
Project id.
uniqueId string · path required
The component's unique_id. This endpoint takes the unique_id only, never our numeric 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.