{
    "openapi": "3.1.0",
    "info": {
        "title": "Dustin's Designer Den API",
        "version": "1.0.0",
        "description": "Programmatic access for external board game design tools: create projects, upload artwork, and sync components into a designer's account.\n\nFree to use. Requires an account and an OAuth token. See https://dev.dustinsdesignerden.com/developers/api for the integration guide."
    },
    "servers": [
        {
            "url": "https://dev.dustinsdesignerden.com/api/v1"
        }
    ],
    "components": {
        "securitySchemes": {
            "oauth2": {
                "type": "oauth2",
                "description": "Authorization code with PKCE. Personal access tokens use the same bearer format.",
                "flows": {
                    "authorizationCode": {
                        "authorizationUrl": "https://dev.dustinsdesignerden.com/oauth/authorize",
                        "tokenUrl": "https://dev.dustinsdesignerden.com/oauth/token",
                        "refreshUrl": "https://dev.dustinsdesignerden.com/oauth/token",
                        "scopes": {
                            "projects:read": "See your projects and their details",
                            "projects:write": "Create new projects, and edit the details of your existing ones, including the description shown publicly",
                            "assets:read": "See the files stored in your projects",
                            "assets:write": "Upload and delete files in your storage",
                            "components:read": "See the components in your projects, how they are grouped into decks and stacks, and your custom mask shapes",
                            "components:write": "Create and update components, decks and stacks, and delete the ones it created",
                            "webhooks:write": "Get notified when your projects or components change"
                        }
                    }
                }
            }
        },
        "schemas": {
            "PaginationLinks": {
                "type": "object",
                "properties": {
                    "first": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "last": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "prev": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "next": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri",
                        "description": "null on the last page. Follow this rather than counting pages yourself."
                    }
                }
            },
            "PaginationMeta": {
                "type": "object",
                "properties": {
                    "current_page": {
                        "type": "integer"
                    },
                    "last_page": {
                        "type": "integer"
                    },
                    "per_page": {
                        "type": "integer",
                        "description": "Clamped server-side; asking for more than the maximum returns the maximum."
                    },
                    "from": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "to": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "total": {
                        "type": "integer"
                    },
                    "path": {
                        "type": "string"
                    }
                }
            },
            "Project": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string"
                    },
                    "short_description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "is_locked": {
                        "type": "boolean",
                        "description": "A locked project refuses every write with 409."
                    },
                    "player_count_min": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "player_count_max": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "playtime_minutes": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "min_age": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "genre": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "theme": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "primary_mechanic": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "edition": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "logo_url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "backdrop_url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "website_url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "bgg_id": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "component_count": {
                        "type": "integer",
                        "description": "Only present on the list endpoint, which counts them."
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                }
            },
            "Component": {
                "type": "object",
                "description": "A component as it is stored. `unique_id` is the sync key you address it by.",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "unique_id": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "type": {
                        "type": "string"
                    },
                    "name": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "book_name": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Rulebooks only: which book this page belongs to."
                    },
                    "quantity": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "notes": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "size": {
                        "type": [
                            "string",
                            "integer",
                            "null"
                        ]
                    },
                    "color": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "color_back": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "shape": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Tokens: published under the same name GET /masks offers, so a value you send reads back as itself."
                    },
                    "card_mask_type": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Cards: as `shape`. The rectangle mask is `default:rectangle`; no mask at all is `none`. They are different options."
                    },
                    "material": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "image_url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "back_image_url": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "uri"
                    },
                    "image_rotation": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "facing": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "phys_width_in": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "phys_height_in": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "weight_lbs": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "cost_per_unit": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "page_name": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "sort_order": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "flippable": {
                        "type": "boolean"
                    },
                    "playtest_scale_x": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "playtest_scale_y": {
                        "type": [
                            "number",
                            "null"
                        ]
                    },
                    "die_faces": {
                        "type": [
                            "array",
                            "null"
                        ],
                        "items": {
                            "type": "string"
                        },
                        "description": "Face IMAGES, not a count. The number of faces is `size`."
                    },
                    "die_face_labels": {
                        "type": [
                            "array",
                            "null"
                        ],
                        "items": {
                            "type": "string"
                        }
                    },
                    "dice_style": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "spinner_sections": {
                        "type": [
                            "array",
                            "null"
                        ],
                        "items": {
                            "type": "object"
                        }
                    },
                    "deck_names": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "The decks this card belongs to. The same values `deck_names` takes on write."
                    },
                    "stack_names": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "source": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Which integration owns this row. Your deletes only ever reach rows carrying your own tag."
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                }
            },
            "ComponentGroup": {
                "type": "object",
                "description": "A deck or a stack: an ordered group of components.",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "kind": {
                        "type": "string",
                        "enum": [
                            "deck",
                            "stack"
                        ]
                    },
                    "name": {
                        "type": "string"
                    },
                    "position": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "member_type": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "card",
                            "token",
                            null
                        ],
                        "description": "What this group holds: decks hold cards, stacks hold tokens."
                    },
                    "member_count": {
                        "type": "integer",
                        "description": "Present when members were loaded."
                    },
                    "members": {
                        "type": "array",
                        "description": "Present when members were loaded. Array order IS the deck order.",
                        "items": {
                            "$ref": "#/components/schemas/GroupMember"
                        }
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                }
            },
            "GroupMember": {
                "type": "object",
                "properties": {
                    "unique_id": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "What you pass back to address this member."
                    },
                    "component_id": {
                        "type": "integer",
                        "description": "Our internal row id, for reference only. Not accepted on write."
                    },
                    "name": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "position": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "count": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "face_up": {
                        "type": [
                            "boolean",
                            "null"
                        ],
                        "description": "Whether this entry starts front-side up. null means \"however the group is dealt\"."
                    },
                    "source": {
                        "type": [
                            "string",
                            "null"
                        ]
                    }
                }
            },
            "Asset": {
                "type": "object",
                "properties": {
                    "key": {
                        "type": "string",
                        "description": "The storage key. This is what DELETE takes."
                    },
                    "name": {
                        "type": "string"
                    },
                    "url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "size": {
                        "type": "integer",
                        "description": "Bytes."
                    },
                    "last_modified": {
                        "type": [
                            "string",
                            "integer",
                            "null"
                        ]
                    }
                }
            },
            "StoredAsset": {
                "type": "object",
                "description": "Send `url` straight back as a component's `image_url`.",
                "properties": {
                    "key": {
                        "type": "string"
                    },
                    "url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "size": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string"
                    }
                }
            },
            "Webhook": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "events": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "is_active": {
                        "type": "boolean"
                    },
                    "last_delivered_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "last_status": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "HTTP status your endpoint answered with last time."
                    },
                    "consecutive_failures": {
                        "type": "integer"
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                }
            },
            "WebhookCreated": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Webhook"
                    },
                    {
                        "type": "object",
                        "required": [
                            "secret"
                        ],
                        "properties": {
                            "secret": {
                                "type": "string",
                                "description": "The signing secret. Returned ONCE, here, and never again. Store it now; it is what you verify deliveries against."
                            }
                        }
                    }
                ]
            },
            "Account": {
                "type": "object",
                "properties": {
                    "user": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "integer"
                            },
                            "name": {
                                "type": "string"
                            },
                            "email": {
                                "type": "string",
                                "format": "email"
                            }
                        }
                    },
                    "storage": {
                        "type": "object",
                        "properties": {
                            "used_bytes": {
                                "type": "integer"
                            },
                            "limit_bytes": {
                                "type": "integer"
                            },
                            "remaining_bytes": {
                                "type": "integer",
                                "description": "Check this before a large upload rather than discovering the quota at 422."
                            },
                            "used_human": {
                                "type": "string"
                            },
                            "limit_human": {
                                "type": "string"
                            }
                        }
                    },
                    "projects": {
                        "type": "object",
                        "properties": {
                            "count": {
                                "type": "integer"
                            },
                            "unlocked_count": {
                                "type": "integer"
                            },
                            "can_create": {
                                "type": "boolean"
                            },
                            "unlocked_limit": {
                                "type": [
                                    "integer",
                                    "null"
                                ],
                                "description": "null means uncapped."
                            }
                        }
                    },
                    "token": {
                        "type": "object",
                        "properties": {
                            "scopes": {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "description": "What this token actually holds. Check here rather than assuming what you asked for was granted."
                            },
                            "app": {
                                "type": "string",
                                "description": "The tag your writes are stamped with (`api:{client}`, or `api:personal`)."
                            }
                        }
                    }
                }
            },
            "MaskOptions": {
                "type": "object",
                "required": [
                    "card",
                    "token"
                ],
                "properties": {
                    "card": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/MaskOption"
                        },
                        "description": "Every value a card's `card_mask_type` accepts."
                    },
                    "token": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/MaskOption"
                        },
                        "description": "Every value a token's `shape` accepts."
                    }
                }
            },
            "MaskOption": {
                "type": "object",
                "required": [
                    "value",
                    "label",
                    "source",
                    "aspect"
                ],
                "properties": {
                    "value": {
                        "type": "string",
                        "description": "Send verbatim. Never blank: `none` is no mask at all, every built-in shape is `default:{key}` (the rectangle mask being `default:rectangle`), and a designer's own are `custom:{id}`."
                    },
                    "label": {
                        "type": "string",
                        "description": "Human name, as the designer sees it in the app."
                    },
                    "source": {
                        "type": "string",
                        "enum": [
                            "preset",
                            "custom"
                        ]
                    },
                    "aspect": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The footprint this shape forces, as a ratio such as `1:1` or `5:7`. null means it forces nothing and the component keeps its own physical size."
                    }
                }
            },
            "Deleted": {
                "type": "object",
                "required": [
                    "deleted"
                ],
                "properties": {
                    "deleted": {
                        "type": "boolean",
                        "const": true
                    }
                }
            },
            "DeletedCount": {
                "type": "object",
                "required": [
                    "deleted"
                ],
                "properties": {
                    "deleted": {
                        "type": "integer",
                        "description": "How many rows were removed. Bulk deletes report a count where single deletes report `true`."
                    }
                }
            },
            "GroupDeleted": {
                "type": "object",
                "required": [
                    "deleted",
                    "components_retained"
                ],
                "properties": {
                    "deleted": {
                        "type": "boolean",
                        "const": true
                    },
                    "components_retained": {
                        "type": "boolean",
                        "description": "Confirms removing the deck or stack left its components in the project."
                    }
                }
            },
            "Error": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "object",
                        "required": [
                            "code",
                            "message"
                        ],
                        "properties": {
                            "code": {
                                "type": "string",
                                "example": "project_locked"
                            },
                            "message": {
                                "type": "string"
                            },
                            "details": {
                                "type": "object",
                                "additionalProperties": true
                            }
                        }
                    }
                }
            },
            "ComponentRow": {
                "type": "object",
                "required": [
                    "name"
                ],
                "description": "One component. `unique_id` is the sync key: send your own stable id and later calls update in place instead of duplicating.",
                "properties": {
                    "unique_id": {
                        "type": "string",
                        "description": "Your stable id for this component. Strongly recommended."
                    },
                    "name": {
                        "type": "string"
                    },
                    "quantity": {
                        "type": "integer",
                        "minimum": 1
                    },
                    "description": {
                        "type": "string"
                    },
                    "notes": {
                        "type": "string"
                    },
                    "image_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "back_image_url": {
                        "type": "string",
                        "format": "uri"
                    },
                    "card_mask_type": {
                        "type": "string",
                        "description": "Card shape. A `value` from GET /masks, or one of the designer's custom mask names."
                    },
                    "shape": {
                        "type": "string",
                        "description": "Token shape, same value space as card_mask_type."
                    },
                    "color": {
                        "type": "string"
                    },
                    "color_back": {
                        "type": "string"
                    },
                    "deck_names": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Cards only. Names of the decks this card belongs to. Created if missing. A pipe-delimited string is also accepted."
                    },
                    "stack_names": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Tokens only. Names of the stacks this token belongs to."
                    },
                    "phys_width_in": {
                        "type": "number"
                    },
                    "phys_height_in": {
                        "type": "number"
                    },
                    "sort_order": {
                        "type": "integer"
                    }
                }
            },
            "BatchResult": {
                "type": "object",
                "properties": {
                    "created": {
                        "type": "integer"
                    },
                    "updated": {
                        "type": "integer"
                    },
                    "deleted": {
                        "type": "integer"
                    },
                    "errors": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Per-row problems. Rows that succeeded still applied."
                    },
                    "kept_artwork": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Components the batch matched but sent no artwork for, so they kept what they had."
                    },
                    "components": {
                        "type": "array",
                        "items": {
                            "type": "object"
                        }
                    },
                    "internalizing": {
                        "type": "boolean"
                    }
                }
            }
        }
    },
    "x-webhook-events": {
        "components.changed": "Components were created, updated or deleted in a project",
        "project.updated": "A project's details were changed"
    },
    "x-component-types": [
        "card",
        "token",
        "dice",
        "board",
        "rulebook",
        "spinner",
        "coin",
        "bag",
        "counter",
        "timer",
        "pack"
    ],
    "x-rate-limits": {
        "reads": 300,
        "component_writes": 60,
        "asset_uploads": 30,
        "project_creation_per_hour": 10,
        "global_per_hour": 1000
    },
    "paths": {
        "/masks": {
            "get": {
                "operationId": "masks.index",
                "x-route-name": "api.v1.masks.index",
                "tags": [
                    "Account"
                ],
                "summary": "Every shape a component can take, including the designer's custom masks",
                "security": [
                    {
                        "oauth2": [
                            "components:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Every shape value cards and tokens accept",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MaskOptions"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Returns `card` and `token` arrays. Send an option's `value` verbatim as `card_mask_type` (cards) or `shape` (tokens).\n\nEach option has a `source`: `preset` for a shape the app ships, or `custom` for one this designer drew in the Mask Editor. Custom masks vary per account, so the list cannot be hardcoded.\n\nEvery option carries exactly ONE ratio, `aspect`, 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, not a decimal of it, so it is the same value the designer typed in the Mask Editor. `null` means the shape forces no footprint and the component keeps its own physical size. A shaped mask has to force its box, because a hexagon squeezed into a 2.5x3.5 card box is not a hexagon.\n\nCall this before offering a shape picker. You may also send a custom mask's NAME as the value on a component write and it will be resolved for you: names are unique per designer and matched ignoring case and surrounding spaces, so a name always identifies exactly one mask. The `custom:{id}` form is the stable one, since a rename changes the name but never the id.\n\nEvery built-in shape is published as `default:{shape}`, whichever way the app happens to draw it. `none` means no mask at all, and the rectangle mask is `default:rectangle`; they are different options and both are offered on every type. Bare built-in spellings such as `rectangle` and `circle` are still accepted on write, but every read reports the `default:` form, so pace your comparisons against that.\n\nA shape that is not in this list and is not one of the designer's own mask names is REJECTED with `422`, rather than stored. A value the app does not recognise would come back describing a shape that appears nowhere here, and the next time the designer opened that component the shape dropdown would match nothing, so saving the form would clear the mask.",
                "x-required-scopes": [
                    "components:read"
                ]
            }
        },
        "/me": {
            "get": {
                "operationId": "me",
                "x-route-name": "api.v1.me",
                "tags": [
                    "Account"
                ],
                "summary": "Who this token belongs to, plus quota and plan limits",
                "security": [
                    {
                        "oauth2": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The designer, their storage, and this token",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Account"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Who the token acts for, what it is allowed to do, and how much room the designer has left. A good first call.\n\nWorth reading before an upload or creating a project, so you can say they are out of space or at their project cap instead of failing halfway through. It also reports the scopes the token actually holds, which can be fewer than you asked for, and the `app` value your writes get stamped with, which is the same value that decides which rows your deletes can reach.",
                "x-any-of-scopes": [
                    "projects:read",
                    "projects:write",
                    "assets:read",
                    "assets:write",
                    "components:read",
                    "components:write",
                    "webhooks:write"
                ]
            }
        },
        "/projects": {
            "get": {
                "operationId": "projects.index",
                "x-route-name": "api.v1.projects.index",
                "tags": [
                    "Projects"
                ],
                "summary": "List the projects this token can reach",
                "security": [
                    {
                        "oauth2": [
                            "projects:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of projects",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data",
                                        "links",
                                        "meta"
                                    ],
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Project"
                                            }
                                        },
                                        "links": {
                                            "$ref": "#/components/schemas/PaginationLinks"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/PaginationMeta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Every project this designer owns, newest first, paginated. Each entry carries `component_count`, so you can show a picker without a second call per project.\n\nThis is normally your first call after `/me`: a designer picks which of their games your tool should work on, and the `id` you get back is the `{project}` in almost every other path on this page.",
                "parameters": [
                    {
                        "name": "per_page",
                        "schema": {
                            "type": "integer",
                            "default": 50,
                            "maximum": 200
                        },
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "page",
                        "schema": {
                            "type": "integer"
                        },
                        "in": "query",
                        "required": false
                    }
                ],
                "x-required-scopes": [
                    "projects:read"
                ]
            },
            "post": {
                "operationId": "projects.store",
                "x-route-name": "api.v1.projects.store",
                "tags": [
                    "Projects"
                ],
                "summary": "Create a project",
                "security": [
                    {
                        "oauth2": [
                            "projects:write"
                        ]
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The project that was created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Project"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Subject to the designer's plan limit. A free account is capped, and exceeding it returns 403 `plan_limit_reached`. Surface that message rather than swallowing it.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "name": "Harvest Moon",
                                "short_description": "A farming game."
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "projects:write"
                ]
            }
        },
        "/projects/{project}": {
            "get": {
                "operationId": "projects.show",
                "x-route-name": "api.v1.projects.show",
                "tags": [
                    "Projects"
                ],
                "summary": "Read one project",
                "security": [
                    {
                        "oauth2": [
                            "projects:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The project",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Project"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "One project, with its component count. Use it to refresh something you already know about, for instance to check `is_locked` before you start writing.\n\nA locked project rejects every write with `409 project_locked`, so looking first lets you tell the designer why, instead of failing halfway through a sync.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "x-required-scopes": [
                    "projects:read"
                ]
            },
            "patch": {
                "operationId": "projects.update",
                "x-route-name": "api.v1.projects.update",
                "tags": [
                    "Projects"
                ],
                "summary": "Update a project's details",
                "security": [
                    {
                        "oauth2": [
                            "projects:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The updated project",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Project"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Updates the project's own details, such as its name or description. Send only the fields you want changed; anything you leave out is untouched.\n\nThis never affects components. Deliberately narrow: it cannot alter ownership, lock state or billing. If at least one field really changes, a `project.updated` webhook fires naming the fields that were written.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "genre": "Worker Placement",
                                "playtime_minutes": 45
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "projects:write"
                ]
            }
        },
        "/projects/{project}/assets": {
            "get": {
                "operationId": "assets.index",
                "x-route-name": "api.v1.assets.index",
                "tags": [
                    "Assets"
                ],
                "summary": "List files stored for this project",
                "security": [
                    {
                        "oauth2": [
                            "assets:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Every file stored for this project",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Asset"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Everything in this project's storage folder: a `key`, a public `url` and a size for each file.\n\nThe `key` is what you pass to the delete endpoint. The `url` is what you put in a component's `image_url`. These files count against the designer's storage quota, so `/me` is worth reading before a large upload.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "x-required-scopes": [
                    "assets:read"
                ]
            },
            "post": {
                "operationId": "assets.store",
                "x-route-name": "api.v1.assets.store",
                "tags": [
                    "Assets"
                ],
                "summary": "Upload artwork (multipart)",
                "security": [
                    {
                        "oauth2": [
                            "assets:write"
                        ]
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The stored file",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StoredAsset"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "413": {
                        "description": "Over the 50.0 MB per-file limit (`file_too_large`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed, the file type is not accepted, or the storage quota would be exceeded",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Field name is `file`. Images only (jpeg, png, gif, webp, avif, svg), 50 MB max. Optional `convert_to_webp` / `convert_to_avif` booleans. Counts against the designer's storage quota.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "file"
                                ],
                                "properties": {
                                    "file": {
                                        "type": "string",
                                        "format": "binary"
                                    },
                                    "convert_to_webp": {
                                        "type": "boolean"
                                    },
                                    "convert_to_avif": {
                                        "type": "boolean"
                                    }
                                }
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "assets:write"
                ]
            },
            "delete": {
                "operationId": "assets.destroy",
                "x-route-name": "api.v1.assets.destroy",
                "tags": [
                    "Assets"
                ],
                "summary": "Delete a file",
                "security": [
                    {
                        "oauth2": [
                            "assets:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Deleted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Deleted"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Removes one stored file and frees the space against the designer's quota. Address it with the `key` from the asset listing.\n\nComponents pointing at that URL are not changed, so deleting a file that is in use leaves them with a broken image. Repoint or remove those components first. Only keys inside this project's folder can be reached: not the designer's avatar, and not another project's artwork.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "key": "users/{slug}/projects/42/dagger.png"
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "assets:write"
                ]
            }
        },
        "/projects/{project}/assets/from-url": {
            "post": {
                "operationId": "assets.storeFromUrl",
                "x-route-name": "api.v1.assets.store-from-url",
                "tags": [
                    "Assets"
                ],
                "summary": "Mirror artwork you already host",
                "security": [
                    {
                        "oauth2": [
                            "assets:write"
                        ]
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The stored file",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StoredAsset"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "The URL was rejected or could not be fetched (`invalid_request`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "413": {
                        "description": "Over the 50.0 MB per-file limit (`file_too_large`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed, the file type is not accepted, or the storage quota would be exceeded",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "The server fetches the URL and stores a copy in the designer's own storage. Public http(s) only; private, loopback and link-local addresses are refused.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "url": "https://cdn.example.com/dagger.png"
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "assets:write"
                ]
            }
        },
        "/projects/{project}/components": {
            "get": {
                "operationId": "components.index",
                "x-route-name": "api.v1.components.index",
                "tags": [
                    "Components"
                ],
                "summary": "List components",
                "security": [
                    {
                        "oauth2": [
                            "components:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of components",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data",
                                        "links",
                                        "meta"
                                    ],
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Component"
                                            }
                                        },
                                        "links": {
                                            "$ref": "#/components/schemas/PaginationLinks"
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/PaginationMeta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Every component in the project, PAGINATED, grouped by type and then in the designer's own ordering.\n\nThis returns one page, not the project. `data` holds at most 50 rows by default and 200 at the very most, so a project with more components than that takes more than one call: follow `links.next` until it is null, or raise `per_page` and still check `meta.last_page`. Reading `data` from a single call and concluding a component is not there is the most common mistake made against this endpoint, and it does not look like a bug: you get a 200, a full-looking array, and silently none of the rows past the first page.\n\nTo find ONE component, do not scan the pages for it. Filter: `?name=Rusty Dagger` or `?unique_id=ext-1` matches exactly and returns the row wherever it sits in the ordering. `?type=card` narrows to a single type.\n\nRead this before writing, to see what already exists and to pick up the `unique_id` values of rows you did not create yourself.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "type",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "card",
                                "token",
                                "dice",
                                "board",
                                "rulebook",
                                "spinner",
                                "coin",
                                "bag",
                                "counter",
                                "timer",
                                "pack",
                                "packaging",
                                "other",
                                "piece",
                                "cube"
                            ]
                        },
                        "description": "Accepts the legacy types too (other, piece, cube), so you can list older rows in order to migrate them. You cannot create those.",
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "name",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Exact match on the component name, the same comparison match_by_name uses on write. The one reliable way to look a component up by name, because paging cannot hide it.",
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "unique_id",
                        "schema": {
                            "type": "string"
                        },
                        "description": "Exact match on your own sync id. The most precise lookup there is, since unique_id is the identity this API is keyed on.",
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "per_page",
                        "schema": {
                            "type": "integer",
                            "default": 50,
                            "maximum": 200
                        },
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "page",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Which page to return. Defaults to 1, which is why a caller that never sends it only ever sees the first 50 rows.",
                        "in": "query",
                        "required": false
                    }
                ],
                "x-required-scopes": [
                    "components:read"
                ]
            },
            "post": {
                "operationId": "components.store",
                "x-route-name": "api.v1.components.store",
                "tags": [
                    "Components"
                ],
                "summary": "Create or update a single component",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Applied, with nothing newly created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BatchResult"
                                }
                            }
                        }
                    },
                    "201": {
                        "description": "Applied, and at least one component was created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BatchResult"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "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.\n\nMatching 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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ComponentRow"
                            },
                            "example": {
                                "type": "card",
                                "component": {
                                    "unique_id": "ext-1",
                                    "name": "Rusty Dagger"
                                }
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "components:write"
                ]
            },
            "delete": {
                "operationId": "components.destroyAll",
                "x-route-name": "api.v1.components.destroy-all",
                "tags": [
                    "Components"
                ],
                "summary": "Delete every component this app created",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "How many were deleted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeletedCount"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Never a full wipe. Scoped to this app's own rows; hand-made and other apps' components are untouched. Optional `type` narrows it further.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "type",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "card",
                                "token",
                                "dice",
                                "board",
                                "rulebook",
                                "spinner",
                                "coin",
                                "bag",
                                "counter",
                                "timer",
                                "pack",
                                "packaging",
                                "other",
                                "piece",
                                "cube"
                            ]
                        },
                        "in": "query",
                        "required": false
                    }
                ],
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/projects/{project}/components/batch": {
            "post": {
                "operationId": "components.batch",
                "x-route-name": "api.v1.components.batch",
                "tags": [
                    "Components"
                ],
                "summary": "Create or update components in bulk (the primary write path)",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Applied, with nothing newly created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BatchResult"
                                }
                            }
                        }
                    },
                    "201": {
                        "description": "Applied, and at least one component was created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BatchResult"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Send up to 500 rows. `unique_id` is the sync key: the first call creates, later calls with the same ids update in place.\n\nOptional 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).\n\nThe whole batch is one transaction; per-row problems come back in `errors` while valid rows still apply.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/BatchResult"
                            },
                            "example": {
                                "type": "card",
                                "remove_missing": false,
                                "components": [
                                    {
                                        "unique_id": "ext-tool-card-001",
                                        "name": "Rusty Dagger",
                                        "quantity": 3,
                                        "image_url": "https://cdn.example.com/dagger-front.png",
                                        "card_mask_type": "rectangle_rounded",
                                        "deck_names": "Weapons|Starter Deck"
                                    }
                                ]
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/projects/{project}/components/{uniqueId}": {
            "delete": {
                "operationId": "components.destroy",
                "x-route-name": "api.v1.components.destroy",
                "tags": [
                    "Components"
                ],
                "summary": "Delete one component",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Deleted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Deleted"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Deletes one component, addressed by its `unique_id`.\n\nOnly 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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "uniqueId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "The component's `unique_id`. This endpoint takes the unique_id only, never our numeric `id`."
                    }
                ],
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/projects/{project}/groups": {
            "get": {
                "operationId": "groups.index",
                "x-route-name": "api.v1.groups.index",
                "tags": [
                    "Groups"
                ],
                "summary": "List decks and stacks",
                "security": [
                    {
                        "oauth2": [
                            "components:read"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Every deck and stack in the project",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/ComponentGroup"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Every deck and stack in the project. A deck holds cards, a stack holds tokens.\n\nContents 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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "kind",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "deck",
                                "stack"
                            ]
                        },
                        "in": "query",
                        "required": false
                    },
                    {
                        "name": "include_members",
                        "schema": {
                            "type": "boolean"
                        },
                        "description": "Include each group's components in order. Off by default because large decks make big payloads.",
                        "in": "query",
                        "required": false
                    }
                ],
                "x-required-scopes": [
                    "components:read"
                ]
            },
            "post": {
                "operationId": "groups.store",
                "x-route-name": "api.v1.groups.store",
                "tags": [
                    "Groups"
                ],
                "summary": "Create a deck or stack",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The deck or stack that was created",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/ComponentGroup"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "`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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "kind": "deck",
                                "name": "Weapons"
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/projects/{project}/groups/{group}": {
            "patch": {
                "operationId": "groups.update",
                "x-route-name": "api.v1.groups.update",
                "tags": [
                    "Groups"
                ],
                "summary": "Rename or reorder a deck or stack",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The updated deck or stack",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/ComponentGroup"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Renames a deck or stack, or moves it in the project's ordering. The contents are untouched: use the members endpoint for those.\n\n`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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "group",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Group id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "name": "Starter Weapons",
                                "position": 2
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "components:write"
                ]
            },
            "delete": {
                "operationId": "groups.destroy",
                "x-route-name": "api.v1.groups.destroy",
                "tags": [
                    "Groups"
                ],
                "summary": "Delete a deck or stack",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Deleted; its components stay in the project",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/GroupDeleted"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "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`.\n\nUse this when a grouping was a mistake. To empty a deck but keep it, send an empty `members` array instead.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "group",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Group id."
                    }
                ],
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/projects/{project}/groups/{group}/members": {
            "put": {
                "operationId": "groups.members",
                "x-route-name": "api.v1.groups.members",
                "tags": [
                    "Groups"
                ],
                "summary": "Set a deck or stack's contents and card order",
                "security": [
                    {
                        "oauth2": [
                            "components:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The group, with its new membership and order",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/ComponentGroup"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The project is locked and refuses writes (`project_locked`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Replaces the membership wholesale. **Array order becomes card order**, so this is how you control the order of a deck.\n\nIdentify each member by its `unique_id` \u2014 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).\n\nSend an empty array to empty the deck without deleting any component.\n\nTwo 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.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Project id."
                    },
                    {
                        "name": "group",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Group id."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "members": [
                                    {
                                        "unique_id": "ext-card-001",
                                        "count": 2
                                    },
                                    {
                                        "unique_id": "ext-card-002"
                                    },
                                    {
                                        "unique_id": "ext-card-003",
                                        "face_up": false
                                    }
                                ]
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "components:write"
                ]
            }
        },
        "/webhooks": {
            "get": {
                "operationId": "webhooks.index",
                "x-route-name": "api.v1.webhooks.index",
                "tags": [
                    "Webhooks"
                ],
                "summary": "List the webhooks this app registered",
                "security": [
                    {
                        "oauth2": [
                            "webhooks:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Every webhook this app registered",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "required": [
                                        "data"
                                    ],
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Webhook"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Only the endpoints YOUR app registered for this designer. Ones belonging to other apps are never visible.\n\nEach entry reports `is_active`, `last_delivered_at`, `last_status` and `consecutive_failures`, which is enough to show an integration-health panel, or to notice that an endpoint was switched off after repeated failures and needs registering again.",
                "x-required-scopes": [
                    "webhooks:write"
                ]
            },
            "post": {
                "operationId": "webhooks.store",
                "x-route-name": "api.v1.webhooks.store",
                "tags": [
                    "Webhooks"
                ],
                "summary": "Register a webhook",
                "security": [
                    {
                        "oauth2": [
                            "webhooks:write"
                        ]
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The webhook, including the signing secret, which is shown once",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/WebhookCreated"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Validation failed (`validation_failed`); `error.details` names the fields",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Returns `secret` exactly once. Store it, you cannot read it back. Sign checks use `X-DDD-Signature: sha256=HMAC_SHA256(\"{timestamp}.{body}\", secret)` with the timestamp from `X-DDD-Timestamp`.\n\nEvents: components.changed, project.updated. Public http(s) URLs only. Max 10 per app per account.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "url": "https://yourtool.example.com/hooks/ddd",
                                "events": [
                                    "components.changed"
                                ]
                            }
                        }
                    }
                },
                "x-required-scopes": [
                    "webhooks:write"
                ]
            }
        },
        "/webhooks/{webhook}": {
            "delete": {
                "operationId": "webhooks.destroy",
                "x-route-name": "api.v1.webhooks.destroy",
                "tags": [
                    "Webhooks"
                ],
                "summary": "Delete a webhook",
                "security": [
                    {
                        "oauth2": [
                            "webhooks:write"
                        ]
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Deleted",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Deleted"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing, malformed, expired or revoked token (`invalid_token`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Missing required scope, not a row this app owns, or a plan limit was reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found, or not owned by this token (`not_found`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limited; see the Retry-After header (`rate_limited`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Something went wrong on our side (`server_error`); retry with backoff",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Stops deliveries and removes the registration. Anything still queued is dropped too, because every delivery re-checks that its webhook exists before sending.\n\nThere is no way to edit a webhook. To change its URL or its events, delete it and register again, which also issues a fresh secret.",
                "parameters": [
                    {
                        "name": "webhook",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        },
                        "description": "Webhook id."
                    }
                ],
                "x-required-scopes": [
                    "webhooks:write"
                ]
            }
        }
    }
}