Skip to content

Tags API

Tags are an organization-wide vocabulary shared by test cases, defects, and test runs. There is one catalog per organization, mounted under /api/v1/orgs/{orgUlid}/tags. Each tag has a server-assigned ULID, a name unique within the organization, an author, and three usage counts.

Entities never reference a tag by ULID. A test case, a defect, and a test run all carry tags: string[] — the names — and the server resolves or creates the catalog row behind them. See Attaching tags to entities.

POST/api/v1/orgs/{orgUlid}/tags

Requires the tag write permission (held by owner/admin/member); a viewer receives 403 forbidden.

FieldRequiredNotes
nameyesTrimmed, 1–80 characters. Must be unique within the organization (see Name rules)

The body is strict: any other key returns 422 validation_failed.

201 with the created tag. Its three counts are 0 — a tag one statement old is attached to nothing. See Response fields.

A duplicate name returns 409 conflict.

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"smoke"}' \
"https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags"
  • The name is trimmed before it is stored and before it is compared, so " smoke " and "smoke" are the same tag.
  • Length is measured after trimming: 1 to 80 characters. A whitespace-only name returns 422 validation_failed.
  • Uniqueness is per organization and case-sensitive. Smoke and smoke are two different tags and may both exist; only a trim-exact repeat returns 409 conflict. The same name is accepted in a different organization.
GET/api/v1/orgs/{orgUlid}/tags

No permission gate: every member of the organization may read the catalog. Page-based pagination.

The order is name ascending and is fixed — there are no sort parameters.

ParameterNotes
pagePage number (1-based, default 1)
pageSizeItems per page (default 50, max 200)
qCase-insensitive substring search on the tag name, max 80 characters
authorUlidComma-separated user ULIDs; matches tags created by any of them. A ULID that resolves to no member contributes nothing, and a filter resolving to no members at all matches nothing rather than degrading to an unfiltered list

The query is strict: a misspelled parameter (authorUlids, pageSizes) returns 422 validation_failed rather than being silently dropped.

200 with { items, page, pageSize, total }. Each item follows the response shape.

Terminal window
curl -sS \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags?q=smoke&pageSize=100"
PATCH/api/v1/orgs/{orgUlid}/tags/{tagUlid}

Requires the tag write permission (held by owner/admin/member); a viewer receives 403 forbidden.

FieldRequiredNotes
nameyesThe new name. Same trim, length, and uniqueness rules as create

name is the only field, so an empty body returns 422 validation_failed.

200 with the renamed tag, the same shape POST returns — with the tag’s real counts, not zeros. A rename reaches every test case, defect, and test run carrying the tag without detaching or otherwise modifying any of them.

A name another tag already holds returns 409 conflict. A {tagUlid} outside the organization returns 404 not_found; a malformed one returns 422 validation_failed.

Terminal window
curl -sS -X PATCH \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"smoke-suite"}' \
"https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/01JTAGXXXXXXXXXXXXXXXXXXXXX"
DELETE/api/v1/orgs/{orgUlid}/tags/{tagUlid}

Requires the tag delete permission (held by owner/admin); a member and a viewer both receive 403 forbidden. The split is deliberate: deleting a tag detaches it from every test case, defect, and test run that carries it, and that reaches further than growing the vocabulary does.

Returns 204. The entities themselves are untouched — only the tag row and its attachments are removed. A {tagUlid} outside the organization returns 404 not_found.

Terminal window
curl -sS -X DELETE \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/01JTAGXXXXXXXXXXXXXXXXXXXXX"
POST/api/v1/orgs/{orgUlid}/tags/bulk-delete

Requires the tag delete permission (held by owner/admin); a member and a viewer both receive 403 forbidden.

FieldRequiredNotes
ulidsyes1 to 100 tag ULIDs. An empty or oversized array returns 422 validation_failed

200 with { deleted: number } — how many rows were actually removed. A batch is forgiving by design: a ULID this organization does not own, or one a concurrent delete already took, is skipped rather than failing the request. deleted is therefore not always the batch size, and it is the only signal that your selection contained something you do not own.

Each deletion detaches the tag exactly as the single DELETE does.

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ulids":["01JTAGAXXXXXXXXXXXXXXXXXXXX","01JTAGBXXXXXXXXXXXXXXXXXXXX"]}' \
"https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/bulk-delete"

POST, PATCH, and each item of the list response return a tag object with the following fields. The object is strict — no other key ships.

FieldTypeNotes
ulidstring (ULID)Server-assigned unique identifier
namestringThe tag name, exactly as entities carry it
createdByobject | nullAuthor summary, or null (see Author)
caseCountnumberTest cases carrying the tag (see Usage counts)
runCountnumberTest runs carrying the tag
defectCountnumberDefects carrying the tag
createdAtnumberUnix epoch milliseconds
updatedAtnumberUnix epoch milliseconds

createdBy, when present, carries ulid, displayName, avatarKey, avatarVersion, and positionTitle.

The three counts are not organization-wide totals. Two properties matter to an integrator:

  • They are scoped to the projects the caller may read. A tag attached to a case in a project the caller has no access to is not counted for that caller. Two credentials can legitimately read different counts for the same tag, and neither is stale.
  • They include archived (trashed) test cases. This is deliberate: a tag whose only user is archived must not read 0 and invite a delete that would strip the tag the moment the case is restored.

A 0 therefore means “nothing visible to this caller carries the tag”, not “the tag is unused”.

createdBy is null when the tag has no resolvable author. Two distinct cases produce it:

  • The tag was created by an API token or an MCP agent. Such a tag has no human author by design.
  • The author’s account was erased.

A tag whose author merely left the organization still carries their summary.

Test cases, defects, and test runs carry tags: string[] — names, not ULIDs — on both the request and the response. Test runs gained the field alongside this catalog: at most 50 names per run, each 1 to 80 characters, on POST and PATCH and in the run response.

The server resolves each submitted name against the catalog and creates the row when the name is new, so posting a name the catalog has never seen mints the tag — no separate POST /tags required. A create body’s array attaches; a PATCH array replaces the entity’s whole tag set.

Two consequences of the name-based contract are worth coding against:

  • A submitted [] (and a submitted null) clears the set, and the entity reads back with tags: null, not tags: []. Omitting the key leaves the existing set untouched.
  • The returned array is always alphabetical, never submission order. Do not compare it positionally to what you sent.

Deleting a tag from the catalog removes it from every entity that carried it; the entities are otherwise untouched, and their tags arrays simply come back shorter.

The same five operations are exposed to MCP clients as list_tags, create_tag, update_tag, delete_tag, and bulk_delete_tags. Each takes org_id (from get_current_organization) and enforces the same permissions as the HTTP route behind it. A tag an MCP agent creates has createdBy: null, as Author describes. See MCP tools.