Skip to content

Quickstart

In this guide you create a private Kit from a minimal manifest, install it in your workspace, add its bot to a channel, create a bot token, and post a message with Blocks: first with curl, then with the @ketvia/kit SDK. A two-minute incoming webhook variant follows at the end.

You need: admin rights in a Ketvia workspace, a channel you are a member of, and a terminal with curl. For the SDK step, Node.js 22 or later.

A manifest is the JSON document that says what your Kit is and what it may do. Save this as manifest.json and change slug, the names and the URLs:

manifest.json
{
"schemaVersion": 1,
"slug": "hello-acme",
"version": "0.1.0",
"displayName": { "en": "Hello Acme" },
"description": { "en": "Posts a first message through the Ketvia Web API." },
"developer": {
"name": "Acme Inc.",
"url": "https://acme.example",
"supportEmail": "[email protected]"
},
"privacyPolicyUrl": "https://acme.example/privacy",
"bot": {
"displayName": { "en": "Hello Acme" },
"scopes": ["conversations:read", "messages:write"]
},
"distribution": "private"
}
  • slug is global across Ketvia: pick one that is unlikely to be taken (lowercase letters, digits and hyphens, 2 to 40 characters, starting with a letter). Some slugs are reserved.
  • bot.scopes asks for two permissions: list the channels the Kit bot was added to (conversations:read) and post messages (messages:write). See Scopes.
  • Every URL must be https. Unknown keys are rejected. The full list of fields is in the manifest reference.
  1. In the Ketvia web app, open Admin → Connections (the “Kits and connections” page, https://ketvia.com/app/w/<workspaceId>/yonetim/baglantilar).

  2. Choose Create a private Kit, paste the manifest and confirm. Ketvia validates it and shows any problem with the field path.

  3. Choose Add a Kit and install your new Kit. The installation belongs to this workspace only.

  4. Open the Kit’s page and go to API access.

  5. Under channels, add the Kit bot to the channel you want to post in. You can only add channels you are a member of.

  6. Create a bot token. It starts with kbot_ and is shown once: copy it now. Ketvia stores only a hash of it, so a lost token cannot be shown again; revoke it and create a new one instead.

Keep the token out of source control. In your terminal:

Terminal window
export KETVIA_TOKEN='kbot_…'

GET /auth/test tells you what the token is and what it may do.

Terminal window
curl -s https://api.ketvia.com/api/v1/auth/test \
-H "Authorization: Bearer $KETVIA_TOKEN"
{
"ok": true,
"tokenType": "bot",
"workspaceId": "0b9f6c1e-2f4d-4c55-9a51-6f7f3c1d2e10",
"installationId": "5d1a8e7c-93b2-4f3e-8c0d-2a6b4e9f1c37",
"kitSlug": "hello-acme",
"botId": "9c4e2b7a-1d3f-4a6e-b8c5-0f2d7e6a9b14",
"userId": null,
"scopes": ["conversations:read", "messages:write"]
}

A 401 with invalid_token means the token was mistyped, revoked, or the Kit was uninstalled.

The Kit bot sees only the channels it was added to:

Terminal window
curl -s https://api.ketvia.com/api/v1/conversations \
-H "Authorization: Bearer $KETVIA_TOKEN"
{
"conversations": [
{
"id": "3e7c1a9d-5b2f-4d8e-a6c4-1f0b9e2d7a53",
"kind": "channel",
"visibility": "public",
"name": "deployments",
"topic": null,
"archived": false,
"createdAt": "2026-09-14T08:12:44.000Z"
}
],
"nextCursor": null
}
Terminal window
export CHANNEL_ID='3e7c1a9d-5b2f-4d8e-a6c4-1f0b9e2d7a53'

text is required: it is what notifications and search use. blocks is optional and renders as a card. The Idempotency-Key header makes a retry safe: sending the same key again within 24 hours returns the stored message instead of posting twice.

Terminal window
curl -s https://api.ketvia.com/api/v1/messages \
-H "Authorization: Bearer $KETVIA_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: quickstart-hello-1' \
-d @- <<EOF
{
"conversationId": "$CHANNEL_ID",
"text": "Hello from Hello Acme",
"blocks": [
{ "type": "header", "text": "Hello from a Kit" },
{
"type": "section",
"text": { "type": "mrkdwn", "text": "This message was posted with the *Ketvia Web API*." }
},
{
"type": "fields",
"fields": [
{ "label": "Kit", "value": "hello-acme" },
{ "label": "Version", "value": "0.1.0" }
]
},
{ "type": "context", "elements": [{ "type": "plain", "text": "Sent by the quickstart" }] }
]
}
EOF

The answer is 201 Created with the message (a replay with the same key answers 200 with the same message):

{
"message": {
"id": "7a2d9f4b-6c1e-4b3a-9e8d-5f0c2b1a7d64",
"conversationId": "3e7c1a9d-5b2f-4d8e-a6c4-1f0b9e2d7a53",
"author": { "kind": "bot", "id": "9c4e2b7a-1d3f-4a6e-b8c5-0f2d7e6a9b14" },
"text": "Hello from Hello Acme",
"blocks": [{ "type": "header", "text": "Hello from a Kit" }],
"threadParentId": null,
"createdAt": "2026-10-05T09:30:00.000Z",
"editedAt": null,
"deletedAt": null
}
}

(The blocks array is shortened here.) Open the channel in Ketvia: the message is there, posted by the Kit bot.

@ketvia/kit is a small TypeScript client for the Web API with no runtime dependencies.

Terminal window
npm install @ketvia/kit
hello.ts
import { KetviaClient, blocks } from '@ketvia/kit';
const ketvia = new KetviaClient({ token: process.env.KETVIA_TOKEN ?? '' });
const me = await ketvia.auth.test();
console.log(`Token for ${me.kitSlug} with scopes ${me.scopes.join(', ')}`);
const { conversations } = await ketvia.conversations.list();
const channel = conversations.find((conversation) => conversation.name === 'deployments');
if (!channel) throw new Error('Add the Kit bot to #deployments first');
const { message, created } = await ketvia.messages.post(
{
conversationId: channel.id,
text: 'Hello from Hello Acme',
blocks: [
blocks.header('Hello from a Kit'),
blocks.section(blocks.mrkdwn('This message was posted with the *Ketvia SDK*.')),
blocks.fields({ Kit: 'hello-acme', Version: '0.1.0' }),
blocks.context(['Sent by the quickstart']),
],
},
{ idempotencyKey: 'quickstart-hello-2' },
);
console.log(created ? `Posted ${message.id}` : `Already posted ${message.id}`);

Run it with any TypeScript runner, for example npx tsx hello.ts. Errors are thrown as KetviaApiError with status, code and correlationId; see SDK.

The two-minute variant: an incoming webhook

Section titled “The two-minute variant: an incoming webhook”

If all you need is to push notifications into one channel, skip tokens entirely and use an incoming webhook.

  1. Create a private Kit with this manifest (change the slug):

    manifest.json
    {
    "schemaVersion": 1,
    "slug": "acme-ci-notify",
    "version": "0.1.0",
    "displayName": { "en": "Acme CI" },
    "description": { "en": "Posts build results to a channel." },
    "developer": {
    "name": "Acme Inc.",
    "url": "https://acme.dev",
    "supportEmail": "[email protected]"
    },
    "privacyPolicyUrl": "https://acme.dev/privacy",
    "bot": { "displayName": { "en": "Acme CI" }, "scopes": ["incoming-webhook"] },
    "incomingWebhooks": { "maxPerInstallation": 5 },
    "distribution": "private"
    }
  2. Install it with Add a Kit, open its page, go to API access and create an incoming webhook for a channel. The Kit bot is added to that channel. The URL is shown once; it is the only credential, so store it like a password.

  3. Post to it:

    Terminal window
    curl -s -X POST "$KETVIA_WEBHOOK_URL" \
    -H 'Content-Type: application/json' \
    -d '{"text":"Build #42 passed","blocks":[{"type":"header","text":"Build #42 passed"}]}'

    The answer is {"ok":true} and the message appears in the channel.

Read Incoming webhooks for the rules and responses, and the ci-webhook sample for a GitHub Actions setup.