REST API · v1

Talk to Kvix
from anything.

Authenticate with an API key, list tickets, change status, post replies. The same data your members see, programmatically. JSON in, JSON out. No SDKs required — but bring your favourite HTTP client.

Base URL
https://app.kvix.ai/api/v1

Authentication

Every request needs an X-API-Key header. Generate keys in your dashboard under Settings → API access. The plain value is shown once at creation time — copy it immediately. Keys start with sk_live_.

curl -H "X-API-Key: sk_live_..." https://app.kvix.ai/api/v1/tickets

Lost a key? Revoke it from the dashboard and mint a new one — there is no recovery flow. Revoked keys return 403 on every request.

Scopes

Each key carries a subset of four scopes. Requests against an endpoint whose required scope is missing return 403 with the message Missing required scope: <name>.

ScopeGrants
tickets:read List tickets, fetch a single ticket and its metadata.
tickets:write Create tickets and update status, priority, assignee, and tags.
messages:read List messages on a ticket, including internal notes.
messages:write Post member replies and internal notes on a ticket.

New keys default to read-only (tickets:read + messages:read) if no scopes are ticked.

Brand scoping

Multibrand companies can scope a key to a single brand at creation time. A brand-scoped key only sees tickets on that brand; company-wide keys see all brands. For company-wide keys posting tickets, omit brand_id to use the default brand, or pass it explicitly.

Rate limits

60 requests per minute per API key. Excess traffic returns 429 with Retry-After in seconds. Need more? Talk to us.

Endpoints

All endpoints return JSON. Errors share one shape: { "message": "...", "errors": { ... } }.

GET /tickets scope: tickets:read

Paginated list of tickets, newest activity first.

Parameters
NameInTypeDescription
status query string Filter by status (new, open, pending, resolved, closed, spam).
per_page query integer Items per page (1–100, default 25).
page query integer Page number (default 1).
Request
curl -H "X-API-Key: $KVIX_KEY" \
  https://app.kvix.ai/api/v1/tickets?status=open&per_page=10
Response
{
  "data": [
    {
      "id": 18,
      "reference_code": "TKT-AC-00018",
      "subject": "Refund question",
      "status": "open",
      "priority": "normal",
      "channel": "email",
      "assigned_to": null,
      "tags": ["billing"],
      "first_response_at": null,
      "resolved_at": null,
      "closed_at": null,
      "last_activity_at": "2026-05-15T09:14:02+00:00",
      "created_at": "2026-05-15T08:55:11+00:00"
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta":  { "current_page": 1, "per_page": 10, "total": 28 }
}
POST /tickets scope: tickets:write

Create a new ticket. Customer is matched by email or auto-created.

Parameters
NameInTypeDescription
email body string Customer email (required).
subject body string Ticket subject (required).
body body string Opening message body (required).
name body string Customer display name (optional).
brand_id body integer Target brand id. Required for company-wide keys when the company has no default brand.
Request
curl -H "X-API-Key: $KVIX_KEY" \
  -H 'Content-Type: application/json' \
  -X POST https://app.kvix.ai/api/v1/tickets \
  -d '{"email":"[email protected]","subject":"Hello","body":"My order is late."}'
Response
{
  "data": {
    "id": 142,
    "reference_code": "TKT-AC-00142",
    "subject": "Hello",
    "status": "new",
    "channel": "api",
    "customer": { "id": 88, "email": "[email protected]", "name": null },
    "brand":    { "id": 3, "slug": "default", "name": "Acme Support" }
  }
}
GET /tickets/{id} scope: tickets:read

A single ticket plus its full message thread.

Parameters
NameInTypeDescription
id path integer The ticket id.
Request
curl -H "X-API-Key: $KVIX_KEY" https://app.kvix.ai/api/v1/tickets/142
Response
{
  "data": {
    "id": 142,
    "subject": "Hello",
    "status": "open",
    "messages": [
      { "id": 401, "sender_type": "customer", "body": "My order is late." },
      { "id": 402, "sender_type": "member", "body": "Sorry — let me check." }
    ]
  }
}
PATCH /tickets/{id} scope: tickets:write

Update one or more of: status, priority, assignee, tags. Returns the refreshed ticket.

Parameters
NameInTypeDescription
status body string One of: new, open, pending, resolved, closed, spam. Moving to resolved/closed timestamps the ticket.
priority body string One of: low, normal, high, urgent.
assigned_to body integer|null User id of a member in your company, or null to unassign.
tags body array Array of tag strings; replaces the existing set.
Request
curl -H "X-API-Key: $KVIX_KEY" \
  -H 'Content-Type: application/json' \
  -X PATCH https://app.kvix.ai/api/v1/tickets/142 \
  -d '{"status":"resolved"}'
Response
{
  "data": {
    "id": 142,
    "status": "resolved",
    "resolved_at": "2026-05-15T10:02:18+00:00"
  }
}
GET /tickets/{id}/messages scope: messages:read

Paginated message list for a ticket, oldest first.

Parameters
NameInTypeDescription
id path integer The ticket id.
per_page query integer Items per page (1–200, default 50).
Request
curl -H "X-API-Key: $KVIX_KEY" https://app.kvix.ai/api/v1/tickets/142/messages
Response
{
  "data": [
    { "id": 401, "sender_type": "customer", "body": "My order is late.",  "is_internal_note": false },
    { "id": 402, "sender_type": "member",    "body": "Sorry — let me check.", "is_internal_note": false }
  ],
  "meta": { "current_page": 1, "per_page": 50, "total": 2 }
}
POST /tickets/{id}/messages scope: messages:write

Append a member reply or internal note. A non-note reply on a resolved ticket reopens it.

Parameters
NameInTypeDescription
body body string Message body (required, max 65535 chars).
is_internal_note body boolean Default false. Set true to add a note visible only to members.
Request
curl -H "X-API-Key: $KVIX_KEY" \
  -H 'Content-Type: application/json' \
  -X POST https://app.kvix.ai/api/v1/tickets/142/messages \
  -d '{"body":"Thanks for your patience — refund issued."}'
Response
{
  "data": {
    "id": 403,
    "ticket_id": 142,
    "sender_type": "member",
    "body": "Thanks for your patience — refund issued.",
    "is_internal_note": false,
    "ai_generated": false,
    "created_at": "2026-05-15T10:05:01+00:00"
  }
}

Errors

Standard HTTP status codes; the body always contains message.

CodeMeaning
400 Malformed request body.
401 Missing or unknown API key.
403 Key revoked, expired, or lacks the required scope.
404 Resource not found, or belongs to another company / brand.
409 No default brand exists; pass `brand_id`.
422 Validation failed; the response body lists per-field errors.
429 Rate limit exceeded (60 requests per minute per key).
500 Unexpected server error. Try again or contact support.

Webhooks

Outbound webhooks for ticket events (created, replied, resolved) are on the roadmap. In the meantime, polling GET /tickets?status=new works well for most integrations.

Changelog

Spotted a gap?

Reach out — we'd rather close the gap than leave you guessing.

Ask a human