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

bash
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

bash
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.

json
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:

json
{
  "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:

json
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.
  • count is how many copies of that component the deck holds. face_up is true for front up and false for 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:

bash
curl https://dustinsdesignerden.com/api/v1/masks \
-H "Authorization: Bearer YOUR_TOKEN"
json
{
  "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 value verbatim. rectangle is a card's default.
  • source is either preset, a shape the app ships, or custom, one this designer drew in the Mask Editor. Custom masks differ per account, so never hardcode the list.
  • aspect is the one ratio each shape carries, written the way ratios are written: 1:1 square, 5:7 a portrait card, 7:5 landscape. It is the ratio the shape was authored at, so divide it yourself if your layout needs a number. null means 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.

json
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

json
{
  "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.