Estimates

12 endpointsEnglish only

Read an estimate

get/estimates/{estimateId}experimentalSession required

Returns the estimate directly, not wrapped in an envelope object (unlike the list endpoint's estimates key). Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

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

Update an estimate's name, recipient, markup or discount

patch/estimates/{estimateId}experimentalSession required

Updates any subset of name, recipientName, markupPercent and discountPercent; unlike the order equivalent, an empty body is accepted, it only bumps updatedAt. The body is strict, an unknown field is a 400. A provided name must be non-blank after trimming; a provided recipientName may be an empty string, which clears the field entirely rather than storing blank text. Line items are not editable through this endpoint, see the line-item endpoints below. None of these fields counts as a new revision, call confirm to snapshot the result. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Request body

FieldTypeRequiredConstraintsDescription
namestringNominLength: 1-
recipientNamestringNo--
markupPercentnumberNominimum: 0-
discountPercentnumberNominimum: 0maximum: 100-
Example
{
  "markupPercent": 15
}

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 15,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 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/estimates/<estimateId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"markupPercent":15}'

Delete an estimate

delete/estimates/{estimateId}experimentalSession required

Permanently deletes the estimate, its revision history, its PDF exports and its hand-off history, and frees the workshop's file storage quota used by its stored PDFs; the PDF files themselves are deleted too. Responds with an empty body on success. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate 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/estimates/<estimateId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

Snapshot an estimate's current content as a new revision

post/estimates/{estimateId}/confirmexperimentalSession required

Estimate creation and duplication already write revision 1; confirm snapshots the estimate's current name, markup, discount and line items as the next revisionNumber. After writing it, revisions beyond the workshop's plan cap (GET /workshops/{workshopId}/entitlements's revisions.limitPerEstimate, null on Pro) are pruned silently, oldest first - there is no error response for exceeding the cap, the history is simply trimmed. The cap is only enforced at confirm time, so a plan downgrade does not retroactively prune existing history until the next confirm. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    estimateEstimateYes-An estimate as returned to a member. Confirming it (see the confirm endpoint) snapshots this content into a new revision; nothing about the estimate itself records whether it has ever been confirmed.
    revisionEstimateRevisionYes-A snapshot of an estimate's content at the moment it was confirmed, numbered from 1.
    Example
    {
      "estimate": {
        "id": "estimate-1",
        "workshopId": "workshop-1",
        "name": "Bespoke chair estimate",
        "recipientName": "Anna Kowalska",
        "createdAt": "2026-08-01T09:00:00.000Z",
        "updatedAt": "2026-08-10T14:30:00.000Z",
        "markupPercent": 10,
        "discountPercent": 0,
        "lineItems": [
          {
            "id": "line-item-1",
            "label": "Oak plank, 2m",
            "unit": "sqm",
            "unitPrice": 120,
            "quantity": 4
          }
        ]
      },
      "revision": {
        "id": "revision-1",
        "revisionNumber": 1,
        "createdAt": "2026-08-01T09:00:00.000Z",
        "createdByUid": "uid-1",
        "name": "Bespoke chair estimate",
        "markupPercent": 10,
        "discountPercent": 0,
        "lineItems": [
          {
            "id": "line-item-1",
            "label": "Oak plank, 2m",
            "unit": "sqm",
            "unitPrice": 120,
            "quantity": 4
          }
        ]
      }
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

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

Duplicate an estimate

post/estimates/{estimateId}/duplicateexperimentalSession required

Creates a new estimate copying name (suffixed with " (copy)"), recipientName, markupPercent, discountPercent and every line item, each assigned a fresh id; the source's revision history is not copied, the duplicate gets its own fresh revision 1 immediately, same as a plain create. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-2",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate (copy)",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

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

Add a line item to an estimate

post/estimates/{estimateId}/line-itemsexperimentalSession required

Appends one line item, assigned a fresh id, and returns the whole updated estimate, not just the new item. The body is not strict, an unknown field is silently dropped rather than rejected. sourceButtonId (a grid button id) is stored as given, not validated against the workshop's grids. Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Request body

FieldTypeRequiredConstraintsDescription
labelstringYesminLength: 1-
unitLineItemUnitYes-A line item's unit of measure, one of these seven values.
unitPricenumberYesminimum: 0-
quantitynumberYesexclusiveMinimum: 0-
sourceButtonIdstringNo--
Example
{
  "label": "Oak plank, 2m",
  "unit": "sqm",
  "unitPrice": 120,
  "quantity": 4
}

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 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 POST 'https://mercastra.cloud/api/estimates/<estimateId>/line-items' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"label":"Oak plank, 2m","unit":"sqm","unitPrice":120,"quantity":4}'

Update a line item

patch/estimates/{estimateId}/line-items/{lineItemId}experimentalSession required

Updates any subset of a line item's label, unit, unitPrice, quantity and sourceButtonId, and returns the whole updated estimate, not just the changed item. The body is strict, an unknown field is a 400. Responds 403 when the caller is not a member of the estimate's workshop and 404 both when no such estimate exists and when the estimate exists but has no line item with this id - the two cases are indistinguishable in the response.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id
lineItemIdstringYes-Line item id

Request body

FieldTypeRequiredConstraintsDescription
labelstringNominLength: 1-
unitLineItemUnitNo-A line item's unit of measure, one of these seven values.
unitPricenumberNominimum: 0-
quantitynumberNoexclusiveMinimum: 0-
sourceButtonIdstringNo--
Example
{
  "quantity": 3
}

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 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/estimates/<estimateId>/line-items/<lineItemId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"quantity":3}'

Remove a line item

delete/estimates/{estimateId}/line-items/{lineItemId}experimentalSession required

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

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id
lineItemIdstringYes-Line item id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

curl -X DELETE 'https://mercastra.cloud/api/estimates/<estimateId>/line-items/<lineItemId>' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>'

List an estimate's revisions

get/estimates/{estimateId}/revisionsexperimentalSession required

Returns every stored revision, newest revisionNumber first, already trimmed to the workshop's plan cap (see the confirm endpoint). Responds 403 when the caller is not a member of the estimate's workshop and 404 when no such estimate exists.

Path parameters

NameTypeRequiredConstraintsDescription
estimateIdstringYes-Estimate id

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    revisionsarray of EstimateRevisionYes--
    Example
    {
      "revisions": [
        {
          "id": "revision-1",
          "revisionNumber": 1,
          "createdAt": "2026-08-01T09:00:00.000Z",
          "createdByUid": "uid-1",
          "name": "Bespoke chair estimate",
          "markupPercent": 10,
          "discountPercent": 0,
          "lineItems": [
            {
              "id": "line-item-1",
              "label": "Oak plank, 2m",
              "unit": "sqm",
              "unitPrice": 120,
              "quantity": 4
            }
          ]
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

  • 404Not found

    No body.

Example request

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

List a workshop's estimates

get/workshops/{workshopId}/estimatesexperimentalSession required

Returns every estimate belonging to the workshop; the collection has no defined order. 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
    estimatesarray of EstimateYes--
    Example
    {
      "estimates": [
        {
          "id": "estimate-1",
          "workshopId": "workshop-1",
          "name": "Bespoke chair estimate",
          "recipientName": "Anna Kowalska",
          "createdAt": "2026-08-01T09:00:00.000Z",
          "updatedAt": "2026-08-10T14:30:00.000Z",
          "markupPercent": 10,
          "discountPercent": 0,
          "lineItems": [
            {
              "id": "line-item-1",
              "label": "Oak plank, 2m",
              "unit": "sqm",
              "unitPrice": 120,
              "quantity": 4
            }
          ]
        }
      ]
    }
  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

Example request

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

Create an estimate in a workshop

post/workshops/{workshopId}/estimatesexperimentalSession required

Creates a new estimate. `name` defaults to "Untitled estimate" when omitted or blank after trimming; markupPercent and discountPercent default to 0 and lineItems to none, each submitted line item is assigned a fresh id. The body is not strict, an unknown field is silently dropped rather than rejected. Creating an estimate also writes its first revision (revisionNumber 1) immediately, before any call to confirm. 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

Request body

FieldTypeRequiredConstraintsDescription
namestringNo--
recipientNamestringNo--
markupPercentnumberNominimum: 0-
discountPercentnumberNominimum: 0maximum: 100-
lineItemsarray of objectNo--
lineItems[].labelstringYesminLength: 1-
lineItems[].unitLineItemUnitYes-A line item's unit of measure, one of these seven values.
lineItems[].unitPricenumberYesminimum: 0-
lineItems[].quantitynumberYesexclusiveMinimum: 0-
lineItems[].sourceButtonIdstringNo--
Example
{
  "name": "Bespoke chair estimate",
  "lineItems": [
    {
      "label": "Oak plank, 2m",
      "unit": "sqm",
      "unitPrice": 120,
      "quantity": 4
    }
  ]
}

Responses

  • 201Created
    FieldTypeRequiredConstraintsDescription
    idstringYes--
    workshopIdstringYes--
    namestringYes--
    recipientNamestringNo-Absent when the estimate has no named recipient
    createdAtstringYesformat: "date-time"-
    updatedAtstringYesformat: "date-time"-
    markupPercentnumberYes--
    discountPercentnumberYes--
    lineItemsarray of LineItemYes--
    Example
    {
      "id": "estimate-1",
      "workshopId": "workshop-1",
      "name": "Bespoke chair estimate",
      "recipientName": "Anna Kowalska",
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-10T14:30:00.000Z",
      "markupPercent": 10,
      "discountPercent": 0,
      "lineItems": [
        {
          "id": "line-item-1",
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "quantity": 4
        }
      ]
    }
  • 400The request body does not match the schema

    The shared validation error body, see Validation errors.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

Example request

curl -X POST 'https://mercastra.cloud/api/workshops/<workshopId>/estimates' \
  -H 'Cookie: session=<session cookie>' \
  -H 'X-CSRF-Token: <csrf-token cookie value>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Bespoke chair estimate","lineItems":[{"label":"Oak plank, 2m","unit":"sqm","unitPrice":120,"quantity":4}]}'

List the workshop's most-used line items

get/workshops/{workshopId}/estimates/most-used-line-itemsexperimentalSession required

Aggregates every line item across the workshop's estimates by label and unit (case-insensitive, trimmed), ranked by how many estimates use them and, as a tiebreaker, by the most recent estimate update. An out-of-range or non-numeric limit responds 400 with an empty body, not a JSON error. 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

Query parameters

NameTypeRequiredConstraintsDescription
limitintegerNominimum: 1maximum: 50Defaults to 15

Responses

  • 200Success
    FieldTypeRequiredConstraintsDescription
    lineItemsarray of objectYes--
    lineItems[].labelstringYes--
    lineItems[].unitLineItemUnitYes-A line item's unit of measure, one of these seven values.
    lineItems[].unitPricenumberYes--
    lineItems[].countnumberYes-How many of the workshop's estimates use a line item with this label and unit
    lineItems[].lastUsedAtstringYesformat: "date-time"The updatedAt of the most recent estimate that uses it
    Example
    {
      "lineItems": [
        {
          "label": "Oak plank, 2m",
          "unit": "sqm",
          "unitPrice": 120,
          "count": 6,
          "lastUsedAt": "2026-08-10T14:30:00.000Z"
        }
      ]
    }
  • 400limit is present but not an integer between 1 and 50

    No body.

  • 401Not authenticated

    No body.

  • 403Forbidden

    No body.

Example request

curl -X GET 'https://mercastra.cloud/api/workshops/<workshopId>/estimates/most-used-line-items?limit=<limit>' \
  -H 'Cookie: session=<session cookie>'