Layer features
List Layer Features
Section titled “List Layer Features”Retrieves a paginated list of layer features based on specified filters. Layer features represent structured geographical data (e.g. anomalies, tickets, detections, etc.).
Endpoint: GET /layerFeatures
Authentication: Required (API Key)
Query Parameters
Section titled “Query Parameters”At least one filter (id, site_id, operation_id, dataset_id, or semantic_type) must be provided.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer[] | No | Filter layer features by one or more layer feature IDs, at most 200. IDs that do not exist or that you cannot read are left out of the result. |
site_id |
(integer|null)[] | No | Filter layer features by one or more site IDs. Supports null to filter for layer features without an associated operation. |
operation_id |
(integer|null)[] | No | Filter layer features by one or more operation IDs. Supports null to filter for layer features without an associated operation. |
dataset_id |
integer[] | No | Filter layer features by one or more dataset IDs. |
semantic_type |
(string|null)[] (enum) | No | Filter layer features by one or more semantic types. See Semantic Types for valid values. Supports null to filter for layer features without an associated operation. |
updated_at[gte] |
timestamp | No | Return only layer features updated at or after this ISO 8601 time. Requires dataset_id. See Changed since a time. |
properties.<uuid> |
string | No | Return only layer features whose custom property <uuid> (a property definition UUID) has exactly this value. Requires exactly one dataset_id. See Filtering by a custom property. |
_page |
integer | Yes | Page number (positive integer) |
_pageSize |
integer | No | Number of items per page (positive integer, max varies by configuration) |
_sortBy |
string | No | Sort order. Format: +field (ascending) or -field (descending); repeat the parameter for tie-breaks (_sortBy=-updated_at&_sortBy=-id). Sortable fields: id, visible_id, description, created_at, updated_at, site, and properties.<key> where <key> is a built-in property key or a custom property definition UUID. Custom-UUID property sorts only resolve when the request is scoped to a single dataset_id; otherwise they fall back to a raw text sort of the stored value (built-in property keys sort correctly regardless). Default: -updated_at then -id |
Unknown query parameters are ignored, so check the spelling of every filter: a misspelled filter returns more layer features than you asked for. Two kinds of parameters return 400 instead of being ignored:
- a
properties.*key that is notproperties.<uuid>; - a parameter with a key in brackets that this endpoint does not support, for example
updated_at[lte]=…orproperties[<uuid>]=…. Empty brackets for a list, such asoperation_id[]=123, are fine.
Changed since a time
Section titled “Changed since a time”updated_at[gte] returns the layer features of the given datasets that changed at or after that time, for example to poll for changes since your last check:
- Send the time in UTC (
2026-10-01T00:00:00Z), or encode the+of an offset as%2B. An unencoded+arrives as a space and returns 400. - Set
updated_at[gte]a few minutes before the time of your previous check. A layer feature’supdated_atcan be slightly earlier than the moment the change becomes visible, so a change could otherwise be missed. If a layer feature comes back twice, match it byid. - Keep the default sort (
-updated_at, then-id) and use a large_pageSize, so one page usually holds all changes. If a layer feature is changed or deleted while you page through the results, rows can shift between pages, so a page walk may skip a row or return it twice. - Comments and relationships between layer features do not change
updated_at. Adding or removing a file in an attachment property does, because it changes that property’s value. Deleted layer features are not returned. - Use site-level datasets, not the company-level dataset they implement (see Dataset relationships). A company-level ticket dataset that site-level datasets implement holds no tickets, so
updated_at[gte]on it returns 400 with that dataset intemplate_dataset_ids.
Filtering by a custom property
Section titled “Filtering by a custom property”properties.<uuid>=<value> returns the layer features of one dataset whose custom property has exactly that value, for example to check whether a ticket already exists for a work order before creating one:
<uuid>is the property definition UUID of a custom property of the dataset, its own or inherited from the company-level dataset it implements. Any other UUID returns 400 with the unknown UUIDs inunknown_property_ids. If the dataset does not exist or your API key cannot read it, every UUID is reported inunknown_property_ids. Built-in properties such asSTATUScannot be filtered this way.- Send each property once, with a non-empty, URL-encoded value. A repeated or empty
properties.<uuid>returns 400. An unencoded+,&or#in the value changes it, so the filter can match nothing or a different layer feature. - The value is compared as text with the stored
value, or withdefaultValuewhen there is novalue. This works for text, yes/no (YESorNO; a yes/no property that was never set matches neither), user (the user or group ID) and option properties (the option’svalue, not its label). Numbers and dates match only their exact stored form (5does not match5.0), and properties with several values never match. - Use the site-level dataset, not the company-level dataset it implements. A custom property filter on a company-level ticket dataset returns 400 with that dataset in
template_dataset_ids.
Response Format
Section titled “Response Format”Returns a paginated response with the following structure:
{ "data": [ { "id": "integer", "visible_id": "string", "properties": {}, "geometry": {}, "dataset_id": "integer", "site_id": "integer", "operation_id": "integer", "company_id": "integer", "semantic_type": "string", "description": "string", "created_at": "timestamp", "updated_at": "timestamp", "parent_layer_feature_id": "integer|null", "related_layer_feature_ids": {} } ], "pagination": { "page": "integer", "pageSize": "integer", "rowCount": "integer", "pageCount": "integer" }}Response Fields
Section titled “Response Fields”-
id: Integer ID of the layer feature. This is the join key used by the
GET /propertyChangesendpoint’slayer_feature_idfilter — unlikevisible_id, it is always present. -
visible_id: Human-readable identifier for the layer feature
-
properties: Object containing the layer feature properties, keyed by property-definition UUID. Since a dataset can inherit property definitions from a parent dataset, a UUID is not guaranteed to belong to the layer feature’s own
dataset_id— call List Property Definitions withoutdataset_idto resolve UUIDs across all datasets you have access to. -
geometry: GeoJSON geometry object
-
dataset_id: Integer ID of the associated dataset
-
site_id: Integer ID of the associated site
-
operation_id: Integer ID of the associated operation
-
company_id: Integer ID of the associated company
-
semantic_type: Type classification of the layer feature
-
description: Text description of the layer feature
-
created_at: ISO 8601 timestamp of when the layer feature was created
-
updated_at: ISO 8601 timestamp of when the layer feature was last updated
-
parent_layer_feature_id: Integer ID of the layer feature this one belongs to, or
null. It is not a relationship type and is not returned by List Layer Feature Relationships. It points at:- for a ticket (including subform tickets): the component it was created on
- for a detection or anomaly: the site component it was matched to (for an anomaly, its panel)
- for a deviation: the misplaced component
It is
nullwhen there is no parent, or when your API key cannot read it. -
related_layer_feature_ids: The layer feature’s direct relationships, keyed by relationship type, each with one layer feature ID. They read from this layer feature:
{ "IS_COMPONENT_OF_MODEL": 42 }means “this layer feature is a component of model 42”. Possible keys:IS_SUBFORM_OF,IS_MECHANICAL_SUBCOMPONENT_OF,IS_ELECTRICAL_SUBCOMPONENT_OF,IS_COMPONENT_OF_MODELandIS_TRIGGERED_BY. It is an empty object when there are none. Related layer features your API key cannot read are left out. For other relationship types, use List Layer Feature Relationships.
Incremental syncs: adding or removing a relationship does not change a layer feature’s updated_at, and removed relationships are deleted outright. A sync that only fetches layer features updated since its last run can therefore miss relationship changes in related_layer_feature_ids; re-read the layer features you need, or use List Layer Feature Relationships, to pick them up.
Example Requests
Section titled “Example Requests”Filter by single operation ID:
curl -X GET "https://api.sitemark.com/layerFeatures?operation_id=123&_page=1&_pageSize=50&_sortBy=-updated_at" \ -H "Authorization: ApiKey your-api-key-here"Filter by multiple operation IDs (without brackets):
curl -X GET "https://api.sitemark.com/layerFeatures?operation_id=123&operation_id=456&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by multiple operation IDs (with brackets):
curl -g -X GET "https://api.sitemark.com/layerFeatures?operation_id[]=123&operation_id[]=456&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by layer features without an operation:
curl -g -X GET "https://api.sitemark.com/layerFeatures?operation_id[]&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by a specific operation OR layer features without an operation:
curl -g -X GET "https://api.sitemark.com/layerFeatures?operation_id[]=123&operation_id[]&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by single semantic type:
curl -X GET "https://api.sitemark.com/layerFeatures?semantic_type=SOLAR_PANELS&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by multiple semantic types (without brackets):
curl -X GET "https://api.sitemark.com/layerFeatures?semantic_type=SOLAR_PANELS&semantic_type=SOLAR_ANOMALIES&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by multiple semantic types (with brackets):
curl -g -X GET "https://api.sitemark.com/layerFeatures?semantic_type[]=SOLAR_PANELS&semantic_type[]=SOLAR_ANOMALIES&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Combine multiple filters:
curl -X GET "https://api.sitemark.com/layerFeatures?operation_id=123&semantic_type=ANNOTATIONS&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by single site ID:
curl -X GET "https://api.sitemark.com/layerFeatures?site_id=5678&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by single dataset ID:
curl -X GET "https://api.sitemark.com/layerFeatures?dataset_id=1234&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Filter by layer feature IDs:
curl -X GET "https://api.sitemark.com/layerFeatures?id=101&id=102&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Layer features of a dataset changed since a time:
curl -g -X GET "https://api.sitemark.com/layerFeatures?dataset_id=1234&updated_at[gte]=2026-10-01T00:00:00Z&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Find the ticket of a work order by a custom property:
curl -X GET "https://api.sitemark.com/layerFeatures?dataset_id=1234&properties.0190d7a4-1b2c-7d3e-8f40-123456789abc=WO-123&_page=1&_pageSize=50" \ -H "Authorization: ApiKey your-api-key-here"Example Response
Section titled “Example Response”{ "data": [ { "visible_id": "LF-001", "properties": { "status": "active", "name": "Building A" }, "geometry": { "type": "Point", "coordinates": [102.0, 0.5] }, "dataset_id": 1234, "site_id": 5678, "operation_id": 7890, "company_id": 9012, "semantic_type": "building", "description": "Main office building", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T14:45:00Z", "parent_layer_feature_id": null, "related_layer_feature_ids": { "IS_COMPONENT_OF_MODEL": 42, "IS_MECHANICAL_SUBCOMPONENT_OF": 17 } } ], "pagination": { "page": 1, "pageSize": 50, "rowCount": 1, "pageCount": 1 }}Rate Limits
Section titled “Rate Limits”GET /layerFeatures has its own limits, on top of the company limits. They are counted separately from the limits of GET /propertyChanges:
- 10 requests per 10 seconds
- 50 requests per minute
- 5 concurrent requests
Error Responses
Section titled “Error Responses”| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid or missing API key |
| 400 | Bad Request - Invalid query parameters, missing required filter, or updated_at[gte] / properties.<uuid> on a company-level ticket dataset |
| 429 | Too Many Requests - A rate limit was exceeded |
| 500 | Internal Server Error |
Note: If you don’t provide at least one filter parameter (id, site_id, operation_id, dataset_id, or semantic_type), you will receive a 400 error.
Create Ticket
Section titled “Create Ticket”Creates a ticket in a site’s ticket form, the same way a user creates one in the Sitemark web app.
Names used in this section:
- Ticket id: the
idof the ticket. Use it as the key in your system. Other endpoints call itlayer_feature_id, because in the API a ticket is a layer feature. - Ticket number: the number shown in the Sitemark app, stored in
properties.VISIBLE_ID. Ticket numbers start again in each ticket form, so don’t use them as a key.
Request
Section titled “Request”Endpoint: POST /layerFeatures
Authentication: Required (API key).
Headers: only Authorization and Content-Type.
| Field | Type | Required | Description |
|---|---|---|---|
dataset_id |
integer | Yes | The ticket form on the site. Find it with List datasets: GET /datasets?site_id=<site-id>&semantic_type=PUNCH_LIST_ITEMS. A site can have more than one ticket form; pick the right one by name. |
geometry |
object | No | A GeoJSON Point: { "type": "Point", "coordinates": [longitude, latitude] }, optionally with a height as a third number (it defaults to 0). When left out, the ticket is placed at the centre of the site. |
description |
string | No | The ticket’s description text, at most 2000 characters. |
properties |
object | Yes | The ticket’s fields, at most 100. Each field is an object with only a value, the same shape as List layer features returns. Other keys inside a field, such as defaultValue, return 400. Built-in fields use their own name, custom fields use their property definition UUID. Send {} to set no fields. |
Built-in ticket fields (not listed by List property definitions):
| Field | Type | Values |
|---|---|---|
STATUS |
option | TO_DO, PLANNED, IN_PROGRESS, RESOLVED, WONT_DO, CLOSED. Optional, defaults to TO_DO. |
PRIORITY |
option | LOW, MEDIUM, HIGH |
ASSIGNEE |
user | The id of a user or group of the ticket’s company, from List users |
TICKET_DUE_DATE |
date | A plain date YYYY-MM-DD, for example "2026-11-05" |
TICKET_PLANNED_DATE |
date range | One ISO 8601 interval string "<start>/<end>" of two timestamps with an offset or Z, start before end, for example "2026-11-03T08:00:00+01:00/2026-11-03T12:00:00+01:00" |
MODULE_SERIAL_NUMBER |
text | A string of at most 2000 characters |
TAGS |
list | A list of 1 to 100 distinct, non-empty strings of at most 2000 characters, for example ["inverter", "urgent"]. [] is refused; leave the field out or send null for no tags. A list containing internal returns 400 for keys whose company uses internal tickets and that can’t see them. |
Set by Sitemark (read-only): don’t send these. They appear in the response.
| Field | Meaning |
|---|---|
id |
The ticket id |
properties.VISIBLE_ID |
The ticket number, set as a defaultValue |
visible_id |
Not used for tickets. Ignore it. |
created_at, updated_at |
When the ticket was created and last changed in Sitemark |
site_id, company_id, semantic_type |
Taken from the ticket form |
operation_id |
Always null for tickets |
parent_layer_feature_id, related_layer_feature_ids |
Always null and {}: a ticket created through the API is not linked to other layer features |
Other keys in properties |
Added by Sitemark. Some have no value. Ignore keys you don’t know. |
Example
Section titled “Example”a7c3e5d1-9b2f-4c6e-8d14-2f6b0c9e7a35 is the UUID of a custom text field that holds the external work
order id. Custom fields are set by their UUID, which you get from
List property definitions. Leave out dataset_id there to also
get the fields a ticket form inherits.
curl -X POST "https://api.sitemark.com/layerFeatures" \ -H "Authorization: ApiKey <your-api-key>" \ -H "Content-Type: application/json" \ -d '{ "dataset_id": 1234, "description": "Inverter 4 offline since 06:10", "properties": { "PRIORITY": { "value": "HIGH" }, "ASSIGNEE": { "value": 3312 }, "a7c3e5d1-9b2f-4c6e-8d14-2f6b0c9e7a35": { "value": "WO-2026-004512" } } }'No geometry is sent, so the ticket is placed at the site centre.
Response
Section titled “Response”201 Created with the new ticket, in the same shape as one item of
List layer features. The geometry comes back as a GeoJSON Feature with
three coordinates:
{ "id": 482107, "visible_id": null, "properties": { "VISIBLE_ID": { "defaultValue": 143 }, "STATUS": { "value": "TO_DO" }, "PRIORITY": { "value": "HIGH" }, "ASSIGNEE": { "value": 3312 }, "a7c3e5d1-9b2f-4c6e-8d14-2f6b0c9e7a35": { "value": "WO-2026-004512" } }, "geometry": { "type": "Feature", "properties": {}, "geometry": { "type": "Point", "coordinates": [4.4699, 51.0259, 0] } }, "dataset_id": 1234, "site_id": 5678, "operation_id": null, "company_id": 9012, "semantic_type": "PUNCH_LIST_ITEMS", "description": "Inverter 4 offline since 06:10", "created_at": "2026-11-02T06:14:03.000Z", "updated_at": "2026-11-02T06:14:03.000Z", "parent_layer_feature_id": null, "related_layer_feature_ids": {}}This response doesn’t include the ticket’s regions (properties.REGIONS): they are added just after
the ticket is saved. Use List layer features to see them.
Error Responses
Section titled “Error Responses”| Status Code | Description |
|---|---|
| 400 | The body doesn’t have the shape above ("Bad Request"), a field can’t be written (issues), a required field is missing (issues), the form isn’t a site’s ticket form, the site has no location and no geometry was sent, or an unsupported case (see Rules). |
| 401 | Missing, invalid, revoked or expired API key |
| 403 | Your API key isn’t allowed to create this ticket, for example in this form, with an ASSIGNEE (even null), or as CLOSED |
| 404 | The form (dataset_id) does not exist, or your key has no access to it: the form belongs to another company (for a partner API key: neither the partner company nor a company it manages), or to a site your key has no access to |
| 429 | Too Many Requests - A rate limit was exceeded |
Errors return a JSON body with a message. When fields are the problem, issues lists each one with
its place in the request (path), a code and a message:
{ "message": "Some fields cannot be written.", "issues": [ { "path": ["properties", "PRIORITY", "value"], "code": "INVALID_VALUE", "message": "PRIORITY must be one of LOW, MEDIUM, HIGH." } ], "public": true}code |
Meaning |
|---|---|
FIELD_NOT_WRITABLE |
The field is set by Sitemark, read-only, hidden or not a field of this form |
UNSUPPORTED_TYPE |
The field’s type can’t be written through the API |
INVALID_VALUE |
The value doesn’t fit the field, or it is a TAGS value the key may not set (see TAGS) |
UNREADABLE_USER |
The user or group is not of the ticket’s company, or the key can’t see it |
REQUIRED |
A required field is missing. These come in a separate 400 with the message "Required fields are empty.", after the other fields pass. |
A body that doesn’t have the shape above (an unknown key, a field without value, more than 100
fields, a description longer than 2000 characters, an invalid point) returns
{ "message": "Bad Request" } without issues. A field value that is too long returns an
INVALID_VALUE issue; only description returns Bad Request.
Required fields. A custom field can be required from a status onwards. In
List property definitions such a field has
config.validation: "REQUIRED" and config.validation_enforcement.STATUS set to the first status
where it is required: "TO_DO", "PLANNED", "RESOLVED" or "CLOSED". It is then required for that
status and every later status, in the order TO_DO → PLANNED → IN_PROGRESS → RESOLVED →
CLOSED. WONT_DO never requires anything. A missing required field returns 400 with a REQUIRED
issue whose path holds the field’s UUID, also for a field the form inherits that
List property definitions doesn’t show to your key. A field with validation: "REQUIRED" but no
validation_enforcement is not enforced. Hidden fields, and fields that don’t apply to the ticket,
are never required. A form whose required field can’t be written through the API (see below) can’t
be used to create tickets.
Fields shown only under conditions are accepted whether or not the conditions hold, and the value is kept.
Supported fields. You can set the built-in fields above and custom fields of the types below
(type in List property definitions). Fields of other types are
refused with 400.
Custom field type |
Value to send |
|---|---|
STRING |
A string of at most 2000 characters, for example "WO-2026-004512" |
URL |
A string of at most 2000 characters, for example "https://example.com/work-orders/4512" |
BOOLEAN |
A yes/no field: "YES" or "NO" (strings, not true or false) |
FLOAT |
A number, for example 12.5. If the field’s config.measure is TEMPERATURE or LENGTH, send °C or metres. |
DATE |
A plain date "YYYY-MM-DD". If the field’s config.includeTime is true, an ISO 8601 date and time with offset, for example "2026-11-05T08:00:00+01:00". |
OPTION |
The value of one of the field’s config.options, not its label |
MULTI_OPTION |
An array of distinct option values, for example ["<uuid>", "<uuid>"] |
USER |
One user or group id (an integer) of the ticket’s company, from List users |
Values.
- A built-in field value outside the table above, or a date in the wrong format, returns
400. - User fields and
ASSIGNEEonly accept a user or group of the ticket’s company that the key can see in List users. Any other id returns400. - Send custom field values in the shapes listed in the table above.
Location. Without geometry the ticket is placed at the centre of the site, so all such tickets
get the same automatic regions. If the site boundary has an unusual shape or several parts, the
centre can be outside the boundary, and the ticket may then get no automatic region. A site without a
boundary has no centre: send geometry there, or the request returns 400.
Emails. Creating a ticket with an assignee does not send an email, the same as in the web app, unless your company has set up an automation for new tickets.
Retries. Each request creates a new ticket. If a request times out or returns a 5xx error, the
ticket may still have been created, so a retry can create a second one. Before you retry, list the
form’s newest tickets with List layer features
(dataset_id=<dataset-id>&_page=1&_sortBy=-created_at) and look for your own reference, such as the
work order number you store in a custom field (see the example).
Not supported. These requests are refused with 400:
- forms linked to Procore;
- forms with sub-tickets;
- linking the new ticket to another item, such as a finding, a component or another ticket (as its parent or as a relationship);
- creating many tickets in one request. Send one request per ticket.
Requests to Procore-linked forms and forms with sub-tickets fail with 400 before the fields are
checked.