Build and run Kiket extensions with a batteries-included, strongly-typed Ruby toolkit.
- 🔌 Webhook decorators – define handlers with
sdk.webhook("issue.created", version: "v1"). - 🔐 Transparent authentication – HMAC verification for inbound payloads, workspace-token client for outbound calls.
- 🔑 Secret manager – list, fetch, rotate, and delete extension secrets stored in Google Secret Manager.
- 🌐 Built-in Sinatra app – serve extension webhooks locally or in production without extra wiring.
- 🔁 Version-aware routing – register multiple handlers per event and propagate version headers on outbound calls.
- 📦 Manifest-aware defaults – automatically loads
extension.yaml/manifest.yaml, applies configuration defaults, and hydrates secrets fromKIKET_SECRET_*environment variables. - 📇 Custom data helper – call
/api/v1/ext/custom_data/...withcontext[:endpoints].custom_data(project_id)using the configured extension API key. - 📉 Rate-limit helper – inspect
/api/v1/ext/rate_limitbefore launching heavy automation bursts. - 🧱 Typed & documented – designed for Ruby 3.2+ with rich documentation.
- 📊 Telemetry & feedback hooks – capture handler duration/success metrics automatically.
gem install kiket-sdk# main.rb
require 'kiket_sdk'
sdk = KiketSDK.new(
webhook_secret: 'sh_123',
workspace_token: 'wk_test',
extension_id: 'com.example.marketing',
extension_version: '1.0.0'
)
# Register webhook handler (v1)
sdk.register('issue.created', version: 'v1') do |payload, context|
summary = payload['issue']['title']
puts "Event version: #{context[:event_version]}"
context[:endpoints].log_event('issue.created', summary: summary)
context[:secrets].set('WEBHOOK_TOKEN', 'abc123')
{ ok: true }
end
# Register webhook handler (v2)
sdk.register('issue.created', version: 'v2') do |payload, context|
summary = payload['issue']['title']
context[:endpoints].log_event('issue.created', summary: summary, schema: 'v2')
{ ok: true, version: context[:event_version] }
end
sdk.run!(host: '0.0.0.0', port: 8080)When your manifest includes custom_data.permissions, the SDK automatically uses the runtime token provided in the webhook payload for API calls via context[:client]:
sdk.register('issue.created', version: 'v1') do |payload, context|
project_id = payload.dig('issue', 'project_id')
custom_data = context[:endpoints].custom_data(project_id)
list = custom_data.list('com.example.crm.contacts', 'automation_records', limit: 10, filters: { status: 'active' })
custom_data.create('com.example.crm.contacts', 'automation_records', {
email: 'lead@example.com',
metadata: { source: 'webhook' }
})
{ synced: list['data'].size }
endYou can also query live SLA alerts from within webhook handlers:
sdk.register('workflow.sla_status', version: 'v1') do |payload, context|
project_id = payload.dig('issue', 'project_id')
sla_events = context[:endpoints].sla_events(project_id)
events = sla_events.list(state: 'imminent', limit: 5)
next { ok: true } if events['data'].empty?
first = events['data'].first
context[:endpoints].secrets # available if you need per-alert secrets
context[:endpoints].log_event('sla.warning', issue_id: first['issue_id'], state: first['state'])
{ acknowledged: true }
endKIKET_WEBHOOK_SECRET– Webhook HMAC secret for signature verificationKIKET_WORKSPACE_TOKEN– Workspace token for API authenticationKIKET_BASE_URL– Kiket API base URL (defaults tohttps://kiket.dev)KIKET_SDK_TELEMETRY_URL– Telemetry reporting endpoint (optional)KIKET_SDK_TELEMETRY_OPTOUT– Set to1to disable telemetryKIKET_SECRET_*– Secret overrides (e.g.,KIKET_SECRET_API_KEY)
Create an extension.yaml or manifest.yaml file:
id: com.example.marketing
version: 1.0.0
delivery_secret: sh_production_secret
settings:
- key: API_KEY
secret: true
- key: MAX_RETRIES
default: 3
- key: TIMEOUT_MS
default: 5000Main SDK class for building extensions.
sdk = KiketSDK.new(
webhook_secret: String,
workspace_token: String,
base_url: String,
settings: Hash,
extension_id: String,
extension_version: String,
manifest_path: String,
auto_env_secrets: Boolean,
telemetry_enabled: Boolean,
feedback_hook: Proc,
telemetry_url: String
)Methods:
sdk.register(event, version:, &handler)– Register a webhook handlersdk.webhook(event, version:)– Decorator for registering handlerssdk.run!(host:, port:)– Start the Sinatra server
Context hash passed to webhook handlers:
{
event: String,
event_version: String,
headers: Hash,
client: KiketSDK::Client,
endpoints: KiketSDK::Endpoints,
settings: Hash,
extension_id: String,
extension_version: String,
secrets: KiketSDK::Secrets,
secret: Proc, # Secret helper with payload-first fallback
auth: {
runtime_token: String, # Per-invocation API token
token_type: String, # Typically "runtime"
expires_at: String, # Token expiration timestamp
scopes: Array<String> # Granted API scopes
}
}The secret proc provides a simple way to retrieve secrets with automatic fallback:
# Checks payload secrets first (per-org config), falls back to ENV
slack_token = context[:secret].call("SLACK_BOT_TOKEN")
# Example usage
sdk.register('issue.created', version: 'v1') do |payload, context|
api_key = context[:secret].call("API_KEY")
raise "API_KEY not configured" unless api_key
# Use api_key...
{ ok: true }
endThe lookup order is:
- Payload secrets (per-org configuration from
payload["secrets"]) - Environment variables (extension defaults via
ENV)
This allows organizations to override extension defaults with their own credentials.
The Kiket platform sends a per-invocation runtime_token in each webhook payload. This token is automatically extracted and used for all API calls made through context[:client] and context[:endpoints]. The runtime token provides organization-scoped access and is preferred over static tokens.
sdk.register('issue.created', version: 'v1') do |payload, context|
# Access authentication context
puts "Token expires at: #{context[:auth][:expires_at]}"
puts "Scopes: #{context[:auth][:scopes].join(', ')}"
# API calls automatically use the runtime token
context[:endpoints].log_event('processed', { ok: true })
{ ok: true }
endExtensions can declare required scopes when registering handlers. The SDK will automatically check scopes before invoking the handler and return a 403 error if insufficient.
# Declare required scopes at registration time
sdk.register('issue.created', version: 'v1', required_scopes: ['issues.read', 'issues.write']) do |payload, context|
# Handler only executes if scopes are present
context[:endpoints].log_event('issue.processed', { id: payload['issue']['id'] })
{ ok: true }
end
# Or check scopes dynamically within the handler
sdk.register('workflow.triggered', version: 'v1') do |payload, context|
# Raises KiketSDK::ScopeError if scopes are missing
context[:require_scopes].call('workflows.execute', 'custom_data.write')
# Continue with scope-protected operations
context[:endpoints].custom_data(project_id).create(...)
{ ok: true }
endThe SDK provides helper methods for building properly formatted responses. These ensure your extension returns data in the format Kiket expects.
# Allow (success) response
KiketSDK.allow(message: 'Operation completed')
# => { status: 'allow', message: 'Operation completed', metadata: {} }
# Deny response
KiketSDK.deny(message: 'Invalid credentials')
# => { status: 'deny', message: 'Invalid credentials', metadata: {} }
# Pending response (for async operations)
KiketSDK.pending(message: 'Awaiting approval')
# => { status: 'pending', message: 'Awaiting approval', metadata: {} }Output fields allow your extension to expose generated data (like email addresses, URLs, or status info) in the extension configuration UI. Users can see and copy these values after setup.
sdk.register('extension.testConnection', version: 'v1') do |payload, context|
# Perform setup (e.g., create Mailjet parse route)
route = create_parse_route(webhook_url)
# Return success with output fields
KiketSDK.allow(
message: 'Successfully configured',
data: { route_id: route.id },
output_fields: {
'inbound_email' => route.email,
'connection_status' => 'active'
}
)
endOutput fields are declared in your extension manifest:
extension:
output_fields:
inbound_email:
label: Inbound Email
description: Send emails to this address to create issues
type: copyable
icon: envelope
connection_status:
label: Connection Status
type: badgeSupported output field types:
copyable- Text field with copy button (default)url- Clickable URL with copy buttoncode- Monospace code blockbadge- Status indicator with colorstatus- Rich status with icon
The SDK includes test helpers:
require 'kiket_sdk'
require 'rack/test'
RSpec.describe 'My webhook handler' do
include Rack::Test::Methods
let(:sdk) do
KiketSDK.new(webhook_secret: 'test-secret')
end
def app
sdk
end
it 'handles issue.created event' do
sdk.register('issue.created', version: 'v1') do |payload, context|
{ processed: payload['issue']['id'] }
end
payload = { issue: { id: '123', title: 'Test Issue' } }
body = payload.to_json
sig_data = KiketSDK::Auth.generate_signature('test-secret', body)
post '/v/1/webhooks/issue.created',
body,
'CONTENT_TYPE' => 'application/json',
'HTTP_X_KIKET_SIGNATURE' => sig_data[:signature],
'HTTP_X_KIKET_TIMESTAMP' => sig_data[:timestamp]
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)['processed']).to eq('123')
end
endWhen you are ready to cut a release:
- Update the version in
kiket-sdk.gemspec. - Run the test suite (
bundle exec rspec) and linting (bundle exec rubocop). - Build gem:
gem build kiket-sdk.gemspec
- Commit and tag the release:
git add kiket-sdk.gemspec git commit -m "Bump Ruby SDK to v0.x.y" git tag ruby-v0.x.y git push --tags - GitHub Actions will automatically publish to GitHub Packages.
MIT
Before enqueueing expensive jobs, inspect the current extension window:
sandbox = sdk.register('automation.dispatch', version: 'v1') do |_payload, context|
limits = context[:endpoints].rate_limit
if limits['remaining'] < 5
context[:endpoints].notify(
'Rate limit warning',
"Only #{limits['remaining']} requests remain (reset in #{limits['reset_in']}s)",
'warning'
)
next({ deferred: true })
end
# Continue with the heavy work
{ ok: true }
end