Bots and integrations
How a Node bot (Telegram, Discord, WhatsApp) reads and edits Axuz tasks with an axz_bot_ integration token, including scopes, rotation and error handling.
A bot is a service of yours (a Node process connected to Telegram, Discord, WhatsApp or Slack) that talks to Axuz through the API: it reads and edits the tasks and boards of one team, without using anyone’s password. Its credential is the integration token axz_bot_…, issued by a team owner or admin.
How the pieces fit
| Credential | What it reaches |
|---|---|
| Session (cookie or JWT) of a team owner/admin | Management under /teams/{teamId}/integrations/** (create, pause, issue tokens) |
axz_bot_… token | Only /v1/integration/self* on axuz_api and the task/board routes of the map below, on axuz_workspace_api |
HMAC signature (whsec_…) | Only the inbound webhook POST /integrations/inbound/{webhookId} |
The token never manages integrations (not even its own) and never reaches IAM, billing, admin, /me, /platform, files, wiki, feeds or another team. With an axz_bot_ bearer on any other axuz_api route, the answer is 403 before any handler runs.
Step by step
-
Authenticate as a team owner or admin and keep the JWT in an environment variable (see Authentication). You also need the
teamId. -
Create the integration.
provideris one ofcustom_bot,github,slack,discord,telegram,whatsapp,webhook.scopesis required; ask only for what the bot uses.metadatais free-form but non-secret (up to 4 KiB; a key that looks like a secret is rejected).
curl -sS -X POST "https://api.axuz.net/teams/$AXUZ_TEAM_ID/integrations" \
-H "Authorization: Bearer $AXUZ_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"telegram","name":"Task bot","scopes":["tasks:read","tasks:write","boards:read","comments:write","integrations:read"]}'
- Issue the token. An omitted
scopesinherits all of the integration’s scopes; a scope outside it is a400.expires_in_days(1 to 730) is optional but recommended.
curl -sS -X POST "https://api.axuz.net/teams/$AXUZ_TEAM_ID/integrations/$INTEGRATION_ID/tokens" \
-H "Authorization: Bearer $AXUZ_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"telegram-prod","expires_in_days":90}'
The 201 response carries the token field (axz_bot_ plus 48 characters). It is shown exactly once: Axuz stores only the SHA-256. Copy it straight into your environment’s secret store. Listings show only prefix (for example axz_bot_AbCd) so you can tell tokens apart.
- Test the token with the “who am I” route, which requires the
integrations:readscope:
curl -sS "https://api.axuz.net/v1/integration/self" \
-H "Authorization: Bearer $AXUZ_BOT_TOKEN"
It returns integration, team_id and token.scopes (the effective scopes).
- Call the task routes on the workspace host, always with the token’s
teamId. See Tasks and boards through the API and the runnable example inexamples/bot-node/.
Scopes
These are the scopes in integration_catalog.go. There is no wildcard: matching is exact and anything not listed is not issued.
| Scope | Allows |
|---|---|
tasks:read | List, search and read tasks, subtasks and assignees; list task comments |
tasks:write | Create, update, move (reorder) and assign tasks. Never deletes |
boards:read | Read boards and columns, including a channel’s board |
boards:write | Create and edit boards and columns. Never deletes |
comments:write | Comment on tasks (target_type must be task) |
integrations:read | Read the integration itself, its state, scopes and deliveries |
A token’s effective scope is the intersection of the token’s scopes and the integration’s: if an admin narrows scopes on the integration, existing tokens lose that permission immediately.
The allowed-routes table (single source: BotRules) maps every call to a scope, for example GET /teams/{teamId}/boards needs boards:read and PATCH /teams/{teamId}/tasks/{id} needs tasks:write. A method or route outside the table is denied, and there is no DELETE for tasks, boards, columns or comments.
Limits
- One team per token. The
teamIdin the path must be the token’s; any other iscross_tenant. - Never deletes. Removing things is for a person, in the interface.
- Comments on tasks only.
POST /commentswith anothertarget_typeis denied. - No access to IAM, billing, admin, files, wiki and feeds.
- Integration metadata and names: up to 4 KiB and 100 characters. Up to 10 webhooks per integration.
Rate limit
In the code we read there is no dedicated limit for integration tokens on the task routes. What exists and applies to a bot:
- In
axuz_workspace_api, mutations (POST,PATCH,PUT,DELETE) under/teams/{teamId}are limited to 60 per minute per key, configurable throughMUTATION_RATE_LIMIT_PER_MIN. Reads do not count. Over the limit:429with{"error":"rate_limited","retry_after":N}and theRetry-AfterandX-RateLimit-Limitheaders. The key today is user plus team; how a bot principal enters that key is not documented yet. - On the inbound webhook: 120 per minute per webhook (
429).
Always treat 429 as “retry after Retry-After”, without assuming the exact number.
Rotation and revocation
- Rotate (
POST …/tokens/{tokenId}/rotate): issues a new token with the same scopes and ends the old one.grace_seconds(0 to 86400, default 0) lets the old one stay valid only for that window, so the bot can switch without downtime. An already revoked or expired token returns409. - Revoke (
DELETE …/tokens/{tokenId}): idempotent, immediate cut-off, no waiting for the cache. - Pausing the integration (
PATCHwithstate: "paused") cuts tokens and deliveries immediately;revokedis terminal (a laterPATCHreturns409 integration_revoked). - Removing the integration (
DELETE) is a soft delete: it revokes the tokens and disables the webhooks.
The resolver caches valid tokens for up to 15 seconds, but revocation and pausing invalidate that cache immediately.
What to do on 401, 403 and 429
| Status | Meaning | Bot action |
|---|---|---|
401 | Token missing, malformed, revoked, expired, or integration paused/removed. The answer is uniform and does not say which | Do not retry. Alert the owner, stop using the token and swap in a new one |
403 | Insufficient scope, route outside the map, different team, or a route that does not accept integration tokens | Do not retry. It is a configuration error: ask for the right scope or fix the route |
404 | The item does not exist in this team (it does not reveal existence in others) | Treat as “not found” |
429 | Rate limit | Wait Retry-After and retry with exponential backoff and jitter |
5xx | Server failure | Retry only GET and PATCH; a POST may have been processed |
The error body is always {"error":"<code or message>"}.
Best practices
- Keep the token in a vault or in the server’s environment variables, never in code, the repository, logs, the URL, or a browser or mobile app. A token in a URL leaks into logs and referrers; the API only accepts the
Authorizationheader. - One token per environment and per bot, with
expires_in_daysand the fewest scopes. Rotate on a schedule and whenever a leak is suspected. - Idempotency is yours.
POST /tasksandPOST /commentsdo not acceptIdempotency-Keytoday (only the channel invite does). Chat providers redeliver events: store the chat message id and ignore repeats, as the example does. - Retry with backoff and jitter, honoring
Retry-After; never on401/403;POSTonly on429. - Fail closed. If the token disappears from the configuration, the bot must not start pretending to work.
- Do not log the token. In logs, show at most the
prefix.
Next steps
- Tasks and boards through the API: real routes, fields and errors.
- A complete example in
examples/bot-node/, with an isolated HTTP client, a fictional chat adapter and a test against a mock.