Tags and tag data
Tag data works like a spreadsheet. Fields are the columns. Each tag holds values for some or all of them. See Tag Data for how it behaves in the hub.
| Method | Path | Does |
|---|---|---|
GET | /tags | List tags with their current data and zone |
GET | /tags/{epc}/data | One tag's current values |
PUT | /tags/{epc}/data | Set or clear values on one tag |
GET | /tags/{epc}/history | Every value a tag's fields have held |
GET | /fields | List fields |
POST | /fields | Add a field |
PATCH | /fields/{key} | Rename a field, or make it the name field |
DELETE | /fields/{key} | Delete a field and its values |
POST | /tags/import | Import a CSV |
GET | /tags/export | Export every tag as CSV |
POST | /tags/bulk-delete | Forget tags entirely |
List tags
GET /tags
Returns up to 500 tags, most recently seen first.
| Query | |
|---|---|
q | Only tags whose EPC or any field value contains this text (case-insensitive). |
hasData=false | Only tags that have no data yet. Useful to find tags that need labelling. |
[
{
"id": "0b7f0b8e-2c55-4a8b-9b1e-6d0f7e1c2a90",
"epc": "E2801160600002084F1A3C21",
"label": "BRK-2210",
"targetType": null,
"attributes": { "sku": "BRK-2210", "qty": "12", "description": "Brake caliper, front left" },
"zoneName": "Receiving",
"firstSeenAt": "2026-10-09T08:12:44.102Z",
"lastSeenAt": "2026-10-10T14:03:22.418Z"
}
]
| Field | |
|---|---|
label | The value of the name field, or null. |
attributes | Current values. In this list, every value is a string, so a number arrives as "12". Use GET /tags/{epc}/data for typed values. |
zoneName | The zone the tag was last seen in, or null if it has never been placed. |
targetType | "asset" if the tag is linked to an asset, otherwise null. |
For the complete set of tags rather than the 500 most recent, use export.
Read one tag's data
GET /tags/{epc}/data
{
"epc": "E2801160600002084F1A3C21",
"tagId": "0b7f0b8e-2c55-4a8b-9b1e-6d0f7e1c2a90",
"attributes": { "sku": "BRK-2210", "qty": 12, "description": "Brake caliper, front left", "hazardous": false }
}
Values here keep their stored type: numbers are JSON numbers and true/false values are booleans. An EPC Titan has never heard of returns 200 with empty attributes and no tagId. It isn't an error.
Set values on one tag
PUT /tags/{epc}/data
The body is an object of field key to value, with every value sent as a string:
curl -X PUT "$API/tags/E2801160600002084F1A3C21/data" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"qty": "11", "bin": "C-04-2", "notes": ""}'
- Only the fields you send change. Fields you leave out keep their values.
- An empty string clears a field on this tag. The old value goes to history.
- Types are worked out for you:
"11"is stored as the number 11,"true"/"yes"/"false"/"no"as booleans, and anything with a leading zero, like"00789", stays text. - New fields are created automatically: send a key that doesn't exist and it becomes a column.
- Unknown EPCs are created, so you can load data before a tag has ever been read.
Returns the tag's full set of current values, in the same shape as the GET.
To update thousands of tags, use CSV import instead of one request per tag.
Tag data history
GET /tags/{epc}/history
Every value every field has held on this tag, newest first. A value that's still current has no validTo.
[
{ "key": "qty", "value": 11, "validFrom": "2026-10-10T14:10:00.000Z" },
{ "key": "qty", "value": 12, "validFrom": "2026-10-09T08:15:31.000Z", "validTo": "2026-10-10T14:10:00.000Z" }
]
List fields
GET /fields
[
{
"id": "a6e1f3c4-5b0d-4f7e-9c2a-1d8b7e6f5a40",
"key": "sku",
"label": "SKU",
"dataType": "text",
"options": null,
"displayOrder": 0,
"showInTable": true,
"isLabel": true
}
]
dataType is Titan's guess from the first value it saw: text, number, boolean or url. It's informational only. You can still store any value in any field.
Add a field
POST /fields
{ "label": "Unit price" }
Creates the field with key unit_price and returns it with 201. You rarely need this, because writing a value to a new key creates the field anyway.
Rename a field, or make it the name field
PATCH /fields/{key}
{ "label": "Part number" }
Renaming changes the key too (sku becomes part_number), and existing values move with it. Returns the updated field. Renaming to a name that's already in use returns 409.
{ "isLabel": true }
Makes this field the name field: its value labels the tag throughout the hub, and appears as label in tag lists and live events. Only one field can be the name field. Returns the full field list.
Delete a field
DELETE /fields/{key}
Removes the field and its values from every tag. Returns 204.
Import a CSV
POST /tags/import
Content-Type: multipart/form-data
Send the file in a form field named file. Add ?dryRun=true to check the file without changing anything.
curl -X POST "$API/tags/import?dryRun=true" \
-H "Authorization: Bearer $KEY" \
The file needs a header row containing epc, and every other column becomes a field. Blank cells leave a field unchanged. They don't clear it. EPCs must be 8 to 64 hex characters, and each may appear once. The limits are 100,000 rows and 64 MB.
{
"total": 1250,
"imported": 1180,
"updated": 68,
"fields": ["bin", "description", "sku"],
"errors": [
{ "line": 17, "epc": "E28011606000", "reason": "duplicate of line 4" },
{ "line": 902, "reason": "empty epc" }
],
"dryRun": true
}
imported counts tags new to Titan, and updated counts ones that already existed. Rows listed in errors are skipped, and the rest are applied. A file that can't be read at all (no epc column, too many rows) returns 400.
Export all tag data as CSV
GET /tags/export
Returns text/csv with one row per tag (every tag, not just the latest 500) and one column per field. The first column is epc. The format is the same one the importer accepts.
curl -o tags.csv "$API/tags/export" -H "Authorization: Bearer $KEY"
Delete tags
POST /tags/bulk-delete
{ "epcs": ["E2801160600002084F1A3C21", "E2801160600002084F1A3C22"] }
Forgets these tags completely: their data, history, read history, location and any asset link. A deleted tag that's read again comes back as a new tag with no data. You can send up to 10,000 EPCs per request. This can't be undone.
{ "deleted": 2 }