Skip to main content
Send every request to https://dash.joinmarkt.com with:
The key must be allowed to access {storeId}. Product scopes also require the key owner to have manageProducts on that store. Successful list, get, create, and update responses are the JSON objects below. They are not wrapped in a data envelope. Errors use this shape:
These apply to every method on this page:
  • 401 Unauthorized, code UNAUTHORIZED — missing bearer token, unknown key, revoked key, or the account deletion has completed.
  • 401 API key expired, code UNAUTHORIZED.
  • 403 Forbidden, code FORBIDDEN — the key cannot access this store, the scope is missing, or the owner lacks manageProducts.
  • 429 Too many requests, code RATE_LIMITED — more than 120 requests in 60 seconds from the same IP.
  • 500 code INTERNAL_ERROR, plus errorId. error is the fallback named on that method.
{productId} in a path is the product id, or a public id PRD- plus 3 to 5 groups of 4 letters or digits (for example PRD-2345-6789-ABCD-EFGH-JKMN). Lookup is case-insensitive and the stored id is uppercased. A JSON body must be an object. storeId inside the body is removed before validation. The store in the path is the one that is written. Top-level fields other than the ones listed for that method are dropped. Keys inside a variant object are not dropped on create.

List products

Scope: product:read Query:
  • page — integer, minimum 1, default 1
  • limit — integer, 1–100, default 50
An invalid page or limit returns 400 with code BAD_REQUEST. The error text is each Zod issue as page: … or limit: …, joined with ; . Products are ordered by sortOrder ascending, then createdAt descending. A page past the end returns an empty products array and the real total.
publicId is null when the product has none. stockAvailable here is the number of license keys with isUsed false. Each object in variants is returned with stockKeys removed. Read keys from the stock routes below. Unexpected failures return 500 Failed to list products.

Create a product

Scope: product:create · 201 Required body fields:
  • name — string, trimmed, 1–200 characters
  • description — string, 1–50000 characters. Create trims it, so a whitespace-only value is stored as ""
  • price — number, 0–1000000
Optional body fields:
  • slashedPrice — number, 0–1000000, or null
  • images — array of strings, max 20 items, each max 500 characters. Omitted images are stored as []
  • visibility — public, private, unlisted, or on-hold. Omitted values are stored as public
  • categoryId — string, max 100 characters, or null. The category must belong to this store. On create, a blank string is stored as null
  • variants — array of objects. See Variant objects
  • slug — string, trimmed, 1–200 characters. It is lowercased, runs of other characters become one hyphen, and hyphens at the ends are removed. Omitted slugs are built from name the same way. A taken generated slug gets a -1, -2, … suffix. A slug you send is stored only when that normalized handle is free
  • metaTitle — string, trimmed, max 200, or null. A blank string is stored as null
  • metaDescription — string, trimmed, max 500, or null. A blank string is stored as null
  • metaImage — string, trimmed, max 500, or null. A blank string is stored as null
  • embedCardType — small or large, or null. Omitted and null are stored as large
  • deliveryConfig — see below. Omitted config is stored as license-key delivery
  • displayConfig — see below. Omitted config stores every flag as false
deliveryConfig fields:
  • methods — required array, at least one of license_key, download_file, delivery_instructions, custom_fields, manual_delivery
  • downloadFiles — optional, max 20. Each file has id (1–100), name (1–200), url (1–500), and optional integer size ≥ 0. Required when methods includes download_file
  • manualDelivery — { "message": "..." } or null. message max 2000. A non-empty message is required when methods includes manual_delivery
  • deliveryInstructions — { "content": "..." } or null. content is 1–50000 characters. Required when methods includes delivery_instructions
  • customFields — optional, max 20. Required when methods includes custom_fields. Each field has id, label (1–120), identifier (1–64, lowercase letter then letters, numbers, or underscores), type (text, textarea, dropdown, checkbox, radio, number), optional placeholder (max 200), optional helpText (max 500), required (boolean, default false), and optional options (max 20 strings, each 1–120)
  • discordRole — { "guildId": "12345", "roleId": "67890" } or null. Both ids are 5–32 digits
  • instructions — string, max 50000, or null. A non-empty value is merged into deliveryInstructions before save
displayConfig booleans, each defaulting to false when the object is present: hideStockCount, liveViewsEnabled, showViewsCount, liveSalesEnabled, showSalesCount, liveSalesNotifications, showSalesNotifications.

Variant objects

On create, each variant is stored with the keys you sent, plus id and publicId. id is kept when it is 8–128 characters after trim; otherwise it is a new UUID. publicId is kept when it is VAR- plus five groups of four characters from 23456789ABCDEFGHJKMNPQRSTUVWXYZ; otherwise a new one is issued. Duplicate publicId values in the same array are re-issued. stockKeys is not returned by list or get. On create it remains in the saved object. It is not inserted as license keys. Any update rewrites every saved variant, including when variants is omitted and the product already has variants. Sending [] clears them. Each saved object then has only id, publicId, name, description, price, slashedPrice, stock, minQuantity, maxQuantity, volumeDiscounts, disableVolumeDiscountsOnCoupon, and deliveryConfig when the variant had one. Missing name, description, minQuantity, or maxQuantity becomes "". price and slashedPrice become "" when missing or numeric 0. stock is a number, parsed from a numeric string, or 0 when missing. volumeDiscounts is the array you sent, or []. disableVolumeDiscountsOnCoupon is true only for true or "true". The response is the full product from Get a product. stockAvailable on that object counts keys that are unused and not attached to an order. Errors:
  • 400 code BAD_REQUEST — invalid JSON (Invalid JSON), a body that is not an object (JSON body must be an object), Zod validation (name: … style, joined with ; ), or Category not found in this store
  • 400 code product_validation — Slashed price must be greater than regular price
  • 400 code product_slug_duplicate — URL handle must use lowercase letters, numbers, and hyphens only. when the normalized handle is empty (including a name that produces no handle), or This URL handle is already used by another product. when the slug you sent is already taken
  • 403 code plan_product_limit, with requiredPlan and upgradePath /billing — the store is at its product limit. error is Your {plan} plan allows up to {limit} products. Upgrade to add more.
  • 403 code plan_file_delivery_limit, with requiredPlan and upgradePath /billing — error is File "{name}" exceeds the {megabytes} MB delivery limit on your {plan} plan. {name} is the file name when that value is a string, including "". Otherwise it is upload. Product files and variant files are both checked
  • 500 Failed to create product

Get a product

Scope: product:read
deliveryConfig and displayConfig are always objects here. Empty or invalid stored JSON is returned as the create defaults: methods is ["license_key"], the delivery arrays are empty, the delivery objects are null, and every display flag is false. A non-empty stored instructions string is copied into deliveryInstructions when that object was empty, delivery_instructions is added to methods, and instructions is null. stockAvailable counts keys with isUsed false and no order, across variants. variants omit stockKeys. groupId is included here; change group membership with Merchant groups. Errors:
  • 404 Product not found, code product_not_found — no product matches that id
  • 404 Product not found, code NOT_FOUND — the product belongs to another store
  • 500 Failed to load product

Update a product (PATCH)

Scope: product:update · 200 Same fields as create. Every field is optional. Omitted fields stay as they are, except variants, which are rewritten as described in Variant objects. null clears slashedPrice, categoryId, metaTitle, metaDescription, metaImage, and embedCardType (embedCardType null is stored as large). A blank metaTitle, metaDescription, or metaImage is stored as null. description is stored as sent.
The response is the full product from Get a product. Errors:
  • 400 code BAD_REQUEST — invalid JSON, a non-object body, Zod validation (name: … style, joined with ; ), or Category not found in this store
  • 400 code product_validation — the merged product fails validation. error is the Zod messages joined with ; , without field paths
  • 400 code product_slug_duplicate — the same URL-handle messages as create
  • 400 code product_license_keys_required — when the variants array that is saved is empty and delivery includes license_key, with no unused keys that have no order: At least one license key must be added before customers can purchase this product. When that array is not empty, the product-level check is skipped. A variant is checked only when its name is non-empty and its deliveryConfig.methods includes license_key. The message is {variant name}: add at least one license key before customers can purchase this variant. Non-blank lines in that variant’s stockKeys string satisfy the check. When there are no such lines, unused keys with no order for that variant name are counted instead. stockKeys is removed before the variant is saved and those lines are not inserted as license keys. A variant deliveryConfig that fails validation is 500 Failed to update product, not this error
  • 403 code plan_file_delivery_limit, with requiredPlan and upgradePath /billing — same error text as create
  • 404 — same product-not-found cases as get
  • 500 Failed to update product

Update a product (PUT)

Scope: product:update · 200 PUT calls the same update as PATCH. Send the same partial object.
The response and errors match Update a product (PATCH), including 500 Failed to update product.

Delete a product

Scope: product:delete · 200 No body. When every license key is unused and not attached to an order, the product and those keys are deleted.
Errors:
  • 400 code product_has_fulfilled_keys — Cannot delete a product that has delivered license keys. Hide it instead. This is returned when any key has isUsed true or an order id
  • 404 — same product-not-found cases as get
  • 500 Failed to delete product

List stock

Scope: product:stock:read Optional query variantName. Omit it, or send a blank value, to read keys whose variant name is null. A non-blank value filters to that variant. This does not list keys that are already attached to an order.
productId is the path value you sent. count is keys.length. A key can be listed with isUsed true when it has no order. Errors:
  • 404 Product not found, code NOT_FOUND
  • 500 Failed to load stock keys

Replace stock

Scope: product:stock:write · 200 Deletes keys for that variant that are not attached to an order, then stores the keys you send. Keys that are only whitespace are dropped. Kept keys are trimmed. variantName null or omitted targets keys whose variant name is null. Body:
  • keys — required array of strings
  • variantName — optional string or null
count is how many non-blank keys were stored. Errors:
  • 400 code BAD_REQUEST — invalid JSON, a non-object body, or Zod validation of keys / variantName (issue messages joined with ; )
  • 404 Product not found, code NOT_FOUND
  • 500 Failed to replace stock keys

Append stock

Scope: product:stock:write · 200 Adds keys. Existing keys stay. Whitespace-only strings are dropped. Kept keys are trimmed. Same body as replace.
added is how many non-blank keys were inserted. count is how many unused keys with no order remain for that variant afterward. Errors:
  • 400 code BAD_REQUEST — invalid JSON, a non-object body, or Zod validation
  • 404 Product not found, code NOT_FOUND
  • 500 Failed to append stock keys