# X-Trader labels API

URL: https://docs.blizz.id/xtrader/labels/
Updated: 2026-10-16

X-Trader labels are a per-account set of up to 20 named, coloured tags shared by sniper, copy-trade and transfer-snipe tasks. They are managed at `/labels` (list, create, get, update, delete) and bound to tasks through the `risk_label_id` field. Label names are unique per account, labels do not use `ETag`/`If-Match`, and templates never take a label.

Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader.

## Endpoints

| Method and path | Required scope | Headers | Success |
|---|---|---|---|
| `GET /labels` | `sniper:read` or `copytrade:read` | `Authorization` | `200`, all labels |
| `POST /labels` | `sniper:write` or `copytrade:write` | `Authorization`, `Idempotency-Key` | `201`, created label |
| `GET /labels/{id}` | `sniper:read` or `copytrade:read` | `Authorization` | `200`, one label |
| `PATCH /labels/{id}` | `sniper:write` or `copytrade:write` | `Authorization` | `200`, updated label |
| `DELETE /labels/{id}` | `sniper:write` or `copytrade:write` | `Authorization` | `204`, empty |

Either product scope is sufficient. The spec lists no `transfersnipe:*` scope for label endpoints, even though transfer-snipe tasks can be labelled, so a transfer-snipe-only token cannot manage labels. Deletion uses a `:write` scope; there is no label `:delete` scope. Responses carry `X-Request-Id` but no `ETag`.

## Label fields

### LabelResource (returned)

| Field | Type | Meaning |
|---|---|---|
| `id` | UUID | Server-generated label id |
| `name` | string, 1-32 characters | Display name, trimmed |
| `color` | string, `^#[0-9A-Fa-f]{6}$` | Colour as `#RRGGBB` |
| `sort_order` | integer | Display order (0-32767 on input) |
| `sniper_count` | integer, 0 or more | Sniper configs using this label |
| `copy_trade_count` | integer, 0 or more | Copy-trade configs using this label |
| `transfer_snipe_count` | integer, 0 or more | Transfer-snipe tasks using this label |
| `created_at` | ISO 8601 date-time with UTC offset | Creation time |
| `updated_at` | ISO 8601 date-time with UTC offset | Last change |

### LabelCreate (POST body)

| Field | Type | Default | Constraints |
|---|---|---|---|
| `name` | string | none | 1-32 characters after trimming leading and trailing spaces; unique per account |
| `color` | string | `#6366f1` | `#RRGGBB` |
| `sort_order` | integer | `0` | 0-32767 |

The schema lists all three as required while also giving defaults for `color` and `sort_order`; the spec's own curl example sends only `name` and `color`. Sending all three avoids any ambiguity.

### LabelPatch (PATCH body)

Any of `name`, `color`, `sort_order` with the same constraints; at least one field is required. Unknown fields and `user_id` are rejected with `validation_error`.

## Rules and limits

| Rule | Behaviour |
|---|---|
| Maximum labels per account | 20; the 21st create returns `quota_exceeded` |
| Unique names | Duplicate name on create or `PATCH` returns `label_name_exists` |
| Shared set | One set for sniper, copy-trade and transfer-snipe tasks |
| List order | `sort_order`, then `created_at` |
| Pagination | None; `GET /labels` returns all labels in `data.items` |
| Concurrency | No `ETag` / `If-Match` on labels; last write applies |
| Idempotency | `Idempotency-Key` required on `POST /labels` (8-128 chars, `A-Za-z0-9._:-`, kept 24 h) |
| Renaming | Bound tasks keep the same label id |
| Delete | Tasks are unassigned (`ON DELETE SET NULL`), not deleted |
| Foreign id | Label owned by another account returns `404 not_found` |

## Binding labels to tasks with risk_label_id

Sniper, copy-trade and transfer-snipe configs have a nullable `risk_label_id` field.

| Action | How |
|---|---|
| Bind on create | Include `"risk_label_id": "<label uuid>"` in the create body |
| Change binding | `PATCH /{product}/configs/{id}` with `risk_label_id` and the current `If-Match` |
| Remove binding | `PATCH` with `"risk_label_id": null` and the current `If-Match` |
| Filter lists | `GET /{product}/configs?label_id=<uuid>` or `label_id=unlabeled` |

Binding or clearing a label is allowed while the task is active; only `config` and `linked_wallets` changes require pausing. The request still needs the latest `If-Match`. A `risk_label_id` that does not exist or belongs to another account returns `404 not_found`.

Templates never accept or return `risk_label_id`; sending it in a template body is rejected. Labels are applied per task, not per template.

## Examples

Create a label:

```bash
curl --request POST 'https://x-trader.ai/api/v1/labels' \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: label-create-001' \
  --data '{"name":"Watchlist","color":"#6366f1","sort_order":0}'
```

Rename and recolour (no `If-Match`):

```bash
curl --request PATCH "https://x-trader.ai/api/v1/labels/$LABEL_ID" \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{"name":"High priority","color":"#ef4444"}'
```

Bind the label to a sniper config:

```bash
curl --request PATCH "https://x-trader.ai/api/v1/sniper/configs/$CONFIG_ID" \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'If-Match: "v3"' \
  --data "{\"risk_label_id\":\"$LABEL_ID\"}"
```

Delete a label:

```bash
curl --request DELETE "https://x-trader.ai/api/v1/labels/$LABEL_ID" \
  --header "Authorization: Bearer $X_TRADER_API_TOKEN"
```

Python: get or create a label by name, then bind it:

```python
import os
import uuid
import requests

BASE = "https://x-trader.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"}

def get_or_create_label(name, color="#6366f1"):
    r = requests.get(f"{BASE}/labels", headers=H, timeout=10)
    r.raise_for_status()
    for label in r.json()["data"]["items"]:
        if label["name"] == name:
            return label
    r = requests.post(
        f"{BASE}/labels",
        headers={**H, "Idempotency-Key": f"label-{uuid.uuid4()}"},
        json={"name": name, "color": color, "sort_order": 0},
        timeout=10,
    )
    r.raise_for_status()  # quota_exceeded once 20 labels exist
    return r.json()["data"]

def bind_label(product, config_id, label_id):
    r = requests.get(f"{BASE}/{product}/configs/{config_id}", headers=H, timeout=10)
    r.raise_for_status()
    r = requests.patch(
        f"{BASE}/{product}/configs/{config_id}",
        headers={**H, "If-Match": r.headers["ETag"]},
        json={"risk_label_id": label_id},  # None to unassign
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["data"]

label = get_or_create_label("Watchlist")
bind_label("copy-trade", "00000000-0000-4000-8000-000000000000", label["id"])
```

## FAQ

### How many labels can an X-Trader account have?

20. Creating the 21st label returns `quota_exceeded`. Sniper, copy-trade and transfer-snipe tasks share this one set.

### Can two labels have the same name?

No. Names are unique per account; creating or renaming to an existing name returns `label_name_exists`.

### Do label updates need If-Match?

No. Labels do not use `ETag` or `If-Match`. Creating a label still requires an `Idempotency-Key` header.

### How do I attach a label to a task?

Send `risk_label_id` with the label UUID when creating a config, or in a `PATCH` with the config's current `If-Match`. Send `risk_label_id=null` to remove it. The task does not need to be paused.

### What happens to tasks when I delete a label?

They are not deleted. Their `risk_label_id` is set to `null` (`ON DELETE SET NULL`).
