Documentation
Everything you need to integrate Tiny Owl, understand the API, and keep your applications observable.
Getting Started
Set up Tiny Owl in under 5 minutes. Install the SDK, create your first project, and start receiving events right away.
SDK Reference
Full reference for the @tiny-owl-kit/observability SDK. Covers initialisation options, severity levels, context payloads, and error handling.
Manual HMAC Integration
Integrate without the SDK using raw HTTP requests. Full code examples for cURL, Node.js, Python, PHP, Go, Ruby, Java, and .NET.
Security
HMAC-SHA256 signing, AES-256-GCM encryption, HTTP-only cookie authentication, and role-based access control explained.
Quick-start guide
From zero to your first logged event in under 5 minutes.
1. Install the SDK & set up credentials
# 1 — Install the SDK npm install @tiny-owl-kit/observability # 2 — Add your credentials to .env TINYOWL_API_KEY=tok_live_xxxxxxxxxxxx TINYOWL_PROJECT_SECRET=sec_xxxxxxxxxxxx
Find your API Key and Project Secret in Dashboard → Project → Settings → Security.
2. Initialise the client and log events
import { TinyOwl } from '@tiny-owl-kit/observability';
const logger = new TinyOwl({
apiKey: process.env.TINYOWL_API_KEY!,
projectSecret: process.env.TINYOWL_PROJECT_SECRET!,
});
// Info — regular operational events
await logger.info('User signed in', { userId: '123' });
// Warning — unusual events worth watching
await logger.warning('API limit at 95%', { used: 950, limit: 1000 });
// Error — failures that need attention
await logger.error('Payment failed', {
orderId: 'ORD-42',
reason: 'Card declined',
});3. Verify in your dashboard
Open your Tiny Owl dashboard, navigate to your project, and click Events. Your events appear within seconds. Filter by severity, search full-text, or export to CSV/JSON.
SDK Constructor Options
Pass these options when creating a new TinyOwl instance.
| Option | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Your project API key from the dashboard |
| projectSecret | string | Yes | Used to sign requests with HMAC-SHA256 |
| baseUrl | string | No | Override for self-hosted instances |
| timeout | number | No | Request timeout in milliseconds (default: 5000) |
Manual HMAC Integration
Don't want to use the SDK? Call the ingest endpoint directly from any language using raw HTTP. Every request must be signed with HMAC-SHA256 using your Project Secret.
Ingest endpoint: POST https://api.tinyowl.dev/api/ingest
Request Structure
Required Headers
x-signatureHMAC-SHA256 hex digest of the signing payloadx-timestampISO 8601 UTC — must be within 60 seconds of server timex-nonce32-char random hex — prevents replay attacksRequest Body (JSON)
apiKeyYour project API key (required) — NOT included in the signaturemessageEvent message string (required)severity"info" | "warning" | "error" (required)contextAny additional JSON object (optional)Signing Rule
Compute HMAC-SHA256 over the compact JSON string {"message":…,"severity":…,"context":…,"timestamp":…,"nonce":…} using your Project Secret. apiKey is intentionally excluded — this prevents a compromised signature from revealing your key.
Code Examples
cURL
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
NONCE=$(openssl rand -hex 16)
API_KEY="YOUR_API_KEY"
PROJECT_SECRET="YOUR_PROJECT_SECRET"
MESSAGE="User logged in successfully"
SEVERITY="info"
CONTEXT='{"userId":"12345","ip":"192.168.1.100"}'
# Sign ONLY these fields — apiKey is intentionally excluded from the signature.
# Key order must match exactly: message, severity, context, timestamp, nonce
SIGNING_PAYLOAD='{"message":"'"$MESSAGE"'","severity":"'"$SEVERITY"'","context":'"$CONTEXT"',"timestamp":"'"$TIMESTAMP"'","nonce":"'"$NONCE"'"}'
SIGNATURE=$(echo -n "$SIGNING_PAYLOAD" | openssl dgst -sha256 -hmac "$PROJECT_SECRET" | cut -d' ' -f2)
# apiKey goes in the request body, not as a header
curl -X POST "https://api.tinyowl.dev/api/ingest" \
-H "Content-Type: application/json" \
-H "x-signature: $SIGNATURE" \
-H "x-timestamp: $TIMESTAMP" \
-H "x-nonce: $NONCE" \
-d '{"apiKey":"'"$API_KEY"'","message":"'"$MESSAGE"'","severity":"'"$SEVERITY"'","context":'"$CONTEXT"'}'Node.js
const crypto = require('crypto');
const fetch = require('node-fetch'); // or built-in fetch in Node 18+
const API_KEY = 'YOUR_API_KEY';
const PROJECT_SECRET = 'YOUR_PROJECT_SECRET';
const BASE_URL = 'https://api.tinyowl.dev/api';
async function ingestEvent(message, severity = 'info', context = {}) {
const timestamp = new Date().toISOString();
const nonce = crypto.randomBytes(16).toString('hex');
// Sign ONLY these fields — apiKey is intentionally excluded from the signature
const signingPayload = { message, severity, context, timestamp, nonce };
const signature = crypto
.createHmac('sha256', PROJECT_SECRET)
.update(JSON.stringify(signingPayload))
.digest('hex');
const response = await fetch(`${BASE_URL}/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-signature': signature,
'x-timestamp': timestamp,
'x-nonce': nonce,
},
body: JSON.stringify({ apiKey: API_KEY, message, severity, context }),
});
return response.json();
}
await ingestEvent('User logged in', 'info', { userId: '12345', ip: '192.168.1.100' });Python
import hashlib, hmac as hmac_lib, json, datetime, secrets, requests
API_KEY = "YOUR_API_KEY"
PROJECT_SECRET = "YOUR_PROJECT_SECRET"
BASE_URL = "https://api.tinyowl.dev/api"
def ingest_event(message, severity="info", context=None):
if context is None:
context = {}
timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z")
nonce = secrets.token_hex(16)
# Sign ONLY these fields — apiKey is intentionally excluded from the signature.
# compact JSON + exact key order must match server verification.
signing_payload = json.dumps(
{"message": message, "severity": severity, "context": context,
"timestamp": timestamp, "nonce": nonce},
separators=(",", ":")
)
signature = hmac_lib.new(
PROJECT_SECRET.encode(), signing_payload.encode(), hashlib.sha256
).hexdigest()
response = requests.post(
f"{BASE_URL}/ingest",
headers={
"Content-Type": "application/json",
"x-signature": signature,
"x-timestamp": timestamp,
"x-nonce": nonce,
},
json={"apiKey": API_KEY, "message": message, "severity": severity, "context": context},
)
return response.json()
ingest_event("User logged in", "info", {"userId": "12345", "ip": "192.168.1.100"})PHP
<?php
$API_KEY = 'YOUR_API_KEY';
$PROJECT_SECRET = 'YOUR_PROJECT_SECRET';
$BASE_URL = 'https://api.tinyowl.dev/api';
function ingestEvent(string $message, string $severity = 'info', array $context = []): array {
global $API_KEY, $PROJECT_SECRET, $BASE_URL;
$timestamp = gmdate('Y-m-d\TH:i:s.000\Z');
$nonce = bin2hex(random_bytes(16));
// Sign ONLY these fields — apiKey is intentionally excluded from the signature.
$signingPayload = json_encode([
'message' => $message,
'severity' => $severity,
'context' => $context,
'timestamp' => $timestamp,
'nonce' => $nonce,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$signature = hash_hmac('sha256', $signingPayload, $PROJECT_SECRET);
$body = json_encode([
'apiKey' => $API_KEY,
'message' => $message,
'severity' => $severity,
'context' => $context,
]);
$ch = curl_init("$BASE_URL/ingest");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"x-signature: $signature",
"x-timestamp: $timestamp",
"x-nonce: $nonce",
],
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
ingestEvent('User logged in', 'info', ['userId' => '12345', 'ip' => '192.168.1.100']);Go
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
APIKey = "YOUR_API_KEY"
ProjectSecret = "YOUR_PROJECT_SECRET"
BaseURL = "https://api.tinyowl.dev/api"
)
func ingestEvent(message, severity string, context map[string]interface{}) error {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
nonceBytes := make([]byte, 16)
rand.Read(nonceBytes)
nonce := hex.EncodeToString(nonceBytes)
// Sign ONLY these fields — apiKey is intentionally excluded from the signature
signingData := map[string]interface{}{
"message": message,
"severity": severity,
"context": context,
"timestamp": timestamp,
"nonce": nonce,
}
signingJSON, _ := json.Marshal(signingData)
mac := hmac.New(sha256.New, []byte(ProjectSecret))
mac.Write(signingJSON)
signature := hex.EncodeToString(mac.Sum(nil))
body, _ := json.Marshal(map[string]interface{}{
"apiKey": APIKey,
"message": message,
"severity": severity,
"context": context,
})
req, _ := http.NewRequest("POST", BaseURL+"/ingest", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-signature", signature)
req.Header.Set("x-timestamp", timestamp)
req.Header.Set("x-nonce", nonce)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
return nil
}
func main() {
ingestEvent("User logged in", "info", map[string]interface{}{
"userId": "12345",
"ip": "192.168.1.100",
})
}Ruby
require 'openssl'
require 'json'
require 'net/http'
require 'securerandom'
require 'time'
API_KEY = 'YOUR_API_KEY'
PROJECT_SECRET = 'YOUR_PROJECT_SECRET'
BASE_URL = 'https://api.tinyowl.dev/api'
def ingest_event(message, severity = 'info', context = {})
timestamp = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z')
nonce = SecureRandom.hex(16)
# Sign ONLY these fields — apiKey is intentionally excluded from the signature
signing_payload = JSON.generate({
message: message, severity: severity, context: context,
timestamp: timestamp, nonce: nonce,
})
signature = OpenSSL::HMAC.hexdigest('SHA256', PROJECT_SECRET, signing_payload)
uri = URI("#{BASE_URL}/ingest")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['x-signature'] = signature
req['x-timestamp'] = timestamp
req['x-nonce'] = nonce
req.body = JSON.generate({ apiKey: API_KEY, message: message, severity: severity, context: context })
response = http.request(req)
JSON.parse(response.body)
end
ingest_event('User logged in', 'info', { userId: '12345', ip: '192.168.1.100' })Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.http.*;
import java.net.URI;
import java.security.SecureRandom;
import java.time.Instant;
public class TinyOwlClient {
private static final String API_KEY = "YOUR_API_KEY";
private static final String PROJECT_SECRET = "YOUR_PROJECT_SECRET";
private static final String BASE_URL = "https://api.tinyowl.dev/api";
public static void ingestEvent(String message, String severity, String contextJson)
throws Exception {
String timestamp = Instant.now().toString();
byte[] nonceBytes = new byte[16];
new SecureRandom().nextBytes(nonceBytes);
String nonce = bytesToHex(nonceBytes);
// Sign ONLY these fields — apiKey is intentionally excluded from the signature
String signingPayload = String.format(
"{\"message\":\"%s\",\"severity\":\"%s\",\"context\":%s,\"timestamp\":\"%s\",\"nonce\":\"%s\"}",
message, severity, contextJson, timestamp, nonce
);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(PROJECT_SECRET.getBytes(), "HmacSHA256"));
String signature = bytesToHex(mac.doFinal(signingPayload.getBytes()));
String body = String.format(
"{\"apiKey\":\"%s\",\"message\":\"%s\",\"severity\":\"%s\",\"context\":%s}",
API_KEY, message, severity, contextJson
);
HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create(BASE_URL + "/ingest"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json")
.header("x-signature", signature)
.header("x-timestamp", timestamp)
.header("x-nonce", nonce)
.build(),
HttpResponse.BodyHandlers.ofString()
);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
public static void main(String[] args) throws Exception {
ingestEvent("User logged in", "info",
"{\"userId\":\"12345\",\"ip\":\"192.168.1.100\"}");
}
}.NET
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
const string ApiKey = "YOUR_API_KEY";
const string ProjectSecret = "YOUR_PROJECT_SECRET";
const string BaseUrl = "https://api.tinyowl.dev/api";
async Task IngestEvent(string message, string severity = "info", object? context = null)
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLower();
// Sign ONLY these fields — apiKey is intentionally excluded from the signature
var signingPayload = JsonSerializer.Serialize(new {
message, severity, context = context ?? new { }, timestamp, nonce,
});
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ProjectSecret));
var signature = Convert.ToHexString(
hmac.ComputeHash(Encoding.UTF8.GetBytes(signingPayload))
).ToLower();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/ingest");
request.Headers.Add("x-signature", signature);
request.Headers.Add("x-timestamp", timestamp);
request.Headers.Add("x-nonce", nonce);
request.Content = new StringContent(
JsonSerializer.Serialize(new { apiKey = ApiKey, message, severity, context = context ?? new { } }),
Encoding.UTF8,
"application/json"
);
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
await IngestEvent("User logged in", "info", new { userId = "12345", ip = "192.168.1.100" });Something missing?
Our docs are always growing. Join our Discord if you can't find what you need.
Join Discord