Skip to content

API write endpoints

Phase 4.7 opens up the bot to programmatic mutation. Every write route is dispatched through the same ActionExecutor as bot-UI and rule-engine writes, so one audit trail / event surface covers all origins.

$BASE is your public URL — https://your-bot.up.railway.app on Railway.

Tickets

POST /api/v1/tickets/{id}/close

Scope: tickets:write

{"reason": "resolved"}

Response:

{"ok": true, "action": "close", "data": {"reason": "resolved"}}

Errors: - 400 {"detail": "ticket_not_found"} - 400 {"detail": "bad_id: …"}

POST /api/v1/tickets/{id}/reopen

Scope: tickets:write

{"reason": "customer followed up"}

POST /api/v1/tickets/{id}/assign

Scope: tickets:write

{"assignee_id": 123456789}   // null → unassign

POST /api/v1/tickets/{id}/tags

Scope: tickets:write. Replacement semantics — the body is the new tag list. The route computes the diff and emits per-tag tag / untag ActionExecuted events.

{"tags": ["billing", "vip"]}

Response:

{"ok": true, "added": ["vip"], "removed": ["old_tag"]}

POST /api/v1/tickets/{id}/priority

Scope: tickets:write

{"priority": "high"}   // low | normal | high

POST /api/v1/tickets/{id}/notes

Scope: tickets:write. Appends an internal note — never forwarded to the customer, stored in tickets.internal_notes (separate from the public history array).

{"text": "customer is a VIP, preferred channel is email"}

POST /api/v1/tickets/{id}/bulk-action

Scope: tickets:write. Thin passthrough to any action registered in the executor. Use this for actions without a dedicated route or for future actions that plugins register.

{"action": "emoji_react", "params": {"emoji": "🔥"}}

Projects

POST /api/v1/projects

Scope: projects:write

Create from a built-in template (see project templates):

{
  "project_slug": "pay",
  "name": "Payments Support",
  "template_slug": "billing"
}

Response:

{
  "ok": true,
  "project_id": "652…",
  "template_slug": "billing",
  "macros_seeded": 3,
  "kb_articles_seeded": 2,
  "routing_rules_seeded": 3
}

Or create a blank project by omitting template_slug.

Errors: - 409 {"detail": "project_slug_taken"} - 400 {"detail": "unknown_template: …"}

DELETE /api/v1/projects/{slug}

Scope: projects:write. Soft-archive (active=false, archived_at set). Existing tickets keep their backreference; new tickets cannot be opened against an archived project.

Webhooks

Full subscription CRUD. Secrets are returned once on create.

GET /api/v1/webhooks

Scope: webhooks:write. Lists all subscriptions (active + revoked) without secrets.

POST /api/v1/webhooks

Scope: webhooks:write

{
  "url": "https://example.com/hook",
  "events": ["ticket.closed", "ticket.sla_breached"],
  "label": "PagerDuty on breach"
}

Response:

{
  "id": "652…",
  "secret": "xtvwh_<40-char-one-time>",
  "url": "https://example.com/hook",
  "events": ["ticket.closed", "ticket.sla_breached"]
}

The secret is shown exactly once — store it to verify future deliveries (see webhooks).

Pass an empty events array to receive every event.

DELETE /api/v1/webhooks/{id}

Scope: webhooks:write. Marks the subscription revoked_at=now; future deliveries are skipped.

Automation rules

Full CRUD via the web admin (/admin/rules in the SPA) and the API.

POST /api/v1/rules

Scope: rules:write. Create a new rule.

{
  "name": "Escalate billing tickets",
  "trigger": "TicketCreated",
  "conditions": [
    {"field": "tags", "op": "contains", "value": "billing"},
    {"field": "priority", "op": "eq", "value": "high"}
  ],
  "actions": [
    {"name": "assign", "params": {"team": "billing"}},
    {"name": "emoji_react", "params": {"emoji": "🔥"}}
  ],
  "cooldown_s": 0,
  "enabled": true
}

Returns the rule with its id and version=1.

PATCH /api/v1/rules/{id}/enabled

Scope: rules:write. Toggle enabled-state without a full update.

{"enabled": false}

DELETE /api/v1/rules/{id}

Scope: rules:write. Hard-delete. Running evaluations finish, new events stop firing the rule immediately.

One execution path

All the ticket routes above go through xtv_support.services.actions.ActionExecutor with origin="api". That means:

  • The same ActionExecuted / ActionFailed events fire regardless of whether the close button was pressed in Telegram, the API, or triggered by a rule.
  • Audit-log entries have a consistent shape.
  • Analytics queries group by origin for attribution (bot | api | rule | bulk).
  • Plugins that subscribe to ActionExecuted work with every path automatically.

Reading actions

GET /api/v1/rules and GET /api/v1/rules/{id} (scope rules:read) let you inspect the automation rules that might run alongside any write. See automation rules.