Skip to content

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.

Terminal window
npm install @ketvia/kit
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.post returns { message, created }: created is false when an Idempotency-Key replay returned the stored message.
  • files.content returns { bytes, contentType }.

page is { cursor?, limit? }.

The client retries up to maxRetries times (default 2):

  • after 429, waiting for Retry-After;
  • after network errors and 5xx on calls that are safe to repeat: reads, and messages.post when you pass an idempotencyKey.

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;
}

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) {
// …
}
}

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.

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 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 }.

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-separated v1= 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.