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 of its own, and Traktor's Telegram bot sends them to a chat linked on Alerts.
GET /v1/notifications
The notifications the account's alerts wrote, newest first, a page at a time.
- Credential
- API key, browser session or Traktor sign-in
limitintegerqueryNotifications per page.
Default
301–100cursorcursorquerymeta.page.cursorfrom the previous page.unreadbooleanquerytruefor unread only,falsefor read only; absent for both.alertuuidqueryOne 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
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
- Detailed notifications300 a clock hour, per account
- Kept for90 days, per account
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
curl -s "https://api.traktor.fun/v1/notifications?limit=20&alert=8f14e45f-ceea-467a-9575-2b0f3e8d1c2a" \
-H "Authorization: Bearer $TRAKTOR_API_KEY"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();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()){
"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
}
}
}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.
- Credential
- API key, browser session or Traktor sign-in
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
Errors
missing_credentials, invalid_key, key_revoked, subscription_inactive, session_expired, sign_in_required, method_not_allowed, rate_limited, rate_limit_unavailable, internal_error
curl -s "https://api.traktor.fun/v1/notifications/unread-count" \
-H "Authorization: Bearer $TRAKTOR_API_KEY"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();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()){
"data": {
"unreadCount": 3
},
"meta": {
"requestId": "req_01m40t5ek8b3x7r2mh5q",
"generatedAt": "2026-10-03T12:00:04.210Z",
"asOf": null,
"caveats": []
}
}POST /v1/notifications/read
Marks notifications read: named ones, or all of them up to an optional bound.
- Credential
- API key, browser session or Traktor sign-in
idslistbodyNotification ids, 1 to 100 of them.
up to 100 items
allbooleanbodyEvery unread notification.
throughobjectbodyWith
all: the newest notification to mark, as itscreatedAtandid.
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
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
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}'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();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()){
"data": {
"updated": 3,
"unreadCount": 0
},
"meta": {
"requestId": "req_01m40t5ek8b3x7r2mh5q",
"generatedAt": "2026-10-03T12:00:04.210Z",
"asOf": null,
"caveats": []
}
}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.
- Credential
- API key, browser session or Traktor sign-in
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
Errors
missing_credentials, invalid_key, key_revoked, subscription_inactive, session_expired, sign_in_required, method_not_allowed, rate_limited, rate_limit_unavailable, internal_error
curl -s "https://api.traktor.fun/v1/webhooks" \
-H "Authorization: Bearer $TRAKTOR_API_KEY"const response = await fetch("https://api.traktor.fun/v1/webhooks", {
headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();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()){
"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": []
}
}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.
- Credential
- API key, browser session or Traktor sign-in
urlstringrequiredbodyThe endpoint:
https://, port 443, a host on the public internet, at most 2,048 characters.
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
- Webhooks3 kept, per account
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
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"}'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();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()){
"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": []
}
}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 lists every endpoint and deletes one.
DELETE /v1/webhooks/{id}
Deletes a webhook; nothing more is posted to it.
- Credential
- API key, browser session or Traktor sign-in
iduuidrequiredpathThe webhook's id.
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
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
curl -s -X DELETE "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a" \
-H "Authorization: Bearer $TRAKTOR_API_KEY"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);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.
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.
- Credential
- API key, browser session or Traktor sign-in
iduuidrequiredpathThe webhook's id.
Limits
- Requests120 a minute, per account
- Session checks120 a minute, per IP
- Invalid keys30 a minute, per IP
- Webhook tests6 a minute, a burst of 3, per account
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
curl -s -X POST "https://api.traktor.fun/v1/webhooks/3c9a7b1e-5d2f-4e8a-9b6c-0f1e2d3c4b5a/test" \
-H "Authorization: Bearer $TRAKTOR_API_KEY"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();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()){
"data": {
"delivered": true,
"status": 200,
"error": null
},
"meta": {
"requestId": "req_01m40t5ek8b3x7r2mh5q",
"generatedAt": "2026-10-03T12:00:04.210Z",
"asOf": null,
"caveats": []
}
}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.