Rybbit

API

Track events from server-side applications, mobile apps, or any platform using the HTTP API

Track events from server-side applications, mobile apps, or any platform using the HTTP API with authentication.

Overview

The /api/track endpoint accepts tracking requests with API key authentication, bypassing domain validation. This enables server-side tracking, mobile app analytics, and programmatic event collection.

Data Enrichment

  • Geolocation: IP addresses are resolved to country, region, city, and coordinates using MaxMind GeoLite2 database
  • Device Info: User agent strings are parsed to extract browser, operating system, and device type information

Steps

Generate API Key

In your site dashboard: Settings → API Key tab → Generate API Key

Include in Requests

curl -X POST 'https://app.rybbit.io/api/track' \
  -H 'Content-Type: application/json' \
  -d '{
    "api_key": "rb_your_api_key_here",
    "site_id": "123",
    "type": "pageview",
    "pathname": "/api/users",
    "hostname": "api.example.com",
    "page_title": "User API Endpoint",
    "user_agent": "MyApp/1.0 (Linux; x64)",
    "ip_address": "192.168.1.1"
  }'
curl -X POST 'https://app.rybbit.io/api/track' \
  -H 'Content-Type: application/json' \
  -d '{
    "api_key": "rb_your_api_key_here",
    "site_id": "123",
    "type": "custom_event",
    "pathname": "/checkout",
    "event_name": "purchase",
    "properties": "{\"amount\": 99.99, \"currency\": \"USD\", \"product_id\": \"abc123\"}"
  }'

Parameters

ParameterTypeRequiredDescription
api_keystringRequiredYour generated API key (starts with rb_)
site_idstringRequiredYour site ID
typestringOptionalEvent type: pageview or custom_event (defaults to pageview)
pathnamestringOptionalPage path
hostnamestringOptionalDomain name
page_titlestringOptionalPage title (max 512 chars)
referrerstringOptionalReferrer URL (max 2048 chars)
user_idstringOptionalCustom user identifier (max 255 chars)
user_agentstringOptionalCustom user agent string (max 512 chars) - will be parsed for browser/OS/device info
ip_addressstringOptionalCustom IP for geolocation - will be resolved to location data
querystringstringOptionalQuery parameters
languagestringOptionalLanguage code (e.g., "en")
screenWidthnumberOptionalScreen width in logical pixels (density-independent pixels)
screenHeightnumberOptionalScreen height in logical pixels (density-independent pixels)
event_namestringCustom events onlyEvent name (required for custom events, max 256 chars)
propertiesstringCustom events onlyJSON string with event data (max 2048 chars)

Language Examples

const trackEvent = async (eventData) => {
  const response = await fetch('https://app.rybbit.io/api/track', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      api_key: process.env.RYBBIT_API_KEY,
      site_id: process.env.RYBBIT_SITE_ID,
      ...eventData
    })
  });
  
  return response.json();
};

// Track a pageview
await trackEvent({
  type: 'pageview',
  pathname: '/api/users',
  hostname: 'api.example.com'
});

// Track a custom event
await trackEvent({
  type: 'custom_event',
  pathname: '/checkout',
  event_name: 'purchase',
  properties: JSON.stringify({ amount: 99.99, currency: 'USD' })
});
import requests
import json
import os

def track_event(event_data):
    response = requests.post(
        'https://app.rybbit.io/api/track',
        headers={'Content-Type': 'application/json'},
        json={
            'api_key': os.getenv('RYBBIT_API_KEY'),
            'site_id': os.getenv('RYBBIT_SITE_ID'),
            **event_data
        }
    )
    return response.json()

# Track a pageview
track_event({
    'type': 'pageview',
    'pathname': '/api/users',
    'hostname': 'api.example.com'
})

# Track a custom event
track_event({
    'type': 'custom_event',
    'pathname': '/checkout',
    'event_name': 'purchase',
    'properties': json.dumps({'amount': 99.99, 'currency': 'USD'})
})
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

type TrackingEvent struct {
    APIKey     string `json:"api_key"`
    SiteID     string `json:"site_id"`
    Type       string `json:"type"`
    Pathname   string `json:"pathname"`
    Hostname   string `json:"hostname,omitempty"`
    EventName  string `json:"event_name,omitempty"`
    Properties string `json:"properties,omitempty"`
}

func trackEvent(event TrackingEvent) error {
    event.APIKey = os.Getenv("RYBBIT_API_KEY")
    event.SiteID = os.Getenv("RYBBIT_SITE_ID")
    
    jsonData, err := json.Marshal(event)
    if err != nil {
        return err
    }
    
    resp, err := http.Post(
        "https://app.rybbit.io/api/track",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    return nil
}

func main() {
    // Track a pageview
    trackEvent(TrackingEvent{
        Type:     "pageview",
        Pathname: "/api/users",
        Hostname: "api.example.com",
    })
    
    // Track a custom event
    trackEvent(TrackingEvent{
        Type:       "custom_event",
        Pathname:   "/checkout",
        EventName:  "purchase",
        Properties: `{"amount": 99.99, "currency": "USD"}`,
    })
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.HashMap;

public class RybbitTracker {
    private static final String API_URL = "https://app.rybbit.io/api/track";
    private static final String API_KEY = System.getenv("RYBBIT_API_KEY");
    private static final String SITE_ID = System.getenv("RYBBIT_SITE_ID");
    
    private final HttpClient httpClient = HttpClient.newHttpClient();
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    public void trackEvent(Map<String, Object> eventData) throws Exception {
        eventData.put("api_key", API_KEY);
        eventData.put("site_id", SITE_ID);
        
        String json = objectMapper.writeValueAsString(eventData);
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();
            
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
            
        if (response.statusCode() != 200) {
            throw new Exception("Tracking failed: " + response.body());
        }
    }
    
    public static void main(String[] args) throws Exception {
        RybbitTracker tracker = new RybbitTracker();
        
        // Track a pageview
        Map<String, Object> pageview = new HashMap<>();
        pageview.put("type", "pageview");
        pageview.put("pathname", "/api/users");
        pageview.put("hostname", "api.example.com");
        tracker.trackEvent(pageview);
        
        // Track a custom event
        Map<String, Object> customEvent = new HashMap<>();
        customEvent.put("type", "custom_event");
        customEvent.put("pathname", "/checkout");
        customEvent.put("event_name", "purchase");
        customEvent.put("properties", "{\"amount\": 99.99, \"currency\": \"USD\"}");
        tracker.trackEvent(customEvent);
    }
}
<?php

class RybbitTracker {
    private $apiUrl = 'https://app.rybbit.io/api/track';
    private $apiKey;
    private $siteId;
    
    public function __construct() {
        $this->apiKey = getenv('RYBBIT_API_KEY');
        $this->siteId = getenv('RYBBIT_SITE_ID');
    }
    
    public function trackEvent($eventData) {
        $eventData['api_key'] = $this->apiKey;
        $eventData['site_id'] = $this->siteId;
        
        $ch = curl_init($this->apiUrl);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($eventData));
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode !== 200) {
            throw new Exception('Tracking failed: ' . $response);
        }
        
        return json_decode($response, true);
    }
}

// Usage
$tracker = new RybbitTracker();

// Track a pageview
$tracker->trackEvent([
    'type' => 'pageview',
    'pathname' => '/api/users',
    'hostname' => 'api.example.com'
]);

// Track a custom event
$tracker->trackEvent([
    'type' => 'custom_event',
    'pathname' => '/checkout',
    'event_name' => 'purchase',
    'properties' => json_encode(['amount' => 99.99, 'currency' => 'USD'])
]);
require 'net/http'
require 'json'
require 'uri'

class RybbitTracker
  API_URL = 'https://app.rybbit.io/api/track'
  
  def initialize
    @api_key = ENV['RYBBIT_API_KEY']
    @site_id = ENV['RYBBIT_SITE_ID']
  end
  
  def track_event(event_data)
    event_data[:api_key] = @api_key
    event_data[:site_id] = @site_id
    
    uri = URI(API_URL)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    
    request = Net::HTTP::Post.new(uri.path)
    request['Content-Type'] = 'application/json'
    request.body = event_data.to_json
    
    response = http.request(request)
    
    unless response.code == '200'
      raise "Tracking failed: #{response.body}"
    end
    
    JSON.parse(response.body)
  end
end

# Usage
tracker = RybbitTracker.new

# Track a pageview
tracker.track_event({
  type: 'pageview',
  pathname: '/api/users',
  hostname: 'api.example.com'
})

# Track a custom event
tracker.track_event({
  type: 'custom_event',
  pathname: '/checkout',
  event_name: 'purchase',
  properties: JSON.generate({ amount: 99.99, currency: 'USD' })
})
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;

public class RybbitTracker
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly string _siteId;
    private const string ApiUrl = "https://app.rybbit.io/api/track";
    
    public RybbitTracker()
    {
        _httpClient = new HttpClient();
        _apiKey = Environment.GetEnvironmentVariable("RYBBIT_API_KEY");
        _siteId = Environment.GetEnvironmentVariable("RYBBIT_SITE_ID");
    }
    
    public async Task TrackEventAsync(Dictionary<string, object> eventData)
    {
        eventData["api_key"] = _apiKey;
        eventData["site_id"] = _siteId;
        
        var json = JsonSerializer.Serialize(eventData);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync(ApiUrl, content);
        
        if (!response.IsSuccessStatusCode)
        {
            var error = await response.Content.ReadAsStringAsync();
            throw new Exception($"Tracking failed: {error}");
        }
    }
    
    static async Task Main(string[] args)
    {
        var tracker = new RybbitTracker();
        
        // Track a pageview
        await tracker.TrackEventAsync(new Dictionary<string, object>
        {
            ["type"] = "pageview",
            ["pathname"] = "/api/users",
            ["hostname"] = "api.example.com"
        });
        
        // Track a custom event
        await tracker.TrackEventAsync(new Dictionary<string, object>
        {
            ["type"] = "custom_event",
            ["pathname"] = "/checkout",
            ["event_name"] = "purchase",
            ["properties"] = JsonSerializer.Serialize(new { amount = 99.99, currency = "USD" })
        });
    }
}
use reqwest;
use serde_json::{json, Value};
use std::env;

#[derive(Clone)]
struct RybbitTracker {
    client: reqwest::Client,
    api_key: String,
    site_id: String,
    api_url: String,
}

impl RybbitTracker {
    fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
            api_key: env::var("RYBBIT_API_KEY").expect("RYBBIT_API_KEY not set"),
            site_id: env::var("RYBBIT_SITE_ID").expect("RYBBIT_SITE_ID not set"),
            api_url: "https://app.rybbit.io/api/track".to_string(),
        }
    }
    
    async fn track_event(&self, mut event_data: Value) -> Result<(), Box<dyn std::error::Error>> {
        event_data["api_key"] = json!(self.api_key);
        event_data["site_id"] = json!(self.site_id);
        
        let response = self.client
            .post(&self.api_url)
            .json(&event_data)
            .send()
            .await?;
            
        if !response.status().is_success() {
            let error_text = response.text().await?;
            return Err(format!("Tracking failed: {}", error_text).into());
        }
        
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tracker = RybbitTracker::new();
    
    // Track a pageview
    tracker.track_event(json!({
        "type": "pageview",
        "pathname": "/api/users",
        "hostname": "api.example.com"
    })).await?;
    
    // Track a custom event
    tracker.track_event(json!({
        "type": "custom_event",
        "pathname": "/checkout",
        "event_name": "purchase",
        "properties": r#"{"amount": 99.99, "currency": "USD"}"#
    })).await?;
    
    Ok(())
}
import Foundation
import UIKit

class RybbitTracker {
    static let shared = RybbitTracker()
    
    private let apiUrl = "https://app.rybbit.io/api/track"
    private let apiKey: String
    private let siteId: String
    
    init() {
        // Ensure API key and site ID are available
        guard let apiKey = ProcessInfo.processInfo.environment["RYBBIT_API_KEY"],
              !apiKey.isEmpty,
              let siteId = ProcessInfo.processInfo.environment["RYBBIT_SITE_ID"],
              !siteId.isEmpty else {
            fatalError("RYBBIT_API_KEY and RYBBIT_SITE_ID environment variables must be set")
        }
        self.apiKey = apiKey
        self.siteId = siteId
    }
    
    func trackEvent(_ eventData: [String: Any]) async throws {
        var data = eventData
        data["api_key"] = apiKey
        data["site_id"] = siteId
        
        // Add device info
        data["user_agent"] = getUserAgent()
        
        // Get screen dimensions in logical pixels (points)
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            let bounds = windowScene.screen.bounds
            data["screenWidth"] = Int(bounds.width)
            data["screenHeight"] = Int(bounds.height)
        } else {
            // Fallback for older iOS versions
            let bounds = UIScreen.main.bounds
            data["screenWidth"] = Int(bounds.width)
            data["screenHeight"] = Int(bounds.height)
        }
        
        data["language"] = Locale.current.languageCode ?? "en"
        
        var request = URLRequest(url: URL(string: apiUrl)!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: data)
        
        let (_, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw NSError(domain: "RybbitTracker", code: 1, userInfo: [NSLocalizedDescriptionKey: "Tracking failed"])
        }
    }
    
    private func getUserAgent() -> String {
        let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
        let osVersion = UIDevice.current.systemVersion
        let model = UIDevice.current.model
        return "MyApp/\(appVersion) (iOS \(osVersion); \(model))"
    }
}

// Usage
Task {
    // Track a pageview
    try await RybbitTracker.shared.trackEvent([
        "type": "pageview",
        "pathname": "/api/users",
        "hostname": "api.example.com",
        "page_title": "User API Endpoint"
    ])
    
    // Track a custom event
    try await RybbitTracker.shared.trackEvent([
        "type": "custom_event",
        "pathname": "/checkout",
        "event_name": "purchase",
        "properties": "{\"amount\": 99.99, \"currency\": \"USD\"}"
    ])
}
import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import android.content.Context
import android.os.Build
import java.util.Locale

class RybbitTracker(private val context: Context) {
    companion object {
        private const val API_URL = "https://app.rybbit.io/api/track"
        private val JSON = "application/json; charset=utf-8".toMediaType()
    }
    
    private val client = OkHttpClient()
    private val apiKey = BuildConfig.RYBBIT_API_KEY // Set in build.gradle
    private val siteId = BuildConfig.RYBBIT_SITE_ID
    
    suspend fun trackEvent(eventData: Map<String, Any>) = withContext(Dispatchers.IO) {
        val data = eventData.toMutableMap().apply {
            put("api_key", apiKey)
            put("site_id", siteId)
            put("user_agent", getUserAgent())
            
            // Get screen dimensions in logical pixels (density-independent pixels)
            val displayMetrics = context.resources.displayMetrics
            put("screenWidth", (displayMetrics.widthPixels / displayMetrics.density).toInt())
            put("screenHeight", (displayMetrics.heightPixels / displayMetrics.density).toInt())
            put("language", Locale.getDefault().language)
        }
        
        val json = JSONObject(data).toString()
        val body = json.toRequestBody(JSON)
        
        val request = Request.Builder()
            .url(API_URL)
            .post(body)
            .build()
            
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw Exception("Tracking failed: ${response.code}")
            }
        }
    }
    
    private fun getUserAgent(): String {
        val appVersion = context.packageManager
            .getPackageInfo(context.packageName, 0).versionName
        return "MyApp/$appVersion (Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
    }
}

// Usage
class MainActivity : AppCompatActivity() {
    private val tracker = RybbitTracker(this)
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            // Track a pageview
            tracker.trackEvent(mapOf(
                "type" to "pageview",
                "pathname" to "/api/users",
                "hostname" to "api.example.com",
                "page_title" to "User API Endpoint"
            ))
            
            // Track a custom event
            tracker.trackEvent(mapOf(
                "type" to "custom_event",
                "pathname" to "/checkout",
                "event_name" to "purchase",
                "properties" to """{"amount": 99.99, "currency": "USD"}"""
            ))
        }
    }
}
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';

class RybbitTracker {
  static final RybbitTracker _instance = RybbitTracker._internal();
  factory RybbitTracker() => _instance;
  RybbitTracker._internal();
  
  static const String _apiUrl = 'https://app.rybbit.io/api/track';
  static const String _apiKey = String.fromEnvironment('RYBBIT_API_KEY');
  static const String _siteId = String.fromEnvironment('RYBBIT_SITE_ID');
  
  final _deviceInfoPlugin = DeviceInfoPlugin();
  
  Future<void> trackEvent(Map<String, dynamic> eventData) async {
    final data = Map<String, dynamic>.from(eventData);
    data['api_key'] = _apiKey;
    data['site_id'] = _siteId;
    
    // Add device info
    data['user_agent'] = await _getUserAgent();
    
    // Get screen dimensions in logical pixels (density-independent pixels)
    final view = WidgetsBinding.instance.platformDispatcher.views.first;
    data['screenWidth'] = (view.physicalSize.width / view.devicePixelRatio).round();
    data['screenHeight'] = (view.physicalSize.height / view.devicePixelRatio).round();
    data['language'] = Platform.localeName.split('_')[0];
    
    final response = await http.post(
      Uri.parse(_apiUrl),
      headers: {'Content-Type': 'application/json'},
      body: json.encode(data),
    );
    
    if (response.statusCode != 200) {
      throw Exception('Tracking failed: ${response.statusCode}');
    }
  }
  
  Future<String> _getUserAgent() async {
    const appVersion = '1.0.0'; // Get from package_info_plus if needed
    
    if (Platform.isAndroid) {
      final androidInfo = await _deviceInfoPlugin.androidInfo;
      return 'MyApp/$appVersion (Android ${androidInfo.version.release}; ${androidInfo.model})';
    } else if (Platform.isIOS) {
      final iosInfo = await _deviceInfoPlugin.iosInfo;
      return 'MyApp/$appVersion (iOS ${iosInfo.systemVersion}; ${iosInfo.model})';
    }
    
    return 'MyApp/$appVersion (${Platform.operatingSystem})';
  }
}

// Usage
void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final tracker = RybbitTracker();
  
  @override
  void initState() {
    super.initState();
    // Track app launch once when the app initializes
    tracker.trackEvent({
      'type': 'pageview',
      'pathname': '/api/users',
      'hostname': 'api.example.com',
      'page_title': 'User API Endpoint',
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              // Track custom event
              tracker.trackEvent({
                'type': 'custom_event',
                'pathname': '/checkout',
                'event_name': 'purchase',
                'properties': json.encode({'amount': 99.99, 'currency': 'USD'}),
              });
            },
            child: Text('Track Event'),
          ),
        ),
      ),
    );
  }
}

Response Format

Success Response

{
  "success": true
}

Error Response

{
  "success": false,
  "error": "Invalid API key",
  "details": {
    "fieldErrors": {},
    "formErrors": ["Custom events require event_name"]
  }
}

Rate Limiting

API key authenticated requests are rate limited to 20 requests per second per API key on Rybbit Cloud. Self-hosted instances have no rate limits.

When you exceed the rate limit, you'll receive a 429 status code:

{
  "success": false,
  "error": "Rate limit exceeded. Maximum 20 requests per second per API key."
}

Best Practices

Environment Variables: Store your API key and site ID as environment variables to keep them secure.

API Key Security: Never expose API keys in client-side code or public repositories.

Error Handling

Common error scenarios:

  • Invalid API key: Check that your API key starts with rb_ and hasn't been revoked
  • Site not found: Verify your site ID is correct
  • Invalid payload: Ensure required fields are present and data types match
  • JSON parsing errors: For custom events, ensure properties field contains valid JSON
  • Rate limit exceeded: Reduce request frequency or implement request queuing with delays