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:

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-key header 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_key

Both 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.

Authorization: Bearer your_api_key_here

Query Parameter (Testing Only)

?api_key=your_api_key_here

Query parameters expose API keys in server logs and browser history. Use only for quick testing.

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

  1. Navigate to Settings → Organization in your Rybbit dashboard
  2. Scroll to the Organization API Keys section (visible to admins and owners)
  3. Enter a name for the key and click Create
  4. Copy the key immediately (it won't be shown again)

Personal keys

  1. Navigate to Settings → Account in your Rybbit dashboard
  2. Scroll to the Personal API Keys section
  3. Enter a name for the key and click Create
  4. 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:

RoleCan
ownerEverything, including billing and deleting the organization
adminManage members, teams and API keys; create, delete and transfer sites. Sees every site
editorConfigure sites: tracking settings, feature flags, experiments, imports, Search Console. Manage everyone's segments and annotations
memberBuild reports: goals, funnels, dashboards, their own segments and annotations
viewerRead 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.

ResourceActionsCovers
analyticsreadOverview, metrics, time series, performance, bots, journeys, retention, PDF export
sessionsreadSessions
eventsreadEvents
usersread, writeUsers and user traits
goalsread, writeGoals
funnelsread, writeFunnels
dashboardsread, writeDashboards
annotationsread, writeAnnotations
segmentsread, writeSaved segments, and the segment_id parameter
flagsread, writeFeature flags
experimentsread, writeExperiments
sitesread, writeSite settings, creating, deleting and transferring sites, imports
gscread, writeGoogle Search Console
orgread, writeOrganizations, members, teams
replayread, writeSession replay
sqlreadCustom SQL queries
ingestwriteSending 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.

PlanBurstDaily quota
Free / BasicAPI keys not available—
Standard50 requests per 10 seconds5,000 requests per day
Pro50 requests per 10 seconds25,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:

CredentialBudget
Organization key (rb_org_)The organization's. All of an organization's keys share it.
Personal keyYours. 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:

HeaderMeaning
X-RateLimit-Burst-Limit / -RemainingBurst bucket size and tokens left
X-RateLimit-Burst-ResetSeconds until the bucket is full again
X-RateLimit-Daily-Limit / -RemainingDaily quota and requests left today
X-RateLimit-Daily-ResetSeconds until 00:00 UTC
RateLimit-Limit / -Remaining / -ResetWhichever 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_date and end_date (both days inclusive; when end_date is today, the range ends now)
  • start_datetime and end_datetime
  • past_minutes_start and past_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_York

Exact 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_York

Relative query (last 60 minutes):

?past_minutes_start=60&past_minutes_end=0

Filter 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=12

Filter 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%5D

Error 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, not 404
  • 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"
}
FieldMeaning
errorHuman-readable error, kept for backward compatibility
codeMachine-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
messageHuman-readable explanation
resolutionWhat 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.

On this page