API / Guides
Syncing components
The order to do things in, and why unique_id matters.
The sync recipe
The order that avoids the common mistakes. Artwork first, components second, because components carry image URLs.
The one thing to get right: unique_id
Send your own stable internal id on every component. The first push creates; every later push with the same ids updates in place. Without it, rows fall back to matching on name, and a rename becomes a duplicate. This is the difference between a sync and re-importing the whole deck every time.
1. Choose how artwork gets there
Three options, and the tradeoff is real:
Link to your own CDN
Put your public URL straight in image_url. Costs the designer
zero storage. The art lives and dies with your CDN.
Hand over a URL to mirror
curl -X POST https://dustinsdesignerden.com/api/v1/projects/PROJECT_ID/assets/from-url \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://cdn.example.com/dagger.png"}'
The server fetches it and stores a copy in the designer's own storage. Survives
independently of you, but spends their quota. Returns a url
to use in the next step.
Upload the bytes
curl -X POST https://dustinsdesignerden.com/api/v1/projects/PROJECT_ID/assets \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@dagger.png"
Images only (jpeg, png, gif, webp, avif, svg), 50 MB per file. Add
-F convert_to_webp=1 or
-F convert_to_avif=1 to shrink it on the way in.
2. Push components in batches
Up to 500 rows per call, one component
type per call. The whole batch is a single transaction.
POST /api/v1/projects/42/components/batch
{
"type": "card",
"remove_missing": true,
"components": [
{
"unique_id": "dxt-card-8817",
"name": "Rusty Dagger",
"quantity": 3,
"image_url": "https://.../dagger-front.avif",
"back_image_url": "https://.../card-back.avif",
"card_mask_type": "rectangle_rounded",
"deck_names": ["Weapons", "Starter Deck"]
}
]
}
remove_missing
Deletes components your app created that this batch left out. Reconciles the whole feed in one call so you do not have to track deletions. Off by default.
match_by_name
Overwrite a same-named component even when your unique_id differs.
Useful for a first import over hand-made rows.
internalize_images
Copies external artwork into the designer's storage. Spends their quota, so it is off by default.
The response tells you exactly what happened. Per-row problems come back in
errors while the valid rows still apply, so always read it:
{
"created": 1,
"updated": 0,
"deleted": 0,
"errors": [],
"kept_artwork": [],
"components": [
{ "id": 918, "name": "Rusty Dagger", "type": "card" }
],
"internalizing": false
}
3. Decks and stacks, including card order
deck_names on a component write is the quick way to drop a
card into a deck, but it cannot say what ORDER the deck is in. When order matters, set the
whole membership in one call:
PUT /api/v1/projects/42/groups/7/members
{
"members": [
{ "unique_id": "ext-card-001", "count": 2 },
{ "unique_id": "ext-card-002" },
{ "unique_id": "ext-card-003", "face_up": false }
]
}
- Array order is card order. Position is taken from the array, so you never set it by hand.
- Members are identified by your own
unique_id, and only that, so you never have to read back an id this API assigned. The lookup is scoped to this project. countis how many copies of that component the deck holds.face_upistruefor front up andfalsefor face down; leave it out and the entry is dealt however the group is.- Send
"members": []to empty a deck without deleting any component. - Deleting a group removes the grouping only. The components stay.
Two rules a deck must satisfy
A deck holds only cards and a stack only tokens, and every member must share the same shape/mask. A mixed deck is rejected with a message naming the shapes it mixed, rather than quietly dropping cards, because it would render wrong on the tabletop. These are the same rules the in-app Deck & Stack Builder enforces.
4. Component types
Valid values for type:
card
token
dice
board
rulebook
spinner
coin
bag
counter
timer
Older components may still carry a legacy type (other, piece or cube).
Those cannot be created: other was folded into token once tokens
could use their own image as a mask. You can still read them, and
GET /components?type= accepts them, so you can list
the old rows in order to migrate them onto a current type.
5. Picking a shape
Shapes go in card_mask_type (cards) or
shape (tokens). Do not hardcode the list: the
default: library changes between releases, and every
designer has their own custom masks. Fetch the vocabulary instead:
curl https://dustinsdesignerden.com/api/v1/masks \
-H "Authorization: Bearer YOUR_TOKEN"
{
"card": [
{ "value": "rectangle", "label": "Rectangle", "source": "preset", "aspect": null },
{ "value": "flat", "label": "Hexagon (Flat)", "source": "preset", "aspect": "1:1" },
{ "value": "default:star", "label": "Star", "source": "preset", "aspect": "1:1" },
{ "value": "custom:8", "label": "My Swoosh", "source": "custom", "aspect": "8:7" }
],
"token": [ ... ]
}
- Send
valueverbatim.rectangleis a card's default. sourceis eitherpreset, a shape the app ships, orcustom, one this designer drew in the Mask Editor. Custom masks differ per account, so never hardcode the list.aspectis the one ratio each shape carries, written the way ratios are written:1:1square,5:7a portrait card,7:5landscape. It is the ratio the shape was authored at, so divide it yourself if your layout needs a number.nullmeans the shape forces nothing and the component keeps its own physical size. A shaped mask has to force its box, because a hexagon squeezed into a 2.5×3.5 card box is not a hexagon.- You can also just send a custom mask's name on a component write and it is resolved to the right reference for you.
6. Fields, per component type
Types are not interchangeable. A rulebook row is a page and needs different fields
from a card, and size means the die type on dice but
a legacy label everywhere else. Each type below lists exactly the fields it honours;
anything else you send for that type is ignored.
Add deck_names to file it into decks. For deck ORDER use the group members endpoint.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
back_image_url
color_back
card_mask_type
deck_names
sort_order
* required
shape takes the same values as a card's card_mask_type. A token can use its own image as its mask.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
back_image_url
color_back
shape
stack_names
* required
size is the DIE TYPE: d2, d3, d4, d6, d8, d10, d12 or d20. Set dice_style to custom to use your own face images.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
die_faces
die_face_labels
dice_style
* required
Boards support a back face.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
back_image_url
color_back
* required
A rulebook row is ONE PAGE. book_name and page_name are required instead of name, and quantity is always 1. Send one row per page and use sort_order for page order.
unique_id
book_name *
page_name *
sort_order
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
* required
spinner_sections is a JSON array of sections; any image_url inside a section is resolved like a top-level one.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
spinner_sections
* required
Coins support a back face and can be flipped.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
back_image_url
color_back
* required
A container pieces are drawn from.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
* required
A numeric tracker.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
* required
A countdown or stopwatch.
unique_id
name *
quantity
description
notes
size
color
material
image_url
image_rotation
facing
phys_width_in
phys_height_in
playtest_scale_x
playtest_scale_y
weight_lbs
cost_per_unit
* required
What each field means
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 | 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. |
A rulebook, end to end
One row per page. There is no name: the page title goes
in page_name, and pages are grouped by
book_name.
POST /api/v1/projects/42/components/batch
{
"type": "rulebook",
"components": [
{
"unique_id": "rb-p1",
"book_name": "Core Rules",
"page_name": "Cover",
"sort_order": 0,
"image_url": "https://cdn.example.com/rules-01.png"
},
{
"unique_id": "rb-p2",
"book_name": "Core Rules",
"page_name": "Setup",
"sort_order": 1,
"image_url": "https://cdn.example.com/rules-02.png"
},
{
"unique_id": "rb-p3",
"book_name": "Core Rules",
"page_name": "Turn Order",
"sort_order": 2,
"image_url": "https://cdn.example.com/rules-03.png"
}
]
}
Dice with custom faces
{
"type": "dice",
"components": [
{
"unique_id": "die-1",
"name": "Combat Die",
"size": "d6",
"dice_style": "custom",
"die_faces": [
"https://cdn.example.com/f1.png",
"https://cdn.example.com/f2.png"
],
"die_face_labels": ["Hit", "Miss"]
}
]
}
7. What your app can and cannot touch
- Deletes only ever reach components your app created. Hand-made components and other integrations' rows are invisible to your delete calls.
- A locked project refuses every write with
409 project_locked. - Asset deletes are confined to the project's own folder, so you cannot remove an avatar or another project's art.
- Nothing in the API reaches billing, subscriptions, playtest sessions, feedback, or another designer's data.