Skip to content

SDK Migration Guide

Migrating your extension to use the official Kiket SDKs provides better authentication handling, automatic telemetry, secret management, and future-proof compatibility with platform updates.

Why Migrate?

Before (Manual) After (SDK)
Manual HMAC signature verification Automatic verification built-in
Hardcoded secrets in ENV Multi-tenant secret resolution
Manual API client setup Pre-configured client with runtime tokens
No telemetry Automatic telemetry and error tracking
Custom error handling Standardized responses and retries

Migration by Language

Ruby: From Sinatra to Ruby SDK

Before (Manual Sinatra):

require 'sinatra'
require 'json'
require 'net/http'

post '/events/issue-created' do
  # Manual signature verification
  signature = request.env['HTTP_X_KIKET_SIGNATURE']
  body = request.body.read
  expected = OpenSSL::HMAC.hexdigest('sha256', ENV['WEBHOOK_SECRET'], body)
  halt 401 unless Rack::Utils.secure_compare(signature, expected)

  payload = JSON.parse(body)

  # Manual API calls with hardcoded token
  uri = URI("https://kiket.dev/api/v1/issues/#{payload['issue']['id']}/comments")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri)
  request['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
  request['Content-Type'] = 'application/json'
  request.body = { body: 'Processed!' }.to_json

  http.request(request)

  status 204
end

After (Ruby SDK):

require 'kiket_sdk'

sdk = KiketSDK.new

sdk.webhook('issue.created', version: 'v1') do |payload, context|
  # Signature verification is automatic
  # Runtime token is automatically used for API calls

  context[:endpoints].comment(
    payload['issue']['id'],
    body: 'Processed!'
  )

  { ok: true }
end

sdk.run!(port: 9292)

Python: From Flask to Python SDK

Before (Manual Flask):

from flask import Flask, request
import hmac
import hashlib
import os
import requests

app = Flask(__name__)

@app.route('/events/issue-created', methods=['POST'])
def handle_issue():
    # Manual signature verification
    signature = request.headers.get('X-Kiket-Signature')
    body = request.get_data()
    expected = hmac.new(
        os.environ['WEBHOOK_SECRET'].encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return '', 401

    payload = request.get_json()

    # Manual API calls
    requests.post(
        f"https://kiket.dev/api/v1/issues/{payload['issue']['id']}/comments",
        headers={'Authorization': f"Bearer {os.environ['API_TOKEN']}"},
        json={'body': 'Processed!'}
    )

    return '', 204

After (Python SDK):

from kiket_sdk import KiketSDK

sdk = KiketSDK()

@sdk.webhook('issue.created', version='v1')
def handle_issue(payload, context):
    # Signature verification is automatic
    # Runtime token is automatically used for API calls

    context.endpoints.comment(
        payload['issue']['id'],
        body='Processed!'
    )

    return {'ok': True}

sdk.run(port=9292)

Node.js: From Express to Node SDK

Before (Manual Express):

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.raw({ type: 'application/json' }));

app.post('/events/issue-created', async (req, res) => {
  // Manual signature verification
  const signature = req.headers['x-kiket-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(req.body)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send();
  }

  const payload = JSON.parse(req.body.toString());

  // Manual API calls
  await fetch(
    `https://kiket.dev/api/v1/issues/${payload.issue.id}/comments`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ body: 'Processed!' }),
    }
  );

  res.status(204).send();
});

After (Node SDK):

import { KiketSDK } from '@kiket/sdk';

const sdk = new KiketSDK();

sdk.webhook('issue.created', { version: 'v1' }, async (payload, context) => {
  // Signature verification is automatic
  // Runtime token is automatically used for API calls

  await context.endpoints.comment(payload.issue.id, {
    body: 'Processed!',
  });

  return { ok: true };
});

sdk.run({ port: 9292 });

Scope Checks Migration

The SDK provides built-in scope verification for OAuth 2.0 permission enforcement.

Specify required scopes when registering handlers:

sdk.register('issue.created', version: 'v1', required_scopes: %w[issues:read]) do |payload, context|
  # Handler only executes if installation has issues:read scope
  # Returns 403 automatically if scope is missing
end

Runtime Scope Checks

For conditional scope requirements, use context[:require_scopes]:

sdk.register('workflow.action', version: 'v1') do |payload, context|
  action = payload['action']

  if action == 'create_issue'
    context[:require_scopes].call('issues:write')
    # Creates issue...
  elsif action == 'add_comment'
    context[:require_scopes].call('comments:write')
    # Adds comment...
  end
end

Available Scopes

Scope Description
issues:read Read issue data
issues:write Create and update issues
comments:write Add comments to issues
projects:read Read project metadata
users:read Read user profiles
secrets:read Access configured secrets
custom_data:read Read custom data modules
custom_data:write Write to custom data modules

JWT Runtime Token Authentication

The SDK automatically verifies JWT runtime tokens included in webhook payloads. This provides:

  • Organization context: Tokens include org_id, proj_id, ext_id
  • Scope enforcement: Token scopes are validated against handler requirements
  • Automatic refresh: Kiket manages token lifecycle

How It Works

  1. Kiket generates a short-lived JWT when dispatching webhooks
  2. Token is included in payload.authentication.runtime_token
  3. SDK verifies token signature against Kiket's public key
  4. Verified claims (org_id, scopes) are available in context[:auth]
sdk.register('notify.send', version: 'v1') do |payload, context|
  # Access verified JWT claims
  org_id = context[:auth][:org_id]
  project_id = context[:auth][:proj_id]
  scopes = context[:auth][:scopes]

  # Runtime token is automatically used for API calls
  context[:endpoints].log_event('notification.sent', { org_id: org_id })
end

Migrating from API Keys

Before (Static API Key):

# Same key for all organizations
token = ENV['KIKET_API_KEY']

After (Runtime Token):

sdk.register('event.handle', version: 'v1') do |payload, context|
  # Runtime token scoped to specific org/project
  # Automatically used by context[:endpoints]
  context[:endpoints].create_issue(project_id: context[:auth][:proj_id], title: 'New Issue')
end

Secret Access Migration

The SDK provides a unified way to access secrets that supports multi-tenant deployments.

Old Pattern (Single-Tenant)

# Hardcoded ENV access - same secret for all organizations
slack_token = ENV['SLACK_BOT_TOKEN']

New Pattern (Multi-Tenant with Fallback)

sdk.webhook('notify.send', version: 'v1') do |payload, context|
  # Checks payload secrets first (per-org), falls back to ENV (extension default)
  slack_token = context[:secret].call('SLACK_BOT_TOKEN')

  # Use the token...
end

Resolution order: 1. payload['secrets']['SLACK_BOT_TOKEN'] - Per-organization/project secret from setup wizard 2. ENV['SLACK_BOT_TOKEN'] - Extension default (Cloud Run environment variable)

This pattern enables: - Multi-tenant: Each customer can configure their own Slack workspace - Fallback: Extensions work with a default configuration if not overridden - Zero code changes: Same code works for both scenarios

Manifest Updates

Ensure your manifest uses the SDK-compatible format:

model_version: "1.0"
extension:
  id: com.yourcompany.extension
  name: My Extension
  version: 2.0.0    # Bump version when migrating
  delivery: http

  callback:
    url: ${EXTENSION_BASE_URL}
    secret: env.KIKET_WEBHOOK_SECRET

  # Define secrets that can be configured per-organization
  configuration:
    SLACK_BOT_TOKEN:
      type: string
      secret: true
      required: true
      label: Slack Bot Token
      description: Your Slack app's bot token (xoxb-...)

Testing After Migration

  1. Run the extension locally:

    bundle exec ruby app.rb  # Ruby
    python app.py            # Python
    npm start                # Node.js
    

  2. Use the CLI replay tool:

    kiket extensions replay \
      --payload fixtures/issue_created.json \
      --url http://localhost:9292
    

  3. Verify in sandbox project:

  4. Install extension in a sandbox project
  5. Trigger events and check Activity logs
  6. Confirm API calls are using runtime tokens

Checklist

  • Install the SDK for your language
  • Replace manual signature verification with SDK handlers
  • Add required_scopes to all handler registrations
  • Use context[:require_scopes] for conditional scope checks
  • Replace manual API client with context.endpoints
  • Update secret access to use context[:secret].call() pattern
  • Remove hardcoded API keys - use JWT runtime tokens instead
  • Update manifest to SDK-compatible format
  • Bump extension version in manifest
  • Test locally with replay fixtures
  • Test in sandbox project
  • Update README with SDK usage examples

Getting Help