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
Parameter | Type | Required | Description |
---|---|---|---|
api_key | string | Required | Your generated API key (starts with rb_ ) |
site_id | string | Required | Your site ID |
type | string | Optional | Event type: pageview or custom_event (defaults to pageview ) |
pathname | string | Optional | Page path |
hostname | string | Optional | Domain name |
page_title | string | Optional | Page title (max 512 chars) |
referrer | string | Optional | Referrer URL (max 2048 chars) |
user_id | string | Optional | Custom user identifier (max 255 chars) |
user_agent | string | Optional | Custom user agent string (max 512 chars) - will be parsed for browser/OS/device info |
ip_address | string | Optional | Custom IP for geolocation - will be resolved to location data |
querystring | string | Optional | Query parameters |
language | string | Optional | Language code (e.g., "en") |
screenWidth | number | Optional | Screen width in logical pixels (density-independent pixels) |
screenHeight | number | Optional | Screen height in logical pixels (density-independent pixels) |
event_name | string | Custom events only | Event name (required for custom events, max 256 chars) |
properties | string | Custom events only | JSON 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