Bulk add tag to competitors
Applies one tag across up to 1000 competitors in a single call.
This is the inverse of Set competitor tags. That endpoint answers “what are this competitor’s tags” and replaces the whole set; this one answers “put this tag on these competitors” and touches nothing else. They are different operations and cannot share a URL — the other names a single competitor in its path.
Requires the write:tags scope, and the workspace must be enabled for API writes.
POST /v1/brands/{brand_id}/tags/{tag_id}/competitorsAuthorization
| Header | Type | Required |
|---|---|---|
Authorization | Bearer asky_sk_… | required |
Idempotency-Key | string | required |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
brand_id | uuid | required | From List brands |
tag_id | uuid | required | From List tags |
Query parameters
None.
Body
application/json
| Field | Type | Required | Description |
|---|---|---|---|
competitor_ids | uuid[], 1–1000 | required | From List competitors. Duplicates are collapsed |
All or nothing. Every id must name a competitor on this brand. If any does not, nothing is written and the error names the offenders (up to 20, with a count of the rest) so one round trip is enough to fix the list.
Request
cURL
curl --request POST \
--url 'https://api.askylabs.com/v1/brands/242546de-4cee-4a7d-952e-f6aa60c63ef8/tags/5524d66a-df1e-47e4-9dbc-802af39235ad/competitors' \
--header 'Authorization: Bearer <api-key>' \
--header 'Idempotency-Key: <unique-per-attempt>' \
--header 'Content-Type: application/json' \
--data '{"competitor_ids":["9f2c1d84-6b31-4a2e-9c77-1e5b0a3f8d42","3c8982b3-c287-4936-ba4a-346ad58a411c"]}'Response
200
{
"data": {
"tag_id": "5524d66a-df1e-47e4-9dbc-802af39235ad",
"requested": 224,
"applied": 224,
"already_present": 0
},
"request_id": "req_7d21b0af59c34e18b6ac"
}Response fields
Counts rather than objects. A bulk response echoing a thousand entities would be stored whole in the idempotency record on every attempt, so read the entities back if you need them.
| Field | Type | Description |
|---|---|---|
data.tag_id | uuid | required |
data.requested | integer | required. Ids after duplicates were collapsed |
data.applied | integer | required. competitors actually changed |
data.already_present | integer | required. Already carried the tag; left alone |
request_id | string | required |
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | competitor_ids missing, empty, over 1000, not uuids, or naming competitors outside this brand |
| 401 | invalid_token | The key is missing, malformed, or does not exist |
| 403 | insufficient_scope | The key does not carry write:tags |
| 404 | not_found | The brand or the tag does not exist, or this key is not allowed to reach it |
| 429 | rate_limited | Over 60 requests per minute |
Notes
Safe to repeat. Applies is idempotent against the unique (tag_id, competitor_id) index, so a retry
after a lost response changes nothing and reports applied: 0. That is also why the response
separates applied from already_present — you can tell a no-op from a failure.
Ids outside this brand are a 400, not a 404. The resource in the path is the tag; the ids
are payload, and payload that fails validation is a bad request. The path itself still returns a
bare 404 for anything you may not reach, so the tenancy boundary is unchanged.
Above 1000, page it yourself. The cap is deliberately conservative and will be reviewed against real traffic. Asynchronous submission for larger sets is not built yet.
Idempotency-Key is required. See Idempotency.