Grids

10 endpointsEnglish only

Read a grid

get/grids/{gridId}experimentalSession required

Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X GET 'https://mercastra.cloud/api/grids/<gridId>' \
  -H 'Cookie: session=<session cookie>'

Rename a grid or change its icon size

patch/grids/{gridId}experimentalSession required

Updates any subset of name and iconSize; the body is strict, an unknown field is a 400. Neither field can be cleared, only replaced with a new value. Buttons are not editable through this endpoint, see the button endpoints below. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id

Request body

FieldTypeRequiredConstraintsDescription
namestringNominLength: 1-
iconSizeGridIconSizeNo-A grid's icon size, one of these three values.
Example
{
  "name": "Standard pricing"
}

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 400The request body does not match the schema

    The shared validation error body, see Validation errors.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X PATCH 'https://mercastra.cloud/api/grids/<gridId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Standard pricing"}'

Delete a grid

delete/grids/{gridId}experimentalSession required

Permanently deletes the grid and its buttons. Responds with an empty body on success. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id

Responses

  • 200Success

    No body.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X DELETE 'https://mercastra.cloud/api/grids/<gridId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

Add a button to a grid

post/grids/{gridId}/buttonsexperimentalSession required

Appends one button, assigned a fresh id, and returns the whole updated grid, not just the new button. The body is not strict, an unknown field is silently dropped rather than rejected. There is no per-grid button count cap, only the per-workshop grid cap above. Responds 403 when the caller is not a member of the grid's workshop and 404 when no such grid exists. Responds 400 when the body fails schema validation, in the usual ValidationError shape, or - with an empty body instead, not ValidationError - when iconFileId is given but does not reference a file uploaded with category grid_tile_icon scoped to this grid's workshop.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id

Request body

FieldTypeRequiredConstraintsDescription
emojiEmojiYes-One emoji from Mercastra's curated icon set (see EMOJI_CATEGORIES).
labelstringYesminLength: 1-
unitLineItemUnitYes-A line item's unit of measure, one of these seven values.
unitPricenumberYesminimum: 0-
backgroundColorHexColorNo-A #RRGGBB hex color.
iconFileIdstringNominLength: 1-
Example
{
  "emoji": "🛠️",
  "label": "Labor",
  "unit": "hour",
  "unitPrice": 80
}

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 400The body fails schema validation (ValidationError shape), or iconFileId does not reference a valid, unused grid_tile_icon file (empty body)

    The shared validation error body, see Validation errors.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X POST 'https://mercastra.cloud/api/grids/<gridId>/buttons' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"emoji":"🛠️","label":"Labor","unit":"hour","unitPrice":80}'

Update a button

patch/grids/{gridId}/buttons/{buttonId}experimentalSession required

Updates any subset of a button's emoji, label, unit, unitPrice, backgroundColor and iconFileId, and returns the whole updated grid, not just the changed button. The body is strict, an unknown field is a 400. backgroundColor and iconFileId are independently clearable: omit a key to leave it untouched, send it as null to remove the button's existing value, or send a new value to replace it - this is distinct from the other three fields, which have no null-clearing behavior since a button always has an emoji, a label and a unit. Sending iconFileId as null skips the icon-validity check below. Responds 403 when the caller is not a member of the grid's workshop and 404 both when no such grid exists and when the grid exists but has no button with this id - the two cases are indistinguishable in the response. Responds 400 when the body fails schema validation, in the usual ValidationError shape, or - with an empty body instead, not ValidationError - when iconFileId is given as a non-null value that does not reference a file uploaded with category grid_tile_icon scoped to this grid's workshop.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id
buttonIdstringYes-Button id

Request body

FieldTypeRequiredConstraintsDescription
emojiEmojiNo-One emoji from Mercastra's curated icon set (see EMOJI_CATEGORIES).
labelstringNominLength: 1-
unitLineItemUnitNo-A line item's unit of measure, one of these seven values.
unitPricenumberNominimum: 0-
backgroundColorHexColor | nullNo--
iconFileIdstring | nullNo--
Example
{
  "unitPrice": 95
}

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 400The body fails schema validation (ValidationError shape), or a non-null iconFileId does not reference a valid grid_tile_icon file (empty body)

    The shared validation error body, see Validation errors.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X PATCH 'https://mercastra.cloud/api/grids/<gridId>/buttons/<buttonId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"unitPrice":95}'

Remove a button

delete/grids/{gridId}/buttons/{buttonId}experimentalSession required

Removes one button and returns the whole updated grid, not an empty body (unlike deleting the grid itself). Responds 403 when the caller is not a member of the grid's workshop and 404 both when no such grid exists and when the grid exists but has no button with this id - the two cases are indistinguishable in the response.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id
buttonIdstringYes-Button id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X DELETE 'https://mercastra.cloud/api/grids/<gridId>/buttons/<buttonId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

Duplicate a grid

post/grids/{gridId}/duplicateexperimentalSession required

Creates a new grid copying name (suffixed with " (copy)"), iconSize and every button, each assigned a fresh id; everything else about a button is copied verbatim, including its iconFileId. Responds 403 when the caller is not a member of the grid's workshop, 404 when no such grid exists, and 409 with an empty body when the workshop already has 10 grids, the same per-workshop cap as creating one, checked against the source grid's workshop.

Path parameters

NameTypeRequiredConstraintsDescription
gridIdstringYes-Grid id

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-2",
      "workshopId": "workshop-1",
      "name": "Standard pricing (copy)",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

  • 409Conflict

    No body.

Example request

curl -X POST 'https://mercastra.cloud/api/grids/<gridId>/duplicate' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

Delete a grid button icon

delete/workshops/{workshopId}/grid-icons/{fileId}experimentalSession required

Deletes a file uploaded with category grid_tile_icon, scoped to this workshop. When one or more grid buttons in the workshop still reference the icon by iconFileId, the default behavior is to respond 409 with the ids of every grid using it and delete nothing; pass detach=true to instead clear iconFileId from every referencing button first and then delete the file. detach is compared to the literal string "true"; any other value, including absent, is treated as false, there is no 400 for an invalid value. Responds 403 when the caller is not a member of the workshop and 404 when fileId does not reference a grid_tile_icon file scoped to this workshop.

Path parameters

NameTypeRequiredConstraintsDescription
workshopIdstringYes-Workshop id
fileIdstringYes-File id

Query parameters

NameTypeRequiredConstraintsDescription
detachstringNo-Pass the literal string "true" to detach and delete; any other value behaves as false

Responses

  • 200Success

    No body.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

  • 409Conflict
    FieldTypeRequiredConstraintsDescription
    gridIdsarray of stringYes--
    Example
    {
      "gridIds": [
        "grid-1"
      ]
    }

Example request

curl -X DELETE 'https://mercastra.cloud/api/workshops/<workshopId>/grid-icons/<fileId>?detach=<detach>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

List a workshop's grids

get/workshops/{workshopId}/gridsexperimentalSession required

Returns every grid belonging to the workshop. Responds 403 when the caller is not a member of the workshop; there is no 404 for an unknown workshop id, a non-member sees 403 either way.

Path parameters

NameTypeRequiredConstraintsDescription
workshopIdstringYes-Workshop id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    gridsarray of GridYes--
    Example
    {
      "grids": [
        {
          "id": "grid-1",
          "workshopId": "workshop-1",
          "name": "Standard pricing",
          "iconSize": "medium",
          "createdAt": "2026-08-01T09:00:00.000Z",
          "updatedAt": "2026-08-10T14:30:00.000Z",
          "buttons": [
            {
              "id": "button-1",
              "emoji": "🛠️",
              "label": "Labor",
              "unit": "hour",
              "unitPrice": 80
            }
          ]
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

Example request

curl -X GET 'https://mercastra.cloud/api/workshops/<workshopId>/grids' \
  -H 'Cookie: session=<session cookie>'

Create a grid in a workshop

post/workshops/{workshopId}/gridsexperimentalSession required

Creates a new grid. name defaults to "Untitled grid" when omitted or blank after trimming; iconSize defaults to medium and buttons default to none, each submitted button is assigned a fresh id. The body is not strict, an unknown field is silently dropped rather than rejected. Responds 403 when the caller is not a member of the workshop and 409 with an empty body when the workshop already has 10 grids, the per-workshop cap; there is no 404 for an unknown workshop id, a non-member sees 403 either way.

Path parameters

NameTypeRequiredConstraintsDescription
workshopIdstringYes-Workshop id

Request body

FieldTypeRequiredConstraintsDescription
namestringNo--
iconSizeGridIconSizeNo-A grid's icon size, one of these three values.
buttonsarray of objectNo--
buttons[].emojiEmojiYes-One emoji from Mercastra's curated icon set (see EMOJI_CATEGORIES).
buttons[].labelstringYesminLength: 1-
buttons[].unitLineItemUnitYes-A line item's unit of measure, one of these seven values.
buttons[].unitPricenumberYesminimum: 0-
buttons[].backgroundColorHexColorNo-A #RRGGBB hex color.
buttons[].iconFileIdstringNominLength: 1-
Example
{
  "name": "Standard pricing"
}

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    iconSizeGridIconSizeYes-A grid's icon size, one of these three values.
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    buttonsarray of GridButtonYes--
    Example
    {
      "id": "grid-1",
      "workshopId": "workshop-1",
      "name": "Standard pricing",
      "iconSize": "medium",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "buttons": [
        {
          "id": "button-1",
          "emoji": "🛠️",
          "label": "Labor",
          "unit": "hour",
          "unitPrice": 80
        }
      ]
    }
  • 400The request body does not match the schema

    The shared validation error body, see Validation errors.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 409Conflict

    No body.

Example request

curl -X POST 'https://mercastra.cloud/api/workshops/<workshopId>/grids' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Standard pricing"}'