Skip to main content
Base URL: https://dash.joinmarkt.com Paths use the prefix /api/merchant/v1. Every method calls requireMerchantAuth before the handler.
The token must start with mk_live_. The rate-limit IP is cf-connecting-ip, then x-real-ip, then the last address in x-forwarded-for. When none of those headers is present, the bucket id is unknown. A full-access key skips the scope-list check and still requires manageGroups. A restricted key needs the method scope on the key and manageGroups. Success bodies are the JSON objects shown on each method. Failures look like this:
An unexpected failure returns 500 with code INTERNAL_ERROR, error set to the method fallback below, and an errorId. storeId inside a JSON body is discarded before validation. The store is the {storeId} path parameter. The body must be a JSON object. Invalid JSON returns 400 Invalid JSON. A body that is not an object returns 400 JSON body must be an object. Both use code BAD_REQUEST. {groupId} is the group id, or a public id shaped GRP-XXXX-XXXX-XXXX-XXXX-XXXX (matched case-insensitively). The group object does not include publicId. A missing group, or a group in another store, returns 404 Group not found with code NOT_FOUND. Validation failures return 400 BAD_REQUEST. The error string is Zod issues joined as field: message, separated by ; .

GET /api/merchant/v1/stores/{storeId}/groups

Scope: group:read
Query (merchantListQuerySchema). Other query keys are ignored. 200:
Each list row is a group object with products set to []. productCount is the number of linked products. Rows are ordered by sortOrder ascending, then createdAt descending. sortOrder is not in the response. A page past the end returns "groups": [] with that page, limit, and total. Group object: 500 fallback: Failed to list groups.

POST /api/merchant/v1/stores/{storeId}/groups

Scope: group:create
Body (merchantCreateGroupSchema):
201 returns the group object from GET one group, including linked products. products lists numeric product ids in this store. An id that is not in the store returns 400 One or more products were not found in this store (BAD_REQUEST). products: [] or an omitted products creates the group with no members. A duplicate name returns 400 Group name already exists for this store (BAD_REQUEST). 500 fallback: Failed to create group.

GET /api/merchant/v1/stores/{storeId}/groups/{groupId}

Scope: group:read
200 returns one group object. products lists members:
Each product in products: productCount is _count.products when the row has it, otherwise the length of products. 500 fallback: Failed to load group.

PATCH /api/merchant/v1/stores/{storeId}/groups/{groupId}

Scope: group:update
Body (merchantPatchGroupSchema, every create field optional):
200 returns the group object from GET one. Omitted fields stay as stored. Omitting products leaves membership unchanged. Sending products unlinks every current member, then links the given numeric product ids. products: [] leaves the group with no members. Product ids must belong to this store (same 400 as create). A duplicate name returns 400 Group name already exists for this store (BAD_REQUEST). 500 fallback: Failed to update group.

PUT /api/merchant/v1/stores/{storeId}/groups/{groupId}

Scope: group:update
PUT uses the same auth scope, the same partial body, and the same update as PATCH. 200 returns the group object from GET one. The 500 fallback is Failed to update group.

DELETE /api/merchant/v1/stores/{storeId}/groups/{groupId}

Scope: group:delete
No body. 200:
The handler unlinks products from the group, then deletes the group. 500 fallback: Failed to delete group.