> The index of these docs, for agents: https://traktor.fun/llms.txt

# Notifications

The list an account's alert matches land in, how to mark it read, and the webhooks they are posted to.

No socket carries notifications: a program reads them from these routes or has them posted to a [webhook](#webhooks) of its own, and Traktor's Telegram bot sends them to a chat linked on [Alerts](https://traktor.fun/docs/alerts.md#telegram).

### `GET /v1/notifications`

The notifications the account's alerts wrote, newest first, a page at a time.

- URL: `https://api.traktor.fun/v1/notifications`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `limit` | query | integer | no | 30 | 1–100 | Notifications per page. |
| `cursor` | query | cursor | no |  |  | `meta.page.cursor` from the previous page. |
| `unread` | query | boolean | no |  |  | `true` for unread only, `false` for read only; absent for both. |
| `alert` | query | uuid | no |  |  | One of the account's alerts: its notifications alone, paged by their own cursor, without the suppression summaries its matches were counted into, which belong to no alert; absent for every alert's. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Detailed notifications: 300 a clock hour, per account
- Kept for: 90 days, per account

```bash cURL
curl -s "https://api.traktor.fun/v1/notifications?limit=20&alert=8f14e45f-ceea-467a-9575-2b0f3e8d1c2a" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/notifications?limit=20&alert=8f14e45f-ceea-467a-9575-2b0f3e8d1c2a", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/notifications",
    params={"limit": 20, "alert": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a"},
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "notifications": [
      {
        "id": "0b7c2d4e-1f3a-4b5c-8d6e-7f8091a2b3c4",
        "alertId": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
        "alertName": "Large BNB buys",
        "sourceKey": "flow:bnb:0x4b8e2f6a1c7d9035e8b2a4f6c1d3e5a7b9c0d2e4f6a8b1c3d5e7f9a0b2c4d6e8:0",
        "title": "Large BNB buys triggered",
        "body": "Trade size matched at $1.2k.",
        "reason": {
          "kind": "alert_trigger",
          "mode": "crossing",
          "ruleHash": "227e1305a2a8d5728b6bdd47502abfaa",
          "subjectKey": "bnb:0x2222222222222222222222222222222222222222:0x1111111111111111111111111111111111111111",
          "conditions": [
            {
              "metric": "usdAmount",
              "op": "gte",
              "value": 1000,
              "observed": 1204.5
            }
          ]
        },
        "context": {
          "chain": "bnb",
          "platform": "fomo",
          "tokenAddress": "0x2222222222222222222222222222222222222222",
          "ticker": "EXAMPLE",
          "name": "Example"
        },
        "arrivedAt": "2026-10-03T12:00:02.120Z",
        "occurredAt": "2026-10-03T12:00:00.000Z",
        "createdAt": "2026-10-03T12:00:02.300Z",
        "readAt": null
      }
    ]
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": [],
    "page": {
      "cursor": null,
      "hasMore": false,
      "limit": 20,
      "returned": 1
    }
  }
}
```

Errors: `invalid_limit`, `invalid_cursor`, `invalid_unread_filter`, `invalid_alert`, `unknown_query_parameter`, `duplicate_query_parameter`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

The list is ordered on `(createdAt, id)`: send `meta.page.cursor` back as `cursor` for the next page, and it is `null` on the last. A query parameter the route does not read is `400 unknown_query_parameter`, and one given twice `400 duplicate_query_parameter`.

Each notification carries three clocks: `occurredAt`, when what matched happened in the market, such as a trade's block time; `arrivedAt`, when Traktor stored it, which cooldowns count from; and `createdAt`, when the notification was written. `readAt` is `null` until it is marked read. A rule's notification has `reason.kind` `alert_trigger`, and `reason.conditions` holds each condition with the figure it `observed`.

## Delivery limits

Past the Detailed notifications limit on the list's block, a match still advances its rule's trigger state, but it is counted into one suppression summary for the hour instead of written on its own. The summary turns unread again each time it grows but keeps its place in the list, so marking `all` read through a newer notification marks it read again. Evaluation per account each minute is bounded too, and so are the subjects a rule keeps trigger state for; `GET /v1/alert-capabilities` publishes both bounds under `limits`. What either bound skipped is never treated as no match: it is counted in a summary of its own, which turns unread and moves to the top of the list each time it grows.

A summary has `alertId: null`, and its `reason.kind` is `notification_suppression` or `alert_resource_limit`. It belongs to no rule, so a rule's own list (`alert=<id>`) leaves out the matches counted into one. Notifications, read or not, are deleted after the days on the list's block, and so is a rule's trigger state for a subject it has not updated in that time.

## Read and unread

### `GET /v1/notifications/unread-count`

How many of the account's notifications are unread.

- URL: `https://api.traktor.fun/v1/notifications/unread-count`
- Credential: API key, browser session or Traktor sign-in

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/notifications/unread-count" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/notifications/unread-count", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/notifications/unread-count",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "unreadCount": 3
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `POST /v1/notifications/read`

Marks notifications read: named ones, or all of them up to an optional bound.

- URL: `https://api.traktor.fun/v1/notifications/read`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `ids` | body | list | no |  | up to 100 items | Notification ids, 1 to 100 of them. |
| `all` | body | boolean | no |  |  | Every unread notification. |
| `through` | body | object | no |  |  | With `all`: the newest notification to mark, as its `createdAt` and `id`. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/notifications/read" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"all":true}'
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/notifications/read", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"all":true}),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/notifications/read",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
    json={"all": True},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "updated": 3,
    "unreadCount": 0
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `invalid_notification_ids`, `invalid_notification_bound`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

Send the newest notification your reader saw as `through`, so one that arrived since stays unread; a `createdAt` with the milliseconds the list gives covers that whole millisecond. A malformed `through`, or one sent without `all`, is `400 invalid_notification_bound`. An id already read, or not the account's, is passed over: `updated` counts what changed, beside the `unreadCount` left.

## Webhooks

An account names up to three HTTPS endpoints, and each is posted every notification written after it was made, within seconds, oldest first, in batches of up to 20. A post carries the list's own notifications, so a program can take them pushed or read them here and handle both the same way.

### `GET /v1/webhooks`

The account's webhooks, oldest first, each with how its last post went; never their secrets.

- URL: `https://api.traktor.fun/v1/webhooks`
- Credential: API key, browser session or Traktor sign-in

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/webhooks" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/webhooks", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "webhooks": [
      {
        "id": "3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a",
        "url": "https://bot.example.com/traktor",
        "enabled": true,
        "disabledReason": null,
        "failures": 0,
        "lastStatus": 200,
        "lastError": null,
        "lastAttemptAt": "2026-10-03T12:00:04.000Z",
        "lastDeliveredAt": "2026-10-03T12:00:04.000Z",
        "createdAt": "2026-10-01T09:00:00.000Z"
      }
    ]
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `POST /v1/webhooks`

Adds an endpoint that every notification the account's alerts write from now on is posted to, while the key or Mini App session that made it lives, signed with the `secret` this answer alone carries.

- URL: `https://api.traktor.fun/v1/webhooks`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `url` | body | string | yes |  |  | The endpoint: `https://`, port 443, a host on the public internet, at most 2,048 characters. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Webhooks: 3 kept, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/webhooks" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://bot.example.com/traktor"}'
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/webhooks", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"url":"https://bot.example.com/traktor"}),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
    json={"url": "https://bot.example.com/traktor"},
)
print(response.json())
```

Response `201`:

```json
{
  "data": {
    "webhook": {
      "id": "3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a",
      "url": "https://bot.example.com/traktor",
      "enabled": true,
      "disabledReason": null,
      "failures": 0,
      "lastStatus": null,
      "lastError": null,
      "lastAttemptAt": null,
      "lastDeliveredAt": null,
      "createdAt": "2026-10-01T09:00:00.000Z",
      "secret": "tk_whsec_4fQ9xZ2mR7bK1vT8nW3cY6pL0sH5jD2a"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `invalid_webhook_url`, `webhook_limit_reached`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

`url` is `https` on port 443, with no user, password or `#fragment`, and a host on the public internet: an address in a private, loopback, link-local, documentation or other reserved range, a name with no dot, and a name under `.local`, `.internal`, `.localhost`, `.lan`, `.home`, `.corp` or `.arpa` are `400 invalid_webhook_url`, and a fourth endpoint is `409 webhook_limit_reached`. The answer carries `secret` this once and no route shows it again, so store it then. Delivery starts after the account's newest notification, so a new endpoint is sent no backlog.

An endpoint lives no longer than the credential that made it. One made with an API key, or with a browser session that key opened, stops for good when the key is revoked or expires, and one made from Traktor's Telegram Mini App stops for good when that Telegram leaves the account, even if it is linked again; the list then shows it switched off, with `disabledReason` `key_revoked` or `telegram_unlinked`. One made from your Traktor sign-in lives as long as the account. Your profile's [API tab](https://traktor.fun/profile?tab=api) lists every endpoint and deletes one.

### `DELETE /v1/webhooks/{id}`

Deletes a webhook; nothing more is posted to it.

- URL: `https://api.traktor.fun/v1/webhooks/{id}`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The webhook's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X DELETE "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
console.log(response.status);
```

```python Python
import os
import requests

response = requests.delete(
    "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.status_code)
```

Response `204`: no body.

Errors: `webhook_not_found`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `POST /v1/webhooks/{id}/test`

Posts a signed batch with no notifications to the endpoint now, and answers what it said: `delivered` on a 2xx, its `status`, and `error` when none came back.

- URL: `https://api.traktor.fun/v1/webhooks/{id}/test`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The webhook's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Webhook tests: 6 a minute, a burst of 3, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a/test" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a/test", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a/test",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "delivered": true,
    "status": 200,
    "error": null
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `webhook_not_found`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

A test posts a signed batch with `test: true` and no notifications, and answers what the endpoint did: `delivered`, its `status`, and an `error` when it failed. It moves nothing, so it works on an endpoint that was switched off too.

### A post

Each post is a `POST` of `{ "webhookId", "test", "notifications" }` as JSON, with the headers `traktor-webhook-id` and `traktor-signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `<t>.<body>` under the endpoint's secret. Compute it over the body's raw bytes before parsing them, compare it in constant time, and refuse a `t` far from your own clock so an old post cannot be replayed.

### Failures and retries

A `2xx` moves the endpoint past what it was sent. Anything else keeps it where it was: another status, a redirect (never followed), no answer within 5 seconds, or a host that resolves to an address off the public internet, which is never posted to. The batch is tried again after 30 seconds, then after twice as long each time, at most an hour apart. After 30 failures in a row, about a day, the endpoint is switched off with `disabledReason` `failing`; to restart it, delete it and create it again, which starts from the newest notification, and read what it missed from the list. The list shows each endpoint's `failures`, `lastStatus`, `lastError`, `lastAttemptAt` and `lastDeliveredAt`.

Delivery is at least once: a post your endpoint kept but answered too late for is sent again, so key on each notification's `id`. A `notification_suppression` summary is posted when it is first written and not again as its count grows; an `alert_resource_limit` summary moves to the top of the list when it grows, and is posted again each time.
