SDK (@ketvia/kit)
@ketvia/kit is the TypeScript SDK for Kits. Version 0 has no runtime dependencies: it uses the global fetch and
node:crypto (Node.js 22 or later). Its types are generated from the same OpenAPI document as the
Web API reference.
npm install @ketvia/kitClient
Section titled “Client”import { KetviaClient } from '@ketvia/kit';
const ketvia = new KetviaClient({ token: process.env.KETVIA_TOKEN ?? '', // kbot_… or kusr_… // baseUrl: 'https://api.ketvia.com/api/v1', // the default // fetch: customFetch, // defaults to the global fetch // maxRetries: 2, // the default});The client never prints its token: JSON.stringify(ketvia) shows only the base URL.
| Method | Web API |
|---|---|
auth.test() |
GET /auth/test |
auth.rotate() |
POST /auth/rotate |
conversations.list(page?) |
GET /conversations |
conversations.get(id) |
GET /conversations/{id} |
conversations.members(id, page?) |
GET /conversations/{id}/members |
conversations.history(id, page?) |
GET /conversations/{id}/messages |
messages.get(id) |
GET /messages/{id} |
messages.replies(id, page?) |
GET /messages/{id}/replies |
messages.post(body, { idempotencyKey? }) |
POST /messages |
messages.update(id, body) |
PATCH /messages/{id} |
messages.delete(id) |
DELETE /messages/{id} |
reactions.add({ messageId, reaction }) |
POST /reactions |
reactions.remove({ messageId, reaction }) |
DELETE /reactions |
files.upload(body) |
POST /files |
files.get(id) |
GET /files/{id} |
files.content(id) |
GET /files/{id}/content |
users.list(page?) |
GET /users |
users.get(id) |
GET /users/{id} |
users.me() |
GET /users/me |
runs.get(id) |
GET /runs/{id} |
installation.get() |
GET /installation |
Methods return the JSON body of the answer, for example { conversations, nextCursor }. Two differ:
messages.postreturns{ message, created }:createdisfalsewhen anIdempotency-Keyreplay returned the stored message.files.contentreturns{ bytes, contentType }.
page is { cursor?, limit? }.
Retries
Section titled “Retries”The client retries up to maxRetries times (default 2):
- after
429, waiting forRetry-After; - after network errors and
5xxon calls that are safe to repeat: reads, andmessages.postwhen you pass anidempotencyKey.
Errors
Section titled “Errors”Every failed call throws KetviaApiError:
import { KetviaApiError } from '@ketvia/kit';
try { await ketvia.messages.post({ conversationId, text: 'Hello' });} catch (error) { if (error instanceof KetviaApiError) { error.status; // HTTP status, 0 for a network error error.code; // 'missing_scope', 'not_found', 'rate_limited', … error.correlationId; // quote it to support error.requiredScope; // from X-Ketvia-Required-Scope error.retryAfterSeconds; // from Retry-After } throw error;}Pagination
Section titled “Pagination”paginate follows nextCursor until the last page:
import { paginate } from '@ketvia/kit';
for await (const page of paginate((cursor) => ketvia.conversations.list({ cursor, limit: 100 }))) { for (const conversation of page.conversations) { // … }}kit.access
Section titled “kit.access”exchangeKitAccess calls POST /oauth/kit.access from your backend. See
the kit.access flow.
import { exchangeKitAccess } from '@ketvia/kit';
const result = await exchangeKitAccess({ grantType: 'authorization_code', clientId: process.env.KETVIA_CLIENT_ID ?? '', clientSecret: process.env.KETVIA_CLIENT_SECRET ?? '', code, // from the redirect redirectUri: 'https://kit.acme.example/ketvia/callback', codeVerifier, // user tokens});
if (result.tokenType === 'user') { // store result.accessToken, result.refreshToken and the expiry (result.expiresIn seconds)}It retries only after 429: codes and refresh tokens are single-use.
Incoming webhooks
Section titled “Incoming webhooks”import { blocks, postToWebhook } from '@ketvia/kit';
await postToWebhook(process.env.KETVIA_WEBHOOK_URL ?? '', { text: 'Build #42 passed', blocks: [blocks.header('Build #42 passed')],});Resolves on 200; throws KetviaApiError for 404, 410, 413 and for 429 after its retries. It never retries
network errors or 5xx, because a webhook post could then appear twice. The URL never appears in error messages.
Blocks builders
Section titled “Blocks builders”blocks has small helpers that shape Blocks objects with the right types. The server
validates the limits.
import { blocks } from '@ketvia/kit';
const card = [ blocks.header('Build #42 passed'), blocks.section(blocks.mrkdwn('*main* at `4f2c1d9`')), blocks.fields({ Branch: 'main', Duration: '3m 12s' }), blocks.metrics([{ key: 'tests', label: 'Tests', value: 1284, format: 'integer', primary: true }]), blocks.table( [ { key: 'job', label: 'Job', align: 'left' }, { key: 'time', label: 'Time', align: 'right' }, ], [{ job: 'unit', time: '1m 02s' }], ), blocks.list([{ title: 'Run log', url: 'https://ci.acme.example/runs/42' }]), blocks.divider(), blocks.context(['Acme CI']),];Helpers: header, section, fields (an array of {label, value} or a record), metrics, table, list,
context, divider, and the text objects plain and mrkdwn. Each accepts an optional { blockId }.
Request signatures
Section titled “Request signatures”Ketvia signs the requests it sends to a Kit’s endpoints (events, interactivity and commands, in a later phase). The scheme is already in the SDK so you can build and test against it:
Ketvia-Request-Timestamp: unix seconds.Ketvia-Signature:v1=<hex HMAC-SHA256(secret, "v1:" + timestamp + ":" + rawBody)>. While a signing secret is being rotated, the header carries several comma-separatedv1=values.- Requests older (or further in the future) than 300 seconds must be rejected.
import { verifySignature } from '@ketvia/kit';
const ok = verifySignature({ secrets: [currentSecret, previousSecret], // or secret: currentSecret timestamp: request.headers['ketvia-request-timestamp'], signature: request.headers['ketvia-signature'], rawBody, // the exact bytes received, before JSON parsing // toleranceSeconds: 300, // the default});if (!ok) return reply.code(401).send();verifySignature compares in constant time and returns false for malformed input. signRequest(secret, timestamp, rawBody) produces a v1= value, which is useful in tests.