Lodd REST API Reference
Base URL: https://api.lodd.dev/v1
OpenAPI spec: lodd.dev/openapi.yaml
Version: 2026-05-08
Authentication
Every request requires an API key. Pass it as either:
X-API-Key: your-api-key
or:
Authorization: Bearer your-api-key
Get an API key by signing up at lodd.dev or by calling the POST /keys endpoint after authenticating via OAuth.
Response format
Success -- all responses wrap the result in a data envelope:
{
"data": { ... },
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
Error -- structured error object:
{
"error": {
"type": "api_error",
"code": "invalid_request",
"message": "site_id parameter is required"
},
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
Response headers
Every response includes:
| Header | Description |
|---|---|
X-Request-Id |
Unique request identifier |
X-API-Version |
API version (2026-05-08) |
X-RateLimit-Limit |
Requests per hour (1000) |
X-RateLimit-Remaining |
Remaining requests |
X-RateLimit-Reset |
Unix timestamp when limit resets |
Quick start
1. List your sites
curl -H "X-API-Key: YOUR_KEY" \
https://api.lodd.dev/v1/sites
2. Get analytics for a site
curl -H "X-API-Key: YOUR_KEY" \
"https://api.lodd.dev/v1/sites/SITE_ID/analytics?start_date=2026-04-01&end_date=2026-04-30"
3. Create an annotation
curl -X POST -H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Deployed v2.0"}' \
https://api.lodd.dev/v1/sites/SITE_ID/annotations
Common query parameters
These parameters are supported on most analytics and breakdown endpoints:
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date |
ISO 8601 date | 30 days ago | Start of date range |
end_date |
ISO 8601 date | Now | End of date range |
limit |
Integer | 10 | Max results (max 1000) |
timezone |
IANA timezone | UTC | Timezone for date bucketing |
Filter parameters
Optional filters supported on analytics, timeseries, pages, sources, events, funnels, and entry/exit pages:
| Parameter | Example | Description |
|---|---|---|
filter_country |
US |
2-letter country code |
filter_browser |
Chrome |
Browser name |
filter_os |
macOS |
Operating system |
filter_device_type |
mobile |
desktop, mobile, or tablet |
filter_utm_source |
twitter |
UTM source value |
filter_referrer_contains |
google |
Substring match on referrer |
Breakdown endpoints exclude the filter matching their own dimension (e.g. GET /countries ignores filter_country).
Endpoints
Sites
GET /sites
List all sites you have access to.
Response:
{
"data": [
{
"id": "uuid",
"name": "My Site",
"domain": "example.com",
"tracking_secret": "ts_...",
"created_at": "2026-01-15T10:00:00Z"
}
],
"request_id": "uuid"
}
POST /sites
Create a new site.
Request body:
{
"name": "My Site",
"domain": "example.com"
}
Response: the created site object including tracking_secret.
GET /sites/resolve?domain=example.com
Resolve a domain to its site UUID. The domain is normalised (strips www., protocol, and trailing paths).
Response:
{
"data": {
"id": "uuid",
"name": "My Site",
"domain": "example.com",
"created_at": "2026-01-15T10:00:00Z"
},
"request_id": "uuid"
}
Analytics
All analytics endpoints require :id (site UUID) in the path and support start_date, end_date, timezone, and filter parameters unless noted otherwise.
GET /sites/:id/analytics
Aggregate stats for a date range.
Response data:
{
"total_page_views": 12450,
"unique_visitors": 3200,
"unique_countries": 42,
"average_duration": 124.5,
"pages_per_visit": 2.3,
"bounce_rate": 0.45
}
GET /sites/:id/snapshot
Today vs yesterday comparison. No date range parameters -- always returns the current snapshot.
Additional parameters: timezone
Response data:
{
"today_visitors": 120,
"yesterday_visitors": 95,
"visitor_change_percent": 26.3,
"top_referrer": "google.com",
"top_country": "US",
"average_duration_today": 132.0,
"average_duration_yesterday": 118.5,
"duration_change_percent": 11.4
}
GET /sites/:id/timeseries
Visitor and page view counts bucketed over time.
Additional parameters:
| Parameter | Default | Description |
|---|---|---|
interval |
day |
Bucket size: day or hour |
Response data:
[
{
"date_label": "2026-04-01",
"page_views": 420,
"unique_visitors": 180
}
]
GET /sites/:id/realtime
Current active visitors (last 5 minutes). No date range parameters.
Response data: an integer (number of active visitors).
GET /sites/:id/performance
Page load time metrics.
Additional parameters:
| Parameter | Default | Description |
|---|---|---|
group_by |
page |
Group by: page, device, country, or browser |
Response data: array of objects with avg, median, and p95 load times.
Breakdowns
All breakdown endpoints support start_date, end_date, limit, timezone, and filter parameters.
GET /sites/:id/pages
Top pages with engagement metrics.
Additional parameters:
| Parameter | Description |
|---|---|
url_pattern |
Filter to pages matching this substring |
Response data: array of pages with views, bounce rate, and average duration.
GET /sites/:id/sources
Traffic sources with engagement metrics (referrers, UTM, trackable links combined).
GET /sites/:id/countries
Visitors by country with engagement metrics.
GET /sites/:id/browsers
Browser breakdown.
GET /sites/:id/operating-systems
Operating system breakdown.
GET /sites/:id/devices
Device type breakdown (desktop, mobile, tablet).
GET /sites/:id/entry-exit-pages
Where sessions start and end.
GET /sites/:id/bots
Bot and crawler traffic report. Does not accept filter parameters.
Events
GET /sites/:id/events
Raw event records.
Additional parameters:
| Parameter | Description |
|---|---|
event_name |
Filter to a specific event name |
Response data:
[
{
"event_name": "signup",
"url": "https://example.com/pricing",
"properties": { "plan": "pro" },
"timestamp": "2026-04-15T14:30:00Z"
}
]
GET /sites/:id/event-counts
Totals per event name.
Response data:
[
{
"event_name": "signup",
"count": 45,
"unique_sessions": 42
}
]
GET /sites/:id/event-timeseries
One event plotted over time. Requires event_name.
Additional parameters:
| Parameter | Required | Default | Description |
|---|---|---|---|
event_name |
Yes | -- | Event to chart |
interval |
No | day |
day or hour |
Conversions and funnels
GET /sites/:id/funnel or POST /sites/:id/funnel
Multi-step conversion funnel.
Steps can be provided as a steps query parameter (JSON-encoded) or as a POST body:
{
"steps": [
{ "type": "pageview", "match": "/pricing" },
{ "type": "pageview", "match": "/checkout" },
{ "type": "event", "match": "purchase" }
]
}
Response data: per-step session counts and conversion rates.
curl -X POST -H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"steps": [{"type":"pageview","match":"/pricing"},{"type":"event","match":"signup"}]}' \
"https://api.lodd.dev/v1/sites/SITE_ID/funnel?start_date=2026-04-01"
GET /sites/:id/conversion-pages
Pages that lead to conversions. Requires event_name.
| Parameter | Required | Description |
|---|---|---|
event_name |
Yes | Conversion event to analyse |
GET /sites/:id/source-conversions
Traffic sources ranked by conversion rate. Requires event_name.
| Parameter | Required | Description |
|---|---|---|
event_name |
Yes | Conversion event to analyse |
Actors
Privacy-safe visitor segments. Actor identifiers are opaque hashes, not user IDs.
GET /sites/:id/actors
Active actors with event counts, first/last seen timestamps.
GET /sites/:id/actors/:actor/activity
Full event timeline for one actor.
GET /sites/:id/actors/retention
Weekly cohort retention table.
Annotations
GET /sites/:id/annotations
List annotations within a date range. Supports start_date, end_date, limit.
Response data:
[
{
"id": "uuid",
"content": "Deployed v2.0",
"timestamp": "2026-04-15T12:00:00Z",
"created_at": "2026-04-15T12:01:00Z"
}
]
POST /sites/:id/annotations
Create an annotation.
Request body:
{
"content": "Deployed v2.0",
"timestamp": "2026-04-15T12:00:00Z"
}
timestamp is optional; defaults to now. content is required, max 500 characters.
Trackable links
Campaign attribution with per-click tracking. Append ?ld=<code> to the destination URL when sharing.
GET /sites/:id/links
List trackable links with click counts.
Additional parameters:
| Parameter | Default | Description |
|---|---|---|
status |
active |
active, archived, or all |
POST /sites/:id/links
Create a trackable link.
Request body:
{
"destination_url": "https://example.com/landing",
"source_type": "linkedin",
"source_label": "May campaign"
}
destination_url is required and must start with http:// or https://.
GET /links/:code/clicks
Click data for a specific link by its short code.
Response data:
{
"link": {
"id": "uuid",
"short_code": "abc123",
"destination_url": "https://example.com",
"source_type": "linkedin",
"click_count": 42
},
"clicks": [
{ "clicked_at": "2026-04-15T14:30:00Z", "referrer": "linkedin.com", "country": "US" }
],
"total_clicks_in_period": 42
}
IP exclusion
POST /sites/:id/exclude-ip
Exclude an IP address from tracking for this site.
Request body:
{
"ip_address": "1.2.3.4"
}
ip_address is optional. If omitted, the API auto-detects the caller's IP. The IP is hashed before storage -- it cannot be reversed.
Account
GET /usage
Current month's event usage.
Response data:
{
"plan": "free",
"used": 1250,
"limit": 2500,
"month": "2026-04",
"percent": 50
}
GET /keys
List all API keys. The response includes a is_current flag marking the key used to make this request.
Response data:
[
{
"id": "uuid",
"prefix": "ab12cd34",
"name": "production",
"created_at": "2026-01-15T10:00:00Z",
"last_used_at": "2026-04-15T14:30:00Z",
"revoked_at": null,
"is_current": true,
"status": "active"
}
]
POST /keys
Create a new API key. The raw key is returned only once -- save it immediately.
Request body:
{
"name": "production"
}
name is optional (max 100 characters).
Response (201):
{
"data": {
"api_key": "the-raw-key-save-this",
"id": "uuid",
"prefix": "ab12cd34",
"name": "production",
"created_at": "2026-01-15T10:00:00Z",
"warning": "Save this key now — it cannot be retrieved again."
},
"request_id": "uuid"
}
POST /keys/:id/revoke
Revoke an API key. You cannot revoke the key currently in use.
Error codes
| Code | HTTP Status | Description |
|---|---|---|
auth_required |
401 | No API key provided |
invalid_api_key |
401 | Key not found or expired |
access_denied |
403 | Key does not have access to this site |
invalid_request |
400 | Missing or invalid parameters |
usage_limit_reached |
402 | Monthly event limit hit |
route_not_found |
404 | Unknown endpoint |
rate_limit_exceeded |
429 | Too many requests this hour |
upstream_unavailable |
502 | Analytics service temporarily down |
Webhooks
Register a URL to receive event-driven notifications. Lodd signs every payload with HMAC-SHA256 so you can verify authenticity.
Events
| Event | Description |
|---|---|
usage.threshold |
Usage crossed 50% or 75% of monthly limit |
usage.limit_reached |
Monthly event limit hit (tracking paused) |
Manage webhooks
# Register a webhook (returns a signing secret — save it)
curl -X POST https://api.lodd.dev/v1/webhooks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.com/webhook", "events": ["usage.threshold", "usage.limit_reached"]}'
# List webhooks
curl https://api.lodd.dev/v1/webhooks -H "X-API-Key: your-api-key"
# Delete a webhook
curl -X POST https://api.lodd.dev/v1/webhooks/{id}/delete \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"webhook_id": "webhook-uuid"}'
Payload format
{
"event": "usage.threshold",
"data": { "plan": "free", "used": 1875, "limit": 2500, "percent": 75, "month": "2026-05" },
"timestamp": "2026-05-08T14:30:00Z"
}
Verifying signatures
Every delivery includes an X-Lodd-Signature header. Verify it by computing HMAC-SHA256 of the raw request body using your webhook secret:
const crypto = require("crypto");
const signature = crypto.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
if (signature !== req.headers["x-lodd-signature"]) {
throw new Error("Invalid signature");
}
Failed deliveries are retried up to 3 times. Webhooks are automatically deactivated after 10 consecutive failures.
Rate limits
1,000 requests per hour per API key. The window resets on the hour (e.g. 14:00–15:00 UTC).
Every response includes:
X-RateLimit-Limit— requests allowed per window (1000)X-RateLimit-Remaining— requests left in the current windowX-RateLimit-Reset— Unix timestamp when the window resets
When you exceed the limit, the API returns 429 Too Many Requests with a Retry-After header.
Pricing
Free: 2,500 events/month (page views + custom events combined, across all sites).
Paid: 100,000 events/month for EUR 9.99/month. All features available on both tiers.
Last updated: 2026-05-08