Sending Events
Track pageviews, custom events, and other events from any platform using the HTTP API.
POST /api/trackSend 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
| Status | Body | Meaning |
|---|---|---|
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. |
200 | Plain text, e.g. Site over monthly limit, event not tracked or Site has no plan, event not tracked | Not 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/identifyLinks 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.
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"
}'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();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$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);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.bodybody := 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)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?;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());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": true
}{
"success": false,
"error": "Invalid payload",
"details": {
"formErrors": ["Unrecognized key(s) in object: 'api_key'"],
"fieldErrors": {}
}
}