{
  "openapi": "3.1.0",
  "info": {
    "title": "TCG Plein Seller API",
    "version": "1.0.0",
    "description": "Read the Pokemon TCG catalog, manage your own listings, read orders, generate shipping labels and register webhooks. Every endpoint requires a Bearer API key. See https://tcgplein.nl/api/docs for the full human-readable documentation, including code samples in curl, PHP and JavaScript.",
    "contact": {
      "url": "https://tcgplein.nl/api/docs"
    }
  },
  "servers": [
    {
      "url": "https://tcgplein.nl/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Sets",
      "description": "The Pokemon TCG set catalog."
    },
    {
      "name": "Cards",
      "description": "Individual card lookups."
    },
    {
      "name": "Sealed products",
      "description": "Booster boxes, elite trainer boxes, tins and similar."
    },
    {
      "name": "Listings",
      "description": "Your own marketplace listings."
    },
    {
      "name": "Orders",
      "description": "Incoming orders, shipping labels and fulfilment."
    },
    {
      "name": "Webhooks",
      "description": "Endpoints that receive order and listing events."
    },
    {
      "name": "Account",
      "description": "Identity and key introspection."
    }
  ],
  "paths": {
    "/series": {
      "get": {
        "operationId": "api.v1.series.index",
        "tags": ["Sets"],
        "summary": "List all series",
        "description": "Every series in the visible catalog with its curated shortcut and the number of visible sets, ordered by set count. The shortcut (for example swsh or sv) is accepted by GET /sets' series filter as an alternative to the full series name; shortcut is null for series without one. Requires the catalog:read scope.",
        "responses": {
          "200": {
            "description": "The series list (not paginated).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string", "example": "Sword & Shield" },
                          "shortcut": { "type": ["string", "null"], "example": "swsh" },
                          "sets_count": { "type": "integer", "example": 95 }
                        },
                        "required": ["name", "shortcut", "sets_count"]
                      }
                    }
                  },
                  "required": ["data"]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/sets": {
      "get": {
        "tags": ["Sets"],
        "operationId": "api.v1.sets.index",
        "summary": "List sets",
        "description": "Requires the catalog:read scope. Sorted newest release first, unless q is present, in which case results are sorted by search relevance.",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "Search by set name." },
          { "$ref": "#/components/parameters/LanguageParam" },
          { "$ref": "#/components/parameters/RegionParam" },
          { "name": "series", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "A series shortcut (swsh, sv, sm, ...) or the series name written in any casing, e.g. \"Scarlet & Violet\". GET /series lists every series with its shortcut. Unknown values filter as a literal name." },
          { "$ref": "#/components/parameters/PageParam" },
          { "$ref": "#/components/parameters/PerPageParam" }
        ],
        "responses": {
          "200": {
            "description": "A page of sets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/CardSet" } },
                    "links": { "$ref": "#/components/schemas/PaginationLinks" },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/sets/{identifier}": {
      "get": {
        "tags": ["Sets"],
        "operationId": "api.v1.sets.show",
        "summary": "Get a set",
        "description": "identifier is the canonical tcg_api_id or the set's shortcut. An ambiguous shortcut (the same shortcut reused across languages or regions) returns 409 ambiguous_identifier; narrow it with language and/or region, or use the canonical id.",
        "parameters": [
          { "name": "identifier", "in": "path", "required": true, "schema": { "type": "string" }, "example": "sv1" },
          { "name": "include", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated, no spaces. Accepted values: cards, products. Embeds the set's full card list and/or sealed products in the response.", "example": "cards,products" },
          { "$ref": "#/components/parameters/LanguageParam" },
          { "$ref": "#/components/parameters/RegionParam" }
        ],
        "responses": {
          "200": {
            "description": "The set, optionally with embedded cards and/or products.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/CardSet" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousIdentifier" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/cards": {
      "get": {
        "tags": ["Cards"],
        "operationId": "api.v1.cards.index",
        "summary": "List cards",
        "description": "Requires the catalog:read scope.",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "Search by card name." },
          { "name": "set", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "A set identifier (canonical id or shortcut). An ambiguous shortcut 409s the same way GET /sets/{identifier} does; language and/or region on the same request narrow it." },
          { "$ref": "#/components/parameters/LanguageParam" },
          { "$ref": "#/components/parameters/RegionParam" },
          { "name": "rarity", "in": "query", "schema": { "type": "string", "maxLength": 50 }, "description": "Exact match, e.g. \"Rare\"." },
          { "$ref": "#/components/parameters/PageParam" },
          { "$ref": "#/components/parameters/PerPageParam" }
        ],
        "responses": {
          "200": {
            "description": "A page of cards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Card" } },
                    "links": { "$ref": "#/components/schemas/PaginationLinks" },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousIdentifier" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/cards/{identifier}": {
      "get": {
        "tags": ["Cards"],
        "operationId": "api.v1.cards.show",
        "summary": "Get a card",
        "description": "identifier is the canonical id or the friendly {shortcut}_{number} id (only the first underscore splits shortcut from number). An ambiguous friendly id returns 409 ambiguous_identifier with a list of candidates.",
        "parameters": [
          { "name": "identifier", "in": "path", "required": true, "schema": { "type": "string" }, "example": "pbl_63" },
          { "name": "include", "in": "query", "schema": { "type": "string", "enum": ["listings"] }, "description": "listings embeds the card's active listings: every active listing, sorted by price ascending, not paginated." },
          { "$ref": "#/components/parameters/LanguageParam" },
          { "$ref": "#/components/parameters/RegionParam" }
        ],
        "responses": {
          "200": {
            "description": "The card, optionally with embedded active listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/Card" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousIdentifier" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/products": {
      "get": {
        "tags": ["Sealed products"],
        "operationId": "api.v1.products.index",
        "summary": "List sealed products",
        "description": "Requires the catalog:read scope.",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "Search by product name." },
          { "name": "set", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "A set identifier (canonical id or shortcut). Unlike sets and cards, this endpoint has no language/region parameters to disambiguate: an ambiguous shortcut 409s, and the only way through is the set's canonical tcg_api_id." },
          { "name": "category", "in": "query", "schema": { "type": "string", "maxLength": 50 }, "description": "Exact match, e.g. elite_trainer_box. Validated only as a plain string server-side (see SealedProductCategory for the known values); an unrecognized value is not rejected, it simply matches no rows and returns an empty page." },
          { "$ref": "#/components/parameters/PageParam" },
          { "$ref": "#/components/parameters/PerPageParam" }
        ],
        "responses": {
          "200": {
            "description": "A page of sealed products.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/SealedProduct" } },
                    "links": { "$ref": "#/components/schemas/PaginationLinks" },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousIdentifier" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/products/{slug}": {
      "get": {
        "tags": ["Sealed products"],
        "operationId": "api.v1.products.show",
        "summary": "Get a sealed product",
        "description": "A sealed product has one id, its slug: there is no separate canonical/friendly distinction and no language/region parameter.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "example": "scarlet-violet-elite-trainer-box" },
          { "name": "include", "in": "query", "schema": { "type": "string", "enum": ["listings"] }, "description": "listings embeds the product's active listings, the same unbounded, price-sorted embed as on a card." }
        ],
        "responses": {
          "200": {
            "description": "The sealed product, optionally with embedded active listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/SealedProduct" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/listings": {
      "get": {
        "tags": ["Listings"],
        "operationId": "api.v1.listings.index",
        "summary": "List your listings",
        "description": "Requires the listings:read scope. Every listing on this endpoint belongs to the caller, so each item includes the owner-only fields (status, raw quantity, listable, created_at, updated_at) in addition to the public listing shape.",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/ListingStatus" } },
          { "name": "updated_since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "ISO date or datetime. Only listings updated at or after this point." },
          { "name": "card", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "A card identifier (canonical or friendly id). Restricts to listings of that card." },
          { "name": "product", "in": "query", "schema": { "type": "string", "maxLength": 150 }, "description": "A sealed product slug. Restricts to listings of that product. An unknown slug returns 404." },
          { "$ref": "#/components/parameters/PageParam" },
          { "$ref": "#/components/parameters/PerPageParam" }
        ],
        "responses": {
          "200": {
            "description": "A page of your listings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Listing" } },
                    "links": { "$ref": "#/components/schemas/PaginationLinks" },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousIdentifier" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Listings"],
        "operationId": "api.v1.listings.store",
        "summary": "Bulk-create listings",
        "description": "Requires the listings:write scope, and completed Stripe payout onboarding (the same requirement as the dashboard): a key on a non-onboarded account gets 403 authorization_error. Creates up to 100 listings in one call. Each item validates and saves independently, so one bad item never sinks the rest of the batch. Because the item-count cap is enforced by request validation before the controller's onboarding check runs, a batch of more than 100 items always gets 422 validation_error even on a non-onboarded account. The response is always HTTP 200, even when every item failed: check each item's own status field, not the HTTP status code.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["items"],
                "properties": {
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "description": "Bulk cap: 100 items per call.",
                    "items": { "$ref": "#/components/schemas/BulkListingItem" }
                  }
                }
              },
              "example": {
                "items": [
                  { "card": "pbl_63", "condition": "NM", "language": "EN", "price": 4.5, "quantity": 3, "photo_urls": ["https://example.com/photos/pbl-63-front.jpg"] },
                  { "product": "scarlet-violet-elite-trainer-box", "language": "EN", "price": 44.95, "quantity": 1 }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item results. Not a data/links/meta envelope: results, created and failed are top-level fields.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkListingResponse" },
                "example": {
                  "results": [
                    { "status": "created", "listing": { "id": 5231, "price": 4.5, "currency": "EUR", "quantity": 3, "status": "active" }, "index": 0 },
                    { "status": "failed", "errors": { "product": ["Unknown identifier."] }, "index": 1 }
                  ],
                  "created": 1,
                  "failed": 1
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/listings/{listing}": {
      "patch": {
        "tags": ["Listings"],
        "operationId": "api.v1.listings.update",
        "summary": "Update a listing",
        "description": "Requires the listings:write scope. A listing id that does not belong to the caller returns 404, the same as if it did not exist. All fields are optional; only send what changes. Stock and status are reconciled automatically: setting quantity: 0 flips an active listing to sold even without sending status, and restocking (quantity greater than 0) flips a sold listing back to active.",
        "parameters": [
          { "name": "listing", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateListingRequest" },
              "example": { "quantity": 0 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated listing, owner shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/Listing" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Listings"],
        "operationId": "api.v1.listings.destroy",
        "summary": "Delete a listing",
        "description": "Requires the listings:write scope. A listing with an open order cannot be deleted until it completes (409 conflict). A listing id that does not belong to the caller returns 404, the same as if it did not exist.",
        "parameters": [
          { "name": "listing", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders": {
      "get": {
        "tags": ["Orders"],
        "operationId": "api.v1.orders.index",
        "summary": "List orders",
        "description": "Requires the orders:read scope. Lists individual order rows, one per sold listing per checkout. Each row includes the sold item, tracking and the caller's financial breakdown for that row. The buyer's name is included, but not their shipping address: that only appears on the slice detail (GET /orders/{id}).",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/OrderStatus" } },
          { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "ISO date or datetime. Only orders created at or after this point." },
          { "$ref": "#/components/parameters/PageParam" },
          { "$ref": "#/components/parameters/PerPageParam" }
        ],
        "responses": {
          "200": {
            "description": "A page of order rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Order" } },
                    "links": { "$ref": "#/components/schemas/PaginationLinks" },
                    "meta": { "$ref": "#/components/schemas/PaginationMeta" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders/{order}": {
      "get": {
        "tags": ["Orders"],
        "operationId": "api.v1.orders.show",
        "summary": "Get an order (slice detail)",
        "description": "Requires the orders:read scope. order can be any row's id from a slice, not necessarily the first one: the response is identical either way, since it always resolves the whole slice (every row from one checkout that belongs to the caller). Returns every row, the buyer's full shipping address, aggregated totals and the current shipping/label status. A non-seller (including the buyer) gets 404, never 403, so an order id's existence is never confirmed to a non-party.",
        "parameters": [
          { "name": "order", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "The order's slice detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/OrderSlice" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders/{order}/label": {
      "post": {
        "tags": ["Orders"],
        "operationId": "api.v1.orders.label",
        "summary": "Generate a shipping label",
        "description": "Requires the orders:write scope. Purchases (or re-reads) a shipping label for the order's slice. Calling it again for the same slice is safe: it does not purchase a second shipment, it re-checks the existing one. label_url, when present, is a signed download URL valid for 5 days requiring no authentication of its own; a fresh URL with a fresh 5-day expiry is minted on every successful call. A successful call also fires an order.label_ready webhook, which can legitimately fire twice for the same order if the carrier backfills a tracking barcode asynchronously.",
        "parameters": [
          { "name": "order", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "The label outcome. A single-resource envelope, not a paginated list shape.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/LabelResult" } }
                },
                "example": {
                  "data": {
                    "status": "ready",
                    "label_url": "https://tcgplein.nl/api-labels/9012?expires=1755680000&signature=abcdef",
                    "expires_at": "2026-08-16T09:14:00+00:00",
                    "message": null,
                    "tracking_code": "3SMYPXXXXXXXXXX",
                    "tracking_url": "https://track.myparcel.nl/...",
                    "provider": "myparcel",
                    "shipped_lines": 1,
                    "order": { "slice_key": "4471-88" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/ProviderError" }
        }
      }
    },
    "/orders/{order}/ship": {
      "post": {
        "tags": ["Orders"],
        "operationId": "api.v1.orders.ship",
        "summary": "Mark an order shipped",
        "description": "Requires the orders:write scope. Marks every still-pending row in the slice as shipped in one call, not just {order}'s own row, and notifies the buyer once per shipped row. If nothing in the slice is still pending, returns 409 conflict.",
        "parameters": [
          { "name": "order", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tracking_code": { "type": ["string", "null"], "maxLength": 100, "description": "Recorded against every row shipped by this call. REQUIRED when the order ships via one of the seller's own TRACKED shipping options (tracking.fulfilment = self_ship); PROHIBITED (422) for the seller's letter options and for platform-label orders, whose tracking comes from the label." }
                }
              },
              "example": { "tracking_code": "3SMYPXXXXXXXXXX" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The order's slice detail with the shipped row count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        { "$ref": "#/components/schemas/OrderSlice" },
                        { "type": "object", "properties": { "shipped_lines": { "type": "integer", "description": "Rows shipped by this call." } } }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": ["Webhooks"],
        "operationId": "api.v1.webhooks.index",
        "summary": "List your webhook endpoints",
        "description": "Requires the webhooks:manage scope. Unlike the catalog and listings endpoints, this is a plain array under data, not paginated: a seller has at most 16 endpoints. The signing secret is never included here, only on the store response.",
        "responses": {
          "200": {
            "description": "All of the caller's webhook endpoints. No links/meta: this endpoint is intentionally unpaginated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEndpoint" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Webhooks"],
        "operationId": "api.v1.webhooks.store",
        "summary": "Register a webhook endpoint",
        "description": "Requires the webhooks:manage scope. Capped at 16 endpoints per seller (a 17th attempt gets 422 validation_error on the url field). url must be a public http or https URL: registration synchronously rejects a non-http(s) scheme or a literal private/reserved IP host, but a hostname that only resolves privately (e.g. localhost) is accepted here and caught later at delivery time instead, failing that delivery outright with no retries. The response includes a one-time secret field, sibling to the endpoint object: shown exactly once, here, and never retrievable again.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreWebhookRequest" },
              "example": {
                "url": "https://example.com/webhooks/tcgplein",
                "events": ["order.created", "order.label_ready", "order.cancelled", "listing.sold"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created endpoint, plus the one-time signing secret.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookEndpointCreated" },
                "example": {
                  "data": {
                    "id": 14,
                    "url": "https://example.com/webhooks/tcgplein",
                    "events": ["order.created", "order.label_ready", "order.cancelled", "listing.sold"],
                    "active": true,
                    "disabled_at": null,
                    "consecutive_failures": 0,
                    "created_at": "2026-08-11T09:00:00+00:00"
                  },
                  "secret": "tcgp_whsec_K7mP2xQ9vZ4nR8wT3yL6bC1jD5sF0gH7uM2oA9iE"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{webhookEndpoint}": {
      "delete": {
        "tags": ["Webhooks"],
        "operationId": "api.v1.webhooks.destroy",
        "summary": "Delete a webhook endpoint",
        "description": "Requires the webhooks:manage scope. An endpoint id that does not belong to the caller returns 404, the same as if it did not exist.",
        "parameters": [
          { "name": "webhookEndpoint", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The webhook endpoint's id." }
        ],
        "responses": {
          "204": { "description": "Deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/me": {
      "get": {
        "tags": ["Account"],
        "operationId": "api.v1.me",
        "summary": "Probe the current key",
        "description": "The only endpoint that needs no scope: any valid, active key can call it. Reports account identity, the key's granted scopes and the effective rate limits (the same for every key; not specific to this one).",
        "responses": {
          "200": {
            "description": "The account behind the key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "data": { "$ref": "#/components/schemas/Me" } }
                },
                "example": {
                  "data": {
                    "id": 88,
                    "shop_name": "CardShopNL",
                    "slug": "cardshopnl",
                    "seller_tier": "Gold",
                    "business": true,
                    "scopes": ["catalog:read", "listings:read", "listings:write", "orders:read", "orders:write"],
                    "key_prefix": "tcgp_live_K7mP",
                    "last_used_at": "2026-08-11T09:14:00+00:00",
                    "rate_limit": { "read": 120, "write": 30 }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tcgp_live_{40 random characters}",
        "description": "Create a key from your API dashboard once you have access. The full key is shown exactly once, right after creation or rotation; only a hash is stored server-side."
      }
    },
    "parameters": {
      "PageParam": {
        "name": "page",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "default": 1 },
        "description": "1-indexed."
      },
      "PerPageParam": {
        "name": "per_page",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "default": 30 },
        "description": "Clamped to the range 1-100, not rejected: per_page=500 silently becomes 100."
      },
      "LanguageParam": {
        "name": "language",
        "in": "query",
        "schema": { "type": "string", "maxLength": 5 },
        "description": "Exact match, e.g. EN or JP.",
        "example": "EN"
      },
      "RegionParam": {
        "name": "region",
        "in": "query",
        "schema": { "type": "string", "maxLength": 5 },
        "description": "Exact match, lowercase, e.g. en or jp.",
        "example": "en"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No key sent, or the key is unknown, revoked or expired.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "authentication_error", "message": "Invalid API key." } } } }
      },
      "Forbidden": {
        "description": "Valid key, but missing the required scope, or the account's API access is not enabled or is suspended.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "authorization_error", "message": "This API key is missing the 'listings:write' scope." } } } }
      },
      "NotFound": {
        "description": "The identifier does not resolve to any record the caller can access.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "not_found", "message": "Resource not found." } } } }
      },
      "Conflict": {
        "description": "The request is valid but conflicts with the resource's current state.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "conflict", "message": "This listing has an open order and cannot be deleted until it is completed." } } } }
      },
      "AmbiguousIdentifier": {
        "description": "A friendly identifier matched more than one record. fields.candidates lists the matches.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "ambiguous_identifier", "message": "Card identifier 'pop_1' matches multiple cards. Use the canonical id, or narrow with ?language= or ?region=.", "fields": { "candidates": [{ "id": "pop1-1", "name": "Pikachu", "number": "1", "set": "POP Series 1", "language": "EN", "region": "en" }] } } } } }
      },
      "ValidationError": {
        "description": "The request body or query failed validation. fields maps field name to messages.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "validation_error", "message": "The request payload is invalid.", "fields": { "price": ["The price must be at least 0.01."] } } } } }
      },
      "RateLimited": {
        "description": "The read or write rate limit was exceeded.",
        "headers": {
          "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait before retrying." },
          "X-RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Unix timestamp when the window resets." }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "rate_limited", "message": "Too many requests." } } } }
      },
      "ProviderError": {
        "description": "The upstream shipping carrier call failed. The request itself was valid; retry the label call later.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "type": "provider_error", "message": "The shipping provider could not be reached." } } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["type", "message"],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "authentication_error",
                  "authorization_error",
                  "validation_error",
                  "not_found",
                  "ambiguous_identifier",
                  "conflict",
                  "rate_limited",
                  "provider_error",
                  "http_error",
                  "server_error"
                ]
              },
              "message": { "type": "string" },
              "fields": { "type": "object", "description": "Present only for validation_error (field name to messages) and ambiguous_identifier (candidates list). Omitted entirely for every other error type, never sent as null." }
            }
          }
        }
      },
      "PaginationLinks": {
        "type": "object",
        "properties": {
          "first": { "type": ["string", "null"] },
          "last": { "type": ["string", "null"] },
          "prev": { "type": ["string", "null"] },
          "next": { "type": ["string", "null"] }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "properties": {
          "current_page": { "type": "integer" },
          "from": { "type": ["integer", "null"] },
          "last_page": { "type": "integer" },
          "path": { "type": "string" },
          "per_page": { "type": "integer" },
          "to": { "type": ["integer", "null"] },
          "total": { "type": "integer" }
        }
      },
      "Language": { "type": "string", "enum": ["NL", "EN", "DE", "FR", "JP", "ES", "IT", "PT", "CN", "KR"] },
      "Printing": { "type": "string", "enum": ["standard", "reverse_holo", "holo", "first_edition", "shadowless", "full_art", "alternate_art", "promo"] },
      "Condition": { "type": "string", "enum": ["M", "NM", "LP", "MP", "HP", "DMG", "G"], "description": "G is Graded; when set, grading_company and grading_grade accompany it." },
      "GradingCompany": { "type": "string", "enum": ["PSA", "BGS", "CGC", "SGC", "TAG", "GRAAD", "ACE", "PCA", "OVERIG"] },
      "GradingGrade": { "type": "string", "enum": ["10.0", "9.5", "9.0", "8.5", "8.0", "7.5", "7.0", "6.5", "6.0", "5.5", "5.0", "4.5", "4.0", "3.5", "3.0", "2.5", "2.0", "1.5", "1.0"] },
      "ListingStatus": { "type": "string", "enum": ["active", "sold", "hidden"], "description": "removed also exists internally (a takedown state) but is never returned or accepted by the seller API." },
      "OrderStatus": { "type": "string", "enum": ["pending", "shipped", "received", "cancelled", "disputed"] },
      "SealedProductCategory": {
        "type": "string",
        "enum": ["elite_trainer_box", "booster_box", "booster_bundle", "booster_pack", "premium_collection", "tin", "mini_tin", "theme_deck", "build_battle_box", "special_collection", "cases", "other"]
      },
      "CardSet": {
        "type": "object",
        "description": "A card and/or product's cards/products arrays are only present when requested via ?include=. Each embedded card carries its own nested set object duplicating these basic fields, intentionally, so a card is usable standalone.",
        "properties": {
          "id": { "type": "string", "description": "Canonical tcg_api_id.", "example": "sv1" },
          "shortcut": { "type": ["string", "null"], "description": "Lowercase friendly identifier. Null for sets with no shortcut (150 of 627 at last count).", "example": "sv1" },
          "name": { "type": "string", "example": "Scarlet Base" },
          "series": { "type": "string", "example": "Scarlet & Violet" },
          "language": { "$ref": "#/components/schemas/Language" },
          "region": { "type": "string", "example": "en" },
          "release_date": { "type": ["string", "null"], "format": "date" },
          "total_cards": { "type": ["integer", "null"] },
          "printed_total": { "type": ["integer", "null"] },
          "logo_url": { "type": ["string", "null"], "format": "uri" },
          "symbol_url": { "type": ["string", "null"], "format": "uri" },
          "cards_count": { "type": "integer", "description": "Only present on GET /sets and GET /sets/{identifier} (a withCount, not part of the ?include=cards embed)." },
          "products_count": { "type": "integer" },
          "cards": { "type": "array", "items": { "$ref": "#/components/schemas/Card" }, "description": "Present only when ?include=cards was requested on GET /sets/{identifier}." },
          "products": { "type": "array", "items": { "$ref": "#/components/schemas/SealedProduct" }, "description": "Present only when ?include=products was requested on GET /sets/{identifier}." }
        }
      },
      "Card": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Canonical tcg_api_id.", "example": "pbl-63" },
          "friendly_id": { "type": ["string", "null"], "description": "{shortcut}_{number}. Null when the card's set has no shortcut.", "example": "pbl_63" },
          "name": { "type": "string", "example": "Pikachu" },
          "number": { "type": "string", "example": "63" },
          "rarity": { "type": ["string", "null"] },
          "supertype": { "type": ["string", "null"] },
          "hp": { "type": ["string", "null"] },
          "types": { "type": ["array", "null"], "items": { "type": "string" } },
          "image_small": { "type": ["string", "null"], "format": "uri" },
          "image_large": { "type": ["string", "null"], "format": "uri" },
          "url": { "type": "string", "format": "uri", "description": "The card's page on the public marketplace." },
          "set": { "$ref": "#/components/schemas/CardSet" },
          "listings": { "type": "array", "items": { "$ref": "#/components/schemas/Listing" }, "description": "Present only when ?include=listings was requested. Every active listing, sorted by price ascending, not paginated." }
        }
      },
      "SealedProduct": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "The product's slug (its only identifier).", "example": "scarlet-violet-elite-trainer-box" },
          "external_id": { "type": ["string", "null"] },
          "name": { "type": "string", "example": "Scarlet & Violet Elite Trainer Box" },
          "category": { "$ref": "#/components/schemas/SealedProductCategory" },
          "release_year": { "type": ["integer", "null"] },
          "released_at": { "type": ["string", "null"], "format": "date" },
          "image": { "type": ["string", "null"], "format": "uri" },
          "url": { "type": "string", "format": "uri", "description": "The product's page on the public marketplace." },
          "set": { "$ref": "#/components/schemas/CardSet", "description": "Null when the product is not tied to a specific set." },
          "listings": { "type": "array", "items": { "$ref": "#/components/schemas/Listing" }, "description": "Present only when ?include=listings was requested." }
        }
      },
      "Listing": {
        "type": "object",
        "description": "The public marketplace shape. status, quantity, listable, created_at and updated_at are owner-only: present only when the requesting key belongs to the listing's seller (e.g. every item on GET /listings, or a create/update response).",
        "properties": {
          "id": { "type": "integer" },
          "price": { "type": "number", "format": "float", "example": 7.25, "description": "A decimal amount in the currency below, never minor units: 7.25 is seven euros twenty-five." },
          "currency": { "type": "string", "example": "EUR" },
          "condition": { "anyOf": [{ "$ref": "#/components/schemas/Condition" }, { "type": "null" }], "description": "Null for sealed-product listings." },
          "language": { "$ref": "#/components/schemas/Language" },
          "printing": { "$ref": "#/components/schemas/Printing" },
          "graded": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "company": { "$ref": "#/components/schemas/GradingCompany" },
                  "grade": { "$ref": "#/components/schemas/GradingGrade" },
                  "label": { "type": "string", "description": "Formatted display label, e.g. \"PSA 8.5\"." }
                }
              },
              { "type": "null" }
            ],
            "description": "Non-null only when condition is G (Graded)."
          },
          "quantity_available": { "type": "integer", "description": "Reservation-aware: how many units can actually be bought right now, with active cart reservations subtracted." },
          "description": { "type": ["string", "null"] },
          "photos": { "type": "array", "items": { "type": "string", "format": "uri" } },
          "seller": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "slug": { "type": ["string", "null"] },
              "url": { "type": ["string", "null"], "format": "uri" },
              "tier": { "type": "string", "enum": ["Bronze", "Silver", "Gold", "Diamond", "Master"] },
              "rating": { "type": ["number", "null"] },
              "reviews": { "type": "integer" }
            }
          },
          "status": { "anyOf": [{ "$ref": "#/components/schemas/ListingStatus" }, { "type": "null" }], "description": "Owner-only." },
          "quantity": { "type": ["integer", "null"], "description": "Owner-only. The raw, un-reserved stock count. Other sellers' listings never expose it." },
          "listable": {
            "type": ["object", "null"],
            "description": "Owner-only. type is card or product.",
            "properties": {
              "type": { "type": "string", "enum": ["card", "product"] },
              "id": { "type": "string" },
              "friendly_id": { "type": ["string", "null"] },
              "name": { "type": "string" },
              "number": { "type": "string" },
              "set": { "type": "string" }
            }
          },
          "created_at": { "type": ["string", "null"], "format": "date-time", "description": "Owner-only." },
          "updated_at": { "type": ["string", "null"], "format": "date-time", "description": "Owner-only." }
        }
      },
      "BulkListingItem": {
        "type": "object",
        "description": "Exactly one of card or product is required. card additionally requires condition; when condition is G, grading_company and grading_grade are also required.",
        "properties": {
          "card": { "type": "string", "description": "A card identifier (canonical or friendly id). Provide exactly one of card or product." },
          "product": { "type": "string", "description": "A sealed product slug. Provide exactly one of card or product." },
          "language_hint": { "type": "string", "description": "Only used to disambiguate an ambiguous card identifier for this item (same role as ?language= on GET /cards/{identifier}); it is not the listing's language field below and is never itself validated or stored." },
          "condition": { "$ref": "#/components/schemas/Condition" },
          "language": { "$ref": "#/components/schemas/Language" },
          "printing": { "$ref": "#/components/schemas/Printing" },
          "grading_company": { "$ref": "#/components/schemas/GradingCompany" },
          "grading_grade": { "$ref": "#/components/schemas/GradingGrade" },
          "price": { "type": "number", "format": "float", "minimum": 0.01, "maximum": 99999.99 },
          "quantity": { "type": "integer", "minimum": 1, "maximum": 999 },
          "description": { "type": ["string", "null"], "maxLength": 1000 },
          "photo_urls": {
            "type": "array",
            "maxItems": 10,
            "items": { "type": "string", "format": "uri", "maxLength": 2048 },
            "description": "Publicly reachable image URLs, fetched and attached asynchronously after the listing is created. No multipart upload in v1. Subject to the same importable-URL safety rules as webhook endpoint URLs."
          }
        },
        "required": ["language", "price", "quantity"]
      },
      "BulkListingResultItem": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["created", "failed"] },
          "index": { "type": "integer", "description": "The item's position in the request's items array." },
          "listing": {
            "type": "object",
            "description": "Present only when status is created.",
            "properties": {
              "id": { "type": "integer" },
              "price": { "type": "number", "format": "float" },
              "currency": { "type": "string", "example": "EUR" },
              "quantity": { "type": "integer" },
              "status": { "$ref": "#/components/schemas/ListingStatus" }
            }
          },
          "errors": { "type": "object", "description": "Present only when status is failed. Field name to messages, the same shape as a validation_error." },
          "candidates": { "type": "array", "items": { "type": "object" }, "description": "Present only when status is failed because of an ambiguous card or set identifier, the same candidate shape as the 409 ambiguous_identifier error." }
        }
      },
      "BulkListingResponse": {
        "type": "object",
        "description": "Not a data-wrapped envelope: results, created and failed are top-level fields.",
        "properties": {
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/BulkListingResultItem" } },
          "created": { "type": "integer" },
          "failed": { "type": "integer" },
          "warnings": { "type": "array", "items": { "type": "string" }, "description": "Present only when the account needs attention, e.g. no shipping options are enabled so buyers cannot order the seller's listings." }
        }
      },
      "UpdateListingRequest": {
        "type": "object",
        "description": "Every field is optional; only send what changes.",
        "properties": {
          "price": { "type": "number", "format": "float", "minimum": 0.01, "maximum": 99999.99 },
          "quantity": { "type": "integer", "minimum": 0, "maximum": 999, "description": "0 flips the listing to sold automatically, even without sending status." },
          "status": { "type": "string", "enum": ["active", "hidden", "sold"], "description": "Sending sold on its own while quantity is still above 0 validates but is immediately reconciled back to active; pair it with quantity: 0 to actually take the listing off-sale." },
          "description": { "type": ["string", "null"], "maxLength": 1000 }
        }
      },
      "Order": {
        "type": "object",
        "description": "One order row: a single sold listing within a (checkout, seller) slice. Never carries the buyer's shipping address, only OrderSlice does.",
        "properties": {
          "id": { "type": "integer" },
          "slice_key": { "type": "string", "description": "Rows sharing this key belong to the same checkout and ship together.", "example": "4471-88" },
          "checkout_id": { "type": ["integer", "null"] },
          "status": { "$ref": "#/components/schemas/OrderStatus" },
          "quantity": { "type": "integer" },
          "price": { "type": "number", "format": "float" },
          "currency": { "type": "string", "example": "EUR" },
          "item": {
            "type": "object",
            "properties": {
              "name": { "type": ["string", "null"] },
              "subtitle": { "type": ["string", "null"] },
              "image": { "type": ["string", "null"], "format": "uri" },
              "condition": { "anyOf": [{ "$ref": "#/components/schemas/Condition" }, { "type": "null" }] },
              "language": { "anyOf": [{ "$ref": "#/components/schemas/Language" }, { "type": "null" }] },
              "type": { "type": ["string", "null"], "enum": ["card", "product", null], "description": "Null for legacy orders created before the catalog-identity snapshot existed." },
              "id": { "type": ["string", "null"] },
              "friendly_id": { "type": ["string", "null"] }
            }
          },
          "listing_id": { "type": ["integer", "null"] },
          "tracking": {
            "type": "object",
            "properties": {
              "code": { "type": ["string", "null"] },
              "url": { "type": ["string", "null"], "format": "uri" },
              "status": { "type": ["string", "null"] },
              "updated_at": { "type": ["string", "null"], "format": "date-time" },
              "fulfilment": { "type": "string", "enum": ["platform", "self_ship"], "description": "self_ship: the seller ships this order themselves via one of their own shipping options — no platform label exists, and POST /orders/{order}/ship requires a tracking_code when the option is tracked. platform: TCG Plein buys the label." }
            }
          },
          "financial": {
            "type": "object",
            "properties": {
              "seller_net": { "type": "number", "format": "float" },
              "seller_fee": { "type": "number", "format": "float" },
              "commission": { "type": "number", "format": "float" },
              "refunded_amount": { "type": "number", "format": "float" }
            }
          },
          "buyer": {
            "type": "object",
            "properties": { "name": { "type": ["string", "null"] } }
          },
          "created_at": { "type": ["string", "null"], "format": "date-time" },
          "shipped_at": { "type": ["string", "null"], "format": "date-time" },
          "received_at": { "type": ["string", "null"], "format": "date-time" },
          "cancelled_at": { "type": ["string", "null"], "format": "date-time" },
          "disputed_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "OrderSlice": {
        "type": "object",
        "description": "The full shipment slice for one (checkout, seller) pair: every order row, the buyer's shipping address, aggregated totals and shipping/tracking status.",
        "properties": {
          "slice_key": { "type": "string", "example": "4471-88" },
          "orders": { "type": "array", "items": { "$ref": "#/components/schemas/Order" } },
          "buyer": {
            "type": "object",
            "properties": {
              "name": { "type": ["string", "null"] },
              "address": {
                "type": ["object", "null"],
                "properties": {
                  "full_name": { "type": "string" },
                  "street": { "type": "string" },
                  "house_number": { "type": "string" },
                  "house_number_suffix": { "type": ["string", "null"] },
                  "zip": { "type": "string" },
                  "city": { "type": "string" },
                  "country": { "type": "string", "example": "NL" },
                  "phone": { "type": ["string", "null"] }
                }
              }
            }
          },
          "totals": {
            "type": "object",
            "description": "Decimal amounts, never minor units, all in the currency named below.",
            "properties": {
              "merchandise": { "type": "number", "format": "float" },
              "shipping": { "type": "number", "format": "float" },
              "seller_net": { "type": "number", "format": "float" },
              "currency": { "type": "string", "example": "EUR" }
            }
          },
          "shipping": {
            "type": "object",
            "properties": {
              "method": { "type": ["string", "null"] },
              "price": { "type": "number", "format": "float" },
              "provider": { "type": ["string", "null"] },
              "has_label": { "type": "boolean" },
              "tracking_code": { "type": ["string", "null"] },
              "tracking_status": { "type": ["string", "null"] },
              "label_url": { "type": ["string", "null"], "format": "uri" },
              "expires_at": { "type": ["string", "null"], "format": "date-time" }
            }
          }
        }
      },
      "LabelResult": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ready", "processing", "action_required", "self_ship"], "description": "self_ship is terminal: the seller fulfils this order themselves via one of their own shipping options, so no platform label will ever exist — ship it and report the tracking code via POST /orders/{order}/ship instead." },
          "label_url": { "type": ["string", "null"], "format": "uri", "description": "Populated only when status is ready. Signed, no auth required, valid for 5 days, freshly minted on every successful call." },
          "expires_at": { "type": ["string", "null"], "format": "date-time" },
          "message": { "type": ["string", "null"], "description": "Human-readable detail, populated for action_required and some edge cases." },
          "tracking_code": { "type": ["string", "null"] },
          "tracking_url": { "type": ["string", "null"], "format": "uri" },
          "provider": { "type": ["string", "null"] },
          "shipped_lines": { "type": "integer" },
          "order": { "$ref": "#/components/schemas/OrderSlice" }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "description": "Never includes the signing secret; that is a one-time sibling field on the store response only.",
        "properties": {
          "id": { "type": "integer" },
          "url": { "type": "string", "format": "uri" },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEvent" } },
          "active": { "type": "boolean" },
          "disabled_at": { "type": ["string", "null"], "format": "date-time", "description": "Set when auto-disabled after 10 consecutive failed deliveries." },
          "consecutive_failures": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookEndpointCreated": {
        "type": "object",
        "properties": {
          "data": { "$ref": "#/components/schemas/WebhookEndpoint" },
          "secret": { "type": "string", "description": "The plaintext signing secret. Shown exactly once, here.", "example": "tcgp_whsec_K7mP2xQ9vZ4nR8wT3yL6bC1jD5sF0gH7uM2oA9iE" }
        }
      },
      "WebhookEvent": { "type": "string", "enum": ["order.created", "order.cancelled", "order.label_ready", "listing.sold"] },
      "StoreWebhookRequest": {
        "type": "object",
        "required": ["url", "events"],
        "properties": {
          "url": { "type": "string", "maxLength": 2048, "description": "Must be a public http or https URL." },
          "events": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/WebhookEvent" } }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "shop_name": { "type": "string" },
          "slug": { "type": ["string", "null"] },
          "seller_tier": { "type": "string", "enum": ["Bronze", "Silver", "Gold", "Diamond", "Master"] },
          "business": { "type": "boolean" },
          "scopes": { "type": "array", "items": { "type": "string", "enum": ["catalog:read", "listings:read", "listings:write", "orders:read", "orders:write", "webhooks:manage"] } },
          "key_prefix": { "type": "string", "description": "The first 14 characters of the key (tcgp_live_ plus 4 characters)." },
          "last_used_at": { "type": ["string", "null"], "format": "date-time" },
          "rate_limit": {
            "type": "object",
            "description": "Effective platform-wide limits, not specific to this key.",
            "properties": {
              "read": { "type": "integer", "example": 120 },
              "write": { "type": "integer", "example": 30 }
            }
          }
        }
      }
    }
  }
}
