All docs

Start

  1. 01Getting started
  2. 02Install
  3. 03Tracking

Features

  1. 04The dashboard
  2. 05Goals
  3. 06Short links
  4. 07AI sources and agents
  5. 08Email reports
  6. 09Scheduled check
  7. 10Ask your AI

Reference

  1. 11Configuration
  2. 12HTTP API

Platforms

  1. 13Standalone server
  2. 14PHP
  3. 15Python
  4. 16WordPress
  5. 17Drupal
  6. 18Craft CMS
  7. 20Rails
  8. 21Ruby

Trust

  1. 19Privacy

Reference

HTTP API

Everything the dashboard shows is JSON at /runlight/api, for your own scripts and tools.

The dashboard is built on this API, so anything it shows you can fetch yourself. Send a token as a bearer.

bash

curl https://example.com/runlight/api/stats?period=30d -H "Authorization: Bearer $RUNLIGHT_TOKEN"

For scripts, make a read-only token in Settings, API and AI, and keep RUNLIGHT_TOKEN to yourself. A read-only token can read every report below, short links and each one’s clicks included. You can limit it to one site, and it stops working the moment you delete it. Endpoints that change something, along with the token, share, and mail endpoints, need RUNLIGHT_TOKEN itself (or your authorize check). The same tokens connect AI assistants, as Ask your AI explains.

A manage token belongs to a standalone server that shows this site. It reads like a read-only token and can also change one site’s goals, funnels, short links, link domains, email reports, and share links, along with its name, timezone, and retention, and get tickets for the element picker. It can never touch other sites, people, tokens, imports, or the mail service, and it cannot change where the site lives. It sees which mail service sends reports and from which address, and the service’s keys and account details stay hidden from it. It adds link domains and email reports only when the install knows its own address, through origin in routes() or RUNLIGHT_URL on the standalone server. The standalone server gets one through OAuth when you connect the site, so you rarely make one by hand.

Requests that change something must send content-type: application/json.

Reading reports

Every report takes the same query parameters.

Parameter Values
site The site id, defaulting to the first site.
period today, yesterday, 7d, 30d, 90d, month, last_month, year, 12mo, or all.
from, to Dates as YYYY-MM-DD, both inclusive, instead of a period.
compare previous (the default), year, custom, or off.
compare_from, compare_to The dates to compare with, for compare=custom.
filter dimension:op:value, where op is is, not, or contains. Repeat it to apply up to six.
interval hour, day, week, or month for series, chosen from the range by default.
Endpoint Returns
GET /api The name, version, and API version of the install, with no token needed.
GET /api/sites Every site, with when it last had a visit.
GET /api/stats Visitors, visits, pageviews, views per visit, bounce rate, and visit duration, with the comparison period’s.
GET /api/series The same numbers for each hour, day, week, or month of the range.
GET /api/breakdown?dimension= Rows for one dimension, with limit (up to 1000) and page.
GET /api/rhythm Visits by weekday and hour.
GET /api/realtime People on the site in the last 5 minutes, pages, sources, countries, pageviews per minute, and recent activity.
GET /api/goals Every goal with its conversions, converted visitors, rate, and revenue.
GET /api/goals/:id One goal with a series and its conversions by channel, source, and page.
GET /api/funnels Every funnel with how many visits reached each step, in order, within one visit.
GET /api/event-props?event= The property names sent with an event, and the values of one of them, chosen with key (the first by default) and limit (up to 1000).
GET /api/export A ZIP of CSV files for the view, with the numbers, the series, and every breakdown, taking the same parameters as the reports.
GET /api/journeys The paths visits take: the top pages at each step, the flows between steps, and the commonest paths. It takes steps (2 to 8), start and end pages, and through as step:page to follow one page.
GET /api/links Short links with their clicks.
GET /api/links/:id One link’s clicks over time, sources, countries, devices, and browsers.
GET /api/icon The site’s icon as an image, fetched from the site’s own domain, or a 404 when it has none. Share links can read it too.

breakdown and filter accept the dimensions page, entry, exit, hostname, event, referrer, source, channel, utm_source, utm_medium, utm_campaign, utm_term, utm_content, country, region, city, browser, browser_version, os, os_version, device, screen, and language. breakdown also takes ai_agent and ai_page.

Changing things

Endpoint Does
PATCH /api/sites/:id Change a site’s name, timezone, or retentionMonths (6, 12, 24, 36, or 60, or null to keep everything). On the standalone server, hostnames too.
POST /api/sites Add a site, when sites are managed in the dashboard as on the standalone server, from { "name", "hostnames", "timezone" }. To connect another install instead, send { "remote": { "url", "token" } }.
DELETE /api/sites/:id Delete a managed site and everything recorded for it, or disconnect a connected one.
POST /api/sites/connect Start connecting another install through its consent page, from { "url" }. It answers with authorize, the address to send the owner to. Add site with the install’s id for a site to offer that one first.
GET /api/sites/connect/done Where the consent page sends the owner back. It finishes connecting and opens the dashboard on the site.
POST /api/goals, PATCH /api/goals/:id, DELETE /api/goals/:id Add, change, or remove a goal.
POST /api/pick Makes a ticket for the element picker from { "origin" }, the dashboard’s own origin, which the picker sends its choice to and nowhere else. The ticket names the site too, and the picker does nothing on another site’s pages. A ticket works for half an hour. Owners only, and a manage token, whose tickets only ever name the hub it connected from.
POST /api/funnels, PATCH /api/funnels/:id, DELETE /api/funnels/:id Add, change, or remove a funnel, sent as { "name", "steps" } with two to eight steps of { "kind", "match" }, where kind is page or event.
POST /api/links, PATCH /api/links/:id, DELETE /api/links/:id Add, change, or remove a short link.
POST /api/links/import Add many short links at once from { "rows" }, up to 5,000 objects with url and, if you like, slug, name, and domain. It answers with how many it made and which rows failed.
POST /api/links/import/:source One step of an import from umami, dub, bitly, shortio, or rebrandly, sent as { "credentials", "cursor", "done" }. Send the cursor it answers with until it comes back null. Credentials are used and never kept.
POST /api/import/umami/websites List the websites in an Umami account, from { "credentials" }.
POST /api/import/umami/visits One step of bringing an Umami website’s visit history in, from { "credentials", "website", "cursor" }, repeated until cursor is null.
POST /api/import/csv/visits Writes up to 2,000 rows of a visits CSV, sent as { "rows": [...] } with each row an object of its columns, and answers { "pageviews", "events", "visits", "skipped" }. Send rows oldest first, with no moment split across two requests, since each request replaces what an earlier import wrote in its time span.
GET, POST /api/link-domains, DELETE /api/link-domains/:domain List, add, or remove custom link domains. A link domain must be a public name, and never the dashboard’s own or a site’s.
GET /api/link-domains/:domain/check Whether requests to a link domain reach Runlight, as { "domain", "working", "reason", "code", "target" }, where reason says what answered instead, code names it, and target gives the host a CNAME record should point to and the public addresses an A or AAAA record should use.
GET, POST /api/shares, PATCH, DELETE /api/shares/:id List, add, rename, or remove share links.
GET, PUT, DELETE /api/mail, POST /api/mail/test Read, set, remove, or test the mail service. Keys are never returned.
GET, POST /api/reports, DELETE /api/reports/:id, POST /api/reports/:id/send List, add, or remove report recipients, or send a sample.
GET or POST /api/check Runs the scheduled check.
POST /api/observe Records a page served to an AI agent, sent as { "url", "userAgent", "at" }, where at is when it was served (within the last week, as epoch milliseconds or an ISO date) and can be left out. Send up to 500 at once as { "fetches": [...] }. It accepts the token or an observe key, and the CMS plugins and the log reader call it.
GET /api/tokens Lists API tokens with each one’s name, site, scope, last four characters, and when it was last used. The tokens themselves are never returned.
POST /api/tokens Makes a token from { "name", "site", "scope" } and returns it once as secret. Leave site empty for every site. scope is read (the default) or manage, which needs a site.
GET /api/observe-key, POST /api/observe-key/new Read a site’s key for CMS plugins, made the first time it is asked for, or replace it with a new one, which stops the old one at once.
GET /api/token Says what the token sent with it may do, as { "scope", "site" }. A hub asks this before it offers to change anything.
DELETE /api/token Deletes the token sent with it. A hub does this when it disconnects a site or is given a new token.
DELETE /api/tokens/:id Deletes a token, which stops it working at once.
GET, PUT, DELETE /api/assistant Read, set, or remove the dashboard assistant’s { "provider", "model", "baseUrl", "key" }. The key is never returned. Owners only, though anyone at the dashboard can ask whether it is set up.
POST /api/assistant/models The models a provider offers, from { "provider", "baseUrl", "key" }, for the settings form. Leave key out to use the saved one, which works only for the same provider and address. Owners only.
POST /api/assistant/chat Ask the assistant, sending { "site", "messages", "view", "language" }, where messages are { "role", "content" } pairs ending with a question. It answers { "reply", "tools" }. People at the dashboard only, never API tokens or share links. Each person can ask thirty questions an hour and two at once, and each viewer the owner’s daily number, with 429 past either.
PUT /api/assistant/limits Set how many questions each viewer can ask a day, from { "viewerDaily" }, a whole number from 0 to 1,000. It starts at 50, and GET /api/assistant shows it to the owner and admins. The owner and admins only.
POST /mcp The MCP server for AI assistants, described in Ask your AI.

Errors

Errors are JSON, { "error", "code", "params" }. error says what went wrong in plain English, code names it, and params, when it has any, fill the placeholders in its words. The dashboard shows each code in its own language, and a script can rely on the code where the English may change.

Status When
400 Something in the request is wrong or missing.
401 The token is missing or not one this install knows.
403 A viewer or an API token tries a change it may not make.
404 What the request names is not there.
409 A name is taken, such as a link domain another site has.
413 More than 500 fetches at once to POST /api/observe.
415 A write that is not application/json.
429 Too many in a short time, such as sign-in tries, samples, questions to the assistant, or OAuth registrations from one address.
500 Something went wrong on the server.
502 A connected install could not be reached, answered with a redirect, or refused the token, or the AI service failed.
503 No token is set outside development.
504 A connected install took too long to answer.

These are the codes and the params each one fills. An error a connected install sends on through a hub keeps its code and params, and its error starts with the install’s host. A link domain’s check answers its reason with a check_ code in the same way.

Code Params What it says
account_exists email {email} already has an account.
admin_only Only the owner or an admin can change this.
assistant_address Enter the service’s address in Settings, AI Assistant.
assistant_address_bad Enter the service’s address, starting with https://
assistant_cancelled The question was cancelled.
assistant_daily limit Viewers can ask {limit} questions a day. Ask again tomorrow.
assistant_dashboard Only the dashboard can use the assistant.
assistant_failed detail The AI service did not answer as expected ({detail}).
assistant_invalid detail The assistant was not saved ({detail}).
assistant_key provider Enter your {provider} key.
assistant_limit Use a whole number from 0 to 1,000.
assistant_model Enter a model in Settings, AI Assistant.
assistant_no_models host {host} listed no models. Type the model’s name instead.
assistant_provider Choose an AI service in Settings, AI Assistant.
assistant_refused detail, host {host} turned the request down ({detail}).
assistant_slow That question took too long to answer. Try asking something narrower.
assistant_soon You have asked a lot in a short time. Wait a little and ask again.
assistant_status host, status {host} answered {status}.
assistant_steps The assistant needed too many steps for that question. Try asking something narrower.
assistant_timeout host {host} took too long to answer.
assistant_unset The assistant is not set up yet. An owner can set it up in Settings, AI Assistant.
check_https it could not be reached over HTTPS
check_not_public it is not a public domain name
check_not_runlight something answered, but not Runlight
check_status status it answered {status}
check_timeout it took too long to answer
code_wrong That code is not right. Check the time on your phone and try the next one.
compare_bad compare "{compare}" is not a comparison Runlight knows.
compare_range_bad Pick both dates to compare with.
connect_again Connect this site again to change it from here.
connect_denied The connection was not allowed.
connect_endpoints url {url} named endpoints on another address.
connect_expired That connection took too long or was already used. Start again.
connect_failed detail Connecting did not start ({detail}).
connect_not_runlight url {url} did not answer like a Runlight install.
connect_old url {url} runs an older Runlight. Update it, or connect it with an API token from its Settings.
connect_refused The install would not let this server connect. Start again, or connect it with an API token.
connect_register reason, url {url} would not let this server connect. {reason}
connect_token The install did not give this server a token. Start again.
connect_url Enter the install’s address, like https://example.com/runlight
domain_in_use domain {domain} is where this dashboard or one of your sites lives. Use a separate domain or subdomain for short links, such as go.{domain}.
domain_invalid That is not a domain name.
domain_not_public domain {domain} is not a public domain name. Use one that browsers anywhere can reach.
domain_taken domain {domain} already belongs to another site.
email_invalid Enter an email address.
event_needed Name the event.
filter_bad filter The filter "{filter}" is not one Runlight reads.
filters_max max Use at most {max} filters at once.
funnel_exists name There is already a funnel called "{name}".
funnel_invalid detail The funnel was not saved ({detail}).
funnel_long A funnel has at most eight steps.
funnel_name Give the funnel a name.
funnel_page_bad match "{match}" is not a path or a URL.
funnel_short A funnel needs at least two steps.
goal_amount Enter an amount, like 49 or 9.99.
goal_click_taken match The click goal "{match}" already sends events with that name.
goal_currency Use a three-letter currency code, like USD or EUR.
goal_event Enter the event’s name.
goal_event_taken name An event goal already counts events called "{name}", so give this click goal another name.
goal_exists name There is already a goal called "{name}".
goal_invalid detail The goal was not saved ({detail}).
goal_kind Pick what the goal counts, an event, a page visit, or a click.
goal_link Enter the link’s address, like https://buy.stripe.com/*
goal_name Give the goal a name.
goal_page Enter a page path, like /thanks or /blog/*
goal_page_bad That page is not a path or a URL.
goal_prop_kind Only an event goal can take its amount from the event. Use a fixed amount instead.
goal_prop_name A property name uses letters, numbers, dots, dashes, and underscores.
goal_selector Enter a CSS selector, like #signup or .buy-button
hub_domains A connected hub cannot change a site’s domains.
icon_none This site has no icon.
import_csv_batch max Send at most {max} rows at a time.
import_csv_format This file is not an Umami export or in Runlight’s visit format. Check its header row.
import_day_full limit One day has more than {limit} events, more than an import step can read.
import_failed detail The import stopped ({detail}).
import_key service Enter your {service} key.
import_refused The key or sign-in was refused.
import_slug_bad slug /{slug} has characters Runlight slugs cannot use.
import_slug_taken name, slug /{slug} is already used by "{name}".
import_source source Runlight cannot import from {source}.
import_status host, status {host} answered {status}.
import_umami_address Enter your Umami address, like https://stats.example.com
import_umami_login Enter an API key, or a username and password.
import_website Pick the Umami website to import.
install_refused That install refused the token.
install_token Enter an API token from that install.
internal Something went wrong on the server. Try again.
invite_gone This invite has expired or was already used. Ask for a new one.
link_domain domain Add {domain} as a link domain in Settings first.
link_long The destination is longer than 2,000 characters.
link_no_slug Could not find a free slug. Try again.
link_protocol The destination must start with http:// or https://
link_slug A slug is letters, digits, dashes, and underscores, up to 100.
link_taken slug /{slug} is already taken.
link_url The destination must be a full URL, starting with https://
mail_failed detail The email could not be sent ({detail}).
mail_field field {field} is needed.
mail_from Enter the address reports come from, like reports@example.com.
mail_https The webhook URL must use https.
mail_in_code The mail service is set in code, so it cannot be changed here.
mail_option field, options {field} must be one of {options}.
mail_refused detail, host {host} turned the email down ({detail}).
mail_region That is not an AWS region, like us-east-1.
mail_service Pick a mail service.
mail_slow host {host} took too long to answer, so the email was not sent.
mail_unreachable host Could not reach {host}.
mail_unset Set up a mail service first.
method_not_allowed That cannot be done here.
not_found That is not here.
observe_many Send at most 500 fetches at a time.
observe_url Send the page’s address.
origin_needed Set this Runlight’s own address first (RUNLIGHT_URL on the server, or origin in routes()), so a connected hub can add link domains and email reports.
owner_hand_over Only the owner can hand over ownership.
owner_needs_admin Make them an admin first.
owner_only Only an owner can change this.
owner_protected Nobody but the owner can change or remove the owner.
password_current_wrong Your current password is not right.
password_short min Use a password of at least {min} characters.
password_wrong Your password is not right.
people_owner Only the owner or an admin can manage people.
pick_hub This hub’s address is not the one it connected from. Connect the site again from here.
pick_origin Send the dashboard’s origin, such as https://stats.example.com
property_bad That is not a property name Runlight can read.
question_needed Ask a question.
range_bad Those dates are not a range Runlight can read.
redirected host {host} answered with a redirect.
remote_slow host {host} took too long to answer. Try a shorter range.
remove_self You cannot remove yourself.
report_exists email {email} already gets this report.
report_limit A site can send to at most 50 addresses.
retention_bad months Keep visits for {months} months, or forever.
role_needed Pick admin, member, or viewer.
rows_needed Send the links as a list of rows.
sample_soon A sample went out a moment ago. Wait a minute and try again.
sample_soon_hub A connected hub can send one sample every ten minutes. Wait a few minutes and try again.
send_json Send this as JSON.
send_object Send a JSON object.
share_gone This share link no longer works.
share_not_available That is not available on a shared dashboard.
sign_in Sign in first.
site_domain_invalid host "{host}" is not a domain name.
site_domain_needed Add the site’s domain, like example.com
site_domain_taken host, site {host} already belongs to {site}.
site_invalid detail The site was not saved ({detail}).
site_name A site name is 1 to 80 characters.
site_remote This site is counted by its own Runlight. Connect it again from its settings to change it from here.
sites_in_code Sites are set in code here, so they are changed there.
smtp_starttls The SMTP server does not offer STARTTLS. Pick TLS or none for Security.
test_email Enter an email address to send the test to.
token_manage_only A manage token changes only its own site’s settings.
token_name Name the token.
token_read_only API tokens can only read.
token_refused host {host} refused the token. Connect it again from the site’s settings.
token_site A token that changes settings is for one site. Pick the site.
token_unset Set RUNLIGHT_TOKEN, or pass token or authorize to routes(). Without one, Runlight only runs open when NODE_ENV is development.
too_many_tries Too many tries. Wait fifteen minutes and try again.
twofactor_off Turn on two-factor sign-in first.
twofactor_restart Too many wrong codes. Start turning on two-factor sign-in again.
twofactor_self Turn off your own two-factor sign-in under Account.
unauthorized Sign in or send a token to see this.
unknown_account That account no longer exists.
unknown_dimension dimension "{dimension}" is not a breakdown Runlight knows.
unknown_domain That domain is not one of this site’s.
unknown_funnel That funnel no longer exists.
unknown_goal That goal no longer exists.
unknown_invite That invite no longer exists.
unknown_link That link no longer exists.
unknown_report That report no longer exists.
unknown_share That share link no longer exists.
unknown_site That site is not here.
unknown_timezone timezone "{timezone}" is not a timezone Runlight knows.
unknown_token That token no longer exists.
unreachable host Could not reach {host}.

Edit this page on GitHub