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 (delivered to the customer) and internal notes on a ticket.
reviews:read List and fetch reviews synced from Trustpilot, Google Business Profile and other review sources.
statistics:read Read aggregate support metrics without customer identities or ticket contents.

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

120 requests per minute per API key. Every key has its own budget, so two integrations on the same server do not compete. Excess traffic returns 429 with Retry-After in seconds; X-RateLimit-Remaining on every response tells you where you stand. A single IP address is additionally capped at 600 requests per minute across all keys. Need more? Talk to us.

Endpoints

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

GET /statistics/support scope: statistics:read

Aggregate support reporting for the key’s company or brand. Includes current and previous summaries, current backlog, daily counts, topics and channels. Maximum 90 local calendar days. Spam and deleted tickets are excluded. Durations are medians for tickets created in the period, including AI first replies. Resolved counts use resolution dates and exclude reopened tickets. AI-only resolutions require a confirmed automatic reply with no public human reply. The previous period has the same local-day span and equivalent cutoff time. Null durations mean no qualifying observations.

Parameters
NameInTypeDescription
from query date Required first date, YYYY-MM-DD (inclusive).
to query date Required last date, YYYY-MM-DD (inclusive), no future dates.
timezone query string IANA timezone, defaults to UTC. Brand scope comes from the key; brand_id is rejected.
Request
GET https://app.kvix.ai/api/v1/statistics/support?from=2026-09-01&to=2026-09-30&timezone=Europe%2FCopenhagen (X-API-Key header)
Response
{"data":{"version":1,"brand":{"id":2,"name":"All2day"},"summary":{"created_tickets":100,"resolved_tickets":90,"ai_only_resolved_tickets":40,"ai_only_resolution_percent":40,"median_first_response_seconds":120,"first_response_sample_count":80,"median_resolution_seconds":600,"resolution_sample_count":70},"backlog":{"total":12,"unanswered":5,"by_status":{"new":5,"open":4,"pending":3},"by_age":{"under_24_hours":6,"24_to_72_hours":4,"over_72_hours":2},"oldest_seconds":300000},"daily":[],"topics":[],"channels":[]}}
GET /me scope: any valid key

Describes the calling key: company, brand, scopes and rate limit. Use it to verify an integration's configuration without touching data.

Request
curl -H "X-API-Key: $KVIX_KEY" https://app.kvix.ai/api/v1/me
Response
{
  "data": {
    "key": { "name": "Production server", "last_four": "tv96", "scopes": ["tickets:read", "messages:read"], "all_access": false, "expires_at": null },
    "company": { "id": 1, "name": "ApexX" },
    "brand": { "id": 2, "name": "All2day" },
    "rate_limit_per_minute": 120
  }
}
GET /tickets scope: tickets:read

Paginated list of tickets with customer and brand, newest activity first. All filters are optional and combined with AND.

Parameters
NameInTypeDescription
status query string Filter by status (new, open, pending, resolved, closed, spam).
priority query string low, normal, high or urgent.
channel query string email, widget, form, api, manual, import, facebook or instagram.
assigned_to query string A member id, or "none" for unassigned tickets.
tag query string Tickets carrying this tag.
customer_email query string Tickets of the customer with exactly this e-mail address.
q query string Free text (2–100 chars) matched against subject, reference code, customer name and e-mail.
updated_since query datetime Only tickets with activity at or after this ISO-8601 time. Ideal for incremental polling.
created_since query datetime Only tickets created at or after this ISO-8601 time.
sort query string -last_activity_at (default), last_activity_at, -created_at or created_at. A leading minus means newest first.
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 reply is delivered to the customer on the ticket's channel (e-mail, Facebook, Instagram), stamps the first response time, moves a new ticket to open and reopens a resolved one. Internal notes are never sent and never change the status.

Parameters
NameInTypeDescription
body body string Message body (required, max 65535 chars). Facebook and Instagram replies must be plain text up to 2,000 characters inside the 24-hour window, otherwise the request fails with 422.
is_internal_note body boolean Default false. Set true to add a note visible only to members.
deliver body boolean Default true. Set false to store a reply without sending it, e.g. when importing a conversation that happened elsewhere. Ignored for internal notes.
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"
  }
}
GET /reviews scope: reviews:read

Paginated list of reviews, newest first. Read-only: replying to a review stays a dashboard action. The author's e-mail and internal AI fields are never returned.

Parameters
NameInTypeDescription
status query string unread, open, replied, ignored or flagged.
provider query string Review source, e.g. trustpilot or google_business_profile.
rating query integer Exactly this many stars (1–5).
min_rating query integer At least this many stars (1–5).
max_rating query integer At most this many stars (1–5). max_rating=2 finds the reviews that need attention.
sentiment query string Sentiment label assigned during analysis.
unanswered query boolean true for reviews without a reply, false for replied ones.
posted_since query datetime Only reviews posted at or after this ISO-8601 time.
q query string Free text (2–100 chars) matched against title, body and author name.
sort query string -posted_at (default), posted_at, -rating or rating. A leading minus means descending.
per_page query integer Items per page (1–100, default 25).
Request
curl -H "X-API-Key: $KVIX_KEY" \
  "https://app.kvix.ai/api/v1/reviews?max_rating=2&unanswered=true"
Response
{
  "data": [
    {
      "id": 88,
      "provider": "trustpilot",
      "external_url": "https://www.trustpilot.com/reviews/…",
      "author_name": "Karen K.",
      "rating": 1,
      "title": "Voucher did not work",
      "body": "…",
      "language": "da",
      "sentiment": "negative",
      "category": null,
      "tags": [],
      "status": "unread",
      "brand": { "id": 2, "name": "All2day" },
      "posted_at": "2026-09-20T18:04:11+00:00",
      "edited_at": null,
      "replied_at": null
    }
  ],
  "links": { "first": "…", "last": "…", "prev": null, "next": null },
  "meta":  { "current_page": 1, "per_page": 25, "total": 1 }
}
GET /reviews/{id} scope: reviews:read

A single review with the replies that were actually posted (drafts and pending approvals are internal).

Parameters
NameInTypeDescription
id path integer Review id.
Request
curl -H "X-API-Key: $KVIX_KEY" https://app.kvix.ai/api/v1/reviews/88
Response
{
  "data": {
    "id": 88,
    "provider": "trustpilot",
    "rating": 1,
    "title": "Voucher did not work",
    "status": "replied",
    "replies": [
      { "id": 12, "body": "We are sorry — a new voucher is on its way.", "ai_generated": false, "posted_at": "2026-09-21T08:12:40+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 (120 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