# Slootly — Complete SDK & API Reference for LLMs # https://slootly.dev # Version: 0.1 # This file contains everything needed to integrate Slootly into a messaging bot. ## What is Slootly? Slootly is a cross-platform subscription infrastructure for messaging bots. It provides a single SDK that works across Telegram, Discord, WhatsApp, Slack, and custom platforms. Revenue settles directly to the developer's bank account via Stripe Connect. No crypto intermediary, no token conversion, no holding periods. Core components: - SDK: Check subscription status by platform user ID (TypeScript and Python) - Hosted Checkout: Payment page powered by Stripe, linked from bot messages - Dashboard: Plan management, analytics, Stripe Connect onboarding - Webhooks: Real-time subscription event delivery to developer endpoints ## Installation ```bash # TypeScript / Node.js npm install @slootly/sdk # Python pip install slootly ``` ## TypeScript SDK — Complete Reference ### Initialization ```typescript import { Slootly } from '@slootly/sdk'; // Simple — API key + project slug const gate = new Slootly('sk_live_your_api_key', { projectSlug: 'my-project' }); // Full configuration const gate = new Slootly({ apiKey: 'sk_live_your_api_key', // Required. Starts with sk_live_ or sk_test_ projectSlug: 'my-project', // Required. Your project slug baseUrl: 'https://api.slootly.dev/v1', // Optional. API base URL cacheTtlMs: 300_000, // Optional. Cache TTL in ms (default: 5 minutes) enableJwtVerification: true, // Optional. JWT offline verification (default: true) timeout: 5_000, // Optional. HTTP timeout in ms (default: 5s) checkoutBaseUrl: 'https://pay.slootly.dev', // Optional. Checkout base URL fetch: customFetch, // Optional. Custom fetch for edge runtimes }); ``` ### gate.check(userId, platform) → Promise Check subscription status for a user. This is the primary SDK method. Lookup order: 1. In-memory cache (instant) 2. JWT offline verification (sub-millisecond) 3. API call to Slootly (network latency) Results are cached. Background refresh triggers at cache half-life. ```typescript const sub = await gate.check('123456789', 'telegram'); // Subscription properties: sub.active // boolean — true if subscribed sub.plan // string | null — plan slug, e.g. 'pro' sub.planId // string | null — plan ID sub.status // 'active' | 'trialing' | 'past_due' | 'canceled' | 'expired' | 'none' sub.currentPeriodStart // Date | null — billing period start sub.currentPeriodEnd // Date | null — billing period end sub.cancelAtPeriodEnd // boolean — will cancel at period end? sub.trialEnd // Date | null — trial end date sub.subscriberId // string | null — Slootly subscriber ID sub.platforms // Platform[] — linked platforms, e.g. ['telegram', 'discord'] sub.metadata // Record — developer metadata ``` Parameters: - userId: string — Platform-specific user ID as string (e.g., Telegram from.id.toString()) - platform: 'telegram' | 'discord' | 'whatsapp' | 'slack' | 'custom' Throws: - ValidationError if userId or platform is empty - AuthenticationError if API key is invalid - NetworkError if API is unreachable (falls back to cached JWT if available) ### gate.checkoutUrl(userId, planSlug, platform, options?) → string Generate a checkout URL for the hosted payment page. Synchronous — no API call. ```typescript // Basic const url = gate.checkoutUrl('123456789', 'pro', 'telegram'); // => https://pay.slootly.dev/checkout/pro?uid=123456789&platform=telegram // With options const url = gate.checkoutUrl('123456789', 'pro', 'telegram', { successUrl: 'https://t.me/mybot', // redirect after success cancelUrl: 'https://t.me/mybot', // redirect on cancel email: 'user@example.com', // pre-fill email metadata: { referral: 'homepage' }, // attach to subscription }); ``` Parameters: - userId: string — Platform-specific user ID - planSlug: string — Plan slug (e.g., 'pro', 'enterprise') - platform: Platform — Platform name - options.successUrl: string (optional) — Success redirect URL - options.cancelUrl: string (optional) — Cancel redirect URL - options.email: string (optional) — Pre-fill customer email - options.metadata: Record (optional) — Extra metadata ### gate.getPlans() → Promise Fetch all active plans for the project. ```typescript const plans = await gate.getPlans(); // Plan properties: plans[0].id // string — plan ID plans[0].name // string — display name, e.g. 'Pro' plans[0].slug // string — URL-safe slug, e.g. 'pro' plans[0].description // string | null — description plans[0].amount // number — price in cents (999 = $9.99) plans[0].currency // string — currency code, e.g. 'usd' plans[0].interval // 'day' | 'week' | 'month' | 'year' plans[0].intervalCount // number — e.g., 1 for monthly, 3 for quarterly plans[0].trialDays // number — free trial days (0 if none) plans[0].features // string[] — feature list plans[0].metadata // Record ``` ### gate.cancelSubscription(subscriberId) → Promise Cancel a subscription. It remains active until the current billing period ends. ```typescript const sub = await gate.check('123456789', 'telegram'); if (sub.subscriberId) { await gate.cancelSubscription(sub.subscriberId); // sub stays active until sub.currentPeriodEnd } ``` ### gate.onEvent(handler) → WebhookMiddleware Create Express/Connect-compatible webhook middleware. ```typescript import express from 'express'; const app = express(); app.post('/webhooks/slootly', gate.onEvent((event) => { // event.id — unique event ID // event.type — event type string // event.createdAt — Date // event.projectId — project ID // event.data — WebhookEventData switch (event.type) { case 'subscription.created': console.log('New sub:', event.data.subscriberId, event.data.plan?.slug); break; case 'subscription.renewed': console.log('Renewed:', event.data.subscriberId); break; case 'subscription.canceled': console.log('Canceled:', event.data.subscriberId); break; case 'subscription.updated': console.log('Updated:', event.data.subscriberId); break; case 'payment.failed': console.log('Failed:', event.data.subscriberId); break; case 'test.ping': console.log('Test event received'); break; } })); ``` ### gate.verifyWebhook(payload, signature, secret) → WebhookEvent Manually verify webhook signature and parse the event. ```typescript const event = gate.verifyWebhook( rawBody, // string — raw request body req.headers['slootly-signature'] as string, // string — Slootly-Signature header 'whsec_your_webhook_secret' // string — webhook secret ); ``` Throws WebhookVerificationError if signature is invalid. ### Cache Management ```typescript gate.clearCache(); // Invalidate all cached subscriptions gate.destroy(); // Clean up SDK resources (timers, caches). Call on shutdown. ``` ## Python SDK — Complete Reference ### Initialization ```python from slootly import Slootly # Simple — API key + project slug gate = Slootly("sk_live_your_api_key", project_slug="my-project") # Full configuration gate = Slootly( "sk_live_your_api_key", project_slug="my-project", # required base_url="https://api.slootly.dev/v1", # optional cache_ttl=300, # seconds, default 5 minutes offline_verification=True, # JWT verification, default True timeout=5.0, # seconds, default 5 checkout_base_url="https://pay.slootly.dev", # optional ) # Async context manager (recommended) async with Slootly("sk_live_...", project_slug="my-project") as gate: sub = await gate.check(user_id, "telegram") ``` ### gate.check(user_id, platform) → Subscription ```python sub = await gate.check("123456789", "telegram") # Subscription properties (dataclass, frozen=True): sub.active # bool sub.plan # Optional[str] — e.g. "pro" sub.plan_id # Optional[str] sub.status # str — "active", "trialing", "past_due", "canceled", "expired", "none" sub.current_period_start # Optional[datetime] sub.current_period_end # Optional[datetime] sub.cancel_at_period_end # bool sub.trial_end # Optional[datetime] sub.subscriber_id # Optional[str] sub.platforms # List[str] sub.metadata # Dict[str, Any] ``` ### gate.checkout_url(user_id, plan_slug, platform, **kwargs) → str ```python # Basic url = gate.checkout_url("123456789", "pro", "telegram") # With options (keyword-only arguments) url = gate.checkout_url( "123456789", "pro", "telegram", success_url="https://t.me/mybot", cancel_url="https://t.me/mybot", email="user@example.com", metadata={"referral": "homepage"}, ) ``` ### gate.get_plans() → List[Plan] ```python plans = await gate.get_plans() # Plan properties (dataclass, frozen=True): plans[0].id # str plans[0].name # str plans[0].slug # str plans[0].description # Optional[str] plans[0].amount # int — cents (999 = $9.99) plans[0].currency # str — e.g. "usd" plans[0].interval # str — "day", "week", "month", "year" plans[0].interval_count # int plans[0].trial_days # int plans[0].features # List[str] plans[0].metadata # Dict[str, Any] ``` ### gate.cancel_subscription(subscriber_id) → None ```python sub = await gate.check("123456789", "telegram") if sub.subscriber_id: await gate.cancel_subscription(sub.subscriber_id) ``` ### gate.verify_webhook(payload, signature, secret) → WebhookEvent ```python event = gate.verify_webhook(payload_str, signature_header, "whsec_...") # WebhookEvent properties: event.id # str — unique event ID event.type # str — "subscription.created", etc. event.created_at # datetime event.project_id # str event.data # WebhookEventData # WebhookEventData properties: event.data.subscriber_id # Optional[str] event.data.subscription_id # Optional[str] event.data.plan # Optional[Dict[str, str]] — {"id", "name", "slug"} event.data.status # Optional[str] event.data.platform # Optional[str] event.data.platform_user_id # Optional[str] event.data.current_period_start # Optional[str] event.data.current_period_end # Optional[str] event.data.cancel_at_period_end # Optional[bool] event.data.metadata # Optional[Dict[str, Any]] ``` ### Cleanup ```python gate.clear_cache() # Invalidate all cached data await gate.close() # Clean up resources (HTTP sessions, caches) ``` ## REST API Reference Base URL: https://api.slootly.dev/v1 Authentication: Bearer token with API key in Authorization header ### GET /v1/subscriptions/check — Check Subscription Check subscription status for a platform user. Uses query parameters. ```bash curl -H "Authorization: Bearer sk_live_..." \ "https://api.slootly.dev/v1/subscriptions/check?user_id=123456789&platform=telegram" ``` Response: ```json { "active": true, "plan": "pro", "plan_id": "plan_abc123", "status": "active", "current_period_start": "2026-03-01T00:00:00Z", "current_period_end": "2026-04-01T00:00:00Z", "cancel_at_period_end": false, "trial_end": null, "subscriber_id": "sub_xyz789", "platforms": ["telegram", "discord"], "metadata": {}, "token": "eyJhbGciOiJFZERTQSJ9..." } ``` ### GET /v1/projects/{project_id}/plans — List Plans Fetch all active plans for a project. Public endpoint. ```bash curl "https://api.slootly.dev/v1/projects/{project_id}/plans" ``` Response: ```json { "data": [ { "id": "plan_abc123", "name": "Pro", "slug": "pro", "description": "Unlimited access", "amount": 999, "currency": "usd", "interval": "month", "interval_count": 1, "trial_days": 7, "features": ["Unlimited queries", "Priority support"], "metadata": {} } ], "total": 1 } ``` ### GET /v1/checkout/{project_slug}/{plan_slug} — Get Checkout Info Fetch project and plan information for the hosted checkout page. ```bash curl "https://api.slootly.dev/v1/checkout/my-project/pro" ``` ### POST /v1/checkout/sessions — Create Checkout Session Create a Stripe Checkout session. Returns a redirect URL. ```bash curl -X POST https://api.slootly.dev/v1/checkout/sessions \ -H "Content-Type: application/json" \ -d '{ "project_id": "uuid", "plan_id": "uuid", "platform": "telegram", "platform_user_id": "123456789", "success_url": "https://pay.slootly.dev/checkout/success?session_id={CHECKOUT_SESSION_ID}", "cancel_url": "https://pay.slootly.dev/checkout/cancel" }' ``` ### POST /v1/subscriptions/{subscription_id}/cancel — Cancel Subscription Cancel a subscription at the end of the current billing period. ```bash curl -X POST https://api.slootly.dev/v1/subscriptions/{subscription_id}/cancel \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"cancel_at_period_end": true}' ``` ### CRUD /v1/projects/{project_id}/webhook-endpoints — Webhook Endpoints Manage webhook endpoints for receiving subscription events. ```bash # Create webhook endpoint curl -X POST https://api.slootly.dev/v1/projects/{project_id}/webhook-endpoints \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/webhooks", "events": ["subscription.created", "payment.failed"]}' # List webhook endpoints curl https://api.slootly.dev/v1/projects/{project_id}/webhook-endpoints \ -H "Authorization: Bearer " ``` ### GET /v1/projects/{project_id}/.well-known/jwks.json — JWKS Public Keys Returns Ed25519 public keys for offline JWT subscription verification. ```bash curl "https://api.slootly.dev/v1/projects/{project_id}/.well-known/jwks.json" ``` ## Webhook Events Event types: - subscription.created — new subscription after first payment - subscription.renewed — recurring payment succeeded - subscription.updated — plan change or metadata update - subscription.canceled — subscription canceled - payment.failed — card declined or payment error - test.ping — test event from dashboard Payload: ```json { "id": "evt_abc123", "type": "subscription.created", "created_at": "2026-03-19T12:00:00Z", "project_id": "proj_xyz", "data": { "subscriber_id": "sub_abc123", "subscription_id": "subs_def456", "plan": { "id": "plan_abc", "name": "Pro", "slug": "pro" }, "status": "active", "platform": "telegram", "platform_user_id": "123456789", "current_period_start": "2026-03-19T00:00:00Z", "current_period_end": "2026-04-19T00:00:00Z", "cancel_at_period_end": false, "metadata": {} } } ``` Signature verification: - Header: Slootly-Signature: t=timestamp,v1=hmac_sha256_hex - Signed payload: `${timestamp}.${rawBody}` - Algorithm: HMAC-SHA256 with webhook secret (whsec_...) - Timestamp tolerance: 5 minutes Retry policy: 5 retries with exponential backoff (30s, 2m, 10m, 1h, 4h). ## Complete Integration Examples ### Telegram Bot — TypeScript (node-telegram-bot-api) ```typescript import TelegramBot from 'node-telegram-bot-api'; import { Slootly } from '@slootly/sdk'; const bot = new TelegramBot(process.env.BOT_TOKEN!, { polling: true }); const gate = new Slootly(process.env.SLOOTLY_API_KEY!, { projectSlug: 'my-project' }); // Feature gate: premium content bot.on('message', async (msg) => { const userId = msg.from!.id.toString(); const chatId = msg.chat.id; const sub = await gate.check(userId, 'telegram'); if (sub.active && sub.plan === 'pro') { bot.sendMessage(chatId, 'Here is your premium content!'); } else { const url = gate.checkoutUrl(userId, 'pro', 'telegram'); bot.sendMessage(chatId, 'Upgrade to Pro for unlimited access:', { reply_markup: { inline_keyboard: [[{ text: 'Subscribe — $9.99/mo', url }]], }, }); } }); // Webhook handler for real-time updates import express from 'express'; const app = express(); app.post('/webhooks/slootly', express.raw({ type: 'application/json' }), (req, res) => { const event = gate.verifyWebhook( req.body.toString(), req.headers['slootly-signature'] as string, process.env.WEBHOOK_SECRET! ); console.log(`Event: ${event.type} for ${event.data.subscriberId}`); res.json({ received: true }); }); app.listen(3000); ``` ### Telegram Bot — Python (python-telegram-bot) ```python from telegram import Update from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters from slootly import Slootly gate = Slootly("sk_live_your_api_key", project_slug="my-project") async def premium_command(update: Update, context): user_id = str(update.effective_user.id) sub = await gate.check(user_id, "telegram") if sub.active and sub.plan == "pro": await update.message.reply_text("Welcome, Pro user!") else: url = gate.checkout_url(user_id, "pro", "telegram") await update.message.reply_text(f"Upgrade to Pro: {url}") app = ApplicationBuilder().token("BOT_TOKEN").build() app.add_handler(CommandHandler("premium", premium_command)) app.run_polling() ``` ### Discord Bot — TypeScript (discord.js) ```typescript import { Client, GatewayIntentBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { Slootly } from '@slootly/sdk'; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const gate = new Slootly('sk_live_...', { projectSlug: 'my-project' }); client.on('interactionCreate', async (interaction) => { if (!interaction.isChatInputCommand() || interaction.commandName !== 'premium') return; const userId = interaction.user.id; const sub = await gate.check(userId, 'discord'); if (sub.active) { await interaction.reply({ content: 'Premium content!', ephemeral: true }); } else { const url = gate.checkoutUrl(userId, 'pro', 'discord'); const row = new ActionRowBuilder().addComponents( new ButtonBuilder().setLabel('Subscribe $9.99/mo').setStyle(ButtonStyle.Link).setURL(url) ); await interaction.reply({ content: 'Get Pro:', components: [row], ephemeral: true }); } }); client.login('DISCORD_TOKEN'); ``` ### FastAPI Webhook Handler — Python ```python from fastapi import FastAPI, Request, Header from slootly import Slootly app = FastAPI() gate = Slootly("sk_live_...", project_slug="my-project") @app.post("/webhooks/slootly") async def webhook(request: Request, slootly_signature: str = Header(..., alias="Slootly-Signature")): payload = (await request.body()).decode() event = gate.verify_webhook(payload, slootly_signature, "whsec_...") if event.type == "subscription.created": # Grant access pass elif event.type == "subscription.canceled": # Revoke access at period end pass elif event.type == "payment.failed": # Notify user pass return {"ok": True} ``` ### Grammy.js (Telegram) ```typescript import { Bot } from 'grammy'; import { Slootly } from '@slootly/sdk'; const bot = new Bot(process.env.BOT_TOKEN!); const gate = new Slootly('sk_live_...', { projectSlug: 'my-project' }); bot.command('start', async (ctx) => { const userId = ctx.from!.id.toString(); const sub = await gate.check(userId, 'telegram'); if (sub.active) { await ctx.reply(`Welcome back! Your ${sub.plan} plan is active.`); } else { const url = gate.checkoutUrl(userId, 'pro', 'telegram'); await ctx.reply('Get started with Pro:', { reply_markup: { inline_keyboard: [[{ text: 'Subscribe', url }]] }, }); } }); bot.start(); ``` ### Slack Bolt ```typescript import { App } from '@slack/bolt'; import { Slootly } from '@slootly/sdk'; const gate = new Slootly('sk_live_...', { projectSlug: 'my-project' }); const app = new App({ token: 'xoxb-...', signingSecret: '...' }); app.command('/premium', async ({ command, ack, respond }) => { await ack(); const sub = await gate.check(command.user_id, 'slack'); if (sub.active) { await respond('You have Pro access!'); } else { const url = gate.checkoutUrl(command.user_id, 'pro', 'slack'); await respond({ blocks: [{ type: 'section', text: { type: 'mrkdwn', text: 'Unlock Pro features:' }, accessory: { type: 'button', text: { type: 'plain_text', text: 'Subscribe' }, url }, }], }); } }); ``` ## Error Handling ### TypeScript ```typescript import { ValidationError, AuthenticationError, NetworkError } from '@slootly/sdk'; try { const sub = await gate.check(userId, 'telegram'); } catch (err) { if (err instanceof ValidationError) { // Bad input: empty userId, invalid platform } else if (err instanceof AuthenticationError) { // Invalid API key } else if (err instanceof NetworkError) { // API unreachable (SDK falls back to cached JWT if available) } } ``` ### Python ```python from slootly.errors import ValidationError, AuthenticationError, NetworkError try: sub = await gate.check(user_id, "telegram") except ValidationError: pass # Bad input except AuthenticationError: pass # Invalid API key except NetworkError: pass # API unreachable, falls back to cached JWT ``` ## JWT Offline Verification Slootly uses Ed25519-signed JWT tokens for offline subscription verification. - Tokens are returned with every /check API response - SDK caches tokens and verifies them locally (sub-millisecond) - JWKS endpoint: GET /v1/projects/{project_id}/.well-known/jwks.json - Key rotation handled automatically by SDK - Token expiry matches cache TTL (default 5 minutes) ## Types Reference ### TypeScript ```typescript type Platform = 'telegram' | 'discord' | 'whatsapp' | 'slack' | 'custom'; type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'expired' | 'none'; type BillingInterval = 'day' | 'week' | 'month' | 'year'; type WebhookEventType = 'subscription.created' | 'subscription.renewed' | 'subscription.updated' | 'subscription.canceled' | 'payment.failed' | 'test.ping'; ``` ### Python ```python Platform = Literal["telegram", "discord", "whatsapp", "slack", "custom"] SubscriptionStatus = Literal["active", "trialing", "past_due", "canceled", "expired", "none"] BillingInterval = Literal["day", "week", "month", "year"] ``` ## Key Design Decisions - API key and project slug are the only required configuration - Single import, no peer dependencies - Async/await everywhere (no callbacks) - TypeScript-first with complete type definitions - Platform-agnostic: same code for all messaging platforms - checkout URLs generated locally (synchronous, no API call) - JWT offline verification eliminates Slootly as a runtime dependency - Stripe Connect Standard: Slootly never holds developer funds ## Links - Website: https://slootly.dev - Docs: https://slootly.dev/docs - TypeScript SDK: https://slootly.dev/docs/typescript - Python SDK: https://slootly.dev/docs/python - API Reference: https://slootly.dev/docs/api - Webhooks: https://slootly.dev/docs/webhooks - GitHub: https://github.com/slootly - npm: https://www.npmjs.com/package/@slootly/sdk - PyPI: https://pypi.org/project/slootly/