Sending Events

Track pageviews, custom events, and other events from any platform using the HTTP API.

POST /api/track

Send events from servers, mobile apps, or any platform that can make an HTTP request. The body is JSON, and every event needs a site_id and a type.

Send an API key as Authorization: Bearer your_api_key_here when you report events on a visitor's behalf. A key only counts here if it has access to the site and is full-access or holds the ingest:write permission. With such a key, Rybbit uses the ip_address and user_agent you send for the visitor ID, geolocation and device detection, and skips every bot check except user-agent classification. Without one (including a key sent as ?api_key=), ip_address and user_agent are ignored and the event is treated as a browser hit from your server. They are still validated, so a malformed value returns 400. There is no domain validation.

Bot detection still runs. Without a qualifying key, a request from a server HTTP client (curl, python-requests, node-fetch, Go's HTTP client and similar) is classified as a bot. With one, the reported user agent is still classified, so pass the visitor's real user agent. With bot blocking on (the default), bot events are stored as bot traffic and never appear in your analytics, but the response is still 200 with {"success":true}.

To keep visitors apart, send an anonymous_id per visitor, or the visitor's ip_address and user_agent with a qualifying key. Otherwise every event shares your server's IP and user agent and counts as one visitor.

Request Body

All parameters are sent in the request body as JSON. Unknown fields are rejected with 400, so don't add fields that aren't listed here.

Prop

Type

Pageview

Standard page view tracking. Set type: "pageview".

Custom Event

Track custom events with optional properties. Set type: "custom_event".

Prop

Type

Performance

Track Core Web Vitals. Set type: "performance".

Prop

Type

Outbound

Track outbound link clicks. Set type: "outbound". Pass details in the properties JSON string; it is required for this type.

Prop

Type

Error

Track JavaScript errors. Set type: "error". event_name (required) is the error type and properties (required) carries the error details as a JSON string.

Prop

Type

Button Click

Set type: "button_click". properties is optional. Any valid JSON string is accepted, but only a JSON object is stored; other values are dropped and the event is kept.

Copy

Set type: "copy". properties is required.

Prop

Type

Form Submit

Set type: "form_submit". properties is required, and every field below must be present.

Prop

Type

Input Change

Set type: "input_change". properties is required.

Prop

Type

Response

StatusBodyMeaning
200{"success":true}Accepted. Events classified as bots get the same response.
200{"success":true,"message":"Event not tracked - ... excluded"}Matched one of the site's exclusions and was not stored.
200Plain text, e.g. Site over monthly limit, event not tracked or Site has no plan, event not trackedNot stored. The body is not JSON, so read it as text.
400{"success":false,"error":"Invalid payload","details":...}Validation failed; details lists the failing fields.
404{"success":false,"error":"Site not found"}Unknown site_id.

Rate Limiting

/api/track itself isn't rate limited, but on Rybbit Cloud every request that carries a Bearer key counts against the key owner's API limits: a burst of 50 requests per 10 seconds, plus 5,000 (Standard) or 25,000 (Pro) requests per day. See Rate Limits.

When a limit is reached, /api/track doesn't return 429. The key simply stops counting: the event is still ingested, but as untrusted traffic, so ip_address and user_agent are ignored and bot detection runs in full. Self-hosted instances have no API rate limits.

Identifying Users

POST /api/identify

Links a visitor to your user ID and stores traits on the user's profile. This is the endpoint rybbit.identify() and rybbit.setTraits() call in the browser. Send the same Authorization: Bearer key as for /api/track if you pass ip_address or user_agent; without a qualifying key they're ignored.

Prop

Type

Responses: 200 with {"success":true}; 200 with the plain-text body Site has no plan, identify not stored; 400 for an invalid payload; 404 for an unknown site.

Request
curl -X POST "https://app.rybbit.io/api/track" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "1",
    "type": "custom_event",
    "pathname": "/checkout",
    "event_name": "purchase",
    "properties": "{\"amount\": 99.99, \"currency\": \"USD\"}",
    "anonymous_id": "visitor-session-id",
    "ip_address": "203.0.113.7",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
  }'
Request
const response = await fetch('https://app.rybbit.io/api/track', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer your_api_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    site_id: '1',
    type: 'custom_event',
    pathname: '/checkout',
    event_name: 'purchase',
    properties: JSON.stringify({ amount: 99.99, currency: 'USD' }),
    anonymous_id: 'visitor-session-id',
    ip_address: '203.0.113.7',
    user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
  })
});

// Some 200 responses are plain text, so read the body as text
const result = await response.text();
Request
import requests
import json

response = requests.post(
    'https://app.rybbit.io/api/track',
    json={
        'site_id': '1',
        'type': 'custom_event',
        'pathname': '/checkout',
        'event_name': 'purchase',
        'properties': json.dumps({'amount': 99.99, 'currency': 'USD'}),
        'anonymous_id': 'visitor-session-id',
        'ip_address': '203.0.113.7',
        'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
    },
    headers={
        'Authorization': 'Bearer your_api_key_here'
    }
)

# Some 200 responses are plain text, so read the body as text
result = response.text
Request
$body = [
    'site_id' => '1',
    'type' => 'custom_event',
    'pathname' => '/checkout',
    'event_name' => 'purchase',
    'properties' => json_encode(['amount' => 99.99, 'currency' => 'USD']),
    'anonymous_id' => 'visitor-session-id',
    'ip_address' => '203.0.113.7',
    'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rybbit.io/api/track');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer your_api_key_here',
    'Content-Type: application/json'
]);

// Some 200 responses are plain text, so treat the body as text
$result = curl_exec($ch);
curl_close($ch);
Request
require 'net/http'
require 'json'

uri = URI('https://app.rybbit.io/api/track')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer your_api_key_here'
request['Content-Type'] = 'application/json'
request.body = {
  site_id: '1',
  type: 'custom_event',
  pathname: '/checkout',
  event_name: 'purchase',
  properties: { amount: 99.99, currency: 'USD' }.to_json,
  anonymous_id: 'visitor-session-id',
  ip_address: '203.0.113.7',
  user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

# Some 200 responses are plain text, so read the body as text
result = response.body
Request
body := map[string]interface{}{
    "site_id":    "1",
    "type":       "custom_event",
    "pathname":   "/checkout",
    "event_name": "purchase",
    "properties":   `{"amount": 99.99, "currency": "USD"}`,
    "anonymous_id": "visitor-session-id",
    "ip_address":   "203.0.113.7",
    "user_agent":   "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest("POST", "https://app.rybbit.io/api/track", bytes.NewBuffer(jsonBody))
req.Header.Set("Authorization", "Bearer your_api_key_here")
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

// Some 200 responses are plain text, so read the body as text
result, _ := io.ReadAll(resp.Body)
Request
let body = serde_json::json!({
    "site_id": "1",
    "type": "custom_event",
    "pathname": "/checkout",
    "event_name": "purchase",
    "properties": r#"{"amount": 99.99, "currency": "USD"}"#,
    "anonymous_id": "visitor-session-id",
    "ip_address": "203.0.113.7",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
});

let client = reqwest::Client::new();
let res = client
    .post("https://app.rybbit.io/api/track")
    .header("Authorization", "Bearer your_api_key_here")
    .json(&body)
    .send()
    .await?;

// Some 200 responses are plain text, so read the body as text
let result = res.text().await?;
Request
String json = """
{
  "site_id": "1",
  "type": "custom_event",
  "pathname": "/checkout",
  "event_name": "purchase",
  "properties": "{\"amount\": 99.99, \"currency\": \"USD\"}",
  "anonymous_id": "visitor-session-id",
  "ip_address": "203.0.113.7",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
}
""";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://app.rybbit.io/api/track"))
    .header("Authorization", "Bearer your_api_key_here")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Request
var body = new
{
    site_id = "1",
    type = "custom_event",
    pathname = "/checkout",
    event_name = "purchase",
    properties = JsonSerializer.Serialize(new { amount = 99.99, currency = "USD" }),
    anonymous_id = "visitor-session-id",
    ip_address = "203.0.113.7",
    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
};

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer your_api_key_here");

var content = new StringContent(
    JsonSerializer.Serialize(body),
    Encoding.UTF8,
    "application/json");

var response = await client.PostAsync("https://app.rybbit.io/api/track", content);
// Some 200 responses are plain text, so read the body as text
var result = await response.Content.ReadAsStringAsync();
Success
{
  "success": true
}
Error (400, unknown field in the body)
{
  "success": false,
  "error": "Invalid payload",
  "details": {
    "formErrors": ["Unrecognized key(s) in object: 'api_key'"],
    "fieldErrors": {}
  }
}