Tags

Tags are lightweight, organization-scoped labels that you can attach to resources in the API. They give you a flexible way to group, filter, and track data without changing the underlying resource schema.

Why use tags?

Data pipelines often need to answer questions like:

  • "Which locations came from this supplier's Q1 upload?"
  • "Which assets have been manually verified by my team?"
  • "Which plots are flagged as high-risk for this commodity?"

Tags let you answer these questions without modifying source data or waiting for new schema fields. You apply them at upload time or through processing pipelines, and then use them to filter or enrich any list endpoint.


Where tags apply

Tags can be attached to the following resource types:

ResourceDescription
FacilitiesLocations of type facility
PlotsLocations of type plot
Supply shedsSupply sheds linked to a facility or plot
Asset tasksIngestion upload tasks (asset_task)
Asset entitiesIndividual rows from an ingestion upload (asset_entity)
Cadastral tasksParcel search tasks created via POST /cadastral/parcels/search
Cadastral resultsParcel results returned from a cadastral search

Tag types

Every tag has a tag_type that records how it was created:

TypeDescription
orgApplied by your organization β€” via upload, API, or user action
systemApplied automatically by the Epoch platform
normalisedApplied during the location normalization pipeline

Tag type is informational. Filtering works the same way regardless of type.


Namespaced tags (key:value)

A tag name may use a namespace:value convention β€” for example partno:12345, region:west, or contract:AB-99. When a tag name contains a colon, Epoch splits it on the first colon into a namespace (the part before) and a value (everything after β€” values may themselves contain colons). Plain tags with no colon simply have no namespace.

Namespaces are what make the grouping and prefix views below possible: one partno:* tag per part number, applied across many facilities/sheds, lets you ask "show me every part number and where it's produced" without changing your data. Namespaces are normalised to lowercase (e.g. Part No β†’ part_no), so group and tag_prefix lookups are case-insensitive on the namespace.


Applying tags at upload time

When you POST /ingestions/assets, you can apply tags using the tags multipart field:

tags: verified,high-priority

Tags are comma-separated strings. They are created automatically if they don't already exist for your organization. Tag names have a maximum length of 100 characters.

Request-level tags are applied to both the asset task and every asset entity (pivot row) in the upload.

Tags on process locations

When you PUT /ingestions/assets, you can include tags in the JSON body. At least one of stats or tags must be provided.

Tags with stats β€” triggers batch processing and applies tags:

{
  "stats": ["deforestation"],
  "tags": ["batch-q1", "priority"]
}

Tags only β€” applies tags to the filtered set without triggering batch processing:

{
  "tags": ["batch-q1", "priority"]
}

When only tags is provided (no stats), tags are applied to the matching asset task and its asset entities (pivot rows) and the endpoint returns 200. When stats is included, batch processing is triggered in the background and the endpoint returns 202.

For the default path (cross-task filtering), tags are applied after rows are copied into the new task.

Per-row tags

You can also tag rows individually by including a tags column in your uploaded file (CSV, GeoJSON, etc.). The value should be a comma-separated list of tag names:

name,latitude,longitude,tags
Farm A,52.1,-1.2,"verified,q1-2025"
Farm B,48.8,2.3,high-risk

Per-row tags are additive β€” if you also supply request-level tags, both sets are applied.

Column templates β€” per-row tags from your upload columns

Instead of adding a dedicated tags column, you can turn any existing column into per-row tags directly from the request-level tags field, using a {Column Header} placeholder:

tags: partno:{Part Number}

At ingestion, the placeholder is replaced with each row's cell value for that column β€” so a row whose Part Number is 12345 gets a partno:12345 tag, the next row gets partno:678, and so on. This is the easiest way to create one namespaced tag per row without pre-splitting your file.

Rules:

  • Namespace is whatever precedes the first colon (partno above). Several templates can share a namespace, and you choose it via the prefix.
  • Missing column or blank cell β†’ that tag is simply skipped for that row (no empty partno: tag is created).
  • Multi-value cells β€” a placeholder cell containing a comma-separated list (12345,678) produces one tag per value (partno:12345, partno:678).
  • Literal braces β€” a {...} that doesn't match any column header is treated as a plain literal tag, not a template, so it is never silently dropped.
  • Static + template can be mixed: tags=verified,partno:{Part Number} applies verified to every row and the derived partno:* per row.

Filtering by tag

Use filter[tags] on any supported list endpoint to narrow results to resources with a specific tag:

GET /locations/facilities?filter[tags]=eq:verified
GET /locations/facilities?filter[tags]=in:verified,high-priority
GET /ingestions/assets/{{task_id}}?filter[tags]=eq:q1-2025
ConditionBehaviour
eqExact name match
inMatches if the resource has any of the listed tags (comma-separated)
containsAccepted for compatibility, but behaves as an exact name match β€” not a substring search

Tag matching is always by exact name β€” there is no substring/partial tag search (contains is an exact-match alias kept for backward compatibility). To match a whole namespace (e.g. "any partno:*"), use the grouping / tag_prefix views below rather than a tag filter.


Grouping by tag namespace

Once you have namespaced tags (e.g. one partno:* per part number), two views let you pivot locations by them. Both key off the namespace prefix and never return individual plots β€” a tagged plot rolls up to its parent supply shed.

Tag-first β€” one group per value

GET /locations/groups/partno

Returns one group per distinct value under the partno namespace, each listing the facilities and sheds that carry it β€” answering "where does part X come from?". Groups are paginated. You can narrow to specific values with the tag filter, e.g. GET /locations/groups/partno?filter[tags]=in:partno:12345,partno:678.

The equivalent view over ingestion assets groups the uploaded rows (before location resolution) by the same namespace:

GET /ingestions/assets/groups/partno

Choosing a grouping axis

The assets view accepts a group_by parameter (comma-separated) to control what forms a group. The default is the tag value:

QueryOne group per…Group id
?group_by=value (default)tag valuepartno:12345
?group_by=location_idresolved locationpartno@<locationId>
?group_by=value,location_idvalue and locationpartno:12345@<locationId>

Add ?include_location=true to resolve each group's location_id / location_type without making location a grouping axis (i.e. keep one group per value, but annotate it with its location). An unknown axis returns 400.

Location-first β€” annotate the locations table

Add tag_prefix to a locations listing to return only the locations carrying a tag under that namespace, each annotated with a part_numbers array (the sorted distinct values under the prefix) β€” answering "which parts does this location supply?":

GET /locations/facilities?tag_prefix=partno
GET /locations/sheds?tag_prefix=partno

Each returned facility/shed includes part_numbers: ["12345", "90012"] alongside its usual attributes, ready to render as chips. Plots are never returned by this view.

Performance β€” narrow large namespaces

Listing every group in a namespace scans all of that namespace's members (across your whole catchment) before paginating β€” for a namespace that spans a large catalogue this can be slow. When you know the values you care about, narrow with the tag filter so the query touches only those tags:

GET /locations/groups/partno?filter[tags]=in:partno:12345,partno:678
GET /ingestions/assets/groups/partno?filter[tags]=in:partno:12345

A filtered request is dramatically cheaper than an unfiltered full-namespace listing, and pages are bounded β€” always page (page[size]) rather than pulling a whole large namespace at once.


Implicit creation

Tags do not need to be created in advance. Supplying a new tag name in an upload or API call creates it automatically under your organization.


Managing tags

The /tags endpoints let you list, rename, and remove tags for your organization.

Listing tags

GET /tags

Returns your organization's tags, ordered alphabetically. The response is paginated like other list endpoints (page / page_size, default page size 500, maximum 1000) with meta.total and links.next β€” page through it if your org holds more tags than one page.

Renaming a tag

PATCH /tags/:name
{ "name": "new-tag-name" }

Renames a tag across all resource associations. Any resource that had the old tag name will now reflect the new name. Returns 409 if the new name already exists.

Renaming a whole namespace

PATCH /tags/namespaces/:namespace
{ "namespace": "new-namespace" }

Renames (and merges) every tag under a namespace in one call β€” e.g. move all partno:* tags to part_number:*, preserving each value. If a target tag name already exists, the associations are merged onto it. Useful for correcting a namespace choice after a large upload without re-tagging every resource.

Deleting a tag

DELETE /tags/:name

Permanently deletes a tag and removes it from all resources (facilities, plots, sheds, etc.) via cascading delete.


Supported endpoints

EndpointFilterTags
GET /locations/facilitiesfilter[tags]β€”
GET /locations/plotsfilter[tags]β€”
GET /locations/facilities/:id/shedsfilter[tags]β€”
GET /ingestions/assetsfilter[tags]β€”
GET /ingestions/assets/:idfilter[tags]β€”
POST /ingestions/assetsβ€”tags multipart field (task + entities)
PUT /ingestions/assetsβ€”tags JSON body field (task + entities)
POST /cadastral/parcels/searchβ€”tags body field (applied to task)
POST /cadastral/parcels/ingest/:idβ€”tags body field (merged with source task tags)
GET /cadastral/parcels/results/:idfilter[tags]β€”
GET /cadastral/parcels/resultsfilter[tags]β€”
GET /cadastral/tasksfilter[tags]β€”
GET /locations/groups/:prefixfilter[tags] (narrow values)Groups facilities + sheds by namespace value
GET /locations/facilities | /locations/sheds | /locationsfilter[tags], tag_prefixtag_prefix adds a part_numbers attribute
GET /ingestions/assets/groups/:prefixfilter[tags] (narrow values)Groups ingestion assets by namespace value
POST /ingestions/assetsβ€”tags multipart field β€” supports {Column} templates
GET /tagsβ€”List tags (paginated)
PATCH /tags/:nameβ€”Rename a tag
PATCH /tags/namespaces/:namespaceβ€”Rename / merge a whole namespace
DELETE /tags/:nameβ€”Delete a tag entirely