Tags API
Every Tags action (6) and data model (2): endpoints, scopes, input, preconditions, events and examples.
One shared set of tags for every record, in your own colours.
Module code tags · 6 actions · 2 data models · 6 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: tags (everything), tags:read (read only) or tags:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
tag_links | tag_link | /api/v1/tag_links | |||
tags | tag | /api/v1/tags | tags.create | tags.update | tags.delete |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
tags.attach | tags:read | Tag a record |
tags.create | tags:manage | Create a tag |
tags.delete | tags:manage | Delete a tag |
tags.detach | tags:read | Remove a tag from a record |
tags.merge | tags:manage | Merge two tags |
tags.update | tags:manage | Rename or recolour a tag |
tags.attach
Tag a record — Puts an existing tag on a record. Needs permission to edit that record.
Needs tags:read · changes data
https://app.getera.io/api/v1/tag_links/actions/attach- Canonical address:
POST /api/v1/actions/tags.attach
Input
| Field | Type | Required | Notes |
|---|---|---|---|
tagId | uuid | yes | Tag · id of tag |
recordType | string | yes | Record type · max 60 |
recordId | uuid | yes | Record |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_record_type | The record type must be one a module owns. |
cannot_write_record | Tagging a record needs write on that record’s module. |
tag_not_found | The tag must exist in this workspace. |
record_not_found | The record must exist. |
Events it publishes: tags.attached
What it writes: tag_link (insert) — Attaches the tag
curl -X POST https://app.getera.io/api/v1/tag_links/actions/attach \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"tagId":"TAG_ID","recordType":"Record type","recordId":"RECORD_ID"}'{
"ok": true,
"result": "…",
"events": [
"tags.attached"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tagLinks.attach({
tagId: "TAG_ID",
recordType: "Record type",
recordId: "RECORD_ID"
})era tag_links attach \
--tag-id "TAG_ID" \
--record-type "Record type" \
--record-id "RECORD_ID"{
"name": "era__tags__attach",
"arguments": {
"tagId": "TAG_ID",
"recordType": "Record type",
"recordId": "RECORD_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:read · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_record_type, cannot_write_record, tag_not_found, record_not_found · 429 slow down
tags.create
Create a tag — Adds an entry to the workspace vocabulary every module shares.
Needs tags:manage · changes data
https://app.getera.io/api/v1/tags/actions/create- REST alias:
POST /api/v1/tags - Canonical address:
POST /api/v1/actions/tags.create
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 120 |
color | string | Colour · Six hex digits after a hash. Left empty, the tag takes a stable colour derived from its id. · max 7 | |
description | string | Description · max 2000 | |
isPinned | boolean | Pin to filters |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
empty_name | A tag needs a name that reduces to a key. |
slug_taken | Two tags may not reduce to the same key. |
bad_colour | A colour must be six hexadecimal digits after a hash. |
Events it publishes: tags.created
What it writes: tag (insert) — Writes the vocabulary entry
curl -X POST https://app.getera.io/api/v1/tags/actions/create \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"tags.created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tags.create({
name: "Name"
})era tags create \
--name "Name"{
"name": "era__tags__create",
"arguments": {
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 empty_name, slug_taken, bad_colour · 429 slow down
tags.delete
Delete a tag — Removes the entry and every attachment it has. Says how many before it does.
Needs tags:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/tags/{id}/actions/delete- REST alias:
DELETE /api/v1/tags/{id} - Canonical address:
POST /api/v1/actions/tags.delete
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Tag · id of tag |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The tag must exist in this workspace. |
Events it publishes: tags.deleted
What it writes: tag (delete) — Removes the entry; tag_link (delete) — Its attachments go with it, by cascade
curl -X POST https://app.getera.io/api/v1/tags/TAG_ID/actions/delete \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"tags.deleted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tags.delete("TAG_ID")era tags delete TAG_ID{
"name": "era__tags__delete",
"arguments": {
"id": "TAG_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
tags.detach
Remove a tag from a record — Takes a tag off a record without touching the vocabulary.
Needs tags:read · changes data
https://app.getera.io/api/v1/tag_links/actions/detach- Canonical address:
POST /api/v1/actions/tags.detach
Input
| Field | Type | Required | Notes |
|---|---|---|---|
tagId | uuid | yes | Tag · id of tag |
recordType | string | yes | Record type · max 60 |
recordId | uuid | yes | Record |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
unknown_record_type | The record type must be one a module owns. |
cannot_write_record | Untagging a record needs write on that record’s module. |
not_tagged | The tag must actually be on the record. |
Events it publishes: tags.detached
What it writes: tag_link (delete) — Detaches the tag
curl -X POST https://app.getera.io/api/v1/tag_links/actions/detach \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"tagId":"TAG_ID","recordType":"Record type","recordId":"RECORD_ID"}'{
"ok": true,
"result": "…",
"events": [
"tags.detached"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tagLinks.detach({
tagId: "TAG_ID",
recordType: "Record type",
recordId: "RECORD_ID"
})era tag_links detach \
--tag-id "TAG_ID" \
--record-type "Record type" \
--record-id "RECORD_ID"{
"name": "era__tags__detach",
"arguments": {
"tagId": "TAG_ID",
"recordType": "Record type",
"recordId": "RECORD_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:read · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 unknown_record_type, cannot_write_record, not_tagged · 429 slow down
tags.merge
Merge two tags — Moves everything from one entry onto another and removes the empty one.
Needs tags:manage · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/tags/{id}/actions/merge- Canonical address:
POST /api/v1/actions/tags.merge
Input
| Field | Type | Required | Notes |
|---|---|---|---|
winnerId | uuid | yes | Filled from the path {id} · Keep · id of tag |
loserId | uuid | yes | Merge away · id of tag |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
same_tag | A tag cannot be merged into itself. |
not_found | Both tags must exist in this workspace. |
Events it publishes: tags.merged
What it writes: tag_link (update) — Repoints the attachments; tag (delete) — Removes the entry that was merged away
curl -X POST https://app.getera.io/api/v1/tags/TAG_ID/actions/merge \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"loserId":"TAG_ID"}'{
"ok": true,
"result": "…",
"events": [
"tags.merged"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tags.merge("TAG_ID", {
loserId: "TAG_ID"
})era tags merge TAG_ID \
--loser-id "TAG_ID"{
"name": "era__tags__merge",
"arguments": {
"winnerId": "TAG_ID",
"loserId": "TAG_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 same_tag, not_found · 429 slow down
tags.update
Rename or recolour a tag — Changes the entry itself. Nothing already tagged is touched.
Needs tags:manage · changes data
https://app.getera.io/api/v1/tags/{id}/actions/update- REST alias:
PATCH /api/v1/tags/{id} - Canonical address:
POST /api/v1/actions/tags.update
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Tag · id of tag |
name | string | Name · max 120 | |
color | string | Colour · Six hex digits after a hash. Left empty, the tag takes a stable colour derived from its id. · max 7 | |
description | string | Description · max 2000 | |
isPinned | boolean | Pin to filters |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The tag must exist in this workspace. |
empty_name | A tag needs a name that reduces to a key. |
slug_taken | Two tags may not reduce to the same key. |
bad_colour | A colour must be six hexadecimal digits after a hash. |
Events it publishes: tags.updated
What it writes: tag (update) — Rewrites the entry
curl -X POST https://app.getera.io/api/v1/tags/TAG_ID/actions/update \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"tags.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.tags.update("TAG_ID", {
name: "Name"
})era tags update TAG_ID \
--name "Name"{
"name": "era__tags__update",
"arguments": {
"id": "TAG_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold tags:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, empty_name, slug_taken, bad_colour · 429 slow down
Data models
TagLink
Table tag_link · Resource tag_links
GET /api/v1/tag_linksGET /api/v1/tag_links/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
tag_id | string (uuid) · uuid | tag | ||
record_type | string · text | |||
record_id | string (uuid) · uuid | |||
tagged_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz |
Primary key: id
Tag
Table tag · Resource tags
GET /api/v1/tagsGET /api/v1/tags/{id}POST /api/v1/tags→tags.createPATCH /api/v1/tags/{id}→tags.updateDELETE /api/v1/tags/{id}→tags.delete
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
slug | string · text | |||
color | string · text | yes | ||
description | string · text | yes | ||
is_pinned | boolean · bool | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((color IS NULL) OR (color ~ '^#[0-9A-Fa-f]{6}$'::text)))CHECK (((name = btrim(name)) AND (length(name) > 0)))CHECK (((slug = lower(slug)) AND (slug = btrim(slug)) AND (length(slug) > 0)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
tags.attached | A record was tagged. | id, recordType, recordId |
tags.created | A tag was added to the vocabulary. | id, name, slug |
tags.deleted | A tag and its attachments were removed. | id, name, detached |
tags.detached | A tag was taken off a record. | id, recordType, recordId |
tags.merged | Two tags became one. | id, losingId, moved |
tags.updated | A tag was renamed or recoloured. | id, name, slug |