{
  "openapi": "3.1.0",
  "info": {
    "title": "Measure Map Online API",
    "version": "1.0.0",
    "summary": "Projects, measurements, folders, products and members of a Measure Map Online account.",
    "description": "The Measure Map Online REST API lets your own software read and change what is in your\naccount: projects and their measurements, folders, the product catalogue and, on\nEnterprise, the members of your organisation. It also mints the tokens that put a\nproject on your website and start guest projects for your visitors.\n\nEvery request is sent to `https://api.measuremaponline.com/api/v1` with your API key in\nthe `Authorization` header. Answers are JSON, except the `.mmp` export, which is XML.\n",
    "contact": {
      "name": "Measure Map Online support",
      "email": "support@blueblinkone.com",
      "url": "https://help.measuremaponline.com"
    },
    "termsOfService": "https://measuremaponline.com/terms"
  },
  "servers": [
    {
      "url": "https://api.measuremaponline.com/api/v1",
      "description": "Live and sandbox. The key you send decides which one a request works in."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Status",
      "x-icon": "monitor_heart",
      "description": "Check that the API answers. This is the only endpoint that needs no key.\n"
    },
    {
      "name": "Authentication",
      "x-icon": "key",
      "description": "Short-lived tokens made with your API key, so the key itself stays on your server.\n\n- An **embed token** opens one of your projects in an `<iframe>` on your website\n  (see [Embedding projects](/guides/embedding)).\n- An **access token** also starts guest projects for your visitors on Open Guest\n  (see [Open Guest](/guides/open-guest)).\n- A **member sign-in token** opens the account of a member of your organisation\n  (Enterprise).\n\nYour account can mint up to 600 embed and access tokens an hour.\n"
    },
    {
      "name": "Projects",
      "x-icon": "map",
      "description": "Projects hold the measurements: areas, lines, spots and image overlays. A key reaches\nthe projects of its own account and, on Enterprise, those of the organisation's members.\n\nDeleting a project through the API is permanent: nothing goes to the trash.\n"
    },
    {
      "name": "Folders",
      "x-icon": "folder_open",
      "description": "Folders group projects. A folder's members can see the projects filed in it.\nDeleting a folder through the API removes it with the folders and projects inside it\nthat belong to your account or your members, permanently.\n"
    },
    {
      "name": "Members",
      "x-icon": "group",
      "x-plans": [
        "enterprise"
      ],
      "description": "The people in your organisation. Each member uses a seat of your Enterprise\nsubscription. These endpoints need a key with **Members** access, which only\nEnterprise keys can have.\n"
    },
    {
      "name": "Products",
      "x-icon": "inventory_2",
      "description": "The product catalogue: the priced materials and services that turn measurements into\nan estimate. Inside an organisation there is one catalogue, the organisation's; its\nowner, Admins and Managers may change it.\n\nProducts are never deleted through the API, only archived, and a product's cost price\nis never returned. Products access is never granted to a key by default: it has to be\nchosen when the key is created.\n"
    }
  ],
  "x-error-codes": [
    {
      "code": "invalid_body",
      "status": 400,
      "description": "A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values."
    },
    {
      "code": "request_body_empty",
      "status": 400,
      "description": "The request needs a JSON body and none was sent."
    },
    {
      "code": "invalid_uid",
      "status": 400,
      "description": "The person's ID in the path is missing or blank."
    },
    {
      "code": "invalid_project_id",
      "status": 400,
      "description": "The project or folder ID in the path is missing or blank."
    },
    {
      "code": "pagination_invalid_page_number",
      "status": 400,
      "description": "The `page` query parameter is missing or not a number."
    },
    {
      "code": "pagination_invalid_page_limit",
      "status": 400,
      "description": "The `limit` query parameter is missing or not a number above 0."
    },
    {
      "code": "invalid_product_pricing",
      "status": 400,
      "description": "A product's prices are not complete or not valid. Check the unit, the currency, the type and every tier."
    },
    {
      "code": "unauthorized_action",
      "status": 401,
      "description": "The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person."
    },
    {
      "code": "budget_limit_reached",
      "status": 402,
      "description": "Open Guest. Your budget limit for this billing period is reached, so no more guest projects are started until the next one or until you raise it."
    },
    {
      "code": "enterprise_seat_payment_failed",
      "status": 402,
      "description": "Every seat is taken, and the payment for the extra seat your automatic seats setting asked for did not go through."
    },
    {
      "code": "invalid_api_key",
      "status": 403,
      "description": "No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced."
    },
    {
      "code": "api_key_suspended",
      "status": 403,
      "description": "API access on your account has been suspended. Contact support."
    },
    {
      "code": "subscription_expired",
      "status": 403,
      "description": "Your subscription is not active. Update your payment method in the app."
    },
    {
      "code": "invalid_subscription",
      "status": 403,
      "description": "Your plan has no API access. The API is part of the Enterprise and Open Guest plans."
    },
    {
      "code": "insufficient_scope",
      "status": 403,
      "description": "The key does not have the access this endpoint needs. Create a key with that access."
    },
    {
      "code": "scope_not_in_plan",
      "status": 403,
      "description": "Your plan does not include this endpoint, for example members on Open Guest."
    },
    {
      "code": "not_found",
      "status": 404,
      "description": "The folder or product does not exist, or is not in your catalogue."
    },
    {
      "code": "project_not_found",
      "status": 404,
      "description": "The project does not exist."
    },
    {
      "code": "user_not_found",
      "status": 404,
      "description": "The person does not exist or is not a member of your organisation."
    },
    {
      "code": "user_email_already_exists",
      "status": 409,
      "description": "An account with this email address already exists."
    },
    {
      "code": "enterprise_add_users_limit",
      "status": 409,
      "description": "Every seat of your Enterprise subscription is taken. Add seats in the app, or turn on automatic seats."
    },
    {
      "code": "enterprise_transfer_required",
      "status": 409,
      "description": "Your organisation requires a member's projects to be transferred before their account is deleted. Transfer them in the app first."
    },
    {
      "code": "too_many_requests",
      "status": 429,
      "description": "Your account asked for more than 600 tokens in an hour. Wait a little and try again."
    },
    {
      "code": "server_error",
      "status": 500,
      "description": "The request could not be completed. Check the IDs you sent and try again; contact support if it persists."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getApiStatus",
        "tags": [
          "Status"
        ],
        "summary": "Check the API",
        "description": "Answers with the API's name and version. Needs no key, and is not counted in your usage.",
        "security": [],
        "responses": {
          "200": {
            "description": "The API is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiStatus"
                },
                "example": {
                  "name": "Measure Map Online Public API",
                  "version": "0.5.0"
                }
              }
            }
          }
        }
      }
    },
    "/auth/token": {
      "post": {
        "operationId": "createAccessToken",
        "tags": [
          "Authentication"
        ],
        "summary": "Create an access token",
        "description": "A token that stands in for your key for a while, so the key never has to leave your\nserver. Put it in an embed's address (`token=`), or send an Open Guest visitor to a\nnew project of their own with it.\n\nThe token belongs to the same environment as the key: a sandbox key mints sandbox\ntokens. Embeds only open live projects, so use a live key for them.\n",
        "x-scopes": [
          "auth"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccessTokenRequest"
              },
              "example": {
                "duration": 3600
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token, when it stops working, and its environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessToken"
                },
                "example": {
                  "token": "7f3c9a1e5b2d4f60a8c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5",
                  "expiresOn": 1792345678000,
                  "environment": "live"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`api_key_suspended`: API access on your account has been suspended. Contact support.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.\n\n`invalid_subscription`: Your plan has no API access. The API is part of the Enterprise and Open Guest plans.\n\n`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_api_key"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_requests`: Your account asked for more than 600 tokens in an hour. Wait a little and try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "too_many_requests"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_api_key",
          "api_key_suspended",
          "subscription_expired",
          "invalid_subscription",
          "insufficient_scope",
          "unauthorized_action",
          "too_many_requests"
        ]
      }
    },
    "/auth/iframe/token": {
      "post": {
        "operationId": "createEmbedToken",
        "tags": [
          "Authentication"
        ],
        "summary": "Create an embed token",
        "description": "A token for one embed, valid for 60 minutes. Your server asks for one before it\nwrites the page, and puts it in the iframe's address instead of your key:\n\n```\nhttps://app.measuremaponline.com/iframe_api?pid=PROJECT_ID&token=TOKEN\n```\n\nUse [Create an access token](/reference/authentication/create-access-token) when you\nneed a different lifetime.\n",
        "x-scopes": [
          "auth"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "responses": {
          "200": {
            "description": "The token and its environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedToken"
                },
                "example": {
                  "token": "2b4d6f8a0c1e3a5c7e9b1d3f5a7c9e0b2d4f6a8c1e3b5d7f9a0c2e4b6d8f1a3c",
                  "environment": "live"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`api_key_suspended`: API access on your account has been suspended. Contact support.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.\n\n`invalid_subscription`: Your plan has no API access. The API is part of the Enterprise and Open Guest plans.\n\n`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_api_key"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_requests`: Your account asked for more than 600 tokens in an hour. Wait a little and try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "too_many_requests"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_api_key",
          "api_key_suspended",
          "subscription_expired",
          "invalid_subscription",
          "insufficient_scope",
          "unauthorized_action",
          "too_many_requests"
        ]
      }
    },
    "/auth/{uid}/login": {
      "post": {
        "operationId": "createMemberSignInToken",
        "tags": [
          "Authentication"
        ],
        "summary": "Sign in as a member",
        "description": "A sign-in token for one member of your organisation, to open their account from your\nown software. Add it to an app address as `sit`; the member is signed in and the\naddress opens:\n\n```\nhttps://app.measuremaponline.com/dashboard?sit=TOKEN\nhttps://app.measuremaponline.com/app/p/PROJECT_ID?sit=TOKEN\n```\n\nUse the token within an hour of getting it. Because it opens the member's whole\naccount, it is held to more than other endpoints:\n\n- the key must have been created with **Members: Read and write** access (a key made\n  before access areas existed does not qualify);\n- it must be a live key;\n- it is never given for your own account, nor for an Admin or a Manager of the\n  organisation.\n",
        "x-scopes": [
          "users:write"
        ],
        "x-plans": [
          "enterprise"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberUid"
          }
        ],
        "responses": {
          "200": {
            "description": "The sign-in token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignInToken"
                },
                "example": {
                  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJtM0s4cVp4THBWNE5yVDJ3In0.c2lnbmF0dXJl"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`user_not_found`: The person does not exist or is not a member of your organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "user_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "insufficient_scope",
          "scope_not_in_plan",
          "unauthorized_action",
          "user_not_found",
          "invalid_api_key",
          "subscription_expired"
        ]
      }
    },
    "/projects/new": {
      "post": {
        "operationId": "createProject",
        "tags": [
          "Projects"
        ],
        "summary": "Create a project",
        "description": "Creates a project in your own account.\n\nOn Open Guest, a project created with the access level `open_guest` is a guest\nproject: it counts towards your Open Guest usage and budget like one started from\nyour guest link, and its guest details are kept with it. Guest projects made with a\nsandbox key are never counted or billed.\n",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              },
              "example": {
                "name": "14 Harbour Road",
                "description": "Front and back lawns, quote by Friday",
                "access_level": "view_only",
                "comments_enabled": true,
                "parentId": "fld_8Jk2LmQ4"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                },
                "example": {
                  "type": "project",
                  "id": "Xr7Lp2Qm9KdT4wZs",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "fld_8Jk2LmQ4",
                  "name": "14 Harbour Road",
                  "createdOn": 1791980000000,
                  "description": "Front and back lawns, quote by Friday",
                  "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                  "totalPolygons": 3,
                  "totalSpots": 1,
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ],
                  "commentsEnabled": true,
                  "defaultProductId": "prd_Lw7Qm2Xc"
                }
              }
            }
          },
          "400": {
            "description": "`request_body_empty`: The request needs a JSON body and none was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "request_body_empty"
                }
              }
            }
          },
          "402": {
            "description": "`budget_limit_reached`: Open Guest. Your budget limit for this billing period is reached, so no more guest projects are started until the next one or until you raise it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "budget_limit_reached"
                }
              }
            }
          },
          "403": {
            "description": "`invalid_subscription`: Your plan has no API access. The API is part of the Enterprise and Open Guest plans.\n\n`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_subscription"
                }
              }
            }
          }
        },
        "x-errors": [
          "request_body_empty",
          "budget_limit_reached",
          "invalid_subscription",
          "insufficient_scope",
          "invalid_api_key",
          "subscription_expired"
        ]
      }
    },
    "/users/{uid}/projects/new": {
      "post": {
        "operationId": "createPersonProject",
        "tags": [
          "Projects"
        ],
        "summary": "Create a project for a person",
        "description": "Creates a project in the account of a member of your organisation, or in your own when\n`uid` is your account's ID. The project is created **view only**, so it can be\nembedded straight away.\n",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PersonUid"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PersonProjectCreate"
              },
              "example": {
                "name": "14 Harbour Road",
                "description": "Front and back lawns",
                "comments_enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                },
                "example": {
                  "type": "project",
                  "id": "Xr7Lp2Qm9KdT4wZs",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "fld_8Jk2LmQ4",
                  "name": "14 Harbour Road",
                  "createdOn": 1791980000000,
                  "description": "Front and back lawns, quote by Friday",
                  "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                  "totalPolygons": 3,
                  "totalSpots": 1,
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ],
                  "commentsEnabled": true,
                  "defaultProductId": "prd_Lw7Qm2Xc"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.\n\n`request_body_empty`: The request needs a JSON body and none was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "request_body_empty",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key",
          "subscription_expired"
        ]
      }
    },
    "/users/{uid}/projects/all": {
      "get": {
        "operationId": "listAllPersonProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List all of a person's projects",
        "description": "Every project the person can open: their own and those shared with them, most\nrecently opened first. For long lists, use\n[List a person's projects page by page](/reference/projects/list-person-projects).\n",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PersonUid"
          }
        ],
        "responses": {
          "200": {
            "description": "The projects.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Project"
                  }
                },
                "example": [
                  {
                    "type": "project",
                    "id": "Xr7Lp2Qm9KdT4wZs",
                    "creatorUid": "m3K8qZxLpV4NrT2w",
                    "folderId": "fld_8Jk2LmQ4",
                    "name": "14 Harbour Road",
                    "createdOn": 1791980000000,
                    "description": "Front and back lawns, quote by Friday",
                    "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                    "totalPolygons": 3,
                    "totalSpots": 1,
                    "members": [
                      "m3K8qZxLpV4NrT2w"
                    ],
                    "commentsEnabled": true,
                    "defaultProductId": "prd_Lw7Qm2Xc"
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key",
          "subscription_expired"
        ]
      }
    },
    "/users/{uid}/projects": {
      "get": {
        "operationId": "listPersonProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List a person's projects page by page",
        "description": "The person's projects, most recently opened first, one page at a time. See\n[Pagination](/guides/pagination).\n",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PersonUid"
          },
          {
            "name": "page",
            "in": "query",
            "required": true,
            "description": "The page to read, starting at 1.",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 1
          },
          {
            "name": "limit",
            "in": "query",
            "required": true,
            "description": "How many projects a page holds. At most 200; a larger number is read as 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "example": 20
          },
          {
            "name": "shared",
            "in": "query",
            "required": false,
            "description": "`true` to list the projects the person can open, shared ones included; by default only the projects they created.",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": false
          }
        ],
        "responses": {
          "200": {
            "description": "One page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectPage"
                },
                "example": {
                  "has_more": true,
                  "page": 1,
                  "items_per_page": 20,
                  "items": [
                    {
                      "type": "project",
                      "id": "Xr7Lp2Qm9KdT4wZs",
                      "creatorUid": "m3K8qZxLpV4NrT2w",
                      "folderId": "fld_8Jk2LmQ4",
                      "name": "14 Harbour Road",
                      "createdOn": 1791980000000,
                      "description": "Front and back lawns, quote by Friday",
                      "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                      "totalPolygons": 3,
                      "totalSpots": 1,
                      "members": [
                        "m3K8qZxLpV4NrT2w"
                      ],
                      "commentsEnabled": true,
                      "defaultProductId": "prd_Lw7Qm2Xc"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.\n\n`pagination_invalid_page_number`: The `page` query parameter is missing or not a number.\n\n`pagination_invalid_page_limit`: The `limit` query parameter is missing or not a number above 0.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "pagination_invalid_page_number",
          "pagination_invalid_page_limit",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/projects/{pid}": {
      "get": {
        "operationId": "getProject",
        "tags": [
          "Projects"
        ],
        "summary": "Get a project",
        "description": "One project's details. Its measurements come from [Get a project's measurements](/reference/projects/get-project-data).",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                },
                "example": {
                  "type": "project",
                  "id": "Xr7Lp2Qm9KdT4wZs",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "fld_8Jk2LmQ4",
                  "name": "14 Harbour Road",
                  "createdOn": 1791980000000,
                  "description": "Front and back lawns, quote by Friday",
                  "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                  "totalPolygons": 3,
                  "totalSpots": 1,
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ],
                  "commentsEnabled": true,
                  "defaultProductId": "prd_Lw7Qm2Xc"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "put": {
        "operationId": "updateProject",
        "tags": [
          "Projects"
        ],
        "summary": "Update a project",
        "description": "Renames a project, changes its description or members, or moves it to another folder.\nOnly the fields you send change.\n\nBy default the `members` you send are added to the project's; with\n`mergeMembers=false` they replace them.\n",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          },
          {
            "$ref": "#/components/parameters/MergeMembers"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemUpdate"
              },
              "example": {
                "name": "14 Harbour Road (revised)",
                "members": [
                  "m3K8qZxLpV4NrT2w"
                ],
                "parentId": "root"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                },
                "example": {
                  "type": "project",
                  "id": "Xr7Lp2Qm9KdT4wZs",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "fld_8Jk2LmQ4",
                  "name": "14 Harbour Road",
                  "createdOn": 1791980000000,
                  "description": "Front and back lawns, quote by Friday",
                  "thumbnailUrl": "https://storage.googleapis.com/example/thumbnails/Xr7Lp2Qm9KdT4wZs.png",
                  "totalPolygons": 3,
                  "totalSpots": 1,
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ],
                  "commentsEnabled": true,
                  "defaultProductId": "prd_Lw7Qm2Xc"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.\n\n`invalid_body`: A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "invalid_body",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "delete": {
        "operationId": "deleteProject",
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project",
        "description": "Deletes the project and everything in it, permanently. Nothing goes to the trash.",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          }
        ],
        "responses": {
          "200": {
            "description": "The project is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/projects/{pid}/data": {
      "get": {
        "operationId": "getProjectData",
        "tags": [
          "Projects"
        ],
        "summary": "Get a project's measurements",
        "description": "Everything drawn on the project: areas and lines (`polygon`), spots (`spot`) and image\noverlays (`image_overlay`), told apart by `type`. A line is a polygon with\n`noSurface: true` and an area of 0; a hole has `isHole: true` and a buffer\n`isBuffer: true`, both with the shape they belong to in `parentId`.\n\nAreas are in square metres and lengths in metres, as last saved.\n",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          },
          {
            "name": "includePoints",
            "in": "query",
            "required": false,
            "description": "`false` to leave out the corners of each shape, for a lighter answer.",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "example": true
          }
        ],
        "responses": {
          "200": {
            "description": "The project's shapes, spots and overlays.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/Polygon"
                      },
                      {
                        "$ref": "#/components/schemas/Spot"
                      },
                      {
                        "$ref": "#/components/schemas/ImageOverlay"
                      }
                    ],
                    "discriminator": {
                      "propertyName": "type"
                    }
                  }
                },
                "example": [
                  {
                    "id": "pg_4Rt8Wq2Z",
                    "type": "polygon",
                    "title": "Front lawn",
                    "description": "",
                    "shape": "polygon",
                    "numPoints": 4,
                    "fillColor": "#22c55e",
                    "fillOpacity": 0.35,
                    "lineColor": "#16a34a",
                    "lineWidth": 2,
                    "totalArea": 412.6,
                    "totalPerimeter": 84.2,
                    "noSurface": false,
                    "isBuffer": false,
                    "isHole": false,
                    "images": [],
                    "productId": "prd_Lw7Qm2Xc",
                    "productIds": [
                      "prd_Lw7Qm2Xc"
                    ],
                    "points": [
                      {
                        "type": "point",
                        "id": "pt_1",
                        "polygonId": "pg_4Rt8Wq2Z",
                        "description": "",
                        "index": 0,
                        "prevPointId": "pt_4",
                        "nextPointId": "pt_2",
                        "latitude": 51.50721,
                        "longitude": -0.12758,
                        "altitude": 0,
                        "images": []
                      }
                    ]
                  },
                  {
                    "id": "sp_9Hd3Ka1V",
                    "type": "spot",
                    "name": "Gate",
                    "description": "Side access, 1.2 m wide",
                    "iconId": 3,
                    "latitude": 51.50733,
                    "longitude": -0.12731,
                    "altitude": 0,
                    "images": []
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/projects/{pid}/comments": {
      "get": {
        "operationId": "listProjectComments",
        "tags": [
          "Projects"
        ],
        "summary": "List a project's comments",
        "description": "The comments left on the project's map, with where on the map each one was placed.",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          }
        ],
        "responses": {
          "200": {
            "description": "The comments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Comment"
                  }
                },
                "example": [
                  {
                    "type": "project_comment",
                    "id": "cm_2Zx8Lq4P",
                    "projectId": "Xr7Lp2Qm9KdT4wZs",
                    "timestamp": 1791987654000,
                    "text": "Could you add the side garden too?",
                    "latitude": 51.50729,
                    "longitude": -0.12744,
                    "uid": "m3K8qZxLpV4NrT2w",
                    "userName": "Maria Lopez",
                    "userPhotoUrl": ""
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/projects/{pid}/mmp": {
      "get": {
        "operationId": "exportProjectMmp",
        "tags": [
          "Projects"
        ],
        "summary": "Export a project as .mmp",
        "description": "The project as a `.mmp` file, the same file the app exports: XML holding every shape,\nspot, picture reference and setting. See [The .mmp format](/guides/mmp-format).\n",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectId"
          }
        ],
        "responses": {
          "200": {
            "description": "The .mmp file.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string",
                  "description": "An .mmp document."
                },
                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mmp version=\"2\">\n  <project id=\"Xr7Lp2Qm9KdT4wZs\" name=\"14 Harbour Road\">…</project>\n</mmp>\n"
              }
            }
          },
          "400": {
            "description": "`invalid_project_id`: The project or folder ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_project_id"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`project_not_found`: The project does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "project_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_project_id",
          "project_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/users/{uid}/folders/new": {
      "post": {
        "operationId": "createFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Create a folder",
        "description": "Creates a folder in the account of a member of your organisation, or in your own when `uid` is your account's ID. A folder created inside another takes on its members.",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PersonUid"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FolderCreate"
              },
              "example": {
                "name": "2026 jobs",
                "description": "Everything quoted this year"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new folder.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Folder"
                },
                "example": {
                  "type": "folder",
                  "id": "fld_8Jk2LmQ4",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "",
                  "name": "2026 jobs",
                  "createdOn": 1788000000000,
                  "description": "Everything quoted this year",
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`request_body_empty`: The request needs a JSON body and none was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "request_body_empty"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "request_body_empty",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/users/{uid}/folders/all": {
      "get": {
        "operationId": "listPersonFolders",
        "tags": [
          "Folders"
        ],
        "summary": "List a person's folders",
        "description": "Every folder the person is a member of, theirs and those shared with them, by name.",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PersonUid"
          }
        ],
        "responses": {
          "200": {
            "description": "The folders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Folder"
                  }
                },
                "example": [
                  {
                    "type": "folder",
                    "id": "fld_8Jk2LmQ4",
                    "creatorUid": "m3K8qZxLpV4NrT2w",
                    "folderId": "",
                    "name": "2026 jobs",
                    "createdOn": 1788000000000,
                    "description": "Everything quoted this year",
                    "members": [
                      "m3K8qZxLpV4NrT2w"
                    ]
                  }
                ]
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/folders/{fid}": {
      "get": {
        "operationId": "getFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Get a folder",
        "x-scopes": [
          "projects:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "description": "One folder's details.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderId"
          }
        ],
        "responses": {
          "200": {
            "description": "The folder.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Folder"
                },
                "example": {
                  "type": "folder",
                  "id": "fld_8Jk2LmQ4",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "",
                  "name": "2026 jobs",
                  "createdOn": 1788000000000,
                  "description": "Everything quoted this year",
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "put": {
        "operationId": "updateFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Update a folder",
        "description": "Renames a folder, changes its description or members, or moves it into another\nfolder. Only the fields you send change. A folder cannot be moved into itself or into\na folder inside it.\n\nBy default the `members` you send are added to the folder's; with\n`mergeMembers=false` they replace them.\n",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderId"
          },
          {
            "$ref": "#/components/parameters/MergeMembers"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemUpdate"
              },
              "example": {
                "name": "2026 jobs (archive)",
                "parentId": "root"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The folder as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Folder"
                },
                "example": {
                  "type": "folder",
                  "id": "fld_8Jk2LmQ4",
                  "creatorUid": "m3K8qZxLpV4NrT2w",
                  "folderId": "",
                  "name": "2026 jobs",
                  "createdOn": 1788000000000,
                  "description": "Everything quoted this year",
                  "members": [
                    "m3K8qZxLpV4NrT2w"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body`: A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_body"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_body",
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "delete": {
        "operationId": "deleteFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Delete a folder",
        "description": "Deletes the folder, the folders inside it and the projects in them that belong to your\naccount or your members, permanently. Projects other people filed in a shared folder\nare left in their accounts.\n",
        "x-scopes": [
          "projects:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FolderId"
          }
        ],
        "responses": {
          "200": {
            "description": "The folder is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/users": {
      "get": {
        "operationId": "listMembers",
        "tags": [
          "Members"
        ],
        "summary": "List members",
        "description": "Every member of your organisation. Your own account is not in the list.",
        "x-scopes": [
          "users:read"
        ],
        "x-plans": [
          "enterprise"
        ],
        "responses": {
          "200": {
            "description": "The members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                },
                "example": [
                  {
                    "id": "m3K8qZxLpV4NrT2w",
                    "fullName": "Maria Lopez",
                    "email": "maria@example.com",
                    "emailVerified": true,
                    "company": "Lopez Landscapes",
                    "phoneNumber": "+44 20 7946 0958",
                    "address": "12 Mill Lane",
                    "zipCode": "SW1A 1AA",
                    "country": "United Kingdom",
                    "photoUrl": "",
                    "createdOn": 1785000000000,
                    "lastSignin": 1791990000000,
                    "deleted": false,
                    "metadata": {
                      "crmId": "4711"
                    }
                  }
                ]
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "insufficient_scope",
          "scope_not_in_plan",
          "unauthorized_action",
          "invalid_api_key",
          "subscription_expired"
        ]
      }
    },
    "/users/new": {
      "post": {
        "operationId": "createMember",
        "tags": [
          "Members"
        ],
        "summary": "Create a member",
        "description": "Creates an account for a new member of your organisation, with the email address and\npassword you choose. It uses a seat, as an invitation does, and gives the member your\norganisation's default role.\n\nWhen every seat is taken and automatic seats are on, a seat is added and charged\nfirst; the member is only created once that payment goes through.\n",
        "x-scopes": [
          "users:write"
        ],
        "x-plans": [
          "enterprise"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberCreate"
              },
              "example": {
                "fullName": "Maria Lopez",
                "email": "maria@example.com",
                "password": "a-long-random-password"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "id": "m3K8qZxLpV4NrT2w",
                  "fullName": "Maria Lopez",
                  "email": "maria@example.com",
                  "emailVerified": true,
                  "company": "Lopez Landscapes",
                  "phoneNumber": "+44 20 7946 0958",
                  "address": "12 Mill Lane",
                  "zipCode": "SW1A 1AA",
                  "country": "United Kingdom",
                  "photoUrl": "",
                  "createdOn": 1785000000000,
                  "lastSignin": 1791990000000,
                  "deleted": false,
                  "metadata": {
                    "crmId": "4711"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "402": {
            "description": "`enterprise_seat_payment_failed`: Every seat is taken, and the payment for the extra seat your automatic seats setting asked for did not go through.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "enterprise_seat_payment_failed"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "409": {
            "description": "`user_email_already_exists`: An account with this email address already exists.\n\n`enterprise_add_users_limit`: Every seat of your Enterprise subscription is taken. Add seats in the app, or turn on automatic seats.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "user_email_already_exists"
                }
              }
            }
          }
        },
        "x-errors": [
          "user_email_already_exists",
          "enterprise_add_users_limit",
          "enterprise_seat_payment_failed",
          "insufficient_scope",
          "scope_not_in_plan",
          "unauthorized_action",
          "subscription_expired"
        ]
      }
    },
    "/users/{uid}": {
      "get": {
        "operationId": "getMember",
        "tags": [
          "Members"
        ],
        "summary": "Get a member",
        "description": "One member of your organisation. Your own account's ID answers with your account.",
        "x-scopes": [
          "users:read"
        ],
        "x-plans": [
          "enterprise"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberUid"
          }
        ],
        "responses": {
          "200": {
            "description": "The member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "id": "m3K8qZxLpV4NrT2w",
                  "fullName": "Maria Lopez",
                  "email": "maria@example.com",
                  "emailVerified": true,
                  "company": "Lopez Landscapes",
                  "phoneNumber": "+44 20 7946 0958",
                  "address": "12 Mill Lane",
                  "zipCode": "SW1A 1AA",
                  "country": "United Kingdom",
                  "photoUrl": "",
                  "createdOn": 1785000000000,
                  "lastSignin": 1791990000000,
                  "deleted": false,
                  "metadata": {
                    "crmId": "4711"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`user_not_found`: The person does not exist or is not a member of your organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "user_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "user_not_found",
          "insufficient_scope",
          "scope_not_in_plan",
          "unauthorized_action"
        ]
      },
      "put": {
        "operationId": "updateMember",
        "tags": [
          "Members"
        ],
        "summary": "Update a member",
        "description": "Changes a member's details or role. Only the fields you send change; an empty string\nclears a field, except the name, which a member always keeps. Your own account cannot\nbe changed here.\n",
        "x-scopes": [
          "users:write"
        ],
        "x-plans": [
          "enterprise"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberUid"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberUpdate"
              },
              "example": {
                "phoneNumber": "+44 20 7946 0958",
                "enterpriseRole": "manager"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.\n\n`invalid_body`: A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`user_not_found`: The person does not exist or is not a member of your organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "user_not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "invalid_body",
          "user_not_found",
          "unauthorized_action",
          "insufficient_scope",
          "scope_not_in_plan",
          "subscription_expired"
        ]
      },
      "delete": {
        "operationId": "deleteMember",
        "tags": [
          "Members"
        ],
        "summary": "Delete a member",
        "description": "Deletes the member's account: they can no longer sign in, and their data is erased\nafter a short grace period. The person is told by email. Their seat stays on your\nsubscription unless automatic seats are on, when one is removed.\n\nIf your organisation requires projects to be transferred before an account is\ndeleted, transfer them in the app first.\n",
        "x-scopes": [
          "users:write"
        ],
        "x-plans": [
          "enterprise"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberUid"
          }
        ],
        "responses": {
          "200": {
            "description": "The member was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Success"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "`invalid_uid`: The person's ID in the path is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_uid"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`scope_not_in_plan`: Your plan does not include this endpoint, for example members on Open Guest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`user_not_found`: The person does not exist or is not a member of your organisation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "user_not_found"
                }
              }
            }
          },
          "409": {
            "description": "`enterprise_transfer_required`: Your organisation requires a member's projects to be transferred before their account is deleted. Transfer them in the app first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "enterprise_transfer_required"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_uid",
          "user_not_found",
          "unauthorized_action",
          "enterprise_transfer_required",
          "insufficient_scope",
          "scope_not_in_plan"
        ]
      }
    },
    "/products": {
      "get": {
        "operationId": "listProducts",
        "tags": [
          "Products"
        ],
        "summary": "List products",
        "description": "The catalogue that applies to your account, by name. Archived products are left out unless you ask for them.",
        "x-scopes": [
          "products:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "name": "includeArchived",
            "in": "query",
            "required": false,
            "description": "`true` to include archived products.",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "example": false
          }
        ],
        "responses": {
          "200": {
            "description": "The products.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Product"
                  }
                },
                "example": [
                  {
                    "type": "product",
                    "id": "prd_Lw7Qm2Xc",
                    "name": "Artificial turf",
                    "description": "40 mm pile, laid on a compacted base",
                    "ownerUid": "m3K8qZxLpV4NrT2w",
                    "createdOn": 1790000000000,
                    "enabled": true,
                    "archived": false,
                    "category": "Surfaces",
                    "color": "#22c55e",
                    "icon": "",
                    "taxRate": 20,
                    "pricing": {
                      "unit": "m²",
                      "currency": "gbp",
                      "type": "range",
                      "price": 32,
                      "prices": [
                        {
                          "min": 0,
                          "max": 100,
                          "price": 32
                        },
                        {
                          "min": 100,
                          "max": -1,
                          "price": 28
                        }
                      ]
                    }
                  }
                ]
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.\n\n`subscription_expired`: Your subscription is not active. Update your payment method in the app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "insufficient_scope",
          "invalid_api_key",
          "subscription_expired"
        ]
      },
      "post": {
        "operationId": "createProduct",
        "tags": [
          "Products"
        ],
        "summary": "Create a product",
        "description": "Adds a product to the catalogue. It needs a name and at least one way to charge: by\narea (`pricing`), by length (`distancePricing`) or per spot (`itemPricing`).\nInside an organisation, only the owner, Admins and Managers may change the catalogue.\n",
        "x-scopes": [
          "products:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductInput"
              },
              "example": {
                "name": "Artificial turf",
                "description": "40 mm pile, laid on a compacted base",
                "category": "Surfaces",
                "color": "#22c55e",
                "taxRate": 20,
                "pricing": {
                  "unit": "m²",
                  "currency": "gbp",
                  "type": "range",
                  "prices": [
                    {
                      "min": 0,
                      "max": 100,
                      "price": 32
                    },
                    {
                      "min": 100,
                      "price": 28
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                },
                "example": {
                  "type": "product",
                  "id": "prd_Lw7Qm2Xc",
                  "name": "Artificial turf",
                  "description": "40 mm pile, laid on a compacted base",
                  "ownerUid": "m3K8qZxLpV4NrT2w",
                  "createdOn": 1790000000000,
                  "enabled": true,
                  "archived": false,
                  "category": "Surfaces",
                  "color": "#22c55e",
                  "icon": "",
                  "taxRate": 20,
                  "pricing": {
                    "unit": "m²",
                    "currency": "gbp",
                    "type": "range",
                    "price": 32,
                    "prices": [
                      {
                        "min": 0,
                        "max": 100,
                        "price": 32
                      },
                      {
                        "min": 100,
                        "max": -1,
                        "price": 28
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body`: A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values.\n\n`invalid_product_pricing`: A product's prices are not complete or not valid. Check the unit, the currency, the type and every tier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_body"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_body",
          "invalid_product_pricing",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    },
    "/products/{id}": {
      "get": {
        "operationId": "getProduct",
        "tags": [
          "Products"
        ],
        "summary": "Get a product",
        "x-scopes": [
          "products:read"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "description": "One product of your catalogue.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductId"
          }
        ],
        "responses": {
          "200": {
            "description": "The product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                },
                "example": {
                  "type": "product",
                  "id": "prd_Lw7Qm2Xc",
                  "name": "Artificial turf",
                  "description": "40 mm pile, laid on a compacted base",
                  "ownerUid": "m3K8qZxLpV4NrT2w",
                  "createdOn": 1790000000000,
                  "enabled": true,
                  "archived": false,
                  "category": "Surfaces",
                  "color": "#22c55e",
                  "icon": "",
                  "taxRate": 20,
                  "pricing": {
                    "unit": "m²",
                    "currency": "gbp",
                    "type": "range",
                    "price": 32,
                    "prices": [
                      {
                        "min": 0,
                        "max": 100,
                        "price": 32
                      },
                      {
                        "min": 100,
                        "max": -1,
                        "price": 28
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "put": {
        "operationId": "updateProduct",
        "tags": [
          "Products"
        ],
        "summary": "Update a product",
        "description": "Changes a product. Only the fields you send change. A price you send without a unit\nor currency keeps the product's own.\n",
        "x-scopes": [
          "products:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductInput"
              },
              "example": {
                "pricing": {
                  "type": "fixed",
                  "price": 30
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The product as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                },
                "example": {
                  "type": "product",
                  "id": "prd_Lw7Qm2Xc",
                  "name": "Artificial turf",
                  "description": "40 mm pile, laid on a compacted base",
                  "ownerUid": "m3K8qZxLpV4NrT2w",
                  "createdOn": 1790000000000,
                  "enabled": true,
                  "archived": false,
                  "category": "Surfaces",
                  "color": "#22c55e",
                  "icon": "",
                  "taxRate": 20,
                  "pricing": {
                    "unit": "m²",
                    "currency": "gbp",
                    "type": "range",
                    "price": 32,
                    "prices": [
                      {
                        "min": 0,
                        "max": 100,
                        "price": 32
                      },
                      {
                        "min": 100,
                        "max": -1,
                        "price": 28
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body`: A value in the request body is missing, has the wrong type, is too long or is not one of the accepted values.\n\n`invalid_product_pricing`: A product's prices are not complete or not valid. Check the unit, the currency, the type and every tier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "invalid_body"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "invalid_body",
          "invalid_product_pricing",
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      },
      "delete": {
        "operationId": "archiveProduct",
        "tags": [
          "Products"
        ],
        "summary": "Archive a product",
        "description": "Archives the product: it leaves the pickers in the app, and every shape already priced\nwith it keeps its price. Products are never deleted through the API. Send\n`archived: false` with [Update a product](/reference/products/update-product) to bring\none back.\n",
        "x-scopes": [
          "products:write"
        ],
        "x-plans": [
          "enterprise",
          "open_guest"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProductId"
          }
        ],
        "responses": {
          "200": {
            "description": "The product, archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ArchivedProduct"
                },
                "example": {
                  "archived": true,
                  "product": {
                    "type": "product",
                    "id": "prd_Lw7Qm2Xc",
                    "name": "Artificial turf",
                    "description": "40 mm pile, laid on a compacted base",
                    "ownerUid": "m3K8qZxLpV4NrT2w",
                    "createdOn": 1790000000000,
                    "enabled": true,
                    "archived": false,
                    "category": "Surfaces",
                    "color": "#22c55e",
                    "icon": "",
                    "taxRate": 20,
                    "pricing": {
                      "unit": "m²",
                      "currency": "gbp",
                      "type": "range",
                      "price": 32,
                      "prices": [
                        {
                          "min": 0,
                          "max": 100,
                          "price": 32
                        },
                        {
                          "min": 100,
                          "max": -1,
                          "price": 28
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized_action`: The item is not on your account or your members', the request came from a web page on a website the key does not allow, or the action is not allowed for this person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "unauthorized_action"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: The key does not have the access this endpoint needs. Create a key with that access.\n\n`invalid_api_key`: No key was sent in the `Authorization` header, or the key is unknown, revoked, or past the time it kept working after being replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "insufficient_scope"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: The folder or product does not exist, or is not in your catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "errorCode": "not_found"
                }
              }
            }
          }
        },
        "x-errors": [
          "not_found",
          "unauthorized_action",
          "insufficient_scope",
          "invalid_api_key"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Your API key, sent as it is: no `Bearer` in front. Create keys on the Developer page\nof the app. See [Authentication](/guides/authentication).\n"
      }
    },
    "parameters": {
      "PersonUid": {
        "name": "uid",
        "in": "path",
        "required": true,
        "description": "The ID of a member of your organisation, or your own account's ID.",
        "schema": {
          "type": "string"
        },
        "example": "m3K8qZxLpV4NrT2w"
      },
      "MemberUid": {
        "name": "uid",
        "in": "path",
        "required": true,
        "description": "The ID of a member of your organisation.",
        "schema": {
          "type": "string"
        },
        "example": "m3K8qZxLpV4NrT2w"
      },
      "ProjectId": {
        "name": "pid",
        "in": "path",
        "required": true,
        "description": "The project's ID, the part after `/p/` in its address in the app.",
        "schema": {
          "type": "string"
        },
        "example": "Xr7Lp2Qm9KdT4wZs"
      },
      "FolderId": {
        "name": "fid",
        "in": "path",
        "required": true,
        "description": "The folder's ID.",
        "schema": {
          "type": "string"
        },
        "example": "fld_8Jk2LmQ4"
      },
      "ProductId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The product's ID.",
        "schema": {
          "type": "string"
        },
        "example": "prd_Lw7Qm2Xc"
      },
      "MergeMembers": {
        "name": "mergeMembers",
        "in": "query",
        "required": false,
        "description": "`false` to replace the members with the ones you send, instead of adding them.",
        "schema": {
          "type": "boolean",
          "default": true
        },
        "example": true
      }
    },
    "schemas": {
      "ApiStatus": {
        "type": "object",
        "required": [
          "name",
          "version"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The API's name."
          },
          "version": {
            "type": "string",
            "description": "The version of the server answering."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "What a failed request answers with, together with its HTTP status. Branch on\n`errorCode`; `errorMessage` is for people and may change.\n",
        "required": [
          "errorCode"
        ],
        "properties": {
          "errorCode": {
            "type": "string",
            "description": "A stable code. See [Errors](/guides/errors).",
            "example": "insufficient_scope"
          },
          "errorMessage": {
            "type": "string",
            "description": "A short explanation in English. Not sent for every code.",
            "example": "This API key does not have the scope required for this endpoint"
          }
        }
      },
      "Success": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "AccessTokenRequest": {
        "type": "object",
        "properties": {
          "duration": {
            "type": "integer",
            "description": "How long the token works, in seconds. 3600 (an hour) when left out; at least 60 and at most 86400 (24 hours).",
            "minimum": 60,
            "maximum": 86400,
            "default": 3600
          }
        }
      },
      "AccessToken": {
        "type": "object",
        "required": [
          "token",
          "expiresOn",
          "environment"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The token. Treat it like a password until it expires."
          },
          "expiresOn": {
            "type": "integer",
            "format": "int64",
            "description": "When the token stops working, in milliseconds since 1 January 1970 (UTC)."
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          }
        }
      },
      "EmbedToken": {
        "type": "object",
        "required": [
          "token",
          "environment"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The token. It works for 60 minutes."
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          }
        }
      },
      "SignInToken": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "A one-time sign-in token for the member. Use it within an hour."
          }
        }
      },
      "Environment": {
        "type": "string",
        "enum": [
          "live",
          "sandbox"
        ],
        "description": "Which world the key, and everything made with it, belongs to. See [Live and sandbox](/guides/environments)."
      },
      "AccessLevel": {
        "type": "string",
        "enum": [
          "private",
          "view_only",
          "public",
          "open_guest"
        ],
        "description": "Who can open the project. `private`: only its members. `view_only` and `public`: anyone\nwith the link or an embed can look at it. `open_guest`: a guest project of an Open\nGuest plan.\n"
      },
      "Project": {
        "type": "object",
        "required": [
          "type",
          "id",
          "creatorUid",
          "name",
          "createdOn"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "project"
          },
          "id": {
            "type": "string",
            "description": "The project's ID."
          },
          "creatorUid": {
            "type": "string",
            "description": "The account that owns the project."
          },
          "folderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The folder it is filed in; empty or absent at the top level."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "createdOn": {
            "type": "integer",
            "format": "int64",
            "description": "When it was created, in milliseconds since 1 January 1970 (UTC)."
          },
          "thumbnailUrl": {
            "type": "string",
            "format": "uri",
            "description": "A picture of the project's map, when one has been drawn."
          },
          "totalPolygons": {
            "type": "integer",
            "description": "How many areas and lines it holds."
          },
          "totalSpots": {
            "type": "integer",
            "description": "How many spots it holds."
          },
          "members": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The accounts that can open it, its owner included."
          },
          "commentsEnabled": {
            "type": "boolean",
            "description": "Whether comments can be left on its map."
          },
          "defaultProductId": {
            "type": "string",
            "description": "The product new shapes start with."
          }
        }
      },
      "ProjectPage": {
        "type": "object",
        "required": [
          "has_more",
          "page",
          "items_per_page",
          "items"
        ],
        "properties": {
          "has_more": {
            "type": "boolean",
            "description": "Whether this page was full, so the next one may hold more."
          },
          "page": {
            "type": "integer"
          },
          "items_per_page": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          }
        }
      },
      "Guest": {
        "type": "object",
        "description": "The guest's contact details, for an Open Guest project. Each is optional.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 160,
            "description": "Dropped when it is not a valid address."
          },
          "phone": {
            "type": "string",
            "maxLength": 40
          }
        }
      },
      "ProjectCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The project's name."
          },
          "description": {
            "type": "string"
          },
          "access_level": {
            "$ref": "#/components/schemas/AccessLevel",
            "description": "`private` when left out. `open_guest` only on an Open Guest plan, where it makes a billed guest project."
          },
          "comments_enabled": {
            "type": "boolean",
            "description": "Allow comments on the map. Always on for guest projects."
          },
          "default_product_id": {
            "type": "string",
            "description": "A product of your catalogue that new shapes start with. Ignored when it is archived or hidden."
          },
          "parentId": {
            "type": "string",
            "description": "A folder of yours to file it in; it takes on the folder's members."
          },
          "members": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "type": "string"
            },
            "description": "Accounts of your organisation that can open the project."
          },
          "guest": {
            "$ref": "#/components/schemas/Guest"
          }
        }
      },
      "PersonProjectCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "comments_enabled": {
            "type": "boolean"
          },
          "default_product_id": {
            "type": "string",
            "description": "A product of the catalogue that new shapes start with."
          },
          "parentId": {
            "type": "string",
            "description": "A folder the person owns or is a member of."
          },
          "members": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "type": "string"
            },
            "description": "Accounts of your organisation that can open the project."
          }
        }
      },
      "ItemUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "A new name. Not blank."
          },
          "description": {
            "type": "string",
            "maxLength": 5000
          },
          "members": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "string"
            },
            "description": "Accounts to add as members (or to keep, with `mergeMembers=false`)."
          },
          "parentId": {
            "type": "string",
            "description": "A folder of yours or your members' to move it into, or `root` for the top level."
          }
        }
      },
      "Folder": {
        "type": "object",
        "required": [
          "type",
          "id",
          "creatorUid",
          "name",
          "createdOn"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "folder"
          },
          "id": {
            "type": "string"
          },
          "creatorUid": {
            "type": "string",
            "description": "The account that owns the folder."
          },
          "folderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The folder it is inside; empty or absent at the top level."
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "createdOn": {
            "type": "integer",
            "format": "int64",
            "description": "When it was created, in milliseconds since 1 January 1970 (UTC)."
          },
          "members": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The accounts that can open it and the projects filed in it."
          }
        }
      },
      "FolderCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "parentId": {
            "type": "string",
            "description": "A folder to create it in; it takes on that folder's members."
          },
          "members": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Accounts that can open the folder."
          }
        }
      },
      "Point": {
        "type": "object",
        "description": "One corner of a shape.",
        "required": [
          "type",
          "id",
          "polygonId",
          "index",
          "latitude",
          "longitude"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "point"
          },
          "id": {
            "type": "string"
          },
          "polygonId": {
            "type": "string",
            "description": "The shape it belongs to."
          },
          "description": {
            "type": "string"
          },
          "index": {
            "type": "integer",
            "description": "Its place along the shape, from 0."
          },
          "prevPointId": {
            "type": "string"
          },
          "nextPointId": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "altitude": {
            "type": "number",
            "format": "double",
            "description": "Height in metres, when known."
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Pictures attached to the corner."
          }
        }
      },
      "Polygon": {
        "type": "object",
        "description": "An area or a line drawn on the map.",
        "required": [
          "type",
          "id",
          "shape"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "polygon"
          },
          "id": {
            "type": "string"
          },
          "parentId": {
            "type": "string",
            "description": "For a hole or a buffer, the shape it belongs to."
          },
          "groupId": {
            "type": "string",
            "description": "The group the shape is in."
          },
          "productId": {
            "type": "string",
            "description": "The first product on the shape (see productIds)."
          },
          "productIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every product on the shape, in order. Work is often layered."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "shape": {
            "type": "string",
            "enum": [
              "polygon",
              "circle",
              "rectangle"
            ]
          },
          "numPoints": {
            "type": "integer",
            "description": "How many corners it has."
          },
          "fillColor": {
            "type": "string",
            "description": "Hex colour."
          },
          "fillOpacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "lineColor": {
            "type": "string",
            "description": "Hex colour."
          },
          "lineWidth": {
            "type": "number"
          },
          "totalArea": {
            "type": "number",
            "description": "Its area in square metres; 0 for a line."
          },
          "totalPerimeter": {
            "type": "number",
            "description": "Its perimeter (or a line's length) in metres."
          },
          "noSurface": {
            "type": "boolean",
            "description": "`true` for a line."
          },
          "isBuffer": {
            "type": "boolean"
          },
          "isHole": {
            "type": "boolean"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "points": {
            "type": "array",
            "description": "Its corners in order. Left out with `includePoints=false`.",
            "items": {
              "$ref": "#/components/schemas/Point"
            }
          }
        }
      },
      "Spot": {
        "type": "object",
        "description": "A marked spot on the map.",
        "required": [
          "type",
          "id",
          "latitude",
          "longitude"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "spot"
          },
          "id": {
            "type": "string"
          },
          "productId": {
            "type": "string"
          },
          "productIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "groupId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "iconId": {
            "type": "integer",
            "description": "The spot's icon, by number."
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "altitude": {
            "type": "number",
            "format": "double"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "ImageOverlay": {
        "type": "object",
        "description": "A picture laid over the map, such as a site plan.",
        "required": [
          "type",
          "id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "image_overlay"
          },
          "id": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "opacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "imageUrl": {
            "type": "string",
            "format": "uri"
          },
          "createdOn": {
            "type": "integer",
            "format": "int64"
          },
          "bounds": {
            "type": "object",
            "description": "Where the picture's edges are.",
            "properties": {
              "south": {
                "type": "number"
              },
              "west": {
                "type": "number"
              },
              "north": {
                "type": "number"
              },
              "east": {
                "type": "number"
              }
            }
          }
        }
      },
      "Comment": {
        "type": "object",
        "required": [
          "type",
          "id",
          "projectId",
          "timestamp",
          "text"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "project_comment"
          },
          "id": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "timestamp": {
            "type": "integer",
            "format": "int64",
            "description": "When it was left, in milliseconds since 1 January 1970 (UTC)."
          },
          "text": {
            "type": "string"
          },
          "latitude": {
            "type": "number",
            "format": "double"
          },
          "longitude": {
            "type": "number",
            "format": "double"
          },
          "uid": {
            "type": "string",
            "description": "Who left it."
          },
          "userName": {
            "type": "string"
          },
          "userPhotoUrl": {
            "type": "string"
          }
        }
      },
      "User": {
        "type": "object",
        "description": "An account.",
        "required": [
          "id",
          "email"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The account's ID."
          },
          "fullName": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "emailVerified": {
            "type": "boolean"
          },
          "company": {
            "type": "string"
          },
          "phoneNumber": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "zipCode": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "photoUrl": {
            "type": "string"
          },
          "createdOn": {
            "type": "integer",
            "format": "int64",
            "description": "When the account was created, in milliseconds since 1 January 1970 (UTC)."
          },
          "lastSignin": {
            "type": "integer",
            "format": "int64",
            "description": "When they last signed in, in milliseconds since 1 January 1970 (UTC)."
          },
          "deleted": {
            "type": "boolean"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Your own data about the member, as you last set it with Update a member."
          }
        }
      },
      "EnterpriseRole": {
        "type": "string",
        "enum": [
          "admin",
          "manager",
          "user",
          "viewer"
        ],
        "description": "A member's role in the organisation. `user` is shown as Member in the app."
      },
      "MemberCreate": {
        "type": "object",
        "required": [
          "fullName",
          "email",
          "password"
        ],
        "properties": {
          "fullName": {
            "type": "string",
            "description": "The member's name."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 6,
            "description": "Their first password. They can change it after signing in."
          },
          "company": {
            "type": "string",
            "description": "Their company. Kept when your own account has a company name."
          }
        }
      },
      "MemberUpdate": {
        "type": "object",
        "properties": {
          "fullName": {
            "type": "string",
            "maxLength": 120
          },
          "company": {
            "type": "string",
            "maxLength": 120
          },
          "address": {
            "type": "string",
            "maxLength": 300
          },
          "zipCode": {
            "type": "string",
            "maxLength": 20
          },
          "country": {
            "type": "string",
            "maxLength": 80
          },
          "phoneNumber": {
            "type": "string",
            "maxLength": 40
          },
          "enterpriseRole": {
            "$ref": "#/components/schemas/EnterpriseRole"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Your own data about the member, such as their ID in your CRM. Replaces what was there."
          }
        }
      },
      "PriceTier": {
        "type": "object",
        "description": "One band of a range price. The whole quantity is charged at the rate of the band it falls in.",
        "required": [
          "min",
          "price"
        ],
        "properties": {
          "min": {
            "type": "number",
            "minimum": 0
          },
          "max": {
            "type": "number",
            "description": "Above `min`, or -1 for no upper bound (the default).",
            "default": -1
          },
          "price": {
            "type": "number",
            "minimum": 0
          }
        }
      },
      "AreaUnit": {
        "type": "string",
        "description": "An area unit. Common spellings such as `sqm`, `sqft` and `acre` are accepted and stored as these.",
        "enum": [
          "m²",
          "km²",
          "a",
          "ha",
          "sq ft",
          "sq yd",
          "sq mi",
          "acres",
          "squares",
          "f(v)",
          "f(ca)",
          "f(co)",
          "c",
          "ts(bu)",
          "tan",
          "li²",
          "mu"
        ]
      },
      "DistanceUnit": {
        "type": "string",
        "description": "A length unit. Common spellings such as `meters`, `feet` and `miles` are accepted and stored as these.",
        "enum": [
          "m",
          "km",
          "ft",
          "yd",
          "mile",
          "nmi",
          "ken",
          "ri",
          "b",
          "l",
          "lk",
          "ch"
        ]
      },
      "Pricing": {
        "type": "object",
        "description": "How a product is charged, by area, by length or per spot. A new price needs its `unit`\nand `currency`; on an update, a price sent without them keeps the product's own.\n",
        "properties": {
          "unit": {
            "type": "string",
            "description": "An area unit for `pricing` (see [AreaUnit](/reference/models/area-unit)), a length unit for `distancePricing` (see [DistanceUnit](/reference/models/distance-unit)), `each` for `itemPricing`.",
            "examples": [
              "m²",
              "ft",
              "each"
            ]
          },
          "currency": {
            "type": "string",
            "pattern": "^[a-z]{3}$",
            "description": "ISO 4217 code, in lower case.",
            "example": "eur"
          },
          "type": {
            "type": "string",
            "enum": [
              "fixed",
              "range"
            ],
            "description": "`fixed`: one price per unit. `range`: a price per band of quantity. When left out, `range` if `prices` are sent, otherwise `fixed`. Per-spot prices are always `fixed`."
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "The price per unit, for `fixed`."
          },
          "prices": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/PriceTier"
            },
            "description": "The bands, for `range`."
          }
        }
      },
      "Season": {
        "type": "object",
        "description": "A window of the year, recurring every year, with prices of its own. A window may wrap the year end.",
        "required": [
          "startMonth",
          "startDay",
          "endMonth",
          "endDay"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 40
          },
          "startMonth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12
          },
          "startDay": {
            "type": "integer",
            "minimum": 1,
            "maximum": 31
          },
          "endMonth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12
          },
          "endDay": {
            "type": "integer",
            "minimum": 1,
            "maximum": 31
          },
          "enabled": {
            "type": "boolean",
            "default": true
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "distancePricing": {
            "$ref": "#/components/schemas/Pricing"
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "type",
          "id",
          "name",
          "ownerUid",
          "enabled",
          "archived"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "product"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "ownerUid": {
            "type": "string",
            "description": "Whose catalogue it belongs to, the organisation owner's or yours."
          },
          "createdOn": {
            "type": "integer",
            "format": "int64"
          },
          "enabled": {
            "type": "boolean",
            "description": "Offered in the pickers."
          },
          "archived": {
            "type": "boolean"
          },
          "category": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "thumbnailUrl": {
            "type": "string",
            "format": "uri"
          },
          "textureUrl": {
            "type": "string",
            "format": "uri",
            "description": "A picture that fills shapes priced with it on the map."
          },
          "textureTileMeters": {
            "type": "number",
            "description": "Ground covered by one tile of the texture, in metres."
          },
          "taxRate": {
            "type": "number",
            "description": "A percentage. Absent means the project's rate applies; 0 means untaxed."
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "distancePricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "itemPricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "seasons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Season"
            }
          }
        }
      },
      "ProductInput": {
        "type": "object",
        "description": "A product's fields. `name` is needed to create one.",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "thumbnailUrl": {
            "type": "string",
            "format": "uri"
          },
          "textureUrl": {
            "type": "string",
            "format": "uri"
          },
          "textureTileMeters": {
            "type": "number",
            "minimum": 0.01,
            "maximum": 20
          },
          "enabled": {
            "type": "boolean",
            "default": true
          },
          "archived": {
            "type": "boolean"
          },
          "taxRate": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "costPrice": {
            "type": "number",
            "minimum": 0,
            "description": "What the product costs you, for your own margin in the app. Stored, never returned."
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "distancePricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "itemPricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "seasons": {
            "type": "array",
            "maxItems": 24,
            "items": {
              "$ref": "#/components/schemas/Season"
            }
          }
        }
      },
      "ArchivedProduct": {
        "type": "object",
        "required": [
          "archived",
          "product"
        ],
        "properties": {
          "archived": {
            "type": "boolean",
            "const": true
          },
          "product": {
            "$ref": "#/components/schemas/Product"
          }
        }
      }
    }
  }
}
