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:
| Resource | Description |
|---|---|
| Facilities | Locations of type facility |
| Plots | Locations of type plot |
| Supply sheds | Supply sheds linked to a facility or plot |
| Asset tasks | Ingestion upload tasks (asset_task) |
| Asset entities | Individual rows from an ingestion upload (asset_entity) |
| Cadastral tasks | Parcel search tasks created via POST /cadastral/parcels/search |
| Cadastral results | Parcel results returned from a cadastral search |
βΆTag types
Every tag has a tag_type that records how it was created:
| Type | Description |
|---|---|
org | Applied by your organization β via upload, API, or user action |
system | Applied automatically by the Epoch platform |
normalised | Applied during the location normalization pipeline |
Tag type is informational. Filtering works the same way regardless of type.
βΆNamespaced tags (key:value)
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 (
partnoabove). 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}appliesverifiedto every row and the derivedpartno:*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
| Condition | Behaviour |
|---|---|
eq | Exact name match |
in | Matches if the resource has any of the listed tags (comma-separated) |
contains | Accepted 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:
| Query | One group per⦠| Group id |
|---|---|---|
?group_by=value (default) | tag value | partno:12345 |
?group_by=location_id | resolved location | partno@<locationId> |
?group_by=value,location_id | value and location | partno: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
| Endpoint | Filter | Tags |
|---|---|---|
GET /locations/facilities | filter[tags] | β |
GET /locations/plots | filter[tags] | β |
GET /locations/facilities/:id/sheds | filter[tags] | β |
GET /ingestions/assets | filter[tags] | β |
GET /ingestions/assets/:id | filter[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/:id | filter[tags] | β |
GET /cadastral/parcels/results | filter[tags] | β |
GET /cadastral/tasks | filter[tags] | β |
GET /locations/groups/:prefix | filter[tags] (narrow values) | Groups facilities + sheds by namespace value |
GET /locations/facilities | /locations/sheds | /locations | filter[tags], tag_prefix | tag_prefix adds a part_numbers attribute |
GET /ingestions/assets/groups/:prefix | filter[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 |