Getting Started
Authentication, common parameters, and best practices for the Rybbit Stats API
The API is currently in beta. There may be breaking changes as we continue to improve and expand the API.
API Playground
The easiest way to explore and test the API is using the built-in API Playground in your Rybbit dashboard. Access it by navigating to any site and clicking "API Playground" in the sidebar.
The playground provides:
- Endpoint browser - Browse all available API endpoints
- Parameter controls - Configure request parameters with a visual interface
- Exact time ranges - Test date ranges with optional start and end times
- Live responses - See real API responses with your actual data
- Code generation - Copy ready-to-use code snippets
This is the recommended way to familiarize yourself with the API before integrating it into your applications.
Guides
Looking for what to build with the API? These end-to-end recipes chain endpoints together to solve a real problem:
- Automated Weekly Report — a scheduled digest of last week's traffic, week-over-week change, and top pages and sources.
- Find Where Users Drop Off — locate the biggest leak in a funnel, then see where the people who abandon go instead.
- Compare Channel Performance — rank acquisition channels by how well they convert, not just how much traffic they send.
- Build a Live Visitor Feed — poll the events endpoint for a real-time activity stream you can render or alert on.
Authentication
Most requests need a credential, sent one of the ways below. Two exceptions:
- Public sites. Read endpoints for a site with Public Analytics turned on answer without any credential.
- Private links. Send a site's private link key in an
x-private-keyheader to read that site's analytics without a credential, the same access a private dashboard link gives. A rotated key stops working immediately.
x-private-key: your_private_link_keyBoth only cover reads that the public dashboard can make (analytics, sessions, events, users, funnels, goals, segments, annotations and similar). Settings, organization endpoints, and writes other than event ingestion need a credential. POST /api/track accepts events without one, as the browser script does; an API key with ingest:write additionally lets you set the visitor's IP and user agent.
Bearer Token (Recommended)
Authorization: Bearer your_api_key_hereQuery Parameter (Testing Only)
?api_key=your_api_key_hereQuery parameters expose API keys in server logs and browser history. Use only for quick testing.
Session cookie
Requests from a signed-in dashboard session (the Better Auth session cookie) are also accepted. A few account endpoints, such as creating a personal API key or managing keys and invitations under /api/auth/*, accept only a session. In production the cookie is named __Secure-better-auth.session_token. A POST to /api/auth/* that sends a cookie must also send an Origin header for your Rybbit instance (for example Origin: https://app.rybbit.io), or it is refused with 403 and code MISSING_OR_NULL_ORIGIN.
Generating API Keys
Rybbit has two kinds of API keys:
- Organization API keys (recommended for integrations) — the organization's own credential. They can access every site in the organization and keep working when team members leave. Only organization admins and owners can create and manage them.
- Personal API keys — act as you, with exactly your access, including any site restrictions, across every organization you belong to. Best for personal scripts and for connecting MCP clients.
Organization keys
- Navigate to Settings → Organization in your Rybbit dashboard
- Scroll to the Organization API Keys section (visible to admins and owners)
- Enter a name for the key and click Create
- Copy the key immediately (it won't be shown again)
Personal keys
- Navigate to Settings → Account in your Rybbit dashboard
- Scroll to the Personal API Keys section
- Enter a name for the key and click Create
- Copy the key immediately (it won't be shown again)
Both kinds support restricting a key to specific scopes when you create it. Leave restrictions off for a full-access key.
Roles and Scopes
A request is allowed only if the caller's role permits it and, for API keys and OAuth tokens, the credential's scopes do too.
Roles
Each organization member has one role. Every role can do everything the roles below it can:
| Role | Can |
|---|---|
owner | Everything, including billing and deleting the organization |
admin | Manage members, teams and API keys; create, delete and transfer sites. Sees every site |
editor | Configure sites: tracking settings, feature flags, experiments, imports, Search Console. Manage everyone's segments and annotations |
member | Build reports: goals, funnels, dashboards, their own segments and annotations |
viewer | Read everything they can reach; change nothing |
Below admin, a member can be limited to specific sites, and a per-site grant (direct or through a team) can raise their role on a site to editor, member or viewer. A grant never lowers a role. Organization keys act with admin authority over every site in their organization. Personal keys act with your role.
Scopes
A scoped key carries resource:action pairs. write implies read on the same resource. Requests outside a key's scopes get 403 with "error": "Insufficient scope" and the missing scope in required.
| Resource | Actions | Covers |
|---|---|---|
analytics | read | Overview, metrics, time series, performance, bots, journeys, retention, PDF export |
sessions | read | Sessions |
events | read | Events |
users | read, write | Users and user traits |
goals | read, write | Goals |
funnels | read, write | Funnels |
dashboards | read, write | Dashboards |
annotations | read, write | Annotations |
segments | read, write | Saved segments, and the segment_id parameter |
flags | read, write | Feature flags |
experiments | read, write | Experiments |
sites | read, write | Site settings, creating, deleting and transferring sites, imports |
gsc | read, write | Google Search Console |
org | read, write | Organizations, members, teams |
replay | read, write | Session replay |
sql | read | Custom SQL queries |
ingest | write | Sending events |
Scopes never raise a role: an org:write key still needs an admin behind it to add members. Some account surfaces (creating API keys, billing, account settings) reject scoped keys and organization keys outright, whatever their scopes. Cookie sessions are never scope-limited.
Rate Limits (Cloud Only)
Requests are limited in two ways: a burst limit that is the same on every plan, and a daily quota that depends on your plan.
| Plan | Burst | Daily quota |
|---|---|---|
| Free / Basic | API keys not available | — |
| Standard | 50 requests per 10 seconds | 5,000 requests per day |
| Pro | 50 requests per 10 seconds | 25,000 requests per day |
The burst limit is a bucket of 50 requests that refills continuously at 5 requests per second. You can spend all 50 at once, then keep going at 5 per second — so short spikes are fine and there is no window boundary to line up with.
The daily quota resets at 00:00 UTC.
Requests refused by the burst limit don't count against your daily quota.
Which budget a request is charged to
Limits apply per credential owner, never per key — creating more keys splits an existing budget rather than adding to it:
| Credential | Budget |
|---|---|
Organization key (rb_org_) | The organization's. All of an organization's keys share it. |
| Personal key | Yours. All of your personal keys share one budget, across every organization you belong to. |
| OAuth token (MCP) | Yours, the same budget as your personal keys. |
A personal key's quota follows the most generous plan among the organizations you belong to.
Self-hosted instances are not rate limited at all — neither tier applies, and no rate limit headers are sent.
Reading your remaining budget
Analytics and management endpoints report your current budget on every response, so you can pace a client without waiting to be rejected:
| Header | Meaning |
|---|---|
X-RateLimit-Burst-Limit / -Remaining | Burst bucket size and tokens left |
X-RateLimit-Burst-Reset | Seconds until the bucket is full again |
X-RateLimit-Daily-Limit / -Remaining | Daily quota and requests left today |
X-RateLimit-Daily-Reset | Seconds until 00:00 UTC |
RateLimit-Limit / -Remaining / -Reset | Whichever of the two is closest to running out |
Treat these as advisory: a few endpoints resolve credentials outside the standard path and omit them. Retry-After on a 429 is always present.
To check an organization's daily quota without spending a request against a site, call GET /api/organizations/:organizationId/api-usage (role viewer or above; keys need org:read). It returns dailyUsed, dailyLimit, dailyRemaining, resetsInSeconds, burstLimit and burstWindowSeconds, plus metered (false on self-hosted) and available (false when the counter could not be read). It covers the organization's own keys only; personal keys are budgeted per user and are not counted here.
When you hit a limit
A 429 names the limit you hit and tells you how long to wait, in seconds:
{
"error": "Rate limit exceeded",
"scope": "daily",
"limit": 5000,
"retryAfter": 34200
}scope is burst or daily, and the same value is in the Retry-After header. The difference matters: a burst rejection clears in about a second, while a daily one lasts until the quota resets. Back off accordingly rather than retrying straight away.
Common Parameters
The following parameters are shared across all analytics endpoints.
Time Parameters
Time parameters are optional on most endpoints. With none, a query covers all time. A few endpoints differ: retention defaults to the last 90 days, and PDF export requires start_date, end_date and time_zone. To narrow a query, pass one of the ranges below:
Prop
Type
Note: Each range's parameters come in pairs. Send both or neither:
start_dateandend_date(both days inclusive; whenend_dateis today, the range ends now)start_datetimeandend_datetimepast_minutes_startandpast_minutes_end
Half a pair, a malformed value, or an invalid time_zone returns 400, as does a datetime or past-minutes range whose start isn't before its end. A date range may start and end on the same day; a reversed date range isn't rejected, it just matches nothing. If you send more than one range, the date range wins, then the datetime range, then past minutes.
Timestamps in responses
Analytics timestamps (events, sessions, users, time-series buckets) and most stored records come back as YYYY-MM-DD HH:MM:SS, sometimes with fractional seconds (2024-01-15 13:00:00.123), without a T or a Z. Event and session timestamps are UTC; time-series bucket values are wall-clock times in the requested time_zone. Some endpoints, such as retention and annotations, return ISO 8601 strings with T and a zone instead. Parse defensively rather than with a parser that assumes local time. In JavaScript, for a UTC value: new Date(/[zZ]|[+-]\d\d:?\d\d$/.test(ts) ? ts : ts.replace(' ', 'T') + 'Z').
Examples
Date-based query (last 30 days):
?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_YorkExact datetime query (8 AM to 10 AM in New York on Jan 15, sent as UTC):
?start_datetime=2024-01-15%2013:00:00&end_datetime=2024-01-15%2015:00:00&time_zone=America/New_YorkRelative query (last 60 minutes):
?past_minutes_start=60&past_minutes_end=0Filter Parameters
The filters parameter accepts a JSON-encoded array of filter objects to narrow down your data.
You can also pass segment_id, the ID of a saved segment, instead of or alongside filters. The segment's filters are expanded on the server and ANDed with any filters you pass. Use the Segments endpoints to find IDs. Public and private-link callers can only use segments marked as shown on the public dashboard. A scoped API key also needs segments:read to pass segment_id, whatever scope the endpoint itself requires.
?start_date=2024-01-01&end_date=2024-01-31&time_zone=UTC&segment_id=12Filter Object Structure
Prop
Type
Filter Types
Prop
Type
The four comparison types only work on lat and lon, and lat/lon take no string types (contains, not_contains, starts_with, ends_with, regex, not_regex). Either mismatch returns 400.
Available Filter Parameters
Browser & Device
Prop
Type
Location
Prop
Type
Page & Traffic
Prop
Type
UTM Parameters
Prop
Type
User & Events
Prop
Type
tag filters on the data-tag value set by the tracking script (Tagging). feature_flag:<key> filters on the value a feature flag had when the event was recorded.
Filter Examples
[
{
"parameter": "country",
"type": "equals",
"value": ["US"]
}
]Multiple filters use AND logic:
[
{
"parameter": "country",
"type": "equals",
"value": ["US", "CA"]
},
{
"parameter": "device_type",
"type": "equals",
"value": ["mobile"]
}
]Substring match:
[
{
"parameter": "pathname",
"type": "contains",
"value": ["/blog"]
}
]Pattern match:
[
{
"parameter": "pathname",
"type": "regex",
"value": ["^/products/[0-9]+$"]
}
]Geolocation comparison:
[
{
"parameter": "lat",
"type": "greater_than",
"value": ["37.0"]
},
{
"parameter": "lat",
"type": "less_than",
"value": ["38.0"]
}
]?filters=%5B%7B%22parameter%22%3A%22country%22%2C%22type%22%3A%22equals%22%2C%22value%22%3A%5B%22US%22%5D%7D%5DError Responses
Endpoints use standard HTTP status codes:
- 200 - Success (some create endpoints return 201)
- 400 - Bad request: invalid parameters, body, filters, or time range
- 401 - No usable credential where one is required (organization and account endpoints), or a credential the endpoint can't act for
- 403 - Forbidden. On site endpoints this is also what a missing or invalid API key gets, and what a site ID that doesn't exist gets, so a site's existence isn't revealed. Also returned when your role or the key's scopes fall short
- 404 - Not found: a malformed site ID, an unknown text site identifier, or a resource that doesn't exist inside a site you can access (a segment, goal, funnel and so on). A numeric site ID you can't access returns
403, not404 - 409 - Conflict with the current state
- 429 - Rate limit exceeded (see Rate Limits)
- 500 - Internal server error
Error Response Format
Every error from /api has the same shape, except the MCP endpoint and Better Auth routes under /api/auth/*, which return their own error bodies:
{
"error": "Insufficient role",
"code": "FORBIDDEN",
"message": "Insufficient role",
"resolution": "Use a credential with the required scope and Site or Organization access.",
"required": "admin"
}| Field | Meaning |
|---|---|
error | Human-readable error, kept for backward compatibility |
code | Machine-readable code. Endpoints that don't set one get a default per status: INVALID_REQUEST, AUTHENTICATION_REQUIRED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED, INTERNAL_ERROR |
message | Human-readable explanation |
resolution | What to do about it |
Some errors add fields. Role and scope failures ("Insufficient role", "Insufficient scope") usually include required, the role or resource:action scope you need; it is omitted when the endpoint rejects scoped keys outright. 429 responses usually include scope and retryAfter, and most include limit; the Retry-After header is always set. Branch on code and required when present, not on the text of error.