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 window
  • X-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