# Getting started > Skald docs: https://skaldanalytics.com/docs/getting-started/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). What Skald counts, how the sections and tabs are laid out, and how to choose a period, filter, drill down, compare and export. To look around first, open the [live demo](https://spcaeo.github.io/skald/): the whole dashboard with two months of sample data for a made-up site. No sign-up. Saving and the settings pages need a running Skald, so in the demo they say so. ## New here? Four short guides take you from nothing to a working install: - [Docker: first install in 10 minutes](https://skaldanalytics.com/docs/docker-first-install.md): run it, sign in, fill the history, check it with the doctor. - [Pick your road](https://skaldanalytics.com/docs/pick-your-road.md): which way your leads and bookings should reach Skald, for your kind of site. - [Send leads from your backend in 5 minutes](https://skaldanalytics.com/docs/leads-from-your-backend.md): the signed lead hook, with copy-paste examples. - [Demo bookings](https://skaldanalytics.com/docs/demo-bookings.md): Calendly and Cal.com, embedded, linked or by webhook, and keeping only your own event types. ### Set up your site For owners and admins, a new site's Overview > Summary starts with a **Set up** list named for the site, for example **Set up example.com**. Each line says in one plain sentence what works or what is left. A line that is left has a **Set up** button: it opens a short dialog that walks you through the fix, step by step. Each line ticks itself once the server's own checks see it working. | Line | Done when | What Set up does | | --- | --- | --- | | Logs arriving | The server's access log is read | Opens **Send this site's visits to Skald**, as in Settings > Connections: choose how, a sending token, install, test | | Visits count as people | Browsers load the pages' own scripts and styles | Asks what the site is built with (Next.js, WordPress, something else, or files from a CDN) and saves it | | Countries shown | Visits carry a country | Says what a country file adds, and writes the mail for whoever runs your Skald server | | Script tag events (optional) | The script tag's events arrive | What the tag adds; the exact tag for this site with Copy, and where it goes (site builder or plain HTML, WordPress, Next.js); **Send these steps to your developer** or **Share with your developer**; a Test that waits for the first event | | Lead hook tested (optional) | A hook passed a test or received a lead, and no hook's last real lead was refused | Where your leads come from (your own form or backend, Zapier or Make, Calendly, Cal.com, Typeform, Stripe, HubSpot); makes the secret; the steps with the address and secret filled in; **Send a test lead**, then waits for a real one | | Mail recipient confirmed | The daily or weekly mail is on, with a recipient who opened the confirm link | Turns the mail on, takes the addresses, shows who still has to confirm, and sends a test mail | | Privacy settings chosen | Someone chose whether IP addresses are kept | The three IP choices (full, shortened, none), leads' names and emails, then the wording for your privacy notice with Copy | The server's own technical words (config keys, commands) are not on the line. Where there are some, they are folded under **For your server admin**. The mail to your developer opens in your own mail program. For the script tag it holds the whole tag: the tag needs no secret. For a lead hook it holds the address and the steps but never the secret: give the secret separately, never by mail or chat. A hook whose last real lead was refused (for example, its signature did not match) is never ticked: the line says leads are not arriving, and the fix is under **For your server admin**. A test that Skald itself sends (Send a test, or `ti.py hooks test`) and that fails is a failed test, never a refused lead. On Overview the list goes away once every line that is not optional is done. **Hide on Overview** hides it there, for this site, in this browser. The same list is always at the top of **Settings › Connections**, even when every line is done (then it shows every tick and **All set**). To show it on Overview again, search for `setup` and choose **Setup checklist**. Each Set up dialog has its own link, with its step: for example `#overview/summary?setup=rsc&step=2`. Send it to a colleague who is an owner or admin, and it opens the same dialog at that step (or at the first step that still needs a choice). ## What Skald counts Skald reads your web server's own access log. Every request the server answers is one line in that log, so Skald sees every visit, including the ones a browser script never sees: AI assistants, search engines and bots. Each request lands in exactly one group, and the groups add up to the log exactly: | Group | What is in it | | --- | --- | | People | A browser that is none of the groups below | | AI assistants | AI crawlers that collect for training, crawlers that index for AI search, fetches made because someone asked a question, and people who followed a link from an AI answer | | Search engines | Classic indexing, such as Google and Bing | | Bots and scanners | Monitors, SEO tools, link previews from chats, and probes | | Our own | Your team, your own crawler and your tests. Kept apart, never counted as people | Keep three words apart: - **Requests** are lines in the log. One page can make many requests. - **People** are browsers that look like real readers. This is an upper bound: when in doubt, a request is not counted as a person. - **Machines** are everything else: AI crawlers, search engines and bots. They are shown in their own units, never added to people. AI traffic is not people. A person who clicks a link inside an AI answer is counted as a person, with the AI assistant as their source. A browser that reads too fast is not a person either. A visit that loads 6 pages or more, all inside 2 seconds, is counted under bots as "too fast to be a person". A quick reader clicking 3 pages in 2 seconds is still a person. A site can change both numbers (`site.too_fast_pages`, `site.too_fast_seconds`). Two kinds of tool also send a browser's name, and both are bots: - **A scanner trying many paths.** A visitor that asks for 5 or more different scanner paths your site does not have (`/info.php`, `/wp-login.php`, `/admin/` and the like, each answered "not found") is a scanner for that whole day, with every page it read between them. One or two missing paths are not enough: a person can mistype. - **A link checker.** A browser opens a page with an ordinary request. A visit that asks for two or more pages with `HEAD` (a request for the headers only) is a tool checking links, and every request of that visit is a bot. What Skald does not do: call a visit a bot because it was short. Real people open one page and leave. A visit from a hosting or cloud network is flagged, not moved, because people on a VPN come from there too. **A visit's pages.** A visit shows the pages it loaded from your server and the steps seen in the browser, both: "3 pages (2 seen in the browser)". A step seen in the browser is a page shown from the browser's cache, or a click your site handled without loading a page. Skald sees it from the next request's referrer. It is drawn dashed in the journey, and it is never counted as a page view. On **Site › Requests**, the script tag's own events to Skald (`/traffic-beacon`) say **script tag event**, and calls to Skald's hooks or log upload say **Skald endpoint**. Only a POST your own pages sent is a **form post**. ### The optional script tag If your site adds the `skald.js` script tag, Skald also counts what happens inside a page: button clicks, form steps, demo bookings, links out, and visitors who reach three quarters of a page. These show on Leads & sales > Events. The log's figures never depend on the tag. See [Tracking script](https://skaldanalytics.com/docs/tracking-script.md). ## The menu The sidebar has six sections. Each section has tabs along its top. | Section | Tabs | | --- | --- | | Overview | Summary, Real time, Insights | | Traffic | Visitors, Sources, Pages, Places & devices, Search, Retention, Time | | Leads & sales | Leads, Summary, Sources, Calendar bookings, Funnels, Events, Find a person | | AI & bots | Assistants, Crawlers, Crawl health, AI answers, Crawler rules, llms.txt | | Security | Is anything exposed?, Your stack, Who is scanning us?, Attack attempts, Fake browsers, Block list, Alerts | | Site | Health, Alerts, Requests, Not found, Speed, Data quality | - **Overview > Summary** is where every visit starts: the period's five key figures (leads among them) with their change, what changed, and whether to work on a page or on its traffic. See [Summary and Pages: is it working?](https://skaldanalytics.com/docs/getting-started.md#summary-and-pages-is-it-working). - **Overview > Real time** is who is on the site now, on a live world map. See [Real time](https://skaldanalytics.com/docs/real-time.md). - **Overview > Insights** lists what is working, what dropped, and what is new or gone, leads too. See [Insights](https://skaldanalytics.com/docs/getting-started.md#insights). - **Traffic** is who came, from where, to which pages, on what, and when; **Search** adds Google's and Bing's own queries ([Search](https://skaldanalytics.com/docs/search.md)) and **Retention** how many came back ([Retention](https://skaldanalytics.com/docs/retention.md)). A source of **Direct** means no referrer and no campaign tag. A country, device or touch the log did not carry says **Not recorded**. - **Leads & sales** is what the visits brought: leads by stage, demo bookings, funnels, events, and one person's story. - **AI & bots** is AI assistants (the pages they fetch to answer a question, and the visitors they send), crawlers, how crawlers find your pages ([Crawl health](https://skaldanalytics.com/docs/crawl-health.md)), what the assistants answer about you ([AI answers](https://skaldanalytics.com/docs/ai-answers.md)), which crawlers may come in, and your llms.txt. - **Security** is who probes your server and what they tried, from the same log: whether any probe got a real file, scanners, attack attempts, fake browsers, a block list to review, your site's stack against known advisories, and security alerts. See [Security](https://skaldanalytics.com/docs/security.md). - **Site** is the server: what it answered, open alerts, the request log line by line, pages not found, page speed and the data's own checks. The address names the section and the tab, for example `#traffic/sources`. Older links, such as `#sources` or `#live`, still open the right tab. ### Every view has its own link Everything you open is in the address, so you can copy it from the address bar and send it, bookmark it, or reload the page and come back to the same place: - the section and tab, the period, page group, filters and drill-down - each table's sort, column filters, columns, search and page, and a tab's own switches (for example All or AI on Site > Alerts, or the engine and period on Traffic > Search) - the drawer or dialog that is open, with what it shows: a visitor, a lead, a request, a cohort, a search row, a Set up or Connections dialog and its step, the saved views, the report builder, the shortcuts Opening a drawer or a dialog adds one step to the browser's history, so **Back** closes it before it leaves the page. Closing it takes it out of the address. A link opens what it names only for someone allowed to see it. When it names something you may not see, or something no longer kept, the page opens with one plain line instead, for example "That visitor is no longer kept (journeys are kept 7 days)" or "Not available to your role." No address ever holds a visitor's IP address, an email or a person search. Where one would be (an address filter on Site > Requests, a drill-down by address, a person search), the link holds a short code instead that only your own browser can read back. Opened in another browser, it says so in one line. Every key the address can hold is listed in [docs/ROUTES.md](https://github.com/spcaeo/skald/blob/main/docs/ROUTES.md). ### The trail and Back When you go deeper (Summary, then a source, then a page, then a visitor), a line over the tab shows the path you took: **← Back to** the step before, then each step. Each step is a link to that exact view, with its period, filters, sort and what was open. The last one is where you are; its copy and open icons copy or open this exact view. - The browser's **Back** and **Forward** go through the same steps, and put the page back where you had scrolled it. - Going back to a step on the trail (with Back, a step or a link) cuts the trail back to it. Moving along a section's tabs does not add steps. - The trail belongs to this browser tab. A reload keeps it; closing the tab forgets it. Nothing is sent anywhere. - A link someone sent you starts its own short trail from the link: the section and tab, then what it opens (for example **Traffic › Pages**, then **/pricing/**). - On a phone the line shows **← Back to** and where you are. ### Copy a page's link, or open it; copy any value Wherever a page of your site or a web address is shown (the Pages and Landing pages tables, referring sites, requests, Search pages, AI and crawler pages, pages not found, a visitor's journey, a lead's landing page, the Live lists, the set-up dialogs and the site switcher), pointing at it or moving to it with Tab shows two small icons at its end: - **Copy** copies the full address, with your site's own address in front of a path (`/pricing/` becomes `https://example.com/pricing/`). The icon turns into a green tick for a moment. If your browser does not allow copying, it says **Copy failed: select and copy**. - **Open in a new tab** opens the full address in a new tab. Any other value you may want to paste somewhere else shows the **Copy** icon alone, the same way: an IP address, a user agent, an email, a name, a visitor ID or a lead's reference, a search query, a referring site, a campaign or UTM tag, a crawler or an AI assistant, a network, a host, a reason or an error, a token's name, a robots.txt line or a suggested server line. It copies the whole value, even where the cell shows a shortened one (a long user agent). Labels, headings, numbers and percentages have none. On a phone or a tablet the icons are always shown. They never open the row's own detail: the row still does that when you select it elsewhere. Two switches sit in the top bar on every page: - **Today · Live** sets every tab to today so far, redrawn every 2 minutes. On Overview > Summary it shows today live: visitors on the site now, today's figures against yesterday at the same time of day, the map and what visitors are doing now (the newest visitors and their pages), sources and the flow, what converted and AI & bots, each linking to its full tab ([Today, live](https://skaldanalytics.com/docs/real-time.md#today-live)). Turn it off to go back to the period you had. - **Compare with…** puts another period beside this one. See Compare, below. When your site reports leads (a form, a booking), they show beside the visitors on Overview > Summary, Traffic > Sources, Traffic > Pages, Traffic > Places & devices and Overview > Insights, not only under Leads & sales. See [Leads on every page](https://skaldanalytics.com/docs/leads-and-conversions.md#leads-on-every-page). **Settings**, **Help** and **About** sit at the foot of the sidebar. Settings (the gear) has its own menu. See [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md). The round **You** button beside the gear holds your own choices: - **Theme**: System, Light or Dark. - **Time zone**: show times in the site's zone (the default), your own zone, or UTC. A time reads the same way everywhere: how long ago, then the moment, such as "3 h ago (18 Oct 13:43 EDT)". - **Your account**, **All settings**, **Help**, **Keyboard shortcuts**, **What's new**, **About Skald** and **Plain page** (every figure, without scripts). - **Sign out**. On a phone, five sections sit in the bottom bar, and a section's tabs scroll sideways. **Menu** in the bottom bar opens Security, Settings, Help, About and your own choices. ## Cities Where is that visitor coming from? Countries come from your log, or from a country file. Cities need a **city file**: DB-IP's free "IP to City Lite" (a new one each month, free under CC BY 4.0, about 120 MB). Point Skald at it with `logs.city_db` in the config, or `SKALD_CITY_DB` in Docker (for example `/geoip/dbip-city-lite.mmdb` with `-v /usr/share/GeoIP:/geoip:ro`). `tools/update-dbip-city.sh` downloads it once a month. With the file set: - **Traffic > Places & devices** has a **Cities** table under the countries: visitors by city, each with its country's flag. Search it, sort it, download it, or select a city for its detail (its trend and the visits behind it). Choosing a country in Filters keeps only that country's cities. - **Each visit** (the visit list, a visit's panel, a person's visits and Real time's newest visits) says where it came from: "Toronto, Ontario, Canada". - **Real time**'s map puts a dot on each city with visitors now. See [Real time](https://skaldanalytics.com/docs/real-time.md#cities-on-the-map). A city is counted for visitors only, never for bots, at the first page of the visit. It is the city's own point, not the visitor's street, and it is a lookup, not a guess: when the file has no city for an address, or names a city in another country than the visit's, the city says **Not recorded**. Cities start on the day the file is set; a period that reaches back before it says how many of its days have them. "IP Geolocation by DB-IP" is shown under the countries and cities, as DB-IP's licence asks. ## Choosing a period The date range sits in the top bar: **Today · Live**, **Yesterday**, **7 days**, **30 days**, **90 days**, **All time** or **Custom**. Every figure on the page follows the period. Each section opens on its own default when the link names no period: **Leads & sales** on the last 7 days (a lead is rarer than a visit, so today is often empty), every other section on Today. A period you pick is kept in the address and stays as you move between tabs and sections; the default of one section is never carried into another, so moving from Overview's Today to Leads & sales opens on 7 days. **All time** runs from the first day this site has data (its first recorded day, or its first lead when that is earlier) to the last whole day, and the period line says the day it starts: "All time: since Fri 21 Aug 2026, 62 days". The dashboard holds the last 180 days; when your site's data begins before that, the line says so ("All time held here: the last 180 days ... this site's data begins ..."), and no total counts days it does not have. A few tabs keep a period of their own, so the date range is not shown there. A fixed **Period** chip takes its place and says the tab's own period: **Now** on Real time, each insight's own two periods on Insights, now and the last 30 days on Alerts, the last few hours on Requests, the rules as they are now on Crawler rules. Settings pages have no period at all. A part of a tab with a shorter period of its own says so beside its heading, for example **Each visit** on Traffic > Visitors (visits are kept 7 days). To choose your own dates: 1. Select **Custom** in the top bar. 2. Pick the first day in **From** and the last day in **To**. Days are counted midnight to midnight in the site's own time zone, whatever time zone you choose for display. The site's zone is set in **Settings > This site > Sites**. > A brand-new site has no whole day yet, so it opens on Today. ## Whole days and withheld days Every figure is counted twice, by two separate roads. A day is shown only when both counts agree. | A day that is | Means | | --- | --- | | Whole | Both counts agree. It is in every total | | Partial | The log covers only part of the day. It is left out of totals | | Withheld | The two counts did not add up. Nothing is shown for it | | Not collected | Skald was not reading the log that day | A withheld day is never shown as zero. A figure that was not recorded is left blank, not zero. It is never guessed. When a period holds such days, the page says how many and which. Something your site has never sent shows **Not set up**, with one line on how to set it up, never 0 or 0%: an event the tracking tag never sent (Leads & sales > Events), a kind of lead the lead store never recorded (Summary, Funnels), a heartbeat that never ran (Site > Health), or page speed never tested (Site > Speed). On Site > Data quality, the reasons a day is **Worth a look** or **Not trusted** are listed above the days, each with how many days it covers. A stretch with no log line counts only when it is long for your site: an hour at least, and longer on a quiet site, where a night's lull is normal. Today's live figures can be withheld too, if they do not add up. They come back at the next recount. ## Filters Select **Filters** in the top bar (or search for `filters`). The panel builds rules: 1. Pick what to filter by: country, city, source type, referring site, campaign or UTM tag, AI assistant, where the visitor first came from, landing page, a page read, pages read, time on site, sent a form (lead), device, browser, operating system, new or returning, or crawler. 2. Pick how: **is**, **is not**, **contains**, **starts with**, **at least** or **at most**. A rule can hold several values: "Country is USA, CAN" keeps either. 3. Choose **All rules** (AND) or **Any rule** (OR). **Add group** puts a few rules in brackets with their own All or Any, for example "Device is Mobile AND (Source is ChatGPT OR Source is Perplexity)". 4. The count at the top says how many visits match as you edit, and each rule says how many it keeps alone. Select **Show N visits** to apply. Time on site takes words: `90 s`, `1 min`, `2:30`. A value the log did not record never matches a rule by chance: "Country is not USA" leaves out visits whose country is not recorded, because they could have been in the USA. To find those, pick **Not recorded** as a value. Where having no value is an answer (no campaign, no AI assistant), **None** is a value too. Active filters show as chips under the top bar. Select the cross on a chip to remove that rule, **Edit filters** to change them, or **Clear all** in the panel. **Where rules apply.** Rules apply in full to the lists of single visits: Traffic > Visitors (the visit list and the visitor flow), Leads & sales > Funnels, and, for what a request carries (crawler, page, country, device), Site > Requests. The counted tabs keep each breakdown on its own, so they apply one value per breakdown and cannot cross them (for example, Canada AND Chrome). A rule a tab cannot apply is greyed out, and a line at the top of the tab says which, with a link to where it does apply, such as **CAN's own view** (the drill-down for that value) or **each visit, on Traffic > Visitors**. The count covers the visits kept for the period: single visits are kept 7 days, and the panel says which days it counts. A country is shown as its flag and three-letter code, such as 🇺🇸 USA or 🇨🇦 CAN. Point at it to see the full name. A table's search finds a country by its code or its full name, and a CSV file has both: the full name and a **Code** column. A country the log did not record says "Not recorded". If your site has several page groups (for example, the blog apart from the product pages), a **Page group** list sits beside the period. ### Saved views Save the tab, period, page group, rules and each table's sort, filters and columns under a name, and come back to them later. In the Filters panel, type a name under **Save as a view** and select **Save view**; the saved views are listed beside the rules, each with **Copy link**, **Rename** and **Delete**. They are also behind the bookmark button in the top bar, and in the **More** menu on a narrow screen. Saved views are kept in this browser. **Copy link** copies an address that holds everything the view shows. Whoever opens it signs in first and sees it only if their account may open this site: there is no public share link. The address bar keeps every choice too, so any page can be shared the same way. ### Tables Every table works the same way: - **Sort**: select a column's header. The first click sorts it (words A to Z, numbers and dates biggest or newest first), the second the other way, the third back to the table's own order. A value that was not recorded is always last. - **Filter columns** opens a row of filters under the headers: words **contain**, numbers have a lowest and a highest (`1 min` works for a time), and a column with a few values lets you pick some. The button shows how many are on. - **Columns** chooses which columns show. The first column, the row's name, always stays. - **Export what you see** downloads a CSV of the rows that match, in their order, with the columns shown: every matching row, not only the ones on screen. - Select a row to open its detail, as before, whatever the order or filters. The sort, column filters, columns and search stay in the address, so a reload, a link or a saved view brings the same table back. Moving to another tab starts its tables fresh. ## Search The magnifier in the top bar (or **Ctrl K**, **Cmd K** on a Mac) searches the figures and the dashboard at once. Type a few letters: - **Pages, countries, sources, referring sites, campaigns, AI assistants and crawlers** seen in the period on screen. Type `pricing` to find /pricing/, `canada` or `CA` for Canada, `claude` for ClaudeBot and claude.ai. Each result says what it is and the period it was seen in. Choose one to open the tab that lists it with its detail panel open. - **Tabs, Settings pages, Help guides, periods, saved views and actions**, such as `mail` for Settings > Mail. - When nothing in the period matches, the days kept before it are searched, and the result says so. Use the arrow keys and Enter, or click. The search only lists names; the figures are on the tab it opens. ### Find a person Owners and admins can also find one person. Type an IP address, an email or a lead number in the search: **Find a person** is the first result. On a wide screen there is also a **Find a person** box in the top bar. You see that person's visits, leads and conversions. Each search is written to the audit log. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## Drill-down Select any country, city, page, source, AI assistant, campaign, referring site, device, browser, hour, address or bot. A panel opens with its trend, the visits and the requests behind it. - In every table and list, the **whole row** opens its value, not only its name. A small arrow after the name shows it. On the keyboard, Tab to a row and press Enter or Space. - A row about one visit opens that visit instead. - Keep selecting inside the panel to narrow further. - At the top of the panel, **Go to** links jump to its visits and how they moved, a page's sources and its story, the Visitors tab filtered to it, and the tab that lists it. - In the latest visits, each page of a visit opens that page's panel. - Step back with the panel's **Back** button, the trail at the top, or the browser's Back button. Closing the panel (Esc or the cross) leaves the tab as it was, so the browser's Back button then goes to the tab and filters you came from. When the panel has visits behind it, **How these visitors moved** draws them as a flow map. See [Visitor flow](https://skaldanalytics.com/docs/getting-started.md#visitor-flow). On Traffic > Pages, select a page to see its story: when it was published, its first crawl, its first AI fetch, its first person and its first lead, and each time it changed according to your sitemap. **Pages by publish date** has a **Changed** column and a **Changed since** filter from the same dates. See [Change dates from your sitemap](https://skaldanalytics.com/docs/crawl-health.md#change-dates-from-your-sitemap). ## Traffic > Visitors Traffic > Visitors shows what visits looked like, top to bottom: - **Visitor flow**: where visits came from, the pages they read in order, and when they left. See [Visitor flow](https://skaldanalytics.com/docs/getting-started.md#visitor-flow) below. - **Each visit**: one row per visit, newest first. When, the country (flag and three-letter code, the name on hover), the city when your server has a city file, the device with its browser and system, the source (with "First came from …" when the visitor first came from somewhere else), **New** or **Returning**, the pages ("3 (2 in the browser)": steps seen only in the browser are said), the time on site and the pages in order. Select a row to open the visitor drawer. See [The visitor drawer](https://skaldanalytics.com/docs/where-visitors-come-from.md#the-visitor-drawer). - **New and returning visitors**: the visits by new and by returning visitors, their shares, and each day as a column. Returning means the same visitor was seen on an earlier day in the 30 before (a keyed hash of the address and browser, kept 30 days). It is counted per visit, so two visits on a visitor's first day are both new. Each day's new and returning visits must add up to that day's visits, or the card is withheld. A day counted before returning visitors were is left out and named, never shown as zero. On Today it counts today so far. - **How engaged visits were**: visits by pages per visit and by time on site, each adding up to the visits, with the typical (median) visit and the share that read one page and left. A log sees what was loaded, not how long someone read, so a long read of one page shows as a short visit. - **Where visits ended**, **Common next steps**, and the engagement tables by source and device. A rate over fewer than 10 visits is left blank, and a note under the table says how many rows that is. With a filter on, the two cards (and the line under the tab's title) count only the visits the filters keep, when that can be done honestly: they are rebuilt from the kept visits, and only when each day's kept visits are exactly that day's own figures (its visits, returning visits, pages per visit and time on site). The card then says **Filtered: N of M visits**, counted from each visit kept and checked against the days' own figures. When the period reaches past the kept visits (they are kept 7 days) or a day's kept visits do not match its figures, the cards show every visit and say **For all visits: the filters above do not narrow this card**, with why. **Where visits ended** and **Common next steps** follow the filters the same way, when each day's kept visits give back that day's own lists. The engagement tables by source and device, and New and returning by source, are always for all visits, and say so. The flow and the visit list follow every filter. ## Visitor flow Traffic > Visitors has a **Visitor flow** map. It shows how visits moved: where they came from, the pages they read in order, and when they left. - Each column is one step: the source, then page 1, page 2 and so on. A bar's height is its visits. A band's width is the visits that moved from one bar to the next. - A visit that ends flows to **Left after 1 page**, **Left after 2 pages** and so on. A visit with more pages than the map shows flows to **Went on to more pages**. - Each column keeps its 7 biggest pages and folds the rest into **Other pages**, so every column still adds up to the visits. - A page loaded twice in a row (a reload) counts once. Moves inside the site count, as in **Common next steps**. - **Start from** picks the first column: the source type, or who sent them (the AI assistant or site). **Pages** picks how many page columns to draw. - Point at or tab to a bar to see its visits, its share and where its visits went next. Click it, or press Enter, to open it. - **Table** shows the same figures as rows, with a CSV. On a phone the table shows first. The map uses the visits whose journeys are kept: today and the last 7 days. For a longer period it says which days it covers. Every filter applies. The same visits reconcile with **Where visits started**, **Common next steps** and **Where visits ended** for those days. A map that does not add up is withheld, not drawn. The drill-down panel draws the same map for its visits (**How these visitors moved**), so a country, a source or a page has one too. [Funnels](https://skaldanalytics.com/docs/funnels-and-goals.md#flow-between-the-steps) draws one between its steps. ## Insights Overview > Insights says what changed, in plain words: what is working, what dropped, what is new or gone, what looks unusual and what is worth fixing. Above the cards, **To do now** lists what to fix this week, worst leak first, each with its proof, its list, a CSV and a link; mark one done and Skald re-checks it. See [What to fix](https://skaldanalytics.com/docs/what-to-fix.md). - **The five biggest come first.** Size is how much moved and what it is worth: a lead counts more than a visit, an AI fetch or a crawl less, and a finding on small numbers less. **Show all** lists the rest. - **One card per subject.** A site that is both a referring site and an AI assistant, or a source's visitors and its leads, is one card; the other findings about it are under **more about this**. - **One sentence, one number, one link** on each card. The link opens the tab that shows it, filtered to it. - **Pick what to compare:** **Yesterday** (against the 14 days before), **7 days** or **30 days** (against the same number of days before). **Today so far** lists only what is seen for the first time today, such as a new page or country; rises and drops need whole days, so they are under the others. A window with nothing in it says so. Insights compare whole, checked days only, so the period in the top bar does not apply here. ## Compare Select **Compare with…** in the top bar and choose what to put beside the period: - **Previous period**: the same number of days just before. - **Same period last year**: 52 weeks earlier, so the weekdays match. - **Custom period**: any whole days you pick, up to 12 months. Tables then show A (the period in the top bar), B (the one you chose) and the change. Charts show B as a dashed line. A tab that does not compare says so in one line. Previous period and last year compare only when both periods are whole and checked; a custom period counts whole days and names any it left out. **Turn compare off** in the same menu. These tabs compare: Overview > Summary, Traffic > Sources, Pages and Places & devices, Leads & sales > Summary, AI & bots > Assistants and Crawlers, and Site > Health. Leads, demos booked and every completed action are compared too, when every day of both periods has checked conversions. Today is never compared: turning on **Today · Live** turns Compare off. ## Summary and Pages: is it working? Overview > Summary and Traffic > Pages answer one question: did what you changed work? ### The period line Under the title of Overview > Summary and Traffic > Pages there are two switches and one line with the exact dates. - **Include today**: with 7, 30 or 90 days, the period ends today and counts today so far. Off, it ends yesterday and counts whole days only. Other periods ignore it. It is kept in the address (`inc=1`) and stays on as you move between Summary and Pages. - **Compare with the period before**: puts the same number of days just before beside the period. It is the same as **Compare with… > Previous period** in the top bar. The line says the dates in words. On 15 Jan: | Period | Dates | The period before | |---|---|---| | 7 days, including today | 9–15 Jan | 2–8 Jan | | 7 days, whole days | 8–14 Jan | 1–7 Jan | | Today · Live | today until now | yesterday until the same time | A period that includes today is **never compared**. Today is not over, so its figures would always look lower than a whole day's, and Skald keeps no earlier day cut at the same hour (only yesterday, for Today · Live). Turn **Include today** off to compare whole days. When the period before was not measured (it is before Skald started on your site), the line says **before: not measured** and the first day a full comparison comes, for example "The first full 7 days against the 7 before is on 5 Nov (29 Oct–4 Nov against 22–28 Oct)". Until then, use Yesterday or Today. It never shows 0 for the earlier period and never a rise against nothing. ### Key figures and their change Summary shows five figures: **People**, **From search engines**, **Returning people**, **Leads** and **Sent by AI assistants**. Select one to open the tab it comes from. With Compare on, each says its change: - **▼ 40% · before: 125**: down 40% from 125. - **≈ about the same · before: 12**: the gap is 3 or fewer. At the small numbers most sites have, that is normal day-to-day swing. - **before: not measured**: the earlier period was not counted. **before: not recorded**: the figure was not kept for those days (for leads, not every day's conversions are checked). ### What changed: is it working? With Compare on and both periods whole and checked, this card lists **Sources** and **Landing pages**, each with **Now**, **Before** and **Change**, and one sentence on the biggest rise and drop. Select a row for its detail. ### Work on the page, or on traffic? **Left at once** means: the visit loaded that one page and no other (one page view in the server's log). A step a single-page app shows without asking the server is not in the log, so on such a site the share is an upper bound. - **Improve the page**: at least 10 visits started on the page, 9 in 10 of them left at once, and no lead began there. The page, not the traffic, is the problem. - **Both**: search found the page at least 3 times and 8 in 10 of its visits left at once. Work on the page and on how it is found. Each list shows at most 5 pages. Leads come from the conversions data; when they are not recorded for those days, a page says **forms: not recorded**. Every page, in the same order, is on Traffic > Pages with the **Needs work** sort. ### New and updated pages: are they getting found? The pages whose change date is in the last 30 days: the website's own date when it sends a page list, else the sitemap's "last changed" (``). Each shows the change date, the visits and search visits since, the first day with a search visit, and whether Google fetched it since. Its period is always the last 30 days to yesterday, whatever the top bar says. When more than half of the sitemap's dates fall on one day, the card says **Dates look like build times**: some sites stamp every page each time they are built, so every page looks changed. ### Traffic > Pages: sort and filter The landing pages grid adds **Pages per visit**, **Left after one page**, **AI reads** (live fetches by ChatGPT-User, Perplexity-User, Claude-User and other assistants answering someone) and **Changed**. The sort chips: **Most visits** (the default), **Rising** and **Falling** (the biggest gain or drop against the period before; they need Compare, and without it the grid stays on Most visits and says so), **Recently changed**, and **Needs work** (pages with 10 visits or more, the most left at once first). The chip is kept in the address (`psort`); a column header click sorts by that column instead. **Changed in the last 30 days** (above the grids, and in **Filters** as a preset) keeps only the pages changed since then. It is the **Changed since** date in the address (`chgfrom`). ### Did my change work? Changed a page? Open it on Traffic > Pages and select **+ Mark a change**: pick the day it changed, what changed (content, title, a link to it, a redirect, other) and add a short note if you like. When your sitemap or your website says the page changed on a day you have not marked, the page asks **Track that change?**. Skald then compares the same number of whole days before and after that day, up to 28 on each side. The day of the change is in neither side. For each of these it shows the figure a day before, after, the change and what it says: - visits that started on the page - leads and conversions that landed there - visits from search engines - visits from AI assistants - AI reads (assistants fetching the page live for someone) - the share who left at once (lower is better) Until 14 whole days have passed it says **Too early to tell**, how many days it has and the day the verdict comes. After that each figure says **Up**, **Down** or **No clear change** (the difference is within the normal day-to-day swing). **Too few to tell** means fewer than 10 on both sides together. A day that is not whole or not recorded is in neither side; when the two sides then have different numbers of days, they are compared per day and the page says so. A figure not recorded says **not recorded**, never 0. The change shows as a marker on every day chart. **Copy link** copies the address of this change; **Stop tracking** removes it from the list (the mark stays in the history). Every change marked on the site is listed on Overview > Insights, under **Done, and did it work?**. Every figure is counted twice, the second time straight from the stored days; a change the two counts disagree on is left out and named. Only owners and admins can mark a change, and only with accounts (signing in). A change marked today shows its figures from the next data update. ### AI reads by page On AI & bots > Assistants, **AI reads by page** lists each page's live fetches by assistants answering someone, now and in the period before (Compare's B when Compare is on), by assistant, then the visits that followed an AI answer's link to the page and the leads that began there. A fetch is not a reader. No AI model is asked anything. These figures come from one day figure, **visits by landing page**, kept from the first day collected after the upgrade. Older days say **not recorded**, never zero. Each has a second count from the raw log. A page group shows these cards for the whole site only. ## Export Select **Export** in the top bar: - **Tables on this tab (ZIP)**: every table, with the current filters, as CSV files. - **This tab as JSON**: the tables and the period's totals. - **Print this tab**: a clean report. Choose Save as PDF in the print dialog to keep a PDF. - **Report builder**: choose several tabs, the period, page group and filters, then print or download them together. To read figures from a script or an AI tool, see [API](https://skaldanalytics.com/docs/api.md) and [MCP](https://skaldanalytics.com/docs/mcp.md). ## Keyboard shortcuts Shortcuts do nothing while you type in a field. | Keys | What it does | | --- | --- | | ? | Show the list of shortcuts | | Ctrl K (Cmd K on a Mac) | Search: pages, sources, countries and more, or go anywhere and run anything | | / | Search the list on this tab | | [ | Shrink the sidebar to icons, or open it again | | Esc | Close a panel or a menu | | g then a letter | Go to a tab | Letters to use after `g`: | Letter | Tab | | --- | --- | | o, i | Overview > Summary, Insights | | l | Overview > Summary with Today · Live on | | c | Overview > Summary compared with the previous period | | v, s, p, d, t | Traffic > Visitors, Sources, Pages, Places & devices, Time | | e, x, f, n | Leads & sales > Leads, Calendar bookings, Funnels, Events | | b | AI & bots > Assistants | | h, a, r, k, w, q | Site > Health, Alerts, Requests, Not found, Speed, Data quality | | u | Settings > Internal traffic | | , (comma) | Settings | ## Your site on Vercel, Netlify or behind Cloudflare Skald needs a record of every request. A site on your own server has one: its access log. Other hosts keep that log to themselves, so it has to be sent to Skald. Whoever runs your Skald server sets this up once: - **Vercel:** a Log Drain sends every request to Skald (Vercel Pro or Enterprise). On the Hobby plan, only the `skald.js` tag works, so Skald sees people but not AI assistants or crawlers. Step by step: [Skald for a site on Vercel](https://github.com/spcaeo/skald/blob/main/docs/VERCEL.md). - **Netlify:** a Log Drain (Netlify Enterprise), set up the same way. - **Cloudflare in front of your site:** pages Cloudflare serves from its cache never reach your server. Cloudflare's own request log (Logpush) or the small Skald Worker, on any plan, sends them to Skald. No visit is counted twice. See the [FAQ](https://skaldanalytics.com/docs/faq.md). The full list of hosts, with what each one lets Skald see: [Hosting](https://github.com/spcaeo/skald/blob/main/docs/HOSTING.md). ## How fresh the figures are The foot of every page says when the data was last updated, and when the next update is due. Today's figures are recounted about every two minutes. Yesterday becomes a whole day after the morning's daily run. If your server allows it, **Refresh** in the top bar recounts today at once. ## Next steps - [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md) - [Internal traffic](https://skaldanalytics.com/docs/internal-traffic.md): keep your own visits out of the figures. - [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md) - [FAQ](https://skaldanalytics.com/docs/faq.md) --- # Docker: first install in 10 minutes > Skald docs: https://skaldanalytics.com/docs/docker-first-install/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). From nothing to a working dashboard on a server that already runs your website with nginx: run the container, sign in, fill the last 14 days, check it with the doctor, then add the script tag and hooks. ## 1. Run it (2 minutes) ``` docker run -d --name skald --restart unless-stopped \ -p 127.0.0.1:8080:8080 \ -v /var/log/nginx:/logs:ro \ -v skald-data:/var/lib/traffic-intel \ -e SKALD_SITE=example.com \ -e SKALD_HOSTS="example.com www.example.com" \ -e SKALD_TIMEZONE=America/New_York \ -e SKALD_BASE_URL=https://www.example.com \ -e SKALD_TRUSTED_PROXIES=private -e SKALD_REAL_IP_HEADER=X-Real-IP \ ghcr.io/spcaeo/skald:2 ``` - `127.0.0.1:` publishes the port on this machine only. nginx in front gives it to the world. Keep the same line in every later `docker run`. - `/var/log/nginx` is read-only: Skald reads the log, never changes it. - `skald-data` holds everything Skald keeps. It must be mounted at `/var/lib/traffic-intel`, where the image keeps its data. Keep the volume. - `:2` follows every 2.x release and never a breaking one. - Not a Next.js site? Add `-e SKALD_ASSET_PREFIXES="/wp-content/ /wp-includes/"` (WordPress), or the folder your pages load their scripts and styles from. A visit counts as people only when its browser fetched one. - The last line is for nginx on the same machine. It lets Skald see each visitor's address, not nginx's. - Countries: nginx's stock log has no country. To get one, give Skald a GeoIP country file. DB-IP's free country file works. Keep it on the host, for example in `/usr/share/GeoIP`, and add `-v /usr/share/GeoIP:/geoip:ro -e SKALD_COUNTRY_DB=/geoip/dbip-country-lite.mmdb`. Each visit then gets its address's country, and so does a lead your backend sends with an address. Refresh the file on the host once a month (your updater or a cron job that downloads DB-IP's new file). Skald reads the new file by itself, no restart. DB-IP's credit is shown under the countries. Without a file, countries say Not recorded. A country your log already carries (Cloudflare's, or nginx's own `cc=`) always wins. - A new install keeps each visit with its IP address, and each lead's name and email, so admins can look a person up. To keep neither, add `-e SKALD_KEEP_IPS=0 -e SKALD_IDENTITY=anonymous`. To keep each address shortened, with its last part removed (203.0.113.57 is kept as 203.0.113.0), add `-e SKALD_KEEP_IPS=short` instead. Admins can change both later in **Settings > Data retention > People**. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## 2. Put it on the web (2 minutes) In your site's HTTPS `server { }` block, before `location /`: ``` location ^~ /traffic { access_log off; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto https; client_max_body_size 6m; } ``` Then check and reload. Careful: your website shares this nginx, so never reload without the check. ``` sudo nginx -t && sudo systemctl reload nginx ``` ## 3. Sign in (1 minute) ``` docker exec -it skald traffic-intel accounts init --org "Example Co" --owner-email owner@example.com ``` It prints a "set your password" link, valid 60 minutes. Open it, set a password, and you are in at `https://www.example.com/traffic/`. See [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md) to invite others. ## 4. Fill the last 14 days (2 minutes) ``` docker exec skald traffic-intel backfill --days 14 ``` It stores each day the log's rotations still hold. Today fills by itself within two minutes. ## 5. Ask the doctor (1 minute) ``` docker exec skald traffic-intel doctor ``` One line per part, each OK, NEEDS ATTENTION or OFF, and the fix in plain words: | Line | Checks | | --- | --- | | Daily run | It ran in the last day, and did not fail | | Days stored | None of the last 14 days is missing or withheld; if some are, the backfill command | | Access log | The log is there, readable, and written today | | Page assets | A browser fetched your page assets: without them nobody counts as a person | | Web app | The dashboard has its menu | | Hook receiver | The part that takes hooks and the script tag answers | | Hook ... | Each hook: its secret, and its last delivery | | One lead, two tools | The same lead arrived through two tools (two hooks, or a hook and the lead database), so it counts twice | | Script tag | Its origins, and the last event from a page | | Lead database | The website's lead database can be read, when one is set | | Mail | The mail settings are complete | | Version | A newer release, from the last daily check (it never goes online itself) | | Crawler checks | A crawler that names itself (Googlebot, GPTBot) is checked against its owner's published addresses, and browsers from hosting networks are flagged. On by default in the container; `-e SKALD_VERIFY_BOTS=off` turns it off | | Disk | Free space where Skald keeps its data | OFF is a part you have not turned on. It exits 1 when anything needs attention, so a script can watch it. `--json` prints the same list as JSON. `--send-test` also sends one test mail to the recipients. The daily run happens at 07:15 in the site's time zone. Before the first one, "Daily run: has never run here" is expected. ## 6. Add the script tag (2 minutes) The tag gives form steps, clicks and booking widgets. Start the container again with your site's origin: ``` docker rm -f skald docker run ... -e SKALD_BEACON_ORIGINS=https://www.example.com ... # the same command, plus this line ``` Then copy the tag from **Settings > Integrations** into every page's head. See [Tracking script](https://skaldanalytics.com/docs/tracking-script.md). Only the first site needs `SKALD_BEACON_ORIGINS`. A site you add later from the dashboard takes events from its own **Host names** (**Settings > This site > Sites**). Its page assets are its own too: a site that is not Next.js sets them in **Settings > This site > Sites > Page assets** (`SKALD_ASSET_PREFIXES` is for the first site only). See [Tracking script](https://skaldanalytics.com/docs/tracking-script.md), "More than one site". ## 7. Add hooks **Settings > Integrations > Make secret** for each tool, or on the server: ``` docker exec skald traffic-intel hooks secret example.com lead ``` Then: [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md), [Demo bookings](https://skaldanalytics.com/docs/demo-bookings.md), and [Webhooks](https://skaldanalytics.com/docs/webhooks.md) for the other tools. Not sure which? [Pick your road](https://skaldanalytics.com/docs/pick-your-road.md). ## Upgrade ``` docker pull ghcr.io/spcaeo/skald:2 docker rm -f skald docker run ... # the same command as before ``` The volume keeps all data. Not during the daily run. ## When something is wrong | You see | Why | Fix | | --- | --- | --- | | A plain page with no menu | An old image | Pull the new image; the doctor's **Web app** line says so | | "nothing parsed" | The log is not mounted, or not readable | `docker exec skald ls -l /logs`; the container's user is in group `adm` (gid 4), which reads `/var/log/nginx` on Ubuntu and Debian | | No person counted | No page asset seen | Set `SKALD_ASSET_PREFIXES` (the first site) or **Settings > Sites > Page assets** (a site added later); the doctor's **Page assets** line says which | | A hook answers 404 | The hook has no secret | Make its secret | | A hook answers 502 | An image older than 2.2.2 | Pull the new image | | "Too many tries" on sign-in for everyone | The proxy in front is not trusted | Set `SKALD_TRUSTED_PROXIES` and `SKALD_REAL_IP_HEADER` | `docker logs skald` shows one line per job. --- # Pick your road > Skald docs: https://skaldanalytics.com/docs/pick-your-road/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Which way your site's leads, bookings and payments should reach Skald, by the kind of site you run: what each road gives you, what it cannot see, and what to set. ## The roads Skald always reads your web server's log: visits, pages, sources, AI assistants and bots. Conversions come in by four other roads. Pick the ones that fit your site. | Road | What it is | Set up in | | --- | --- | --- | | The script tag | One line in every page's head. It reports form steps, phone and email clicks, booking widgets and button clicks | [Tracking script](https://skaldanalytics.com/docs/tracking-script.md) | | The lead hook | Your site's own server tells Skald "a lead was made", in a signed message | [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md) | | Tool hooks | Calendly, Cal.com, Stripe, HubSpot, Typeform, Zapier or Make send their own signed messages | [Webhooks](https://skaldanalytics.com/docs/webhooks.md) | | The lead database | Skald reads your site's own lead store, with no change to the website | the server's config (`leads.db`) | The lead database road reads **SQLite only**: a Payload-style `submissions` table. It cannot read MySQL, Postgres or a WordPress database. For those, use the lead hook. ## By kind of site | Your site | Road | What you get | What you do not get | What to set | | --- | --- | --- | --- | --- | | WordPress or another CMS with a form plugin | Script tag, plus the `json` hook through Zapier or Make (or the plugin's own webhook, if it can sign) | Forms started and sent, phone and email clicks, each lead with its source | The lead's later status (won, lost), unless your CRM sends it | The tag on every page; `data-skald="contact"` on the contact form; a Zap or scenario that posts to the `json` hook | | Next.js on Vercel | Script tag, plus the signed `lead` hook from an API route or server action | Every lead your server makes, with its first and last touch | The visit figures, until Vercel's logs reach Skald through a log drain | The tag; the hook secret in Vercel's environment variables; a log drain (`docs/CDN-LOGS.md`) | | A site with a Postgres or MySQL backend | The signed `lead` hook from the backend | Every lead, on the server's word, even when a browser blocks scripts | Form steps, unless you add the script tag too | The hook secret in the backend's environment | | A site that keeps leads in SQLite (Payload CMS) | The lead database | Every lead, its status (won, lost, spam) and its touches, with no code | Leads from other places | `leads.db` in the server's config | | No-code site (Webflow, Squarespace, Wix) | Script tag, plus Zapier or Make to the `json` hook | Form steps and clicks from the tag; the form's leads from Zapier or Make | Anything the builder will not let you add to the page head | The tag in the site's custom code; a Zap that posts to the `json` hook | | Calendly or Cal.com | The tool's hook, plus the embed through the script tag | Bookings, cancellations, reschedules and no-shows; opened and time chosen when embedded | Opened and time chosen when you only link to the booking page | See [Demo bookings](https://skaldanalytics.com/docs/demo-bookings.md) | | Stripe | The `stripe` hook | Each paid checkout, and one paid later by a slow method | Refunds, subscriptions, customer details (never read) | The endpoint's signing secret (`whsec_...`), pasted on the Integrations page; the visitor ID as the session's `client_reference_id` | | HubSpot | The `hubspot` hook, or Zapier to the `json` hook | New contacts, booked meetings, deals won | Anything HubSpot does not send | The app's client secret, pasted on the Integrations page | | Zapier, Make, n8n | The `json` hook | Any lead any tool knows about, up to 100 in one message | Nothing by itself: it sends what the Zap sends | **Make secret** for `json`, and the signing step in the Zap | Never sign a hook in the browser. The secret must stay on a server. A browser can only use the script tag. ## What each road costs in privacy - **Visitor IDs are off by default.** Then the script tag makes no ID, and a hook's `vid` is dropped before anything is kept. Journeys, abandoned forms and assisted sources need the IDs on. Your privacy notice must say so. - **Names and emails are kept only when Leads' names and emails is full** (**Settings > Data retention > People**). That is the default for a new install; an install upgraded from before 2.4.0 keeps none until an admin turns it on. Then the name and the primary email only. Nothing else, ever: no phone, no address, no message. - **Addresses and browsers.** A lead hook may send the visitor's address and browser once. Skald looks up the country and the device, then drops both. - **The log itself.** With **Keep visitors' IP addresses** on (also the default for a new install), each visit in the log is kept with its address, so admins can look a person up. See [Privacy](https://skaldanalytics.com/docs/privacy.md). - **The lead database** is opened read-only. Only the columns Skald needs are read. ## One lead, one road Send each lead through one road only. Each tool keeps its own row, so the same lead sent through two (for example the lead hook and Zapier, or the lead hook and the lead database) counts twice. **Settings > Integrations > Status** flags it: "One lead, two tools", with the leads it saw. Turn one of the two roads off for those leads. ## Check it - On the server: `sudo traffic-intel doctor` (Docker: `docker exec skald traffic-intel doctor`). It lists every part with OK or NEEDS ATTENTION, and the fix. - In the dashboard: **Settings > Integrations > Status** shows the same lines for the roads in, and each hook says "ready", "never received" or "last received 2 h ago". --- # Sign-in and team > Skald docs: https://skaldanalytics.com/docs/sign-in-and-team/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). How to sign in, keep your account safe with two-step sign-in, and invite your team with the right role. These pages come with accounts. If your server uses one shared login instead, its users are listed under **Settings > Team & access > Users**, and the server admin manages them. ## Sign in 1. Open your dashboard's address. The sign-in page opens. 2. Type your **Email** and **Password**. 3. Select **Sign in**. Every password field has a **Show** button at its right edge. Select it to see what you typed, and **Hide** to cover it again. The password is always covered again before the form is sent. ### Keep me signed in **Keep me signed in** is ticked by default. You then stay signed in for 14 days on that device, counted from your last use. Untick it on a shared computer: you are signed out when the browser closes. Your organisation can set a shorter session length. See [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md). ### Too many tries After 5 failed tries for one email address in 15 minutes, each new try waits 30 seconds. You are never locked out for good, so nobody can lock you out by guessing. ## Forgot your password 1. On the sign-in page, select **Forgot password?**. 2. Type the email address you sign in with. 3. Select **Send the link**. 4. Open the mail and follow the link. It works once, for 60 minutes. 5. Type a **New password** twice and select **Save the new password**. The page says the same words whether or not the address has an account. No mail after a few minutes? Check the spam folder, or ask an admin of your team. Saving a new password signs out every other session of your account. > If your server cannot send mail, ask the person who runs the server. They can make a reset link for you. ### Password rules - At least 12 characters. - Not your email address. - Not a common password. ## Two-step sign-in Two-step sign-in asks for a 6-digit code from an app on your phone, after your password. Any authenticator app works. ### Turn it on 1. Go to **Settings > You > Your account**, or open Your account. 2. Under **Two-step sign-in**, type your password and select **Turn on two-step sign-in**. 3. In your authenticator app, add an account and type the key the page shows. If your app takes an address instead, use the address shown. 4. Type the 6-digit code the app shows, then select **Confirm**. 5. The page shows 10 recovery codes. Select **Copy all** or **Download as text** and keep them somewhere safe. 6. Select **I have saved them**. The recovery codes are shown only this once. Each one lets you sign in once if you lose your phone. ### Sign in with two-step sign-in 1. Type your email and password as usual. 2. In **Code**, type the 6 digits your app shows now, or one of your recovery codes. Codes change every 30 seconds. If a code is refused, try the one the app shows now. ### Turn it off 1. Go to Your account. 2. Under **Two-step sign-in**, type your password and select **Turn off**. If your organisation requires two-step sign-in, the sign-in page asks you to set it up the next time you sign in. > Lost your phone and your recovery codes? Ask the person who runs the server to turn two-step sign-in off for you. ## Your account **Settings > You > Your account** changes only your own account. Everyone has this page, viewers too. - **You**: your name, and the address and role you are signed in with. Select **Save name** after a change. - **Password**: type your current password and the new one twice, then select **Change password**. - **Two-step sign-in**: turn it on or off. - **Where you are signed in**: each browser, when it signed in and when it was last seen. To end every other session, select **Sign out everywhere else**, then **Yes, sign them out**. Use this if you signed in on a computer you no longer use. ## Roles A person has one role in each organisation. | Role | May | | --- | --- | | Viewer | See every page of the organisation's sites and the team list. Change their own name, password and two-step sign-in. Changes nothing else. Cannot look a person up, and sees the left-out lists without names, emails or visitor IDs | | Admin | All of that, plus: invite, remove and change members (not owners), make API tokens, read the audit log, change the site's settings, and look a person up (the search at the top: visits with their IP address, emails, names) | | Owner | All of that, plus: make, change or remove owners, set the security rules, remove a site, and handle billing | An organisation always keeps at least one owner. Some settings apply to every site on the server, not only yours: the AI crawler rules, saved funnels, the internal-address list, the mail server and the log file paths. Only an **install admin** may change those. This is a flag the server admin gives to a person. ## Invite people You must be an admin or owner. 1. Go to **Settings > Team & access > Team**, or open Team. 2. Under **Invite someone**, type the person's **Email**. 3. Pick a **Role**. Only an owner can invite another owner. 4. Select **Send invite**. The person gets a mail with a link. It works once, for 7 days. On the invite page they pick a name and a password and select **Create account and join**. If they already have an account with that address, the button reads **Join** and the organisation's name. They keep their own password and sign in with it. If mail is not set up on your server, the page says "Mail is not set up; send this link yourself." Copy the link and send it to the person yourself. They then choose a new password at their first sign-in, so you never know the password they go on to use. ### Pending invites Open invites are listed under **Pending invites**, with who sent them and when they expire. - **Resend** sends a new link and replaces the old one. - **Cancel** stops the link at once. ### Change a role or remove someone Under **Members**, each person's email, name, role, last seen and two-step sign-in are listed. - To change a role, pick a new one in the **Role** list. It saves at once. - To remove someone, select **Remove**, then **Yes, remove**. Their sessions for this organisation end at once. You cannot change your own role or remove yourself. Viewers see the team list with every control turned off. ## Sign out - Select the round **You** button at the foot of the sidebar, then **Sign out**. - Or, on Your account, select **Sign out of this browser**. ## Related guides - [Getting started](https://skaldanalytics.com/docs/getting-started.md) - [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md) - [API](https://skaldanalytics.com/docs/api.md): tokens for scripts. --- # Real time: who is on your site now > Skald docs: https://skaldanalytics.com/docs/real-time/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). See how many people are on your site right now, where they are on a world map, what they are reading, and which AI assistants and crawlers are fetching pages at this moment. This guide covers where it is, what each figure means, and how fresh it is. ## Where to find it - **Overview › Real time**: its own tab. It always shows now, whatever period is picked at the top. - **Overview › Summary in Live mode**: turn on **Today · Live** in the top bar. The page opens on the short version: visitors on the site now, how many arrived in the last 5 minutes, the last 30 minutes minute by minute, the map and what visitors are doing now (the newest visitors and the pages they read), from the same figures as this tab and under one **Pause live** button. See **Today, live** below. ## How fresh it is Real time has two clocks, and it shows both at the top: - **Visitors as of 14:02:10**: the people figures. Skald counts them every 2 minutes, with the same rules as every other figure on the dashboard, so a crawler pretending to be a browser, your own office and the heartbeat are never in them. - **Requests as of 14:03:45, read from the log every 15 s**: every request your server logged, read from the end of the log. These are not sorted into people and machines yet, so they are always called requests, never visitors. The page asks for fresh figures every 15 seconds while it is open and visible. **Pause** holds everything still; **Resume** starts again. If the visitor clock says **late**, the 2-minute count has not run for more than 10 minutes: check Site › Data quality. ## What the figures mean | Figure | What it is | |---|---| | Active now | Visitors with any request in the last 5 minutes. A visitor is a visit that read a page, the same count as **Visitors** everywhere else. | | Last 30 minutes | Visitors with any request in the last 30 minutes. | | Visitors today | Today so far, the same number as Overview's Visitors tile. | | Pages read, 30 min | Page views by visitors in the last 30 minutes. | | Visitors on the site, minute by minute | For each of the last 30 minutes, the visitors whose visit was under way: their first request before the minute ended and their last one in it or after. The tooltip also says how many arrived in that minute. The last bar is the minute still running. | | Requests per minute | Every request the server logged in each minute: pages, images, scripts, bots and people alike. A spike here with no visitors is usually a crawler. | ## The world map Each country is shaded by its **visits today**: the darker the blue, the more visits. The legend under the map gives the range of each shade. A **green bubble** marks a country with visitors active in the last 30 minutes; the bigger the bubble, the more of them. - Point at a country, or move to it with **Tab**, to see its visits today and how many are active. - Click it, or press **Enter**, to open the country's detail, as from any country in the dashboard. - The **By country** table beside the map has the same figures, so you can search, sort and download them. On a phone the table sits under the map. ### Cities on the map When a city file is set (see [Places & devices](https://skaldanalytics.com/docs/getting-started.md#cities)), the map also shows cities. A **pulsing green dot** sits on each city with visitors active in the last 30 minutes, at the city's own point; a small **hollow dot** marks a city with visits today and nobody active now. A country's own bubble then counts only its active visitors whose city is not recorded. Point at a dot for its figures; click it to open the city's detail. The **By city** table under the countries holds the same figures, and says how many of today's visits have no city recorded. Countries too small for the map's outlines (Singapore, Malta and others) are drawn as a dot. The map is drawn from Natural Earth's public-domain outlines and is part of the page: no map service is asked for anything. ## The lists - **Pages being read**: page views in the last 30 minutes, by page. - **Newest visits**: the ten newest visits, with time, city (or country, when no city is recorded), device, where they came from, the page they landed on and how many pages they have read so far. **active now** means a request in the last 5 minutes. No address and no browser string is shown here. - **Sources now** and **Devices now**: visitors active in the last 30 minutes, by where they came from and by device. - **AI and crawlers now**: requests in the last 30 minutes by AI assistants and search crawlers, by name, and other bots and scanners as one count. These are machines, counted in requests, never as visitors. Select any page, source, device or crawler to open its detail. ## Today, live Overview › Summary with **Today · Live** on keeps one place for each answer, each with a link to the tab that lists it all: 1. **The live strip.** Visitors on the site now (a request in the last 5 minutes), how many arrived in the last 5 minutes, and the last 30 minutes as bars. Beside it, **Visitors**, **Pages read**, **Leads** and **Visitors from AI** for today so far. 2. **Where visitors are**, the map across the page (today by country, a pulsing dot for each city with someone on the site in the last 30 minutes), and under it **What visitors are doing now**: - on the left, the five visitors with the newest activity: where they are, their device, where they came from, and the pages they read in order, `/ → /pricing → /book-a-demo`, the page they are on now marked, with "now on /pricing · 20 s ago". A long path is cut in the middle; point at it for the whole path. As visitors move, a new page joins their path, a new visitor slides in at the top, and one who has gone quiet fades out. Select one to open the visitor drawer with its journey. **See every visit** opens Traffic › Visitors. - on the right, **Pages read in the last 30 minutes**, the same figure as before. Press a page to see only the visitors who read it; press it again for the page's detail. - Point at a row and its city pulses on the map; point at a city and its rows stand out. - With a drill-down open (for example a country, `dd=country:CA`), the map shows that country and the list only its visitors, with a line saying how many are shown. The list can follow a country, a city, a source, a referring site or assistant, a device, a landing page or a page read; another kind of filter (a campaign, an hour) says so in one line and the list shows everyone. The pages read are always every visitor's. 3. **Where today's visitors came from** and **How today's visits moved** (the flow, two pages deep). 4. **What converted today** (forms sent and demos booked) and **AI & bots today** (pages assistants fetched for people, visitors they sent, crawler requests by bot). ### Vs yesterday at this hour Each of the four tiles compares today so far with **yesterday up to the same time of day**: at 14:33 today, yesterday from midnight to 14:33. Never with all of yesterday, which would make every morning look like a fall. The small chart under each tile is the running total by hour: today as a line, yesterday dashed. Point at it for the totals at each hour. - Visitors, pages read and visitors from AI: Skald counts yesterday again from the log up to that time, every 2 minutes, with the same rules as today's figures. - Leads: yesterday's completed forms and demo bookings up to that time, from the lead store. Test rows are never counted. - Under 10 yesterday, the change is a count (**+2**); from 10, a percent (**+18%**). - **Not recorded** means yesterday cannot be compared: it was withheld (its second count disagreed), is only partly in the log, or was not recorded. It never shows as a fall to zero. - On a page group, the comparison says not recorded: it is counted for the whole site. `ti.py audit --live` counts yesterday at this hour again from the raw log lines, hour by hour, and the leads again from the lead store. ## Page groups Real time always covers the whole site. On a page group, a note says so. ## How the figures are checked Every Real time figure is counted a second time from the raw log lines by `ti.py audit --live`, which also recounts the last 15-second request count and yesterday at this hour. If the second count of today's figures disagrees, today's figures are withheld and Real time shows no visitor figures until they match; the request count stays, because it is a plain count of log lines. The 15-second count is recounted only when it is less than 5 minutes older than today's figures: Skald saves it only while someone has Real time open, and lines that reach the log late (from a log shipper, say) would make an old count look wrong. An older one is reported as skipped, with its time. ## In the demo The demo has no running Skald behind it, so its requests per minute come from the 2-minute count, and the page says so. --- # Where visitors come from, and their journeys > Skald docs: https://skaldanalytics.com/docs/where-visitors-come-from/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Most visits say **Direct**: the browser sent no referrer. Many of them are people coming back, who first found you through a search, an AI assistant or a post days before. Others came back from a payment page, opened your link inside an app, or followed a shared link. This guide covers visitors by social network (X, LinkedIn, ...) on each site and across all your sites, how Skald finds a visitor's first visit, what a Direct visit likely was, where you see both, and the visitor drawer with every visit of one visitor. ## Social networks: X, LinkedIn and the rest A visit from X can arrive as `t.co`, `x.com`, `twitter.com` or the X app; one from LinkedIn as `linkedin.com`, `lnkd.in` or the LinkedIn app. **Traffic › Sources › Social network** puts each network in one row, so you see at once how many visitors X sent: - **X**: t.co, x.com, twitter.com (and mobile.twitter.com), the X app. **LinkedIn**: linkedin.com, lnkd.in, the LinkedIn app. **Facebook**: facebook.com (and l., m., lm.facebook.com), fb.com, the Facebook app. **Instagram**, **Threads**, **Reddit**, **YouTube** (youtube.com, youtu.be, its app), **TikTok**, **Pinterest** (any country domain), **Bluesky**, **Mastodon** (mastodon.social only), **Hacker News**, **Quora**, **Telegram**, **WhatsApp**, **Slack** and **Discord**. - A tagged link with no referrer counts for the network its `utm_source` names (`utm_source=twitter` or `x` is X), else its `utm_medium`, else its ad click id (`fbclid` is Facebook, `li_fat_id` LinkedIn, `ttclid` TikTok, `twclid` X). A tag that names two networks, or only "social", counts as **Social, network not named**. - Every visit counts once, in one network, and the networks add up to the **Social media** row of Source type. Nothing is reclassified: a site Skald does not call social (Product Hunt, threads.com, fb.me, a Mastodon server other than mastodon.social) stays under Other website. Click a network to open it under the grid: **how they were matched** (each host, app or tag, with its visitors) and **where they landed**. The address keeps it, for example `#traffic/sources?src_seg=social&net=x`, so you can copy the link, and Back closes it. Before, Change, the trend and Compare mode work as for source types, when every day of both periods recorded the networks. Each day is counted twice, by two separate roads; a day where they disagree is withheld. A day counted before this release (2.34.0) says "Not recorded", never zero. ### Fill in the days before 2.34.0 The person who runs your Skald server can count those days again from the raw log, for the days the log still holds: 14 by default (the web server's log rotation, and the folder that keeps shipped logs). Older days stay "Not recorded". After the upgrade, away from the daily run (07:15 in the site's time zone): ``` docker exec skald traffic-intel backfill --days 14 --force docker exec skald traffic-intel audit --day 2026-10-20 --withhold ``` - `backfill --force` counts each of the last 14 days again with the current version, for every site (`--site example.com` before `backfill` picks one). It fills in the networks, and any other figure those days did not record yet, such as Direct, likely. - Then run `audit --day ... --withhold` for each of those days, one date at a time. The audit counts the networks a second time from the raw log, and must end with "all match". With `--withhold`, as in the daily run, a day that does not match is withheld and not shown; find why before you go on. - The recount also counts the rest of those days again with the current rules, so a figure of those days can change a little if a rule changed since they were stored. The chart marks the recount. ## All your sites side by side On the site picker (**All sites**, `/traffic/`), **Where visitors came from, all your sites** has one row per site you may open: visitors from **Search**, **Social** and its top networks (**X**, **LinkedIn**, **Facebook**, **Instagram**, **Reddit**, **YouTube**), **AI** and **Direct**, for **Today**, **7 days**, **30 days** or **90 days**. The period is in the address (`#sources-30d`), and Back and Forward move between periods. Each figure opens that site's Sources with the network open. It shows no new figure: each cell is the site's own verified figure, the same its Sources tab shows. A site's row says how many days of the period are verified; a withheld day is left out and named. A cell no verified day recorded says **Not recorded**, never zero. A site you may not open is not listed at all. One person who visited two sites counts on both. ## First touch: the first visit we know of For every visit, Skald looks for the same visitor's first visit it knows of. It tries three links, in this order, and the first one that finds an earlier visit wins: 1. **The visitor ID**, when the script tag has visitor IDs on (Settings › Tracking). The visit is the ID's when the ID's page view of the landing page arrived within seconds of the visit, and no other ID did. Its first touch is the ID's earliest page view. 2. **The address and browser**, when the site keeps addresses (Settings › Data retention › People). The earliest kept visit from the same address with the same browser and system, even earlier the same day. 3. **The 30-day code**, when the visitor key is set. It gives the day and the source type of the first visit within 30 days, not the site that sent it. When none finds an earlier visit, the visit says **first visit we know of**. Skald never guesses. A site that keeps no addresses uses only the visitor ID and the 30-day code, and says which one linked each visit. One address can be several people (an office), and one person on two devices is two visitors. A visitor ID is one browser. ## Where you see it - **Traffic › Visitors**, in **Each visit**: under a visit's source, a line such as "First came from google.com, 3 days ago". It shows only when the first visit came from somewhere else. - **Overview › Real time**, in **Newest visits**: the same line, and each visit opens the visitor drawer. - **Traffic › Sources**, in **Where returning visitors first came from**: for the period, how many Direct visits came before, by where they first came from and by the site that sent them; and every returning visit by where it first came from. Each day's figures are counted twice, by two separate roads; a day where the two disagree is withheld. A day counted before this existed is left out and said. ## Direct, likely: what a Direct visit probably was Some Direct visits say more in their own request, or in the log just before them. Skald shows those as **likely** a cause, never as fact. Each Direct visit gets one reason at most, checked in this order: 1. **Direct, likely checkout or payment return.** The visit landed on a page with a payment provider's return code in its address (Stripe, PayPal, Razorpay, Checkout.com, Adyen), or on a checkout's end page, such as `/checkout/order-received/` or `/order/thank-you`. A bare `/thank-you` is not enough: a form's thank-you page looks the same. 2. **Direct, likely in-app browser.** The visit was opened inside an app's own browser: Instagram, Facebook, LinkedIn, TikTok, Snapchat, Pinterest, X, Gmail, the Google app, WeChat, Line, or an app that did not say its name. Those apps often send no referrer. 3. **Direct, likely shared link.** The link carried a share marker (such as `igshid=` from Instagram or `ref=whatsapp`), or an app such as WhatsApp, Slack, iMessage or Discord fetched a preview of the same page up to 30 minutes before. The preview comes from the app or the sender's phone, not from the person who clicked, so it is matched by page and time only. Anything else stays plain Direct. Where you see it: - **Traffic › Sources**, in **Direct, likely (inferred)** under the sources grid: each reason, its visits, its share of Direct and what it showed (for example "Stripe 3" or "WhatsApp 2"), and how many stayed plain Direct. The Direct row above keeps every one of these visits: nothing is moved out of it. The (i) says how it is worked out. In Compare mode it is not shown. - **The visitor drawer**, under **This visit**: a line such as "Direct, likely in-app browser (Instagram app)". Each day is counted twice, by two separate roads; a day where the two disagree is withheld, and a range that does not add up shows "Withheld". A day counted before this release says "Not recorded", never zero. Only the reason, the app or provider and counts are kept: never the codes in the address. An admin can turn it off, or change the 30-minute preview window, with `attribution.direct_likely` and `attribution.share_window_minutes` in the config file. This is a different estimate from **Likely from AI assistants** (AI & bots › Assistants). A visit can be in both. ## The visitor drawer Every visit and every visitor opens in the same drawer: a row of the visit list, a newest visit in Real time, a funnel's journey, a visit picked out of the visitor flow, a person found with **Find a person**, and a converted person or conversion under Leads & sales. - **The title** names the visitor by city ("Visitor from Toronto"), else by country, and the line under it says how many visits over how many days, and any booked demo or lead. - **At the top**: first seen, how many visits over how many days, where the visitor first came from (and how long ago; the link that tied the visits is on hover), the city (or the country) with its flag and three-letter code, the device with its browser and system, and the outcome: a booked demo, a lead, a form sent, or none linked. - **Journey, newest visit first**: each visit as a card, its steps down a line. Where it came from, each page in order with the time between pages on the line, and how it ended: left, sent a form, a lead, or a booked demo. Between visits it says how long after ("12 days later"). The steps appear one by one when the drawer opens; **Replay**, beside the close button, plays the visits again, oldest first, at their real pace (one second per minute, at most four seconds a visit). With reduced motion on, it stays still. Hover or focus a page for its time; click it to open the page. - **Every visit as a table**: open it for the same visits as rows, the first visit highlighted. - **Leads and conversions** linked to the visits. A lead is matched to a visit by the time a form was sent: likely, never certain. - **This visit**: the visit's own labels and campaign tags. A visit alone is anonymous. Every visit of the visitor shows only to admins and owners, and only where the site keeps addresses. Each time it is opened, the audit log records it (never the address). Pages seen only by the script tag, with no visit in the server log, are marked "seen by the script". --- # Retention: who came back > Skald docs: https://skaldanalytics.com/docs/retention/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). **Traffic › Retention** answers one question: of the visitors first seen in a week, how many came back in each week after? Or by day: of those first seen on a day, how many came back each day after. Like the retention reports in Google Analytics 4 or PostHog, with Skald's rules: every figure is counted twice, and what Skald cannot know is said, never shown as 0. ## Who is the same visitor Skald has no cookie. It knows a visitor again in one of two ways, and the page says which one your site uses, at the top of the tab: - **Kept visits**, when the site keeps IP addresses (Settings › Data retention › People). The same IP address with the same browser and system is the same visitor. Kept 395 days unless you changed it. - **The 30-day code**, when the site keeps no addresses but the visitor key is set. An address and browser family, hashed with a secret. It keeps a day and a source type only, so there is no split by country, device or landing page, and no "read 2 or more pages". With neither, the tab says so and counts nothing. The page script's visitor ID is not used here: it is kept 30 days and only for visits that ran the script, so it would make the same visitor count once or twice depending on the script. One address can be several people (an office), and one person on two devices is two visitors. ## New, and how far back A visitor is **new** in a week when its first visit there has no visit of the same visitor in the days before it: 332 days with kept visits (395 days kept), 15 days with the 30-day code. The rest of the days are needed to follow the cohort afterwards: Skald counts a cell only while every day it reads is still kept. So each identity can follow a visitor so far after the first visit: **62 days** with kept visits (all 8 weeks), **14 days** with the 30-day code (the next week, not the one after). A later cell says **not recorded**, never 0. While your history is young, "new" means first seen since Skald started keeping visits; the tab gives that day. ## Reading the triangle - Each **row** is a cohort: the visitors first seen that week (Monday to Sunday, in the site's time zone) or that day, and how many. - Each **column** is a later week (+1, +2 ...) or day. A cell shows the share that came back and how many. Darker is more, scaled to the highest share on the screen. - A **blank** cell: that week is not over yet. - A **hatched** cell: not recorded (later than the identity can follow, or its days were removed before it was counted), or held back (a day in it is not verified). - A cohort with fewer than 10 new visitors shows counts only: a share of a tiny group misleads. Hover or focus a cell for its figures. Click it to list the visitors behind it, each opening in the visitor drawer with every visit (admins and owners, on kept visits only; each list is written to the audit log, never the address). The **retention curve** under it is the share back at each step, over every cohort counted there: the visitors back, summed, over the new visitors in those cohorts. On a phone the triangle and the curve become two tables. ## Options - **Weeks or Days**: weekly cohorts over the last 12 weeks (8 weeks after each), or daily cohorts over the last 30 days (14 days after each). - **Came back** or **Read 2+ pages**: any visit, or a visit that read 2 or more pages. - **Split by** the first visit's source type, country, device or landing page, then pick a value. The 8 biggest values of each split are kept, the rest are "Other". The date range at the top does not apply: the tab has its own period. ## Checked twice The daily run counts the cohorts after it collects the day, and keeps them. The audit then counts each one again from the same kept rows with its own code. A cohort whose two counts disagree is withheld. A cohort counted on the last run says "not checked yet" until the next daily run. Kept visits and codes are removed after their keeping time. A cohort whose days are gone stays as last counted and checked, marked **inputs expired**: it can no longer be checked again. A visitor you forget (`ti.py forget --ip`) leaves every cohort still counted at the next run. --- # Internal traffic: leave your own visits out > Skald docs: https://skaldanalytics.com/docs/internal-traffic/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Tell Skald which internet addresses belong to you and your team, so your own visits do not count as visitors. This guide covers adding your address, adding a range by hand, pausing one while you test, editing and deleting one, and what changes in the figures. ## Why your own visits should not count You and your team open the site all day: to check a page, test a form, or show a client. If those visits counted, your figures would look busier than they are. A new page would seem popular because you opened it ten times. Skald keeps these visits apart. They are not deleted. They go into their own group, **our own traffic**, so the five groups still add up to everything in the server log. They are never counted as visitors or people. ## Where to find it Open **Settings > This site > Internal traffic**, or go straight to Internal traffic. Settings opens from the gear at the foot of the sidebar. The page is headed **Our own traffic**. The list of addresses is in the section **Internal addresses**. ## Add your own address This is the quickest way. Do it once from each place you work: the office, home, a laptop on another network. 1. Open **Settings > This site > Internal traffic**. 2. Under **Internal addresses**, type a label in the box beside **Add my current address**, for example "Office Mac" or "Home". 3. Press **Add my current address**. 4. Check that the new line shows in the list. Skald adds the internet address your browser is using right now. You do not need to know it or type it. > Many home connections get a new address from time to time. If your visits start to show up as visitors again, add your current address again, or ask your internet provider for your range and add the range instead. > On a VPN, the address added is the VPN's. Your visits count as yours only while you use that same VPN. ## Add an address or range by hand Use this for a teammate who is not on your network, a test machine, or a whole office range. 1. Open **Settings > This site > Internal traffic**. 2. In **Address or range**, type one address, such as `192.0.2.10`, or a range, such as `198.51.100.0/24`. 3. In **Label**, type whose it is, for example "Berlin office". 4. Press **Add address**. Both the address and the label are needed. A label can be up to 60 characters. What you can enter: - one IPv4 address, such as `203.0.113.5`; - one IPv6 address; - a range written with a slash, such as `198.51.100.0/24`. A range may be at most `/16` wide for IPv4 and `/32` for IPv6. Wider ranges are refused, because they would hide real visitors. > Ask your IT team or internet provider for your office range. Do not guess: a range that is too wide hides real visitors from you. ## Pause an address while you test Sometimes you want your own visits to count, for example to test a form or a campaign link the way a visitor would. Pause the address instead of deleting it. 1. Open **Settings > This site > Internal traffic**. 2. Find the line in the list. Its **Status** says **Active**. 3. Press the switch. It turns to **Paused**, with the time it was paused. 4. When you are done testing, press the switch again. It turns back to **Active**. While an address is paused, its visits count as visitors. You do not need to type the address again to turn it back on. The pauses are kept with the address, so a day counted again later gives the same figures. > Lines from the config file always apply. They show **Always on** and cannot be paused here. ## Edit an address 1. Open **Settings > This site > Internal traffic**. 2. Find the line and press **Edit**. 3. Change the address, the label, or both. 4. Press **Save**, or **Cancel** to keep it as it was. The change applies from now on. Days before keep the old label and address. A paused address stays paused after an edit. ## Delete an address 1. Open **Settings > This site > Internal traffic**. 2. Find the line in the list and press **Delete**. 3. Read the note and press **Yes, delete**, or **Keep it** to change your mind. From then on, visits from that address count as an ordinary visitor's again. Days already stored keep their figures. If you only want your visits to count for a while, pause the address instead. ## The list Each line shows the **Address or range**, its **Label**, **Since** (the day it was added), and **Status**: **Active** or **Paused** (with the time it was paused). - The list holds at most 50 entries. When it is full, remove one before you add another. - Some lines show a lock and the word **locked**. They were set on the server, in its config file, and can only be changed there. Ask whoever runs your server. - The list applies to every site on your Skald server, not only the one you are looking at. ## What changes in the figures, and from when An address counts as yours **from the moment you add it**. Nothing is recounted backwards. - **From now on:** visits from the address go to **our own traffic**, in the live view and in every day collected after that moment. - **Earlier today:** visits from before you pressed the button stay where they were counted. - **Past days:** they stay exactly as they were. A past visit from a newly added address stays a visitor. - **While it is paused:** visits from the address count as visitors. When you turn it back on, they count as yours again from that moment. - **After you edit it:** the new address or label applies from that moment. Past days keep the old one. - **After you delete it:** visits from the address count as visitors again from that moment. Past days keep what they had. This is on purpose. A day's figures never change after they are stored, so a report you sent last month still matches the dashboard today. > Add your addresses on your first day with Skald, before you start testing the site. That keeps your early figures clean. ## Our machines and the heartbeat The same page shows everything Skald counts as your own traffic, not only the addresses in the list: - **Our own requests**: every request from your own machines and tools together. - **By machine or tool**: each one by its label, such as the office, your own crawler, browser tests, or the heartbeat. Labels for tools are set on the server. - **How fast the server answered**: server time per hour for the last whole day. Uptime from the heartbeat, and every outage, is on Site > Health, under **Is the site up?**: - **Uptime, from the heartbeat**: the heartbeat is one request every five minutes to your site. If it is answered, the site is up. A red day had a gap of more than 10 minutes or a failed answer: the site was down, or the heartbeat itself stopped. - **Outages and heartbeat gaps**: each gap, with when it started, when it ended, and how many minutes it lasted. You see the heartbeat parts only if your server runs a heartbeat. If no heartbeat has ever reached the log, **Is the site up?** says **Heartbeat not set up** and how to add it, with no uptime figure and no outages. ## Who may change the list It depends on how people sign in to your server. | Server type | Who may add or remove | Who may see the list | | --- | --- | --- | | Team accounts | An install admin only, because the list applies to every site on the server | Install admins see every entry. Other admins see the entries added on their own sites, but cannot change them | | One shared login | Admins | Everyone who signs in; viewers cannot change it | Pausing, resuming, editing and deleting follow the same rule as adding. On a server with team accounts, a viewer does not see the addresses at all. The page says the list is for admins, because it holds the addresses of the people who work on the site. If the buttons are greyed out, hover over one to see why. Ask an install admin to add your address for you. See [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md) for roles. ## Privacy Your team's list of addresses is shown only on this page and kept in the server's change log. Like every visitor's address, your team's requests also show, behind sign-in, in the last 6 hours of Site > Requests. The list itself never appears in the data files, CSV downloads, the API or mail. Every add, pause, resume, edit and delete is logged with who did it and when. See [Privacy](https://skaldanalytics.com/docs/privacy.md) for more. ## If something goes wrong | Message | What it means | | --- | --- | | "Give it a label first" | Type a label before you press **Add my current address** | | "Both an address (or range) and a label are needed" | Fill in both boxes | | "is not an IP address or range" | Check the address for typing mistakes | | "that range is too wide" | Use a narrower range: at most `/16` for IPv4, `/32` for IPv6 | | "is already listed" | The address is in the list already, perhaps as a locked line | | "is already paused" / "is not paused" | Someone changed it a moment ago. Reload the page | | "that entry is in config.yml" | The line is set on the server and always applies. Ask whoever runs your server | | "The list is full" | Remove an entry first; the list holds 50 | | "your address is not known here" | The server cannot see your address. Add it by hand instead | | "Only an install admin can change this" | Ask an install admin | ## From the server's command line Whoever runs the server can do the same from a shell. In Docker, put `docker exec skald` in front: | Command | What it does | | --- | --- | | `skald ours list` | Every line by label: where it is set, **Active** or **Paused**, and since when | | `skald ours pause "Office Mac"` | Pause it (a label or the address) | | `skald ours resume "Office Mac"` | Turn it back on | | `skald ours edit "Office Mac" --label "Office" --address 192.0.2.20` | Change the label, the address, or both | | `skald ours delete "Office Mac" --yes` | Delete it | The commands never print an address: lines are shown by label. The page shows a change within two minutes. ## Related guides - [Getting started](https://skaldanalytics.com/docs/getting-started.md): the first things to set up. - [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md): the other settings pages. - [FAQ](https://skaldanalytics.com/docs/faq.md): common questions about the figures. --- # Leads and conversions > Skald docs: https://skaldanalytics.com/docs/leads-and-conversions/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Where Skald's leads and conversions come from, what the **Leads & sales** section shows, what "abandoned" means, and how to see every lead and conversion the figures leave out. The **Leads** tab lists every lead of a period, one row each; owners and admins see each lead's name and email there, when your site keeps them. ## Leads & sales: one question per tab Everything about results sits in **Leads & sales** in the menu. Each tab answers one question, written under its title, and has the same shape: one row of options, at most one row of numbers, then one main table or chart. The period is the one in the top bar, the same on every tab. A small **i** beside a title or a number says what it counts; **Learn more** opens this guide at the right part. | Tab | The question it answers | | --- | --- | | Leads | Who are my leads? One row per lead: when, who (name and email, owners and admins), what they did, its stage now, where it came from | | Summary | How many leads came in, and is it better than before? | | Sources | Where did the leads come from? First, last and assisted touch, the time to convert, the pages seen before | | Revenue | Which sources, pages and campaigns made money? Sales with a value, per currency, refunds taken off, new customers, renewals and lifetime value. See [Revenue](https://skaldanalytics.com/docs/revenue.md) | | Calendar bookings | Who booked a call? Bookings made through Calendly and Cal.com, step by step, and payments Stripe reports where your site sends them | | Funnels | Any series of pages and events. See [Funnels and goals](https://skaldanalytics.com/docs/funnels-and-goals.md) | | Events | What visitors did inside your pages, from the script tag | | Find a person | Everything kept about one address, email, lead or visitor (owners and admins) | Older links keep working: `#sales/pipeline` opens Summary with the same options, `#leads` opens Summary counting from the lead database, and `#conversions` opens it counting from the tools. ### The Leads tab Leads & sales > Leads lists every lead of the period, newest first: when it came in; the name and email (owners and admins only, and only when **Leads' names and emails** is **full**, in **Settings > Data retention > People**); what they did (a sign-up, a contact form, a demo booking), with its label; its stage now (completed, won, lost, cancelled, no-show); where it came from (**Lead database**, a booking tool, the page's script tag, or your site's server); the source of the visit it came in on and the first source the browser remembered; the landing page and the country. A row opens the visitor drawer with the lead's steps and pages. - **Reference.** Each lead carries the ID the sender gave it: your site's own account ID (`u-119797`), a booking's address at Calendly. Turn on the **Reference** column under **Columns** to see it; the search box finds a lead by it either way, and the CSV export always holds it. The copy icon beside it copies the whole ID, so you can find the lead in your own admin. The lead's drawer shows it too, with where it came from ("Reference: u-119797 (from example.com's server)") and every step of that reference, oldest first. Everyone who can see the leads sees the reference: it is your system's record number, not a name or an email. One exception: a lead the page's script tag reported has a reference made from the browser's visitor ID, so only owners and admins see that one. - Leads & sales opens on the **last 7 days** when the link names no period. Pick another in the top bar (or **All time**) and it stays while you move between the section's tabs. - **On Leads, 7, 30 and 90 days include today.** 7 days are today so far and the 6 whole days before it: 7 calendar days. The heading gives the dates from and to, for example "Last 7 days: Fri 16 Oct to Thu 22 Oct (today, so far)"; the year shows only when the days cross into a new year. Today's leads are in the list, newest first, each with a small green **live** dot. - **Today so far.** A line under the count says how many of the period's leads came in today ("Today so far: 2 of these 5 leads"), with **Show today (live)** for today alone. It also says what is verified: the whole days are checked by the second count as always; today is live, counted from the lead store now, and checked again once the day is whole. - **The change compares like for like.** Today is not over, so the period before is counted to the same hour: its whole days, then its last day up to the time it is now. For example "25% more than the 7 days before (4 leads: Fri 9 Oct to Thu 15 Oct, up to 14:00 EDT)". A part of a day is never compared with a whole one. With a **Label**, or a source, country, device or campaign filter on, no change is shown (the day cut to the hour is counted by kind of lead only), and the line says so. - **The other tabs keep whole days.** Summary, Sources, Calendar bookings, Funnels and Events count 7, 30 and 90 days as whole days up to yesterday, with the "Today so far: 3 new leads" line above them. The Leads line names their dates, so the two periods are never confused. **Yesterday**, **Today**, **All time** and a custom period are the same on every tab. - The count above the list is the **Leads** figure itself, the same as Overview's Leads tile and Summary on the same days, with the change against the period before, and what is applied: "5 leads · filtered: Source = Search engine". The list never makes a count of its own. The CSV export is named after the period's days, for example `leads-2026-10-16-to-2026-10-22.csv`. - **One filter row.** **What**, **Label**, **Stage now** and **Source** narrow the list; under **More**, **Show left out** adds the test, spam and duplicate leads, marked, never counted. The top bar's **Filters** apply here too, for what a lead carries: its source, country, device and campaign. Every filter that is on, from either place, is one chip in the **Filtered by** line under the row; its **×** takes it off, and **Clear all** takes them all off. A top-bar filter a lead does not carry (a page read, a browser, a referring site) shows greyed, with the reason, and is not applied. - The count follows the filters: What, Label and one of source, country, device or campaign narrow the Leads figure itself. Two of those at once (say a source and a country), a label with one of them, **Stage now** or **Show left out** cannot be counted as the figure, because the figure keeps each breakdown on its own; the line then counts the rows listed ("8 listed"), beside the period's figure, and says why. - **A period with no lead** says where the leads are: "No new leads in the last 7 days, Fri 16 Oct to Thu 22 Oct (today, so far). 5 leads in the last 30 days, Wed 23 Sep to today.", with **Show last 30 days** and **All time**, or the leads since the first day when the last 30 days hold none. Both counts are the same Leads figure over those days. With no lead at all it says **No leads yet** and how to connect a source of leads. - The search box, **Columns** and **Export** work as on every table. The Leads table has no **Filter** button of its own: the row above it is its filter row. - The same leads come in four shapes, picked with the small buttons at the right of the options: **List**, **Board** (a column per stage now: new or open, won, lost, cancelled or no-show; read-only, because stages come from your tools), **Timeline** (leads per day, split by what they did or by source; press a day to list its leads) and **Map** (leads by country). The timeline and the map draw the Leads figure itself, so they always agree with the count. They count a lead when it came in, so **Stage now** and **Show left out** are greyed there; the map counts by country alone, so a source, device or campaign filter is greyed on it, and with a label on it says the map is not split by label. - Every load that shows names and emails is written to the audit log as "lead list viewed", with the period and the number of rows, never a name or an email. Members who are not owners or admins see the same list without names and emails. ### Count from: two roads, never added Summary and Sources have one row of options above their figures. At its right end, **Count from** picks where the figures come from: - **Lead database** (the default, when your site has one) counts the leads in your website's own lead database: sign-ups, contact forms, early-access requests and demo bookings, as your team marks them. - **Tools** counts every action that matters, from every road at once: your booking, form and payment tools, the script tag on your pages, your site's server, and the lead database's rows. The two are never added together, and never share a table. A booking that reaches Skald from Calendly, or a form from Typeform, shows only with **Tools**. The row's **i** says which road the figures come from. A site with no lead database opens with **Tools**. If you choose **Lead database** there, the tab says so in one line ("This site has no lead database connected. Its leads come from its tools") with a button, **Show leads from the tools**. To connect one, see [The lead database](https://skaldanalytics.com/docs/leads-and-conversions.md#the-lead-database) below. The other options narrow what you see: - **What**: one kind of lead or action, for example Demo booking or Sign-up. - **Label**: one label your site's server sent with its leads, for example a contact form's topic: sales, support or partnership (tools only; see [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md)). **Every action** then counts only the leads with that label. Where they came from, the touches and the time to convert are not split by label, so they are left out while a label is chosen. The lists of conversions show each lead's label as a small tag. - **Status**: one status: New, Contacted, Qualified, Won or Lost (lead database only). - **Source**: one source, on Sources (lead database only; the tools do not split their stages by source). The address keeps every option, so a link or a reload opens the same view. ## Leads on every page Leads and demo bookings also show where you look at your visitors, so you can see which source, page or country brings them. All of these use the tools' figures (Count from: Tools), from every road at once. A **lead** here is a form sent (contact, sign-up, early access, trial) or a demo booked, once completed. **Demos booked** are counted on their own as well. Payments, deals and clicks are not leads. A conversion marked test is never counted. | Tab | What you see | | --- | --- | | Overview > Summary | **Leads** and **Demos booked** tiles for the period, with the change against the period before. Each tile says which count it is ("From the tools", "From the booking tools", or "From the lead database") and links to the same figure: Summary counting from the tools, or Calendar bookings. Pink dots on the main chart: each day's leads. | | Overview > Summary, with **Today · Live** on | The **Leads** tile for today so far, against yesterday up to the same time of day ([vs yesterday at this hour](https://skaldanalytics.com/docs/real-time.md#vs-yesterday-at-this-hour)), and **What converted today**: each completed form and demo booking with its time, what it was, the page, the country with its flag, and where it came from (and where the visitor first came from, when that differs). A link opens all of today's on Leads. | | Traffic > Sources | **Leads** and **Lead rate** beside each source's visitors. | | Traffic > Pages | **Landing pages, with leads**: visitors who started on each page, its leads and lead rate. | | Traffic > Places & devices | **Leads** and **Lead rate** beside each country. | | Overview > Insights | Findings about leads, for example "Leads from AI assistant up 60% week on week" or "/pricing/ is the page that started the most leads". | With **Compare with…** on, the Traffic grids add the other period's visitors and the change; their lead columns stay the period in the top bar's. Which touch each column uses: - **Sources**: the last touch, the source of the visit the lead was sent from. - **Pages**: the landing page recorded with the lead (its first visit's, when known). - **Places & devices**: the lead's country, from the tool or the visitor's address. The honesty rules are the same as everywhere: - Only days with **checked** conversions count. A day the second count disagreed on is withheld. A day before conversions were recorded is "not recorded", never zero. - The **lead rate** is leads per visitor over the same days only: days whole in the visitor figures *and* checked in conversions. It is given over at least 10 visitors. Below that you see the count alone, with "too few to say". - A **change** (Overview, Compare with…) is shown only when both periods are whole and every day of both has checked conversions. - **Insights** about leads follow the same rules as the others: two whole periods, at least 5 leads, a change of at least 20%. A finding the second count does not confirm is left out, and the tab says so. - Leads are counted for the whole site. On a page group (for example one language), the lead columns are not shown. > A lead reported by two roads (for example the script tag and the lead database, for the same form) counts on each, as on Summary with Count from: Tools. A lead that arrived from two tools with the same reference (for example the lead hook and Zapier, or a hook and the lead database) is flagged as "One lead, two tools" under **Settings > Integrations > Status** and by the server's doctor command. The script tag's own rows are not part of that check. ## Where leads and conversions come from There are four sources. You can use one or all of them. | Source | What it gives | Shown on | | --- | --- | --- | | Your website's lead database | every lead, its kind, its status, first and last touch | Leads; Summary and Sources (Lead database and Tools) | | Your site's lead push (the `lead` hook) | a lead your own server reports the moment it is made | Leads; Summary and Sources (Tools) | | Webhooks from your tools | bookings, cancellations, no-shows, form answers, payments, CRM deals | Leads; Summary and Sources (Tools); Calendar bookings | | The `skald.js` script tag | forms started and sent, booking widgets opened and booked, phone and email clicks | Leads; Summary and Sources (Tools); Calendar bookings; Events | ### The lead database If your website keeps its leads in its own database, the person who runs your Skald server can point Skald at it. Skald opens it read-only and never changes it. Every two minutes it reads the leads that changed. - Leads marked `spam` or `duplicate` are left out of every count, and counted apart. They are listed in full on Leads, under **More > Show left out**. - A lead's status (new, contacted, qualified, won, lost) is read as it is now. When your team changes a status, past days change with it. - Names and emails are read and kept only when **Leads' names and emails** is **full** (Settings > Data retention > People). Owners and admins then see them on Leads, in the person search and in the left-out lists. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ### Your site's lead push Your site's own server can tell Skald "a lead was made" with a signed message. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md), section "Your own site's lead push". ### Webhooks from your tools Calendly, Cal.com, HubSpot, Typeform, Stripe, Zapier and Make can send Skald a signed message for each booking, form or payment. This is how Skald learns about cancellations, no-shows and payments, which the page itself never sees. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md). ### The script tag One line on every page lets Skald see what happens inside the page: a form started, a form sent, a booking widget opened and booked. Only the script tag can see a *start*, so only it makes "abandoned" possible. See [The tracking script](https://skaldanalytics.com/docs/tracking-script.md). ## One name per action Whatever reports it, an action gets one name, so the same thing is never counted under two names. | Action on the page | Means | Reported by | | --- | --- | --- | | Demos | a demo or meeting booked | Calendly, Cal.com, HubSpot meetings, the script tag, the lead database | | Contact forms | a form sent | the script tag, Typeform, HubSpot contacts, the lead database | | Sign-ups | a sign-up form sent | the script tag, the lead database | | Early access | an early-access or waitlist form sent | the script tag, the lead database | | Payments | a Stripe Checkout paid | Stripe | | Deals | a HubSpot deal moved to won | HubSpot | | Phone clicks, Email clicks, Button clicks | a `tel:` link, a `mailto:` link or a marked button clicked | the script tag | Each action has a *stage*: started, completed, paid, cancelled, rescheduled, no-show, won or lost. ### A lead counts once, even when it is won or lost later A lead is counted once, on the day it was completed: a form sent, a demo booked (or a payment made). When your team later marks it **won** or **lost**, in your lead database or through your tools, it is still that one lead, on that same day: - the outcome never adds a second lead and never takes the first one away; - the day the outcome arrives counts no lead for it; - won and lost are shown apart, as outcomes: the **Status** option on Summary, **Stage now** on Leads, and **Won** on Calendar bookings; - a "won" or "lost" for something that was never completed is not a lead. This is the same on Overview's **Leads** tile, the daily and weekly mails, client reports and Leads & sales. Only spam and duplicates are taken out of the leads. ## Summary and Sources, counting from the lead database Open Leads & sales > Summary (the figures) or Sources (where they came from). **Count from: Lead database** is the default. The period governs most of the tab. Overview's **Leads** and **Demos booked** tiles count from the tools when the tools are set up: choose **Tools** here for the same figures. A kind of lead the lead store has never recorded (often **Demo opened** and **Demos booked**, which come from the booking tool) shows **Not set up** with a link to Calendar bookings, never 0. ### The figures at the top - **Leads**: sign-ups, contact forms and early access, with spam and duplicates removed. - One figure per kind: Sign-ups, Contact forms, Early access, Demo opened, Demos booked. - **Leads per 100 visitors**: leads against the visitors of the same verified days. It is given only over at least 10 visitors. Under them, a line says how many spam and duplicate leads were left out. Select **Show them, with test leads**, or turn on **Show left out** in the options, to see each one: when it came, its kind, page, first and last source, and its status. Leads marked test are listed there too. Owners and admins also see the email and name, when your site keeps them. ### Per day, by kind A chart of each kind of lead per day. A hatched stub is a day with no lead counts. That day is *not recorded*, never zero. ### Where they came from Each lead is shown twice, by its **First touch** and by its **Last touch**: - **First touch** is the source the lead's own browser remembered from its very first visit. - **Last touch** is the visit the form was sent from. There are four tables: by Source, by AI assistant, by Campaign and by Landing page. A lead without a value is said under the table, not shown as a row. ### Likely from an AI assistant Many people who click a link in a ChatGPT, Perplexity, Claude, Copilot or Gemini answer arrive with no referrer. Their touch then says "Direct" (no referrer and no campaign tag). The **Likely AI (inferred)** table, beside the four above, gives Skald's best guess for those touches, by first touch and by last touch. It is an estimate, never a count. How Skald guesses: 1. The touch came with no referrer and no campaign tag. 2. When someone asks an assistant, the assistant fetches the page from your server at that moment. Skald keeps those fetches: which assistant, which page, which minute. 3. If an assistant fetched the touch's landing page in the same minute or up to 15 minutes before, the touch is **likely** from that assistant. Your server's admin can change the window (`attribution.ai_window_minutes`). 4. Unless chance explains it: when an assistant fetched a page so often that day that the page's ordinary direct visits would land after its fetches anyway (your home page, typically), its fetches of that page that day are left out. What it shows: - **ChatGPT** (or another assistant): one assistant fetched the page in the window. - **More than one**: two or more did. The lead counts once. - A touch that was not direct, or whose page no assistant fetched in the window, is not in the table. - **Not recorded**, under the table: Skald did not keep the fetches for that time. For example, the touch is from before this feature, more than 180 days ago, or today (today's fetches are kept the next morning). > Why only "likely"? A fetch can be for someone who read the answer and never clicked. A direct visit a minute later can be someone else. Skald never adds these to the AI assistant source or to any count. The (i) beside the title says the same. ### Lead quality by source Statuses by first-touch source: New, Contacted, Qualified, Won, Lost, Spam, Duplicates. The column **Qualified or won, of reviewed** is the share of leads that are no longer new and were marked qualified or won. It is given over at least 10 reviewed leads. If every lead is still "new", the tab says so and shows no zeros. ### Time to convert - **From first touch to lead**: how long after the first visit the lead came, in buckets from "Under an hour" to "Over 90 days". - **Visits up to the lead**: the browser's own count of its visits when it sent the form. ### Funnel, by source From visitors to demos booked, per source: **Visitors**, **Saw pricing**, **Opened a form**, **Opened the demo**, **Became a lead**, **Booked a demo**. Each step is shown as a share of that source's visitors, over at least 10 visitors. > The first three steps are visits from your server log. The last three come from the lead database, by the lead's first-touch source. Skald puts them side by side for the same days. It never joins one lead to one visit. ## Summary and Sources, counting from the tools Open Leads & sales > Summary and set **Count from** to **Tools**. If nothing is set up yet, the tab lists each source as **On** or **Off** and says what to turn on. Summary shows **Leads**, **Demos booked**, **Won** and **Lost** against the period before, **Every action** side by side and the **Best sources**; Sources shows where the completed ones came from, the touches, the time to convert and the pages seen before. ### Every action One row per action, with these columns: - **Started**: starts seen by the script tag. "not seen" means no start was reported, for example an action only a tool reports. - **Completed**: completed and paid together. A lead that sent both counts once. - **Abandoned**: see below. - **Completion rate**: completed out of started, given over at least 10 starts. - **Cancelled or no-show**, **Won**, **Lost**. ### Calendar bookings: the demo funnel Leads & sales > Calendar bookings holds the calls booked through Calendly and Cal.com, and the payments Stripe reports where your site sends them, with the script tag and the lead database. Pick the booking action in **Booking**. For demos: **Opened the booking**, **Chose a time**, **Booked**, then **Cancelled**, **Rescheduled**, **No-show** and **Won**. "Chose a time" is a count of page events from a Calendly embed, not of browsers. Cancelled, rescheduled and no-show need the booking tool's webhook. Under **Where the booked ones came from**, a breakdown the booking tool does not carry (first touch, landing page, country, device) is named in one line instead of a card that only says "Not recorded". ### Where the completed ones came from Completed conversions by Source (last touch), First touch, Campaign, Landing page, Country, Device and **Reported by** (which source told Skald). A value Skald was not told is shown as "Not recorded", never guessed. ### First, last and assisted touch - **First touch**: where the browser, or the lead, first came from. - **Last touch**: the visit it converted in. - **Assisted**: a source seen earlier in a converting browser's journey that was not its last touch. Each conversion counts once per source. Assisted needs anonymous visitor IDs (below). ### Likely from an AI assistant (conversions) The section **Likely from AI assistants (inferred)**: completed conversions whose first touch was direct, by the assistant that likely sent them, the same guess as with the lead database. The first touch is the lead's or the tool's first touch; for the script tag, the browser's first visit. "Not recorded", under the table, means the first touch, or the fetches around it, were not kept. Likely, never certain, and not part of the sources above. ### Time to convert **From first touch to converting** and **Visits before converting**, for completed conversions. A conversion whose first touch is dated after it is counted as a clock error and left out. ### Pages seen before converting For the last 30 days: the share of converting browsers that saw each page before converting, beside the share of browsers that did not convert. Use it to ask "does the pricing page help?". It needs visitor IDs. ### The latest leads, on Leads Leads & sales > Leads lists every lead of the period, newest first (see [The Leads tab](https://skaldanalytics.com/docs/leads-and-conversions.md#the-leads-tab)). A row opens its steps and the pages its browser read, for example "Demo started, then completed". Owners and admins find one person, with every visit and lead, with **Find a person**. ### Cancelled, removed and test Nothing is hidden. On Leads, **More > Show left out** lists the test, spam and duplicate leads beside the counted ones. Calendar bookings lists, in full, the conversions the figures do not show as completed, under **More > Show left out**: - **Cancelled or no-show**: bookings the booking tool reported as cancelled, or as a no-show. - **Removed**: rows a lead-database sync took away. For example, the lead was deleted, or marked spam. Each row is kept whole, with when and why. - **Test**: conversions marked as a test (see below). They are kept and listed, and never counted. Each one shows its row and, when it had a visitor ID, the pages it read. ## Marking a conversion as a test A test booking or form should not count. There are three ways to mark a test lead. All three make the same mark, on every stage of the same booking or lead (the same reference from the same tool): 1. **In the dashboard (owners and admins).** Open the lead: a row on Leads, a booking on Calendar bookings, a conversion in a visitor's drawer, in **Find a person** or in the **Show left out** lists. Under its reference, press **Mark as test**. Skald asks first: "Mark u-119797 as test? It stays listed under Show left out and leaves every count." Press **Mark as test** again to do it, or **Cancel**. A short message says how many steps were left out, and the list and the counts update. On a test lead the same button says **Not a test**: it counts the lead again. Viewers do not see the button. 2. **From your site's server.** Send your team's own accounts with `"test": true`, or mark leads you already sent with the signed `leads/test` call. See [Leads from your server](https://skaldanalytics.com/docs/leads-from-your-backend.md#10-mark-leads-as-test). 3. **On the server.** The server admin runs `ti.py conversions list example.com` to see the latest conversions with their numbers, then `ti.py conversions mark-test example.com 6 7` to mark conversions 6 and 7 as test, or `ti.py conversions mark-test example.com --ref u-1001` to mark every step of the lead with that reference. To count one again: `ti.py conversions mark-test example.com 6 --unmark`. A lead sent with the wrong step or action can be taken out of the counts without deleting it: the server admin runs `ti.py conversions remove example.com --ref u-1001 --why "sent twice" --dry-run` to see what would go, then the same without `--dry-run`. Each removed step is kept, with when and why, under **Show left out > Removed** on Calendar bookings. Your site can then send the lead again, correct. One lead counts once: if your site sends both a **completed** and a **paid** step for the same reference and kind of lead (a sign-up that later paid), it is one lead, on the day of the first one. The second step shows in the lead's drawer. A test conversion is left out of every figure, and of the second count too. It stays listed, marked test, under **Show left out** on Leads and on Calendar bookings. Each change is written to the audit log with who made it, never the reference or a name. A later step of a marked lead (won, lost) is test too. For a lead in your website's lead database, mark it spam there, or give it the status `test`. ## What "abandoned" means A conversion is **abandoned** when all of this is true: 1. The script tag saw it start (a form field used, a booking widget opened). 2. The start carried an anonymous visitor ID. 3. Neither the same browser nor the same person (the same email, when your site sends it) completed or paid for the same action within the abandon window. The window is 24 hours unless your server sets another. So a demo opened today and booked tomorrow from another device is a booking, not an abandoned demo. So: - A start still inside the window is not counted as abandoned yet. - A start without a visitor ID cannot be judged, and is never counted as abandoned. - Abandoned is worked out each time the tab is drawn. It is not stored. ## How a conversion is tied to a visit Skald ties a conversion to a journey only through an **anonymous visitor ID**: a random ID the browser makes, such as `skv_` followed by 22 letters and digits. It holds no name, email or fingerprint. - The script tag makes the ID and sends it with each event. - It adds the ID to Calendly links and embeds as `utm_content`, and Calendly sends it back in its webhook. - For other tools, the ID is passed in a hidden field or a metadata value. [Webhooks](https://skaldanalytics.com/docs/webhooks.md) says how for each tool. - Your site's lead push and lead database can carry the same ID. The figures never match by IP address, and never link a new ID to an old one. A visitor ID is a *browser*, not a person: one person on a phone and a laptop is two browsers. The person search can show, apart from the figures, the visits that sent a form at a lead's time: a match by time, never certain. See [Privacy](https://skaldanalytics.com/docs/privacy.md). > Visitor IDs are off by default. An owner or admin turns them on per site in **Settings > This site > Sites > Script tag**. Integrations says whether they are on. Your privacy notice must say so, and some places need consent first. See [Privacy](https://skaldanalytics.com/docs/privacy.md). Without visitor IDs, conversions are still counted, but there are no journeys, no abandoned counts, no assisted sources and no pages seen before converting. With them off, Skald keeps no visitor ID from any road: not from the script tag, not from a webhook's `vid`, and not from your lead database's session column. ## Goals Skald's goals are not set on Leads & sales' figures. A goal is a page event, such as "demo booked" or "sign-up sent", shown per 100 visits by source on Leads & sales > Events. Admins choose them in **Settings > This site > Sites > Goals**. See [Funnels and goals](https://skaldanalytics.com/docs/funnels-and-goals.md). ## When a figure is missing - **Withheld**: every figure is counted twice, by two separate roads. When they disagree on a day, that day is withheld and left out of every sum. The tab says which days and why. - **Not recorded**: days before a source was set up are not recorded, never zero. - **Too few to say**: no rate is given over fewer than 10. ## Finding one person Owners and admins can type an IP address, an email, a lead's number or a visitor ID in Leads & sales > Find a person, or in the search box at the top (or **Ctrl K**). It shows that person's visits, leads and conversions, spam, test, cancelled and removed ones too, linked together. Each search is written to the audit log. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## Removing a person's data To delete one person's data (for a privacy request), ask the person who runs your Skald server. They run `ti.py forget` with the email, IP address, visitor ID or lead reference. It deletes their conversions, the removed rows, their name and email, and the visits kept with their address. --- # Revenue > Skald docs: https://skaldanalytics.com/docs/revenue/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Which sources (AI assistants among them), pages and campaigns made money: send a value and a currency with each sale, and **Leads & sales > Revenue** sums them per currency, takes refunds off, counts new customers and renewals, and shows lifetime value by first source. No adapter per payment tool: any tool that can reach Zapier, Make, n8n or your own server can send its sales. ## What the Revenue tab shows Open Leads & sales > Revenue. The period is the one in the top bar. - **The numbers, per currency.** **Net revenue** (what was sold, less what was refunded in the period), **Sales**, **Refunds**, **New customers** (a customer's first paid sale), **Renewals** (a paid sale of a customer who had paid before) and **Revenue per visitor**. - **Revenue per visitor** is net revenue divided by the visitors of the same verified days: two counted figures, divided. It is given over at least 10 visitors, and only when every day of the figures has its whole visitor count. - **Currency.** With sales in more than one currency, a **Currency** switch picks which one the numbers show. The others are listed under the numbers, never added. Skald never converts one currency into another: an exchange rate is not something it can check. - **Breakdown.** The table shows net revenue and sales by **Source (last touch)**, **First touch**, **Campaign**, **Landing page**, **Country**, **Device**, **Reported by** (the tool), **Label** or **Tag**. A landing page has copy and open icons at its end, as every page does. - **Tag.** Choose one tag (for example `plan = pro`) and the numbers become that tag's own net revenue and sales, and the table lists every value of that tag. Refunds, customers, renewals and the other breakdowns are not split by tag, so they show with **Every tag**. - **Lifetime value, by first source** (folded at the bottom). Every customer, by the first source of their first paid sale: how many customers, what they paid in all (refunds taken off), the average per customer, their renewals and the share who paid again (given over at least 10 customers). It counts every sale Skald keeps, not the period in the top bar; **Settings > Data retention** applies. The currency, the breakdown and the tag are in the page's address (`#sales/revenue?cur=EUR&by=campaign&tag=plan:pro`), so a link or a reload opens the same view. A lead's drawer on Leads shows the sale's own **Value**, what was **Refunded** on it, the **Customer** reference your tool sent (with a copy icon) and its **Tags**. ## The rules behind the numbers - **A sale** is a completed or paid row that carries a value, counted once per sale (the paid step when a sale reports both). A sale without a valid value and currency is still a lead, as before; it is not revenue. - **A refund** counts on the day it arrives, against the sale's own source, campaign and page, not on the sale's day. A refund sends the total refunded on that sale so far; a later, larger total counts the difference on its own day, so two part refunds of $10 and then $15 are sent as 10, then 25. It counts only against a sale Skald counts, in the sale's own currency, and never more than the sale's value. A refund with no value takes off the whole sale. A refund of 0 is no refund. A refund that arrives before its sale waits and joins the sale when it comes. - **A refund that came before its sale gets no note later.** When the sale arrives, a refund that was waiting for it is cut to the sale's value, or kept without its value when the sale is in another currency. Skald answered the refund when it came, before the sale was known, so nobody is told about the cut: check the figures on this tab or the lead's drawer. - **A renewal** is a paid sale with its own reference (the invoice) and the same customer reference as an earlier paid sale. The first paid sale of a customer is a new customer. A sale without a customer reference is its own customer. - **A refund counted nowhere** is kept but never taken off: its sale is a test, was removed or never came, it was sent in another currency than its sale (kept without its value), or its value could not be read. The tab lists how many under the table, over every row kept, with the total of those kept with a value. - **Test rows and removed rows never count.** - **A withheld day.** Every revenue figure is counted twice, the second time on its own road (from the stored rows and again from the deliveries as they arrived). A day the two disagree on is left out of every figure and said once at the top: "1 day withheld (...): left out of every figure below, never counted as zero." - **Not recorded is never zero.** Revenue is recorded from the first day a sale or a refund with a value was kept (in the site's time zone). The days before it have no revenue figures at all, not zero: the tab says from which day values are recorded, and revenue per visitor counts only the days with figures. ## Send value and currency Add these to the lead body your site's server, Zapier, Make, n8n or a lead file already sends (see [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md) and [Webhooks](https://skaldanalytics.com/docs/webhooks.md)): ``` { "external_ref": "inv_1042", "action": "paid", "stage": "paid", "value": 49.00, "currency": "USD", "customer_ref": "cus_1001", "tags": {"plan": "pro"} } ``` | Field | What to send | | --- | --- | | `value` | The amount in the currency's normal units: `49.00` dollars, `1200` yen. A number, or a number as text (`"49.00"`), zero or more, with no more decimals than the currency has, and at most 30 characters long. Many tools send amounts in cents: divide by 100 first (not for a currency without decimals, such as JPY) | | `currency` | The three-letter code: `USD`, `EUR`, `GBP`, `JPY`. Needed with `value`. Lower case is fine (`usd`) | | `customer_ref` | Your customer's own ID in your system or tool (`cus_1001`, `u-119797`): letters, digits and `_ . : -`, at most 128. Never an email, a phone number (only digits and `+ ( ) . -`, with 7 or more digits) or the shape of an IP address: such a reference is dropped, with a note. It is what joins a renewal to the first sale | | `tags` | Up to 5 labels of your own, such as `{"plan": "pro", "seats": "5"}`. Names: lowercase letters, digits and `_`, at most 24. Values: lowercase letters, digits and `_ . -`, at most 40, and never 7 or more digits in all (`-` and `.` not counted) or the shape of an IP address. A name that is a person's detail (`email`, `phone`, a name) is never kept | | `stage: "refunded"` | A refund: the same `external_ref` as the sale, with `value` the total refunded on that sale so far: for a second part refund, send the new total (leave it out for a whole refund). At most the sale's value, in the sale's currency | A bad value is never a refused lead: the lead is kept without the value, and the answer and the delivery log say what was dropped. The same for a bad currency, customer reference or tag. **Renewals.** Send each paid invoice as its own sale: its own `external_ref` (the invoice ID) and the same `customer_ref`. **From the lead files and the WordPress plugin.** The PHP, Node and Python lead files take `value`, `currency`, `customer_ref` and `tags` in their last argument (see [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md)). The WordPress plugin takes money only from your own code, through its `skald_leads_lead` filter: a form field never sets a value, since any visitor could post any amount. ## Stripe Stripe needs no field mapping: Skald reads the amount, the currency and the Stripe customer from Stripe's own events. Subscribe the Stripe webhook to `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `invoice.paid` (renewals), `invoice_payment.paid` (on Stripe's API 2025-03-31 and later, the only event that ties a renewal's payment to its invoice, so its refund finds it) and `charge.refunded` (refunds). See [Webhooks](https://skaldanalytics.com/docs/webhooks.md). ## Other payment tools Polar, Dodo Payments, Paddle, Lemon Squeezy and Gumroad send webhooks in their own shape. Point the tool's webhook at Zapier, Make or n8n (or a small script on your server), map its fields to the body above and post it to the **Zapier, Make, n8n (json)** hook. The field names for each tool, with a link to its own documentation: [docs/INTEGRATIONS-REVENUE.md](https://github.com/spcaeo/skald/blob/main/docs/INTEGRATIONS-REVENUE.md). Tools change their payloads: check your tool's payload against its test event before you rely on the mapping. What to map, in short (amounts these tools send are in cents: divide by 100 for a two-decimal currency): | Tool | Sale event | `external_ref` | `value` | `currency` | `customer_ref` | Refund event | | --- | --- | --- | --- | --- | --- | --- | | Polar | `order.paid` | `data.id` | `data.total_amount` / 100 | `data.currency` | `data.customer_id` | `order.refunded`: `data.refunded_amount` / 100 | | Dodo Payments | `payment.succeeded` | `data.payment_id` | `data.total_amount` / 100 | `data.currency` | `data.customer.customer_id` | `refund.succeeded`: `data.amount` / 100, on `data.payment_id` (two refunds: send their sum) | | Paddle | `transaction.completed` | `data.id` | `data.details.totals.total` / 100 | `data.currency_code` | `data.customer_id` | `adjustment.updated`, action `refund`, status `approved`: `data.totals.total` / 100, on `data.transaction_id` (two refunds: send their sum) | | Lemon Squeezy | `order_created`; renewals `subscription_payment_success` | `data.attributes.identifier`; the invoice's `data.id` | `data.attributes.total` / 100 | `data.attributes.currency` | `data.attributes.customer_id` | `order_refunded`: `data.attributes.refunded_amount` / 100 | | Gumroad (Ping) | `resource_name` = `sale` | `sale_id` | `price` / 100 | always `USD` (Gumroad's amounts are USD cents) | `subscription_id`, else `purchaser_id` | `resource_name` = `refund`: no value (the whole sale) | For every tool, send `action` and `stage` as `paid`, and a refund as `stage` `refunded` on the sale's own `external_ref`. To join a sale to its visits, pass the page's visitor ID into the tool's custom data at checkout (Polar and Dodo `metadata.skv`, Paddle `custom_data.skv`, Lemon Squeezy `checkout[custom][skv]`, Gumroad `?skv=` on the product link) and map it back as `vid`. ## Sale alerts Skald can tell you when a sale comes in. It is off until an admin turns it on, per site, on Site > Alerts under **Which alerts are on**: - **Sale**: one alert when a lead gains a sale value that this tab counts (a paid row with a value, or a completed one). Test leads never alert. - **Refund on a sale**: one alert when a refund that this tab counts takes money back from a sale. Off by default too. - **Sale alert minimum**: type the least amount per currency, for example `USD 100, EUR 90.50`, and press **Save minimum**. A sale under it does not alert, nor does a refund on it. A currency you do not list alerts at any amount. Leave it empty for every sale. The alert goes the same way as the site's other alerts: it shows on Site > Alerts (under **Closed recently**, since a sale is a one-off notice), it is mailed to the site's confirmed alert recipients when **Mail alerts** is on, and it goes to your webhooks with the event `alert.opened`, Slack included ([Webhooks](https://skaldanalytics.com/docs/webhooks.md)). **All alerts** must be on. It says the site, the amount as this tab shows it (for example `$49.99`), the stage, the tool that reported it, where the lead came from (source, first touch, campaign, landing page, label), whether it is a new customer or a renewal, and the time. It never holds a name, an email, an address, the customer reference or your tool's own id. Each sale alerts once, ever: not again after a restart, a recount or a delivery sent twice. Only sales Skald gets after you turn the alert on, and made in the last 72 hours, alert, so turning it on or loading old sales never sends a flood. At most 20 go in one check; the rest follow 2 minutes later. A mail that fails is noted on the alert and not sent again, as for every alert. ## Troubleshooting - **"No revenue yet"**: no sale has arrived with a value and a currency. Look at **Settings > Integrations > Status**: a delivery whose value was dropped says why. - **The numbers look 100 times too big**: the tool sent cents and they were posted as `value`. Divide by 100 in the mapping. - **A renewal shows as a new customer**: the renewal was sent without the same `customer_ref` as the first sale. - **Two currencies, one total**: there is none, on purpose. Pick each currency with the **Currency** switch. - **No sale alert came**: check that **All alerts** and **Sale** are on at Site > Alerts, that the sale is over its currency's minimum, and that it is not a test lead. A sale made more than 72 hours before Skald got it never alerts. --- # The tracking script > Skald docs: https://skaldanalytics.com/docs/tracking-script/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). How to add Skald's one-line `skald.js` tag to your pages, what it sees, what it never collects, and how to check that it works. ## What it is for Your server log already counts visits and pages. It cannot see what happens *inside* a page: a form started and not sent, a phone link clicked, a demo booked in a widget. The `skald.js` tag reports those. - It is under 6.5 KB of plain JavaScript. - It sends events to the address it loads from: your Skald server, or your own site if you serve the tag from there (below). Never to anyone else. - It uses no cookies, unless you ask it to share an ID across subdomains (below). What it keeps in the browser is listed under "What the tag keeps in the browser" below. - Skald's figures from the log never depend on it. Without the tag, you lose nothing you already have. ## Add the tag You need to be an admin of the site. 1. Open **Settings > This site > Integrations**, or go to Integrations. 2. If you manage more than one site, choose the site at the top. 3. Under **The script tag**, tick the options you need (see "Options" below). The tag updates as you tick. 4. Copy the tag. 5. Paste it into the `` of every page of your site. In most site builders this is a "custom code" or "head code" setting that applies to all pages. 6. Publish your site. The tag looks like this. Always copy yours from the page, because the address and site name are your own: ``` ``` > Nothing changes on your web server. The page loads the tag from your Skald server and sends its events there. You can also serve it from your own domain (below). If the Integrations page is not in your Settings menu, your server does not use accounts, or you are not an admin. Ask the person who runs your Skald server for the tag. ## More than one site Since Skald 2.9.0, every site on your Skald can use the tag, not only the first one. - Each site's events go to that site. Skald knows the site by the address the page is on. - A site added from the dashboard takes events from its own **Host names** (Sites, under **Settings > This site > Sites**). Nothing else to set. This works once the person who runs your Skald server has turned the tag on for the first site. - A host name you add there works within a minute. - Anonymous visitor IDs (below) are set per site. One site can have them on and another off. ### A site on another server The tag sends its events to the address it was loaded from. With the tag as Skald gives it, that is your Skald server, so a site on any server works with no change there. Skald takes an event only from a page on one of the site's own host names. Before Skald 2.24.0 the tag sent its events to the page's own address. If your browser's Network panel shows requests to your own site's `/traffic-beacon` answered 308, 404 or 405, the page still has an older tag or an older copy of the script: copy the tag again. ### Serve it from your own domain (optional) Some ad blockers stop scripts that load from another address. To keep the tag on your own domain, your web server passes two addresses on to Skald: `/traffic-js/` and `/traffic-beacon`. Your developer or host sets this up once (a "reverse proxy" or "rewrite"). The tag then loads from your own site, and sends its events there: ``` ``` The person who runs your Skald server must also tell it to trust your site's server for the visitor's address. Without that, every visitor looks like one address, and the limit on events per visitor applies to all of them together. Skald uses the address only in memory and never keeps it. The set-up dialog **Add the script tag**, step 2, has these lines for nginx, Apache and Caddy under **Advanced: serve it from your own domain**. Connections > **Send this site's visits to Skald** > **Only the script tag** shows them too, and tests the tag. See [Connections](https://skaldanalytics.com/docs/connections.md). ## Options | Tick box on the page | Adds to the tag | What it does | | --- | --- | --- | | Wait for consent | `data-consent="required"` | sends nothing and keeps no ID until your consent banner says yes | | Share across subdomains | `data-share-subdomains="true"` | `www.` and `app.` see the same anonymous ID, kept in a first-party cookie. An admin can switch this on in Skald instead (below) | | Other domains you own | `data-link-domains="..."` | links to those domains carry the anonymous ID | | Accept the ID from your other domains | `data-accept-id="true"` | this site takes the ID a link from your other domains brought (below) | ## What it tracks by itself Once the tag is on a page, it reports these events with no extra work: | Event | When | | --- | --- | | Page view | the page loads; each time a single-page app changes the address (a link inside the page, the back and forward buttons); and when the browser shows the page again from its back-forward cache | | Form started | a field of a form is first used (once per form per page) | | Form sent | a form is submitted | | Sign-up started, Sign-up sent | the same, for a form marked `data-skald="signup"` | | Contact sent | a form marked `data-skald="contact"` is submitted | | Early access sent | a form marked `data-skald="waitlist"` is submitted | | Phone click | a `tel:` link is clicked (never the number) | | Email click | a `mailto:` link is clicked (never the address) | | Link out | a link to another site is clicked (the other site's name only) | | Button click | an element marked `data-skald-cta=""` is clicked | | Read ¾ | the reader scrolls to three quarters of the page (once per page) | | Copied | text is copied from the page (once per page; never the text) | | Not found | a page marked `data-skald="404"` is shown, such as your "page not found" page | | Calendly opened, time chosen, booked | inside a Calendly embed on your page | | Cal.com opened, booked | inside a Cal.com embed | | HubSpot meeting booked | inside a HubSpot meetings embed | A form records only its own `id` or `name`, such as `contact`. It never reads what a person typed. ### Marking forms and buttons - To track a button or link by name, add `data-skald-cta="pricing_top"` to it. It is reported as a Button click named `pricing_top`. - To count a form as a sign-up, add `data-skald="signup"` to the form. - To count a form as a contact form, add `data-skald="contact"`; for an early-access or waiting list form, `data-skald="waitlist"`. Starting either one still counts as Form started. - To leave a form out, add `data-skald="off"`. Search forms (`role="search"`) are always left out. - To count your "page not found" page, add `data-skald="404"` to its ``, or put `` anywhere on it. This also works in a single-page app. > A form whose own code refuses the submit (for example, a missing field) still counts as sent. If that matters, your developer can send the event only after success: `skald('track', 'form_submit', {id: 'contact'})`. ### What it does not track by itself Two events on Leads & sales > Events come only from your site's own code: **Demo opened** (`book_demo_open`) and **Demo booked** (`demo_booked`). Until your site sends them, the Events tab shows them as **Not set up**, with the line to add. Any event your site has never sent shows the same way, never as 0 or "0% went on". Your developer sends them when your own booking step opens and succeeds: ``` skald('track', 'demo_booked', {id: 'header'}) ``` A booking made inside a Calendly, Cal.com or HubSpot embed is already counted by the tag as Calendly booked, Cal.com booked or HubSpot meeting booked. Time on page, pages per visit and one-page visits come from your server log, not from the tag. ### Page views from the tag are not the log's page views On a Next.js or other single-page site, a click inside the site loads no new HTML page. The tag still sends a page view then, and when the browser restores a page from its back-forward cache (the back button, with no request to your server at all). These are the tag's own events, a separate road: they can show that a browser really drew the page, and with visitor IDs on they make up the visitor's journey, but they never add people or page views to the figures Skald counts from your server's log. For the log to count a page opened inside a Next.js site, its `log_format` needs one more field: see [Troubleshooting connections](https://skaldanalytics.com/docs/troubleshooting-connections.md). ## What it never collects - No text a person typed, and no form field's value. - No phone number or email address from a link. - No IP address and no browser details are stored. Skald uses the address only in memory, to find the country and to limit abuse, and then drops it. - No third-party code, and nothing sent anywhere but your Skald server (or your own site, if you serve the tag from there). - No fingerprinting. With visitor IDs on, the tag also keeps one random ID per browser (below). It holds no name or email. ## What the tag keeps in the browser - **Always, with visitor IDs on or off: the visit's source, for the tab only.** When a page is opened from another site, or from an address with a `utm_source` tag, the tag keeps that site's name (for example `news.example.org`) and the `utm_source` value in the tab's session storage, under `skald.src`. Later pages of the same visit then send the same source, after the address has lost its tag. Session storage belongs to that one tab, and the browser deletes it when the tab is closed. It holds no ID, no name and no email. - **With visitor IDs on, also:** the random ID in local storage (`skald.vid`), which stays until the visitor clears the site's data; a note of whether Skald shares the ID across subdomains, renewed each day (`skald.sh`, below; not when the tag itself has `data-share-subdomains="true"`); and with sharing on, the `skald_vid` cookie. - **Nothing at all** when the browser sends Do Not Track or Global Privacy Control, and nothing before consent with **Wait for consent**. When your banner calls `skald('consent', false)`, the tag deletes the ID, the cookie and the stored source. ## Anonymous visitor IDs Visitor IDs let Skald follow one browser's anonymous journey up to a conversion. They make "abandoned", "assisted" and "pages seen before converting" possible. See [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). - They are **off by default**. The person who runs your Skald server turns them on per site. - The Integrations page says, under the tag, whether they are on for your site. - When they are off, the tag makes no ID at all. It still keeps the visit's source in the tab's session storage until the tab is closed (above). - When they are on, the ID is kept in the browser's local storage. With "Share across subdomains", it is also kept in a first-party cookie called `skald_vid` for up to 400 days. ### Share across subdomains without changing the tag If your web developer cannot add `data-share-subdomains="true"` to the tag, an admin can switch sharing on in Skald instead of the tag: **Settings > Sites > Script tag > Share the ID across subdomains**. The tag stays exactly as it is. - The tag learns it from Skald's answer to its first page view, and remembers the answer for a day in the browser's local storage (`skald.sh`). So a visitor's browser starts sharing on its next visit, and at the latest a day after you switch it on. - Switching it off works the same way: within a day, each browser stops sharing and deletes the shared cookie. - It needs visitor IDs on. Do Not Track, Global Privacy Control and **Wait for consent** work exactly as before: no ID, no cookie. - The Integrations page says, under the tag, when sharing is on in Skald. The privacy notice wording then has the cookie line ready, without "Keep this line only if". > Turn visitor IDs on only when your privacy notice says so. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## Consent and Do Not Track - **Do Not Track and Global Privacy Control**: when the browser sends either one, the tag does nothing at all. No ID, no storage, no event. - **Consent first**: in places that need consent before any ID (the EU, Quebec, often the rest of Canada), tick **Wait for consent**. The tag then waits. When the visitor agrees, your consent banner must call this on every page: ``` skald('consent', true) ``` If the visitor says no, or withdraws consent, call `skald('consent', false)`. This deletes the ID, the cookie and the stored source in that browser. To call `skald` before the tag has loaded, your developer can put this line before the tag: ``` ``` ## Your other domains (data-link-domains) If your visitors move between domains you own, such as `example.com` and `app.example.net`, type the other domains in **Other domains you own**, separated by commas. When a visitor clicks a link to one of them, the tag adds `?skv=` to the link. On the other domain, the same tag picks the ID up, so the visit continues the same anonymous journey: 1. Put the tag on the other domain's pages too (it can report to the same site, or to its own). 2. On that domain's tag, either list your first domain in **Other domains you own** (for example `example.com`), or tick **Accept the ID from your other domains** (`data-accept-id="true"`). 3. Publish both. When a page opens with a valid `skv` in its address, the tag keeps that ID as the visitor's ID and takes `skv` out of the address bar, so it is not bookmarked or shared. Without `data-accept-id`, it takes the ID only when the visitor came from a domain listed in **Other domains you own**; any other `skv` is ignored and left alone. - With **Wait for consent**, the ID is kept only after `skald('consent', true)`. The address is cleaned at once either way. - With Do Not Track or Global Privacy Control, the tag does nothing, so `skv` stays in the address. - If your other domain does not run the tag, its own code can read `skv` and pass it to Skald, for example as `vid` in your site's lead push. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md). For subdomains of one domain (`www.` and `app.`), tick **Share across subdomains** instead, or switch it on in Skald (above). If the shared cookie does not work on your host (some free hosting addresses refuse it), the ID still works on each subdomain on its own. ## Calendly links get utm_content With visitor IDs on, the tag adds the visitor ID to every Calendly embed and Calendly link on your page, as `utm_content=`. Calendly sends this back in its webhook, so the booking joins the visitor's journey. - If a Calendly link already has a `utm_content`, the tag leaves it alone. That booking then has no journey. - This works for Calendly iframes, inline widgets and plain links. A popup opened only from your own JavaScript is not changed. - You also need the Calendly webhook. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md), section "Calendly". ## Check that it works 1. Open your site in a normal browser window. Make sure Do Not Track and Global Privacy Control are off in that browser, or the tag sends nothing. 2. Open a page, click a link and start a form. 3. In Skald, open Leads & sales > Events and choose **Today**. 4. You should see "Today so far, not yet checked" and your events. Today's events are checked after midnight. 5. At the bottom of the Events tab, a line says what the beacon took in today: accepted, declined, from machines, from your own addresses, refused. > Your own office is likely marked as internal traffic. Its events are kept apart as "ours" and not shown with people's. Test from a phone on mobile data, or see [Internal traffic](https://skaldanalytics.com/docs/internal-traffic.md). An admin can also press **Test connection** in Connections > **Send this site's visits to Skald**, with **Only the script tag** chosen. It says whether a page opened with the tag and how many events were counted. If you know your browser's developer tools, open the Network panel. Each event is a request to `traffic-beacon`, on your Skald server (or on your own site, if you serve the tag from there). An answer of 204 means it was counted. An answer of 202 means it was taken and nothing was kept, which is normal for page views when visitor IDs are off. ### If nothing shows - The tag is not on the page: view the page's source and look for `skald.js`. - `skald.js` does not load: copy the tag again. If you serve it from your own domain, your web server does not pass `/traffic-js/` on to Skald yet. - Requests go to your own site's `/traffic-beacon` and are answered 308, 404 or 405: an older tag or script. Copy the tag again, or pass `/traffic-beacon` on to Skald. - Your page's Content-Security-Policy leaves your Skald server out of `connect-src`: the browser blocks every event. Add it. - Requests to `/traffic-beacon` are refused (403): the page's address is not on the server's list of allowed origins. For a site added from the dashboard, check that the page's exact host name (with or without `www.`) is in its **Host names** under **Settings > This site > Sites**. - Your browser sends Do Not Track or Global Privacy Control. - The tag waits for consent and your banner never calls `skald('consent', true)`. --- # Webhooks > Skald docs: https://skaldanalytics.com/docs/webhooks/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). How to connect Calendly, Cal.com, HubSpot, Typeform, Stripe, Zapier, Make and your own site to Skald, so bookings, forms and payments show under Leads & sales. Also how Skald can send conversions out to your CRM. ## How it works Each tool gets its own address in Skald and its own secret. When something happens (a booking, a payment, a form), the tool sends a signed message to that address. Skald checks the signature, records the conversion once, and lists it on Leads & sales > Leads, counts it on Summary (with **Count from: Tools**) and, for a booking or a payment, on Calendar bookings. All of this is set up on one page: **Settings > This site > Integrations**, or Integrations. - You need to be an admin or owner of the site's organisation. Viewers do not see this page. - The page needs a server with accounts. Without it, the page says so, and the person who runs your Skald server sets up hooks on the server instead. - If you manage more than one site, choose the site at the top first. ## The Integrations page At the top, **Status** lists each road into Skald for this site, with **OK**, **Needs attention** (and what to do) or **Off**: the hook receiver, the script tag, each hook that is set up, one lead sent by two tools, and the lead database. The server's doctor command (`traffic-intel doctor`; Docker: `docker exec skald traffic-intel doctor`) shows the same lines, and checks the rest of the install too. Under **Incoming hooks**, each tool has a box with: - Its **address**, with a copy button. It looks like `https://analytics.example.com/traffic-hooks/example.com/calendly`. Always copy yours from the page. - A badge: **no secret**, **secret set**, or **set (environment ...)** when the secret is kept on the server. - Its state in words: **needs a secret**, **never received** (set up, but nothing has come, not even a test), **ready** (a test passed, nothing real yet), or **last received 2 h ago**. When the last message was refused, it says why and what to do. - **Make secret** (or **Rotate secret** once there is one), for the tools that take a secret Skald makes. - **Paste the signing secret from ...**, for the tools that make their own secret (Stripe, HubSpot). Calendly, Cal.com and Typeform have both: use either one. - **Send a test event**. - **Remove**, once the hook has a secret. - **Last deliveries**: the last 50 messages, with when, the answer, the outcome and any detail. A hook does nothing until it has a secret. Without one, its address answers "not found". ### The secret Who makes the secret depends on the tool: | Tool | The secret | | --- | --- | | Your site's server (lead), Zapier, Make, n8n (json) | Skald makes it: **Make secret** | | Calendly, Cal.com, Typeform | Either one: **Make secret** and give it to the tool, or paste the tool's own | | Stripe, HubSpot | The tool makes it: **Paste the signing secret from ...** | To make a secret in Skald: 1. Press **Make secret**. 2. The secret is shown once, with the address. Copy both. 3. Paste the secret into the other tool (each section below says where). 4. Press **I have copied it**. The secret is not shown again. To paste a tool's own secret: 1. Copy the signing secret in the other tool (each section below says where). 2. In that tool's box in Skald, paste it into **Paste the signing secret from ...**. 3. Press **Save secret**. The box says **secret set**. Skald never shows the secret again, not even to you. To replace a pasted secret, paste the new one and save again. > Treat a secret like a password. Never put it in an email, a chat, a ticket or a shared document. Anyone with it can send fake conversions. If one leaks, press **Rotate secret** (or make a new secret in the tool and paste it): the old one stops working at once, and you give the new one to the tool. If the box says **set (environment ...)**, the secret is kept on the server. It cannot be made, pasted or removed on this page: ask the person who runs your Skald server. ### The test button **Send a test event** sends a signed sample to Skald's own receiver for that tool. It checks the signature and reads the sample, then keeps nothing and counts nothing. The page says "the test was signed, received and read", and what a real message would keep: the action, its stage, the source, country and device, and whether the visitor ID is kept. With visitor IDs off for the site, the visitor ID is dropped, as it is for a real message. The test shows as "test, read and not kept" in Last deliveries. The server has the same test: `traffic-intel hooks test example.com lead`. The test checks Skald's side only. To check the other tool, use its own "send test" button, if it has one, and look at Last deliveries. ## Calendly Calendly sends webhooks on its paid plans (Standard or higher). It has no screen for them: they are made through Calendly's API, usually by a developer. 1. In Skald, open the **Calendly** box, press **Make secret**, and copy the address and the secret. 2. Make a webhook subscription in Calendly with that address. Use the secret as the subscription's **signing key**. 3. Choose these events: `invitee.created`, `invitee.canceled`, `invitee_no_show.created`. If your subscription already has a signing key from Calendly, paste that key into **Paste the signing secret from Calendly** instead of making one. A developer can make the subscription with one request: ``` curl -X POST https://api.calendly.com/webhook_subscriptions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"url": "https://analytics.example.com/traffic-hooks/example.com/calendly", "events": ["invitee.created", "invitee.canceled", "invitee_no_show.created"], "organization": "https://api.calendly.com/organizations/", "scope": "organization", "signing_key": ""}' ``` What Skald records: a booking (Demos, completed), a cancellation (cancelled, or rescheduled when Calendly says so) and a no-show. To join a booking to the visitor's journey, the tracking script adds the visitor ID to Calendly links as `utm_content`. See [The tracking script](https://skaldanalytics.com/docs/tracking-script.md). ### Only some event types One Calendly account often books for several businesses, and a Calendly webhook cannot be limited to some event types. So tell Skald which are this site's: 1. In the **Calendly** box, find **Only these Calendly event types**. 2. Enter one per line: the event type's address from Calendly's API (`https://api.calendly.com/event_types/...`), or the booking page's name at the end of its link (`product-demo` in `https://calendly.com/you/product-demo`). 3. Press **Save event types**. Any other event type is answered "OK" and left out: nothing is kept, and Last deliveries shows "not a conversion" with the event type it named, so you can copy it. A no-show is kept when its booking was kept. Leave the box empty to keep every event. Calendly does not send the booking page's name, so Skald compares it with the event type's name ("Product demo" is `product-demo`). If you rename the event type, use its address instead. See [Demo bookings](https://skaldanalytics.com/docs/demo-bookings.md). ## Cal.com 1. In Skald, open the **Cal.com** box, press **Make secret**, and copy the address and the secret. 2. In Cal.com, go to **Settings > Developer > Webhooks > New**. 3. **Subscriber URL**: paste the address. 4. **Event triggers**: Booking Created, Booking Cancelled, Booking Rescheduled. 5. **Secret**: paste the secret. Save. If the Cal.com webhook already has a secret of its own, paste it into **Paste the signing secret from Cal.com** in Skald instead. To join a booking to the journey, pass the visitor ID in the embed as `metadata[skv]`, or add a hidden booking question named `skv`. To keep only some event types, enter their ids (`1234`) or names (`30min`) in **Only these Cal.com event types**, as for Calendly above. ## HubSpot HubSpot signs its messages with your HubSpot app's **client secret**. Skald cannot make one for it. 1. In your HubSpot app, open **Auth** and copy the **Client secret**. 2. In Skald's **HubSpot** box, paste it into **Paste the signing secret from HubSpot** and press **Save secret**. 3. In your HubSpot app, open **Webhooks** and set the **Target URL** to the address from the **HubSpot** box. 4. Subscribe to `contact.creation` (counted as a Contact form), to `object.creation` for meetings (counted as a Demo), and to `deal.propertyChange` for `dealstage` (a deal moved to closed won counts as a won Deal). You can also use a HubSpot workflow's **Send a webhook** action, signed by the same app. Skald counts it as a Contact form. To count it as something else, add a property `skald_action`, for example `demo` in a "meeting booked" workflow. Skald reads the properties `skv` (the visitor ID) and `utm_*`. > If your HubSpot account cannot sign a workflow webhook, send it through Zapier or Make instead (below). ## Typeform 1. In Skald, open the **Typeform** box, press **Make secret**, and copy the address and the secret. 2. In Typeform, open your form, then **Connect > Webhooks > Add a webhook**. Paste the address. 3. Open the webhook's settings (**Edit**) and paste the secret into **Secret**. Save, and turn the webhook on. If the Typeform webhook already has a secret of its own, paste it into **Paste the signing secret from Typeform** in Skald instead. Typeform sends every response. Skald counts each as a Contact form, completed. To join it to the journey, add a hidden field `skv` to the form and fill it in the embed. Hidden `utm_*` fields give the last touch. Skald never reads the answers. ## Stripe Stripe makes its own signing secret, which starts with `whsec_`. 1. In Stripe, go to **Developers > Webhooks > Add endpoint**. 2. **Endpoint URL**: paste the address from the **Stripe** box. 3. Events: `checkout.session.completed` and `checkout.session.async_payment_succeeded`; for revenue (2.32.0) also `invoice.paid` (a subscription's renewals), `invoice_payment.paid` (ties a renewal's payment to its invoice on Stripe's API 2025-03-31 and later, so its refund is found) and `charge.refunded` (refunds). 4. Save, then reveal the endpoint's **Signing secret**. 5. In Skald's **Stripe** box, paste it into **Paste the signing secret from Stripe** and press **Save secret**. Skald cannot make a secret for Stripe. Skald counts a paid Checkout as a Payment. A Checkout paid by a delayed method counts as started first, then paid. To join it to the journey, set the visitor ID as the Checkout Session's `client_reference_id`, or as `metadata.skv`. Skald keeps each payment's amount and currency, and the Stripe customer ID (`cus_...`) so a renewal joins its first sale; a refund takes its amount off on the day it arrives, and a later, larger total takes the difference off on its own day. All of it shows on Leads & sales > Revenue ([Revenue](https://skaldanalytics.com/docs/revenue.md)). Skald never reads the customer's name, email or address. ## Zapier and Make Use the **Zapier, Make, n8n (json)** box. This hook takes the same fields as your own site's lead push (next section). 1. Press **Make secret** in that box, and copy the address and the secret. 2. Keep the secret in the tool's own secure storage, not in a plain text step. 3. Send the secret as a plain key, with no code: a POST to the address with these headers and a JSON body. ``` POST https://analytics.example.com/traffic-hooks/example.com/json Authorization: Bearer Content-Type: application/json {"external_ref": "form-123", "action": "contact", "email": "person@example.com", "name": "A Person"} ``` A sale carries its value too: `{"external_ref": "inv_1042", "action": "paid", "stage": "paid", "value": 49.00, "currency": "USD", "customer_ref": "cus_1001"}`. Polar, Dodo Payments, Paddle, Lemon Squeezy and Gumroad reach Skald this way: [Revenue](https://skaldanalytics.com/docs/revenue.md) lists what to map for each. - In **Zapier**: add **Webhooks by Zapier**, event **POST**. URL: the address. Payload Type: `json`. Data: `external_ref` (map the entry's id), `action` (such as `contact`), and `email`, `name` if you like. Headers: `Authorization` with the value `Bearer `. - In **Make** or **n8n**: the HTTP module or node, method POST, body type JSON, the same header. - **Form plugins** that post a webhook with your own headers (WordPress form add-ons, Webflow, Tally and others) work the same way. For a WordPress site, the Skald Leads plugin needs no set-up at all: see [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md). The plain key works on this hook and on **Your site's server (lead)** only, and only when the message has no `X-Skald-Signature`. A wrong key is refused like a bad signature. Use the address exactly as shown, starting with `https://`: the key travels with each message. A tool that can sign may sign instead (next section); in Make, set the header `X-Skald-Signature` to `t={{timestamp}},v1={{sha256(timestamp + "." + body; "hex"; secret)}}`. One message can carry one record, a list of them, or `{"records": [...]}`, up to 100. ## Your own site's lead push If your website's database is not read by Skald directly, your site's server can tell Skald each time a lead is made. Use the **Your site's server (lead)** box. The quickest way: download the one file for your site's language from that box (PHP, Node, Python, or a WordPress plugin that needs no code) and call one function when a form is saved. See [Send leads from your backend](https://skaldanalytics.com/docs/leads-from-your-backend.md). To write it yourself: 1. Press **Make secret**, and copy the address and the secret. 2. Give both to your developer. They keep the secret in the server's settings, never in the code. 3. Your server sends a `POST` with a JSON body and this header: ``` X-Skald-Signature: t=,v1=."> ``` The key is the secret. Skald refuses a time more than 5 minutes off its own clock. A sender that cannot sign can send `Authorization: Bearer ` instead (see Zapier and Make, above). | Field | Needed | What | | --- | --- | --- | | `external_ref` | yes | the lead's own ID in your site | | `action` | yes | a short word: `demo`, `signup`, `contact`, `trial` | | `stage` | no | `started`, `completed` (the default), `cancelled`, `rescheduled`, `no_show`, `paid` | | `at` | no | when, as an ISO time with its offset, such as `2026-09-25T14:30:00-04:00`; default: when it arrived. A time without an offset is read in the site's time zone, and Last deliveries says so | | `vid` | no | the visitor ID the page had | | `first_touch`, `last_touch` | no | referrer, landing page, `utm_*` tags, ad click ids, time | | `country` or `ip` | no | two letters, or the visitor's address: Skald finds the country and drops the address | | `device` or `user_agent` | no | Desktop, Mobile or Tablet, or the browser string: Skald works out the device and drops it | | `name`, `email` | no | kept only when the site's **Leads' names and emails** is **full**; otherwise dropped | A short test from a terminal: ``` BODY='{"external_ref":"L-1001","action":"demo"}' T=$(date +%s) SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "" -hex | sed 's/^.* //') curl -X POST 'https://analytics.example.com/traffic-hooks/example.com/lead' \ -H 'Content-Type: application/json' -H "X-Skald-Signature: t=$T,v1=$SIG" --data-raw "$BODY" ``` > If Skald already reads your website's lead database, you do not need the lead push. See [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). The full how-to, with examples in curl, Node, Python and PHP, retries and backfilling: [Send leads from your backend in 5 minutes](https://skaldanalytics.com/docs/leads-from-your-backend.md). If your server and Skald share a machine behind Cloudflare, post to Skald's local address: the guide says how. ## What Skald stores, and what it drops - **Kept**: the action, its stage, the time, the tool, the tool's own reference, the visitor ID (only when the site has visitor IDs on), and the source, campaign, landing page, country and device when known. - **Kept for checking**: a copy of each verified message, cut down to the fields Skald reads. It is kept as long as the leads (400 days, or for good: **Settings > Data retention > Leads**), so every figure can be counted a second time. - **Always dropped**: IP addresses, browser strings, phone numbers, extra email addresses, form answers, customer details, and Calendly's cancel and reschedule links. - **Names and emails**: kept only when **Leads' names and emails** is **full** (**Settings > Data retention > People**). That is the default for a new install. An install upgraded from before 2.4.0 drops them until an admin turns it on. Your privacy notice must say so. See [Privacy](https://skaldanalytics.com/docs/privacy.md). - **The delivery log**: when, the outcome and the answer, never the message itself. Kept 90 days. ## Duplicates and retries - A tool sends again when it does not get an "OK" answer. This is harmless. - Skald keeps each conversion once per tool, reference and stage. A repeat is answered "OK" and shows as "already had it" in Last deliveries. Nothing is counted twice. - Cal.com and Typeform sign no time, so Skald also remembers each message for 30 days and ignores an exact repeat. - **One lead, one road.** Each tool keeps its own record, so the same lead sent through two tools (your lead push and Zapier, or the lead push and the lead database) counts twice. The **Status** list names any lead that arrived from two tools: send each lead one way only. - If Skald cannot save a message, it answers with an error, and the tool sends it again later. ## Remove an integration 1. Delete the webhook in the other tool (Calendly, Cal.com, HubSpot, Typeform, Stripe, Zapier or Make). 2. In Skald, press **Remove** in that tool's box, then **Yes, remove**. Skald deletes the hook's secret. Its address then refuses every message, as if it had never been set up. **Last deliveries** stays, and the removal is in the audit log. To use the tool again, make or paste a secret. If the secret is kept on the server (**set (environment ...)**), there is no Remove button: ask the person who runs your Skald server. Conversions already recorded stay. To delete one person's data, see [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). ## Sending conversions out (outgoing webhooks) Skald can tell your CRM, Zapier, Make or Slack each time something happens. This is under **Outgoing webhooks** on the same page. 1. Under **Add an outgoing webhook**, paste the **Address**. It must start with `https://`. 2. Tick the **Events** you want: `conversion.started`, `conversion.completed`, `conversion.cancelled`, `conversion.rescheduled`, `conversion.no_show`, `conversion.abandoned`, `lead.won`, `lead.lost`, `ai.reading_changed` (an AI assistant started or stopped reading a page, or visits likely sent by AI answers jumped or vanished: see the FAQ), and `alert.opened` (any alert, when it opens). 3. Press **Add webhook**. Copy the secret shown once, and give it to the receiving tool so it can check each message. 4. Press **Send a test** to check that the address answers. An `ai.reading_changed` message names the assistant, the pages, the days and the numbers, and is sent once per alert, only while alerts are on. An `alert.opened` message is sent once for each new alert of any kind (site down, a first AI citation, a crawler gone silent, a sale or a refund when those alerts are on, ...): the site, the alert, its text, its figures with their unit (visits or requests) and the time. A sale alert's figures also carry the amount, its currency and where the lead came from, never a name, an email or the customer reference. Each conversion message names the action, its source, campaign, landing page, country, device, visits and time before converting, and the anonymous journey. It never holds a name or an email. A failed delivery is tried again for about 15 hours. Only conversions recorded after you add the webhook are sent. **Remove** stops it at once. ### Alerts in Slack Slack's incoming webhooks take only a short text. Skald knows a Slack address and sends the alert's text in Slack's own shape. 1. In Slack, add an **Incoming Webhook** to the channel and copy its address. It starts with `https://hooks.slack.com/`. 2. In Skald, under **Add an outgoing webhook**, paste it, tick `alert.opened`, and press **Add webhook**. Slack does not check the secret, so you can ignore it. 3. Press **Send a test**. A line appears in the channel. Each new alert then posts one message: the site, the title and the detail with its numbers. Any other address gets the full message, signed. Sending is opt-in: nothing leaves Skald until an admin adds a webhook. Each delivery has 10 seconds. A failed one is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then given up. ## Troubleshooting Open the tool's box and look at **Last deliveries**. The **Outcome** column says what happened: | Outcome | What it means | What to do | | --- | --- | --- | | kept | recorded | nothing | | already had it | a repeat of a message already kept | nothing | | not a conversion | an event Skald does not count, or (with a detail "other event type") a Calendly or Cal.com event type this site does not keep | choose the events listed above; or add the event type to the list | | test, read and not kept | a test | nothing | | bad signature | the secret (or the `Bearer` key) does not match, or the time is more than 5 minutes off | paste the secret again, or rotate it and give the new one; check the sender's clock | | not readable | the message is not what this tool sends | check you used the address for the right tool | | too large | the message is over 64 KB | send fewer records at once | | too many | over 60 messages a minute | the tool will retry; send in smaller batches | | error | Skald could not save it | the tool will retry; tell the person who runs your server if it repeats | - **No deliveries at all**: the tool is not sending. Check the address is pasted exactly, and that the webhook is turned on in the tool. - **The box says "no secret"**: the address answers "not found" until a secret is set. A removed hook says this too. - **"A Stripe signing secret starts with whsec_"**: you copied something else, such as an API key. Copy the endpoint's **Signing secret** in Stripe. - **A red note says the site's hooks are not answered**: this server does not report on that site yet. - **Bookings show but have no journey**: the visitor ID did not reach the tool. Check visitor IDs are on, and the hidden field or `utm_content` above. - **HubSpot "bad signature" behind a proxy**: the person who runs your server may need to set Skald's public address. --- # Send leads from your backend in 5 minutes > Skald docs: https://skaldanalytics.com/docs/leads-from-your-backend/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Your site's server tells Skald each time a lead is made. The quick way: download one file and call one function after your form is saved. WordPress needs no code at all: a plugin does it. The rest of this guide has the details the file does for you (the body, the signature, retries), for anyone who wants to write it themselves. ## The quick way: one file, one function 1. In the dashboard, open **Settings > Integrations > Your site's server (lead)** and press **Make secret**. The secret is shown once. 2. In the same box, download the file for your site's language. Downloaded right after you made the secret, the file holds it and works as it is. Downloaded later, it holds no secret: set the environment variable `SKALD_LEAD_SECRET` on your server (every file reads it first, so it is the better place anyway). 3. Put the file next to your code and add one line where your form is saved: | Your site | File | The line | | --- | --- | --- | | PHP | `skald-lead.php` | `require_once __DIR__ . '/skald-lead.php';` then `skald_lead($id, 'contact', $email, $name);` | | Node 18 or later | `skald-lead.cjs` | `const { skaldLead } = require("./skald-lead.cjs");` then `await skaldLead(id, "contact", { email, name });` | | Python 3.8 or later | `skald_lead.py` | `from skald_lead import skald_lead` then `skald_lead(id, "contact", email=email, name=name)` | | WordPress | `skald-leads.zip` | No code: **Plugins > Add New > Upload Plugin**, choose the zip, then **Activate** | - `id` is your own id for the lead, such as the form entry's id. The same lead always sends the same id: a repeat is never counted twice. - The second word is what was done: `contact`, `demo`, `signup`, `trial`. - Email and name are optional. Other fields from section 2 go in the last argument: `label`, `stage`, `test`, `vid`. PHP: `skald_lead($id, 'demo', $email, $name, ['label' => 'sales']);`. - A sale with its value (2.32.0): PHP `skald_lead($order_id, 'paid', $email, $name, ['stage' => 'paid', 'value' => 49.00, 'currency' => 'USD', 'customer_ref' => $customer_id, 'tags' => ['plan' => 'pro']]);`, Node `await skaldLead(orderId, "paid", { stage: "paid", value: 49.00, currency: "USD", customer_ref: customerId })`, Python `skald_lead(order_id, "paid", stage="paid", value=49.00, currency="USD", customer_ref=customer_id)`. A refund: the same order ID with `stage` `refunded` and the total refunded on it so far as `value`. See [Revenue](https://skaldanalytics.com/docs/revenue.md). - Each function waits at most 3 seconds, never throws, and returns true when Skald kept the lead. Your form never breaks because of Skald. - Node in an ES module: `import { skaldLead } from "./skald-lead.cjs";` works too. - The files sign each lead (section 3), so they work with every version of Skald. **WordPress.** The plugin works with Contact Form 7, WPForms, Gravity Forms, Elementor Pro forms and Ninja Forms. Each form sent becomes a lead with the action `contact`, the email and name fields it finds, and an id like `wpforms-123`. It sends in the background, so a form is never slower. To keep the secret out of the plugin file, add `define('SKALD_LEAD_SECRET', 'your-secret');` to `wp-config.php`, and download the plugin without the secret. **Join the lead to the visit.** With Skald's script tag on your pages and visitor IDs on, the page knows the visitor's ID as `skald('id')`. Bring it to your server in a hidden form field named `skald_vid`, or read the `skald_vid` cookie (it exists when "Share across subdomains" is on). The PHP file and the WordPress plugin read both by themselves (the plugin also fills the hidden field for you). In Node and Python, pass it as `vid`. See [The tracking script](https://skaldanalytics.com/docs/tracking-script.md). **No code at all.** Zapier's **Webhooks by Zapier** and form plugins that can add a header send the secret as a plain key instead of signing: `Authorization: Bearer `. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md#zapier-and-make). Keep the secret on your server: in the environment variable, in `wp-config.php`, or in the downloaded file. Never in a web page, never in a public code repository. ## 1. Make the secret On the server: ``` sudo traffic-intel hooks secret example.com lead ``` Or in the dashboard: **Settings > Integrations > Your site's server (lead) > Make secret**. Docker: `docker exec skald traffic-intel hooks secret example.com lead` (in Docker, run every `traffic-intel` command in this guide the same way, without `sudo`). It prints the secret once, and the address to post to. With `access.base_url` set, the address is the real one, such as `https://stats.example.com/traffic-hooks/example.com/lead`. Keep the secret in an environment variable on your backend, such as `SKALD_LEAD_SECRET`. Never in the code, never in the browser. ## 2. The body One JSON object per lead: ``` { "external_ref": "L-1001", "action": "demo", "stage": "completed", "at": "2026-09-25T14:30:00-04:00", "vid": "skv_Q2xhc3NpY0V4YW1wbGVJRA", "first_touch": {"referrer": "https://www.google.com/", "landing": "/", "at": "2026-09-20T09:12:00Z"}, "last_touch": {"landing": "/pricing/", "utm_source": "newsletter", "utm_medium": "email"} } ``` | Field | | | | --- | --- | --- | | `external_ref` | required | The lead's own id in your system. Keep it stable: the same lead always sends the same id | | `action` | required | What was done, a short lowercase word: `demo`, `contact`, `signup`, `trial` | | `stage` | default `completed` | `started`, `completed`, `cancelled`, `rescheduled`, `no_show`, `paid`, `won`, `lost`, and `refunded` (2.32.0: a refund on the sale's own `external_ref`, never a lead) | | `at` | default: when it arrived | When it happened. A full ISO time with its offset: `2026-09-25T14:30:00-04:00` or `2026-09-25T18:30:00Z` | | `vid` | | The visitor ID the page had. Kept only when the site has visitor IDs on | | `first_touch`, `last_touch` | | Where the visitor came from: `referrer`, `landing`, `utm_source`, `utm_medium`, `utm_campaign`, `at` | | `ip`, `user_agent` | | Skald looks up the country and the device, then drops both | | `name`, `email` | | Kept only when the site's **Leads' names and emails** is **full**; otherwise dropped | | `label` | optional | A short word that sorts leads of one action, such as a contact form's topic: `sales`, `support`, `partnership`. Lowercase letters, digits, `-` and `_`, at most 40, starting with a letter or digit (`Sales` is read as `sales`). Anything else is dropped: the lead is still kept, without a label, and the answer and the delivery log say so | | `value`, `currency` | optional | 2.32.0: a sale's amount in the currency's normal units (`49.00`, not cents) and its three-letter code (`USD`). Shown on Leads & sales > Revenue, summed per currency, never converted. See [Revenue](https://skaldanalytics.com/docs/revenue.md) | | `customer_ref` | optional | Your customer's own ID (`u-119797`, never an email): it joins a renewal to the first sale | | `tags` | optional | Up to 5 labels of your own, such as `{"plan": "pro"}`: lowercase names and values | | `test` | optional | `true` for a lead your own team made (a team account, a QA run): Skald keeps it and lists it, and never counts it. `false` or left out: a real lead. Only `true` or `false` (not `"true"`, not `1`); anything else is ignored, the lead counts, and the answer and the delivery log say so | Each stage keeps its own label: send it again with a later stage if that stage should carry it too. Leads and Summary (Count from: Tools) filter by label, and the lists of leads and conversions show it as a small tag. Always send the offset. A time without one ("2026-09-25" or "2026-09-25T14:30:00") is read in the site's own time zone, and the delivery log says so. That guess is right only when your server writes local times in the same zone. ## 3. Sign it and send it The header: ``` X-Skald-Signature: t=,v1=."> ``` The key is the secret. Sign the exact bytes you send. Skald refuses a `t` more than 5 minutes from its own clock, so keep the server's clock right. A sender that cannot sign may send the secret itself instead, with no `X-Skald-Signature` header: ``` Authorization: Bearer ``` It works on the `lead` and `json` hooks only, over `https://` only in practice (the key travels with each message), and a wrong key is refused like a bad signature. Signing is better when you can: a stolen signed message is useless after 5 minutes, a stolen key is not until you rotate it. Each example below signs, posts, and tries again when the post fails. curl: ``` URL='https://stats.example.com/traffic-hooks/example.com/lead' BODY='{"external_ref":"L-1001","action":"demo","at":"2026-09-25T14:30:00-04:00"}' for wait in 0 5 30 120; do sleep "$wait" T=$(date +%s) SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SKALD_LEAD_SECRET" -hex | sed 's/^.* //') CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$URL" \ -H 'Content-Type: application/json' -H "X-Skald-Signature: t=$T,v1=$SIG" --data-raw "$BODY") case "$CODE" in 2*) break ;; 400|401|404|413) echo "refused: $CODE"; break ;; esac done ``` Node (18 or later): ``` import crypto from "node:crypto"; const URL = "https://stats.example.com/traffic-hooks/example.com/lead"; export async function sendLead(lead) { const body = JSON.stringify(lead); for (const wait of [0, 5, 30, 120]) { await new Promise((r) => setTimeout(r, wait * 1000)); const t = Math.floor(Date.now() / 1000); const sig = crypto.createHmac("sha256", process.env.SKALD_LEAD_SECRET).update(`${t}.${body}`).digest("hex"); try { const res = await fetch(URL, { method: "POST", body, headers: { "Content-Type": "application/json", "X-Skald-Signature": `t=${t},v1=${sig}` } }); if (res.ok) return true; if ([400, 401, 404, 413].includes(res.status)) return false; } catch { /* the network: try again */ } } return false; } ``` Python (standard library only): ``` import hashlib, hmac, json, os, time, urllib.error, urllib.request URL = "https://stats.example.com/traffic-hooks/example.com/lead" def send_lead(lead: dict) -> bool: body = json.dumps(lead).encode() secret = os.environ["SKALD_LEAD_SECRET"].encode() for wait in (0, 5, 30, 120): time.sleep(wait) t = str(int(time.time())) sig = hmac.new(secret, t.encode() + b"." + body, hashlib.sha256).hexdigest() req = urllib.request.Request(URL, data=body, method="POST", headers={ "Content-Type": "application/json", "X-Skald-Signature": f"t={t},v1={sig}"}) try: with urllib.request.urlopen(req, timeout=10): return True except urllib.error.HTTPError as why: if why.code in (400, 401, 404, 413): return False except OSError: pass return False ``` PHP: ``` function skald_send_lead(array $lead): bool { $url = 'https://stats.example.com/traffic-hooks/example.com/lead'; $body = json_encode($lead); foreach ([0, 5, 30, 120] as $wait) { sleep($wait); $t = time(); $sig = hash_hmac('sha256', $t . '.' . $body, getenv('SKALD_LEAD_SECRET')); $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-Skald-Signature: t=$t,v1=$sig"]]); curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code >= 200 && $code < 300) return true; if (in_array($code, [400, 401, 404, 413])) return false; } return false; } ``` In a web request, send the lead after the page has answered, or from a queue, so a slow answer never holds up your visitor. ## 4. Retries are safe Try again on anything but a 2xx answer, and on a network error, with a growing wait. A repeat changes nothing: Skald keeps a conversion once per site, tool, `external_ref` and stage. Do not retry a 400. The body is wrong: the answer says what, and so does the delivery log. Fix the body first. ## 5. One tool per lead Send each lead through one road only. If the same lead also goes through Zapier (the `json` hook), or Skald also reads it from the lead database, it counts once per road. **Settings > Integrations > Status** flags such leads as "One lead, two tools". ## 6. Backfill old leads Send each old lead with its original `at`, with the offset. Then it lands on the day it happened. - Each hook takes at most 60 messages a minute by default (`hooks.per_minute`). Space them out, or retry on 429. - Or send up to 100 at once through the `json` hook: `{"records": [ {...}, {...} ]}`. Make its secret first, the same way. - An `at` more than a day in the future is refused. ## 7. Same server as Skald, behind Cloudflare When your backend runs on the same server as Skald, and the public address goes through Cloudflare (or another CDN proxy), post to Skald's local address instead. Cloudflare can block a server that posts to its own public address: 403, error 1010, a bot check. | Skald runs | Post to | | --- | --- | | In Docker | `http://127.0.0.1:8080/traffic-hooks/example.com/lead` (the port you published) | | Installed with the install script | `http://127.0.0.1:8766/traffic-hooks/example.com/lead` (the hook receiver itself) | Or allow the path `/traffic-hooks/` in Cloudflare's security rules for your server. The signature is the same either way. ## 8. Test it ``` sudo traffic-intel hooks test example.com lead ``` It posts a signed sample, marked as a test: read, never kept. It says what a real one would keep: the action, the stage, the source, and whether the visitor ID is kept. **Send a test event** on the Integrations page does the same. Then send a real lead. The Integrations page shows "last received 2 min ago (kept)", and `sudo traffic-intel doctor` shows the hook as OK. ## 9. Check that Skald has every lead Your server can ask Skald for the leads it holds from this hook, compare them with its own, and send again any that are missing. It is read only, and signed with the **same secret** as the lead hook. ``` GET /traffic-hooks/example.com/leads?since=2026-10-19T00:00:00Z&limit=500 X-Skald-Signature: t=,v1=.GET ?"> ``` Sign the method, the path and the query exactly as you send them, such as `1792540800.GET /traffic-hooks/example.com/leads?since=2026-10-19T00%3A00%3A00Z&limit=500`. So nobody can change a parameter on the way. Without a query, sign `.GET ` with no `?`. The same 5-minute clock rule as the POST. | Parameter | | | | --- | --- | --- | | `since` | optional | Leads Skald **received** at or after this time. ISO with its offset (`Z` is simplest; a `+` must be sent as `%2B`) or Unix seconds | | `at_since`, `at_until` | optional | Leads whose `at` (when they happened) is from `at_since`, and before `at_until` | | `limit` | default 200 | Rows a page, 1 to 500 | | `cursor` | | The `next_cursor` of the page before | `since` is when Skald got the lead, not its `at`. A lead you backfill today about a day last month has an old `at`, and it is found by today's `since`. Use `at_since` and `at_until` to ask about the days the leads happened. The answer: ``` { "ok": true, "site": "example.com", "since": "2026-10-19T00:00:00Z", "at_since": null, "at_until": null, "limit": 500, "rows": [ {"external_ref": "L-1001", "action": "contact", "stage": "completed", "at": "2026-10-19T14:30:00Z", "label": "sales", "test": false, "received_at": "2026-10-19T14:30:02Z"} ], "next_cursor": null } ``` - Only the leads that came through this site's `lead` hook. Never a name, an email, an address, a browser, a visitor ID or a source. - Rows are in the order Skald received them. While `next_cursor` is not `null`, ask again with `cursor=` (and the same other parameters) for the next page. New leads arriving meanwhile come after it: no page skips or repeats one. - Leads marked test are in it, with `"test": true`: you can see that your tests landed. (A delivery sent with `X-Skald-Test: 1` is never kept, so it is not in it.) - A lead older than the site's keeping time is not in it: Skald deletes conversions whose `at` is more than 400 days old (`conversions.retention_days`). Nor is a lead deleted on request (`traffic-intel conversions forget`). curl (GNU `date`; on a Mac, `date -u -v-2d +%Y-%m-%dT%H:%M:%SZ`): ``` BASE='https://stats.example.com' P='/traffic-hooks/example.com/leads' Q="since=$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ)&limit=500" T=$(date +%s) SIG=$(printf '%s.GET %s?%s' "$T" "$P" "$Q" | openssl dgst -sha256 -hmac "$SKALD_LEAD_SECRET" -hex | sed 's/^.* //') curl -s "$BASE$P?$Q" -H "X-Skald-Signature: t=$T,v1=$SIG" ``` Node (18 or later): ``` import crypto from "node:crypto"; const BASE = "https://stats.example.com"; const PATH = "/traffic-hooks/example.com/leads"; // Every lead Skald received since `since` (a Date), as "ref/stage" -> row. export async function leadsInSkald(since) { const found = new Map(); let cursor = null; do { const q = new URLSearchParams({ since: since.toISOString(), limit: "500", ...(cursor ? { cursor } : {}) }).toString(); const t = Math.floor(Date.now() / 1000); const sig = crypto.createHmac("sha256", process.env.SKALD_LEAD_SECRET).update(`${t}.GET ${PATH}?${q}`).digest("hex"); const res = await fetch(`${BASE}${PATH}?${q}`, { headers: { "X-Skald-Signature": `t=${t},v1=${sig}` } }); if (!res.ok) throw new Error(`Skald answered ${res.status}`); const page = await res.json(); for (const row of page.rows) found.set(`${row.external_ref}/${row.stage}`, row); cursor = page.next_cursor; } while (cursor); return found; } ``` Python (standard library only): ``` import hashlib, hmac, json, os, time, urllib.parse, urllib.request BASE = "https://stats.example.com" PATH = "/traffic-hooks/example.com/leads" def leads_in_skald(since: str) -> dict: """{(external_ref, stage): row} for every lead Skald received since `since`.""" secret = os.environ["SKALD_LEAD_SECRET"].encode() found, cursor = {}, None while True: params = {"since": since, "limit": 500, **({"cursor": cursor} if cursor else {})} query = urllib.parse.urlencode(params) t = str(int(time.time())) sig = hmac.new(secret, f"{t}.GET {PATH}?{query}".encode(), hashlib.sha256).hexdigest() req = urllib.request.Request(f"{BASE}{PATH}?{query}", headers={"X-Skald-Signature": f"t={t},v1={sig}"}) with urllib.request.urlopen(req, timeout=20) as res: page = json.load(res) for row in page["rows"]: found[(row["external_ref"], row["stage"])] = row cursor = page["next_cursor"] if not cursor: return found ``` PHP: ``` function skald_leads_since(string $since): array { $base = 'https://stats.example.com'; $path = '/traffic-hooks/example.com/leads'; $found = []; $cursor = null; do { $params = ['since' => $since, 'limit' => 500]; if ($cursor) $params['cursor'] = $cursor; $query = http_build_query($params); $t = time(); $sig = hash_hmac('sha256', "$t.GET $path?$query", getenv('SKALD_LEAD_SECRET')); $ch = curl_init("$base$path?$query"); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_HTTPHEADER => ["X-Skald-Signature: t=$t,v1=$sig"]]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code !== 200) throw new RuntimeException("Skald answered $code"); $page = json_decode($body, true); foreach ($page['rows'] as $row) $found[$row['external_ref'] . '/' . $row['stage']] = $row; $cursor = $page['next_cursor']; } while ($cursor); return $found; } ``` **A daily check.** Once a day: 1. List the leads your server made in the last 2 days, each with its stage. 2. Ask Skald with `since` a little before that window, such as 3 days ago. Skald receives a lead after you make it, so a lead of your window is never missed; the overlap with yesterday's check does no harm. 3. Send every lead (with its stage) that is not in the answer again, through the lead hook, with its original `at`. A lead Skald has already is answered `"duplicates": 1` and changes nothing: Skald keeps one row per lead and stage. So when in doubt, send it. Two cautions: never send again a lead that was deleted on request, or it comes back. And a lead older than the keeping time (400 days) is gone from the answer on purpose. A lead sent again keeps the label of its first delivery. The answers: 200 as above; 400 a parameter is wrong (the answer says which; any other parameter name is refused); 401 no signature, a wrong one, or `t` more than 5 minutes off; 404 the site is not set up or its lead hook has no secret (the same answer for both); 429 more than 60 a minute for the site (`hooks.leads_per_minute`), wait and ask again. A 403 from an install set up before 2.18 means its nginx lets only POST through to `/traffic-hooks/`: the line `limit_except POST { deny all; }` must read `limit_except GET POST { deny all; }` (INSTALL.md, DEPLOY.md). ## 10. Mark leads as test Leads your own team makes, from a team account or a QA run, should not count. **Send your team's own accounts with `test: true`; to fix ones already sent, call `leads/test`.** Either way Skald keeps the lead, lists it (Leads > Left out, the person drawer, and the check above with `"test": true`), and never counts it. An owner or admin can also do it by hand: open the lead in the dashboard and press **Mark as test** under its reference (or **Not a test** to count it again). It is the same mark as the call below. See [Marking a conversion as a test](https://skaldanalytics.com/docs/leads-and-conversions.md#marking-a-conversion-as-a-test). **When you send it.** Add `"test": true` to the body (section 2). It marks that message only: send it on every stage of a test lead (`started`, `completed`, `won`...). Sending a lead again with only `test` changed does nothing, like any repeat. Use the call below to change one Skald already has. **After you sent it.** Post the refs to mark, signed with the **same secret** and the same way as a lead (section 3): ``` POST /traffic-hooks/example.com/leads/test X-Skald-Signature: t=,v1=."> {"refs": ["L-1001", "L-1002"], "test": true} ``` | Field | | | | --- | --- | --- | | `refs` | required | 1 to 100 of your `external_ref`s | | `test` | default `true` | `false` takes the mark off, and the lead counts again | It marks every stage of each ref that came through this site's lead hook (not Zapier's `json` hook, a booking tool or the lead database). And a stage you send later for a marked ref is marked too, so a team account's next steps stay out. The answer: ``` {"ok": true, "test": true, "marked": 3, "already": 0, "not_found": ["L-1002"]} ``` `marked` is how many rows changed (one per stage), `already` how many were so already, `not_found` the refs Skald has no lead for. Calling it twice is safe. curl: ``` URL='https://stats.example.com/traffic-hooks/example.com/leads/test' BODY='{"refs":["L-1001","L-1002"],"test":true}' T=$(date +%s) SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SKALD_LEAD_SECRET" -hex | sed 's/^.* //') curl -s -X POST "$URL" -H 'Content-Type: application/json' -H "X-Skald-Signature: t=$T,v1=$SIG" --data-raw "$BODY" ``` Node (18 or later): ``` import crypto from "node:crypto"; const URL = "https://stats.example.com/traffic-hooks/example.com/leads/test"; // Mark (test = true) or unmark (test = false) up to 100 leads by your own ids. export async function markTest(refs, test = true) { const body = JSON.stringify({ refs, test }); const t = Math.floor(Date.now() / 1000); const sig = crypto.createHmac("sha256", process.env.SKALD_LEAD_SECRET).update(`${t}.${body}`).digest("hex"); const res = await fetch(URL, { method: "POST", body, headers: { "Content-Type": "application/json", "X-Skald-Signature": `t=${t},v1=${sig}` } }); if (!res.ok) throw new Error(`Skald answered ${res.status}`); return res.json(); } ``` Python (standard library only): ``` import hashlib, hmac, json, os, time, urllib.request URL = "https://stats.example.com/traffic-hooks/example.com/leads/test" def mark_test(refs: list, test: bool = True) -> dict: """Mark (or unmark) up to 100 leads by your own ids.""" body = json.dumps({"refs": refs, "test": test}).encode() t = str(int(time.time())) sig = hmac.new(os.environ["SKALD_LEAD_SECRET"].encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest() req = urllib.request.Request(URL, data=body, method="POST", headers={ "Content-Type": "application/json", "X-Skald-Signature": f"t={t},v1={sig}"}) with urllib.request.urlopen(req, timeout=10) as res: return json.load(res) ``` PHP: ``` function skald_mark_test(array $refs, bool $test = true): array { $url = 'https://stats.example.com/traffic-hooks/example.com/leads/test'; $body = json_encode(['refs' => array_values($refs), 'test' => $test]); $t = time(); $sig = hash_hmac('sha256', $t . '.' . $body, getenv('SKALD_LEAD_SECRET')); $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-Skald-Signature: t=$t,v1=$sig"]]); $answer = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code !== 200) throw new RuntimeException("Skald answered $code"); return json_decode($answer, true); } ``` The figures follow at the next update of the dashboard: a lead marked today leaves the day it happened, even weeks ago, and comes back when unmarked. The check above (section 9) shows the change at once. Each call is in the delivery log and the audit log, with the refs it named. The answers: 200 as above; 400 the body is wrong (not JSON, no `refs`, more than 100, an empty ref, `test` not `true` or `false`, or another field), the answer says which; 401 the signature does not match, or `t` is more than 5 minutes off; 404 the site is not set up or its lead hook has no secret; 429 more than 60 calls a minute for the site, wait and try again. Not the same as section 8's test: a message with the header `X-Skald-Test: 1` is only read and checked, never kept. A lead with `"test": true` is kept, as a test lead. ## What Skald answers | Answer | Means | Do | | --- | --- | --- | | 200 | Taken: `{"ok":true,"recorded":1,"duplicates":0}`. A repeat shows as a duplicate | Nothing | | 400 | The body is not JSON, or a field is wrong (no `external_ref`, a time that is not a time) | Fix the body; do not retry | | 401 | The signature (or the `Bearer` key) does not match, or `t` is more than 5 minutes off | Check the secret and the clock | | 404 | No such site or tool, or the hook has no secret | Make the secret; check the address | | 413 | The body is over 64 KiB | Send fewer records at once | | 429 | Too many at once | Wait and try again | | 500 | Not kept (disk or database) | Send it again | --- # Demo bookings > Skald docs: https://skaldanalytics.com/docs/demo-bookings/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). How booked calls from Calendly and Cal.com reach Skald, what each way can see, and how to keep only your own event types when one account books for several businesses. ## Three ways Calendly reaches Skald | | Embedded on your page | A link to calendly.com | The Calendly webhook | | --- | --- | --- | --- | | Needs | The script tag | The script tag | A paid Calendly plan (Standard or higher) and the `calendly` hook | | Opened the booking | Yes (`calendly_open`) | No: only a click out to calendly.com | No | | Chose a time | Yes (`calendly_time_selected`) | No | No | | Booked | Yes (`calendly_booked`) | No | Yes | | Cancelled, rescheduled, no-show | No | No | Yes | | Booked off your site (a link in an email) | No | No | Yes | | Joined to the visitor's journey | Yes, with visitor IDs on | With visitor IDs on, through `utm_content` | Yes, through `utm_content`, with visitor IDs on | - **Embedded** means Calendly's inline widget or popup on your own page. The script tag listens to Calendly's own messages in the page. - **A link** sends the visitor away to calendly.com. The page sees only the click out. The script tag adds the visitor ID to the link as `utm_content`, so the webhook can join the booking to the journey. - **The webhook** is Calendly telling Skald about each booking. It sees every booking, wherever it was made. The best of both: embed the booking on your page, and turn on the webhook. Then you see opened, time chosen, booked, and what happened after. ## Set up the Calendly webhook 1. In Skald: **Settings > Integrations > Calendly > Make secret**. Copy the secret and the address. 2. In Calendly: make a personal access token (on Calendly's integrations page, under API and webhooks). 3. Find your organisation's URI: ``` curl -s https://api.calendly.com/users/me -H "Authorization: Bearer $CALENDLY_TOKEN" ``` 4. Make the subscription. The `signing_key` is Skald's secret: ``` curl -sS -X POST https://api.calendly.com/webhook_subscriptions \ -H "Authorization: Bearer $CALENDLY_TOKEN" -H 'Content-Type: application/json' \ -d '{"url": "https://stats.example.com/traffic-hooks/example.com/calendly", "events": ["invitee.created", "invitee.canceled", "invitee_no_show.created"], "organization": "https://api.calendly.com/organizations/", "scope": "organization", "signing_key": ""}' ``` 5. Press **Send a test event** on the Integrations page. Then book a test call. A booking counts as a demo completed; a cancellation as cancelled (or rescheduled, when Calendly says so); a no-show as no-show. ## Keep only your own event types One Calendly account often books for several businesses. A Calendly webhook cannot be limited to some event types: it sends every booking of the account. So Skald filters them. In **Settings > Integrations > Calendly**, fill **Only these Calendly event types**, one per line. Any of these works: - The event type's URI: `https://api.calendly.com/event_types/AAAA1111`. This always matches. - Its id alone: `AAAA1111`. - The booking page's slug: `product-demo`, or the page's address `https://calendly.com/example/product-demo`. Calendly does not send the booking page's slug. So Skald compares a slug with the event type's **name** written as a slug: "Product demo" becomes `product-demo`. If you rename the event type, a slug stops matching. The URI never does. To find the URI: ``` curl -s "https://api.calendly.com/event_types?user=" -H "Authorization: Bearer $CALENDLY_TOKEN" ``` Or wait for a booking of another type: the delivery log names each event type it left out, so you can copy the URI from there. What happens then: - A booking of a listed type is kept, as before. - A booking of another type is answered 200, so Calendly does not send it again. It shows as "not a conversion" in Last deliveries, with the detail "other event type" and the type it named. Nothing else is kept: no conversion, no copy of the message. - A no-show names only the person, not the event type. It is kept when its booking was kept. - An empty list keeps every booking. On a server without accounts, set it in the config: ``` hooks: calendly_event_types: [https://api.calendly.com/event_types/AAAA1111] ``` With PyYAML installed, the nested form works too: `hooks: {calendly: {event_types: [...]}}`. ## Cal.com - **The webhook:** Settings > Developer > Webhooks > New. Subscriber URL: the `calcom` hook's address. Triggers: Booking Created, Booking Cancelled, Booking Rescheduled. Secret: the hook's secret. - **The embed:** with the script tag on the page, Skald sees the booking open (`cal_open`) and booked (`cal_booked`). - **The visitor ID:** pass it in the embed as `metadata[skv]=skv_...`, or as a hidden booking question named `skv`. - **Event types:** **Only these Cal.com event types** on the Integrations page takes event type ids (`1234`) or slugs (`30min`, or the page's address `https://cal.com/example/30min`). Config: `hooks: {calcom_event_types: [...]}`. ## A relay of your own Some sites want their own filtering, or to add something before Skald sees it. Then the site receives the tool's webhook itself: 1. Check the tool's signature with the tool's key. 2. Keep what you want. Drop the rest, and store nothing of it. 3. Sign the kept body again with Skald's `calendly` hook secret, in Calendly's scheme: `Calendly-Webhook-Signature: t=,v1=.">`. 4. Post it to `/traffic-hooks/example.com/calendly`. For the common case, keeping some event types, the filter above does this for you. You do not need a relay. See also [Webhooks](https://skaldanalytics.com/docs/webhooks.md) for every tool, and [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md) for what started, completed and abandoned mean. --- # Funnels and goals > Skald docs: https://skaldanalytics.com/docs/funnels-and-goals/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). How to build a funnel of pages and events, save it and read its history, how goals work on the Events tab, and how to find badly tagged campaign links. ## Funnels A funnel answers "how many visits went through these pages, in this order, and where did the rest leave?". Open Leads & sales > Funnels. ### How steps are counted - A visit reaches a step when it reads a page that matches it, after the step before. - The page that met one step may also meet the next. So "Any page" then "/pricing/" counts a visit that landed straight on pricing. - The page steps are built from each visit's pages in order. These are kept for a few days (7 by default), so a new funnel shows only those days, and the line under the title says so when the period above is longer. Save it to count it every day (see "Funnel history"). ### Build a funnel Each page step has a choice of how it matches pages: | Choice | Matches | Example | | --- | --- | --- | | Any page | every page; as a first step, the visit's start | - | | Page is | that exact page, with or without the last slash | `/pricing/` | | Starts with | every page whose address begins this way | `/blog/` | | Page named | a page whose last part is this word, in any language folder | `pricing` matches `/pricing/` and `/fr/pricing/` | 1. Open Leads & sales > Funnels. 2. In **Starting from**, choose where the visits came from: **Any source**, a kind of source (such as Search engine or AI assistant), or one assistant under **One assistant**. 3. For each step, choose how it matches pages (above), then type the page. 4. To let a step match several pages, type them separated by a vertical bar, such as `/pricing/ | /plans/`. 5. Press **Add a step** for the next one. A funnel has up to 8 steps. 6. To remove a step, press the **×** beside it. The tab suggests pages your visitors read as you type. ### Event steps A step can also be an event from the [tracking script](https://skaldanalytics.com/docs/tracking-script.md), such as a button click or a demo booked. 1. Press **Add a step**. 2. In the step's list, look under **An event, from the beacon** and choose the event. > Event steps are counted as events on the same days, beside the visits. They are never joined to a visit, so they are not a share of the step before. When **Starting from** is a single assistant or site, an event step shows "not split by this source". ### Lead steps A funnel can also end with steps from your website's lead database: Opened Book a demo, Sign-up, Contact form, Early access, Demo booked. They are counted by the lead's first-touch source, beside the visits, never joined to them. They are counted once the funnel is saved. The chart keeps each kind of step on its own scale, under its own heading. Page steps are visits, each bar a share of the visits at step 1. Lead steps (and beacon event steps) are drawn apart, against the longest bar of their own group, and the heading says how many that is: they are never a share of the visits. A lead step the lead database has never recorded, or an event step your site has never sent, shows **Not set up** with one line on what is missing, never 0. Demo bookings often come from the booking tool rather than the lead database: they are on Leads & sales > Calendar bookings. Lead steps cannot be added from the step list. The built-in funnel **Landing to lead** has them: landed, pricing, a sign-up or contact page, then demo opened, lead and demo booked. ### Read the result - The heading says how many visits went through every page step, out of how many started. - Each step shows its visits and its share of the start. - Between steps, a line says how many left before this step. A drop of half or more is marked. ### Flow between the steps With two page steps or more, a flow map follows the chart. It shows where the funnel's visits came from, each page step they reached, and **Left after step 1**, **Left after step 2** and so on for those that went no further. Its step figures are the chart's. - Click a step, or a **Left after** bar, to list those visits. Click a source to start the funnel from it. - **Table** shows the same figures as rows. ### Open a step's visits 1. Click a page step's bar. 2. The visits that reached it are listed below, each with its pages in order. Press **Show 20 more** for more. 3. Press **Left after it** to see the visits that stopped at this step instead. Press **Reached it** to go back. 4. Click the bar again to close the list. ### Save a funnel 1. Type a name in **Name this funnel**. If you leave it empty, the steps become the name. 2. Press **Save funnel**. Saved funnels show as buttons above the builder. Click one to open it. Press **×** beside one to delete it. **Landing to lead** is built in and cannot be deleted. - **Saved for everyone**: your server keeps saved funnels for the whole team. - **Saved on this browser**: your server does not keep them, so they live only in this browser (up to 20). > With accounts, saved funnels are shared by every site of the install, so only an install admin can save or delete them. Viewers can open them but not change them. A funnel also lives in the page's address. Copy the link to share a funnel you have not saved. ### Funnel history A saved funnel is counted every day from the day it is saved, so it covers any date range, not only the days visits are kept. Its first day shows after the next daily run. The **History** section shows: - The visits at each step over the date range, and what share of the step before went on. - **Visits at each step, per day**: one line per step. The gap between two lines is the visits lost between them. - **By source type** and **By device**: each step per source or device, and **Went through** (the last step as a share of the first). - **Where they went next**: for each step, the next page of the visits that went no further, or "Left the site". - **Time to go through**: from the first step to the last, from under 10 seconds to 30 minutes or more. History counts page steps only. Days before the funnel was saved are "not recorded", never zero. A day that did not pass Skald's checks is withheld and left out. No share is given over fewer than 10 visits. ## Goals A goal is an event from the tracking script that counts as success for you, such as "Demo booked" or "Sign-up sent". Skald shows each goal per 100 visits, by source. ### Where goals are set You need to be an admin of the site. 1. Open Leads & sales > Events and select **Edit goals** beside the Goals heading. Or open **Settings > This site > Sites** (Sites). 2. In the **Goals** box, tick the events that count as success, such as **Sign-up sent** or **Demo booked**. Up to 10. 3. Select **Save**. The events your site has already counted come first. Events not counted yet are under **Not counted yet**, so you can set a goal before the event starts. Goals show from the next run, for every day already counted. To have no goal, untick them all and save. > On a server without accounts, there is no Settings page for goals. The person who runs your Skald server names them in the server's settings (`beacon.goals`). A goal saved in the app replaces that list for your site. ### Where goals show Open Leads & sales > Events and choose a date range. The **Goals** section has one table per goal: - **Source**, **Visits** from that source, the goal's events, and **Per 100 visits**. - The heading gives the whole site's rate. How to read it: - Visits come from your server log; events come from the tracking script. Two counts side by side, never joined per visit. - A day counts only when both passed their checks. Today's goals appear after midnight. - No rate is given over fewer than 10 visits. - Goals are for the whole site. The page filter does not apply to them. If no goal is set, the section says so, with a link to set one. For conversions from all sources (bookings, payments, CRM deals), with completion rates and sources, use Leads & sales > Summary and Sources with **Count from: Tools**. See [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). ## The Events tab Leads & sales > Events shows what people did inside your pages, as totals. Nothing here is tied to a visit or a person. Bots are left out. - **The figures at the top**: each kind of event, with its rate per 100 visitors. - **Through the forms**: Sign-up started to Sign-up sent, and Demo booking opened to Demo booked, with the share that went on (from five starts). - **Per day**: a line per main event. A hatched stub is a day not counted. - **Goals**: see above. - **Which button or form**: each event by its name and page, such as a Button click named `pricing_top` on `/pricing/`. - **By page** and **By source**: one column per kind of event. Today's events are shown as "not yet checked". Events are checked each night against your server log. A day that does not match is left out, and the tab says why. A page marked **flagged** had more events than its page views allow on some day. ## Campaign tag problems Badly tagged links split one campaign into several, or hide where visitors came from. Skald finds four kinds of problem. Open Traffic > Sources and scroll to **Campaigns and referrers**, then **Tag problems**. | Problem | What it means | How to fix it | | --- | --- | --- | | Source tag without a medium | the link has `utm_source` but no `utm_medium`, so tools cannot tell the channel | add `utm_medium`, such as `email` or `cpc` | | Medium no tool knows | `utm_medium` is not a usual channel name, so it lands in "other" | use a usual name: `cpc`, `email`, `social`, `referral` | | Ad click id without tags | an ad click id (such as `gclid` or `fbclid`) and no `utm_` tag: the network is known, the campaign is not | turn on auto-tagging, or add `utm_campaign` in the ad | | Tags on links between our own pages | a `utm_` tag on a link from one of your own pages starts a new source mid-visit | never tag links between your own pages | Each problem shows how many visits had it, up to three example landing addresses with their tags, and the pages they landed on. A visit can have more than one problem. The same section may also list campaigns that appear in your leads but were never seen in your server log. This often means a redirect dropped the tags. Above it, **UTM tags** lists visits by Source, Medium, Campaign, Term and Content, each with pages per visit, broken links met and form sends. --- # AI & bots: what each crawler costs > Skald docs: https://skaldanalytics.com/docs/ai-and-bots/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). See which AI assistants, search engines and bots fetch your site, how much work each one makes for your server, and what one of them did in detail. This guide covers the cost table, the detail view, and what each figure means. ## Where to find it Open **AI & bots**. The table **What each crawler costs** is on two tabs: - **Crawlers**: AI collecting pages for training or AI search, search engines, and bots and scanners. - **Assistants**: AI assistants fetching a page because someone asked them a question. Here it is headed **What each assistant costs**. Pick the period at the top as on any other page: today, yesterday, a number of days, or your own dates. ## What the columns mean | Column | What it is | |---|---| | Name | The name the crawler gives itself. Bots without a known name go by the name in their user agent. Scanner probes are one row, **(scanner probes)**; everything else a scanner asked for that day (its home page, say, between two probes) is another, **scanner (asked for secret or config files)**. A visitor that asked for 5 or more scanner paths your site does not have is **scanner (asked for many missing probe paths)**, and a browser that checked pages with `HEAD` requests is **browser-like link checker (HEAD requests)**. | | Requests | Every request it made in the period. | | Bytes sent | How much your server sent it, from the size field of each log line. | | Mean time | How long your server took per request, on average. | | p90 time | 9 in 10 of its requests took no longer than this. Shown for a single day only. | | Errors | The share of its requests that got an error (4xx or 5xx). | | Distinct paths | How many different addresses on your site it asked for. | | Re-crawl | Requests per distinct path: 3.0× means it fetched each path three times on average. | | Last seen | Its last request in the period. | Select a name to open its detail. An assistant is called by the name people know it by: **ChatGPT**, not `chatgpt.com`. A bot keeps its own name, because it is one fetcher, not the assistant. A bot that reads a page only when one user asks the assistant about it (`ChatGPT-User`, `Claude-User`, `Perplexity-User`) is marked **user fetcher**. A person who clicks a link in an AI answer is counted as a person, with the assistant as the source. Skald knows these assistants by the address they send people from (or the tag they put on the link): **ChatGPT**, **Perplexity**, **Claude**, **Gemini**, **Copilot** (copilot.microsoft.com, copilot.com and Bing's chat), **Meta AI**, **DeepSeek** (chat.deepseek.com), **Grok** (grok.com, and Grok inside X at x.com/i/grok; the rest of x.com is X), **Mistral**, **You.com**, **Phind** and **Poe**, and the Android apps of ChatGPT, Perplexity, Claude, Gemini, Copilot, Meta AI, DeepSeek and Grok. An iPhone app sends no address of its own: its visits count only when the link carries the assistant's tag (ChatGPT adds `utm_source=chatgpt.com`); otherwise they are Direct. **OAI-AdsBot** is OpenAI's crawler that checks the landing pages of ads placed on ChatGPT. It is listed with the AI crawlers when its address is OpenAI's. A request that uses its name to look for secret files (`.env`, keys) is not believed: it stays under bots, as a scanner, unless the address is checked as OpenAI's. ## When a figure says "not recorded" or "partial" Skald only shows what your server log holds. It never fills a gap with zero. - **Bytes: not recorded.** None of that crawler's log lines had a size. Some log formats and hosting platforms do not write one. - **Bytes: partial.** Only some lines had a size. The figure is the sum over those lines; hover over it to see how many. - **Mean time: -** The log does not record how long the server took. nginx does when its log format has `$request_time`. Vercel, Caddy and edge records do not. - **p90 time** shows only when the period is a single day. A p90 over many days cannot be built from each day's p90, so for longer periods use the mean, or pick one day. ## Figures over several days For a period of more than one day: - Requests, bytes and errors are added up over the days. - The mean time is weighted by request: a busy day counts more than a quiet one. - Distinct paths are counted per day and added up. A path fetched on two days counts twice. Re-crawl is therefore requests per path within a day. - Each day keeps its 50 busiest names per group. The rest are one row, **(all others)**. ## The detail view Select a name in the table, or any crawler name elsewhere on AI & bots. The detail opens on the right. Under **What it costs and what it fetched** you see: - **Requests, bytes sent, mean server time and errors** for the period. - **From an address checked as theirs**: the share of its requests from an address that really belongs to its owner. Requests that use a crawler's name from an address that is not theirs are counted apart, as "claims to be ...". The check needs the owners' published address lists, which the daily run fetches. A Docker install does this by default from this release on; before, no Docker install checked, so earlier days show every request as not checked, and a fake crawler counted as the crawler it named. `doctor` says "crawler checks off" when they are off. - **Requests per day**, as a chart. - **Pages it fetched**: the pages it asked for most. - **Status mix**: how the server answered. - **Countries of its requests**: from the log's country field or the GeoIP file. - **User agents it sent**: the exact strings, the five most used each day. These are the crawler's own, never a person's. For scanner probes and the rest of a scanner's requests, link previews and browser-like bots the strings are not kept, because they may be a real person's browser. - **Last seen**. Pages, countries and user agents are kept for the last 30 days. For an older period the detail says how many of its days have them. No address is ever shown. ## Can AI read a page well? (AI readiness) Open a page's story (**Traffic › Pages**, then select a page). The block **AI readiness** answers six questions from your server's log and Skald's own copies of your robots.txt and sitemap. No AI is asked anything. | Line | What it checks | |---|---| | robots.txt | Whether each of the main AI crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Perplexity-User, Google-Extended, Applebot-Extended) may read this page. It shows the rule that decides, and the lines to add if you want to let one in. | | Crawler errors | Server errors, refusals (403, 429...) and slow answers (a second or more) AI got from this page in the last 30 days. Redirects are named but not counted as a problem: the log does not say where a redirect leads. | | In the sitemap | Whether the page is in your sitemap, and its last-changed date (lastmod). | | Markdown copy | Whether the Markdown copy AI asked for (`page.md`, `page/index.md`) answered. Some plugins send assistants to a page's `.md` copy; if that copy does not exist, AI gets a 404 instead of your page: "AI asked for the Markdown copy 6 times; it answered 404 each time." | | AI asking for a 404 | Whether AI asked for this address and got 404 or 410. | | Last AI read | When an assistant last fetched the page for someone, and how many times in the last 30 days against the 30 before. | Each line is **Good**, **Needs attention** or **Not known**, with one line of evidence and what to do. "Not known" means Skald does not have what it needs (no copy of robots.txt yet, no sitemap, or the days before this release). It never guesses and never shows a 0 it did not count. Crawler names, status codes and robots.txt lines have a copy button. **Pages AI can't read well** is on **AI & bots › Assistants**: every page where AI got an error, a 404 or a slow answer, asked for a Markdown copy that does not answer, or that robots.txt shuts them out of, worst first. Filter it by kind with the chips above it. Select a row to open that page's AI readiness. A crawler that robots.txt keeps out of the whole site is said once, above the list. What AI got back is recorded from this release on. A page group (view) shows it for the whole site only. robots.txt says what a crawler may read; assistants that fetch for one user (ChatGPT-User, Perplexity-User) may not follow it. ## How the figures are checked Every figure here is counted twice, by two pieces of code written apart: the daily count, and an independent recount from the raw log lines. If the two disagree, the day is withheld until someone finds out why. A withheld day is left out of every total and named at the top of the page. --- # AI & bots: crawl health > Skald docs: https://skaldanalytics.com/docs/crawl-health/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). See which of your pages Google, Bing and each AI crawler actually fetch, which they never reach, which pages they fetch that your sitemap does not list, and the pages that answer crawlers with an error, a redirect, slowly or with a large body. All of it comes from your server log, set beside your own sitemap. ## Where to find it Open **AI & bots**, then **Crawl health**. Pick **Last 7 days and today** or **Last 30 days and today** at the top of the tab. The date range in the top bar does not apply here. ## The crawlers it follows - **Google** (Googlebot) and **Bing** (bingbot), the two search crawlers that matter most for most sites. - **Every AI crawler** by its own name: GPTBot, ClaudeBot, PerplexityBot, meta-externalagent, CCBot and the rest. These collect pages for training or AI search; nobody is waiting on them. - **Every AI user fetcher** by its own name: ChatGPT-User, Claude-User, Perplexity-User and the rest. These fetch a page because one person asked an assistant about it just then. They are marked **user fetcher**. Other search crawlers (Applebot, DuckDuckBot, Yandex), bots, people and your own traffic are not part of crawl health. ## Your sitemap Skald reads your sitemap once a day, as its own check (user agent `site-version-check`, the same one that reads robots.txt, counted as your own traffic). It tries the files named on the `Sitemap:` lines of your robots.txt and `/sitemap.xml` on your site's own host. A sitemap index is followed to its sitemaps; gzip files and plain text lists are read too. Only addresses on your own site's hosts are fetched or kept. If a try fails, the list kept last is used, and the tab says so. The line at the top says how many pages the sitemap lists, which files they came from and when it was read. ### Change dates from your sitemap The same daily read keeps each page's ``: the date your sitemap says the page last changed. Nothing more is fetched, and it does not change which pages crawl health counts. Skald keeps every date a page had, with the day it first read it, so you can see which pages changed since a given day. - Where it shows: Traffic > Pages, in **Pages by publish date** (the **Changed** column, marked "from your sitemap", and the **Changed since** filter, kept in the page's address), and in each page's story (Traffic > Pages, select a page), where each change is a step on the timeline. - A lastmod is a change date, not a publish date. When your website gives Skald its own page list with publish and updated dates, its updated date wins; without that list, the publish date stays the first day Skald saw the page in your logs. - Dates are read as your sitemap writes them: a date (2026-10-24), or a date and time with a time zone (2026-10-24T09:30:00+00:00), turned into your site's time zone. A value that is not a date (a year alone, text, a day that does not exist) and a date in the future are left out and counted; the note under the table says how many. - A page your sitemap lists that nobody has asked for yet has a row too, "not seen yet". - Some site builders stamp every page with the build time. Then every page looks changed on each build; that is what the sitemap says, and Skald shows it as it is. - Change dates start on the first sitemap read after the upgrade (the next local day). Days before that have none. ### No sitemap yet The tab says so and shows how to add one. Coverage and orphans need a sitemap; the problem lists below still work without one. 1. Make a sitemap. Most site builders and CMSs write one (WordPress, Shopify, Webflow, Squarespace); most frameworks have a plugin. 2. Publish it on your site's own host, for example `https://www.example.com/sitemap.xml`. 3. Name it in robots.txt with a line like `Sitemap: https://www.example.com/sitemap.xml`. You can also name it under `crawl_health.sitemaps` in the config. The next live run reads it; coverage counts from the next collected day. To stop Skald reading the sitemap at all, set `crawl_health.sitemap: false`. ## Coverage by crawler One card per crawler: the share of your sitemap's pages it fetched at least once in the window, with how many of them it never fetched. The small line under it is each recorded day's share. The table **Each crawler** has every crawler with: | Column | What it is | |---|---| | Requests | Every request it made in the window. | | Coverage | Sitemap pages it fetched at least once, out of all of them. | | Never fetched | Sitemap pages it did not fetch once in the window: orphans, as far as that crawler knows. | | Not in sitemap | Pages it fetched (and your site served) that the sitemap does not list. | | 404 / 410, 5xx, Redirects | How many of its requests got each answer. | | Last crawl | Its last request. | | Kind, Fetches per page | Hidden at first, so the table fits the screen: turn them on under **Columns**. Kind is search, AI crawler or AI user fetcher; fetches per page is its requests per sitemap page it fetched, how often it comes back. Your choice stays in the page's address. | The chart **Sitemap pages fetched per day** shows one crawler at a time: pick it beside the title. A day it made no request is 0; a day that was not recorded is a gap, never a 0. ## Orphans: sitemap pages a crawler never fetched **Sitemap pages and when each was fetched** lists, for the crawler you pick, the sitemap pages it did not fetch in the window, with its last fetch in 30 days and how many other crawlers did fetch each. Switch to **Every sitemap page** to see all of them. A page no crawler reaches usually has few links to it from pages crawlers do read, or is blocked. ## Fetched but not in the sitemap Pages crawlers fetched as a page (and your site served) that the sitemap does not list, with how many times, by which crawlers, on how many days, and the last time. Add the ones people should find to the sitemap; redirect, remove or keep out the rest. Markdown copies and other page-like addresses show here too. ## Problems crawlers hit Five lists, each with the crawlers that met the problem and whether the page is in your sitemap (**Listed**: the sitemap points crawlers at a broken page): - **Not found (404 and 410)**: pages crawlers keep asking for that are gone. [Site > Not found](https://skaldanalytics.com/docs/what-to-fix.md#lost-pages) suggests the closest live page for each and the redirect line to copy. - **Server errors (5xx)**: the server failed a crawler. - **Redirects (3xx)**: crawlers still asking for an old address. A log does not record where a redirect pointed, so a chain of redirects is counted one redirect at a time. - **Slow for crawlers**: the server took a second or more. - **Large answers**: bodies of 1 MB or more. Slow and large need the response time and the body size in your log; a log without them leaves those lists empty. ## How the figures are checked Every figure is counted twice, by two pieces of code written apart: the daily count, and an independent recount from the raw log lines and the stored sitemap list. If the two disagree, the day is withheld. The change dates are checked the same way: each morning a separate check reads the stored sitemap files again and works out every page's change dates on its own; if they differ from what the pages show, the morning run says so. Each day is judged by the sitemap kept that day; the windows here use today's list, so a page added to the sitemap counts from then on. --- # What to fix: to-dos and lost pages > Skald docs: https://skaldanalytics.com/docs/what-to-fix/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Skald lists what to fix on your site this week, worst leak first, each with its proof and the full list behind it. It also finds the old addresses people, search engines and AI still ask for, suggests the closest live page for each, and gives you the server line to copy. Everything comes from your server log; Skald never changes your web server. ## To do now Open Overview > Insights. The to-do list sits above the cards that say what changed. Each to-do has: - **A title** that says what to do, and a tag (Lost traffic, Broken, AI, Search, Leaving). - **Proof:** the figure and the days it covers, for example "288 old page addresses answered 'not found' from 21 Oct to 24 Oct". - **Do:** what fixes it. - **Skald will check:** the measure Skald re-checks once you mark it done. A to-do shows only when its figure passes a threshold over the last whole days (up to 30, not today). The kinds: - **Send old pages somewhere useful:** old page addresses that answered "not found" to people, search engines or AI. - **Fix the server errors:** 5xx answers, by address. - **Give AI the Markdown copies it asks for:** AI fetchers asked for the `.md` copy of pages you have. - **Fix the sitemap address crawlers ask for:** a sitemap file search engines or AI crawlers asked for that is not there. - **Make a page lead somewhere:** a page many visits started on, where nearly every one left after that page and no form was sent (from the stored visits). ## What you can do with a to-do - **Show the list** opens its rows under it. The address keeps it open, so a link brings the same list back. - **Download CSV** saves the rows. A list longer than 1,000 rows says so; the CSV has the first 1,000. - **Copy link** copies the address of this to-do, with its list open. The button turns into a green tick. - **Send to team** mails the to-do to a member of the site's team, with its rows as a CSV attached and the link. It shows only when Skald has a mail server (Settings > Mail) and the team has a confirmed member; otherwise the page says why. Someone who opens the link without rights to the site sees "not available to you"; the CSV works for anyone. - **Open in** goes to the tab that shows it. - **Mark done** moves it to **Done, and did it work?**. From the next data update, Skald re-checks it on every whole day after the day you marked it, with the same measure: "Fixed: 0 not-found answers since 25 Oct", or "Not yet: 5 not-found answers since 25 Oct". **Undo** puts it back. - **Not now** folds it away under **Not now**. **Put back** brings it back. Under the done to-dos, **Pages you changed: did it work?** lists every page change marked on Traffic > Pages, with its visits a day before and after and what Skald says. Select one to open the page with that change. See [Did my change work?](https://skaldanalytics.com/docs/getting-started.md#did-my-change-work). Marks are kept by Skald, so the whole team sees the same list. Only owners and admins can mark or send. Without accounts (signing in), the list shows but nothing can be marked or sent. ## Lost pages Open Site > Not found. At the top, **Old addresses still asked for** covers its own last 30 whole days, not the date range in the top bar. Scanner and hacking paths (`/.env`, `/wp-login.php`) are never listed. Three tabs: - **Lost pages:** each old page address, most asked for first, with how many times people, search engines and AI asked, the **closest live page** Skald found and how close it is: - **Same page:** the same address written another way (a capital letter, a missing slash, `index.html`, a broken link with junk on the end). - **Same topic:** most of its words match a live page. - **Loose:** only its section is still live. Check it before you use it. - **No good match:** nothing close; answer 410 gone. - **AI asks for Markdown:** the `.md` addresses AI asked for whose page is live, and the other `.md` copies of old pages. - **After the fix:** each address you marked fixed, with what came for it on the whole days after the mark: not-found answers, and the times it was served as a page. Zero not-found answers after the mark: fixed. ## The line to copy Pick **nginx** or **Apache**. Each row has the line to add: a 301 redirect to the closest live page, or 410 gone. When a redirect is suggested, the 410 line is under it, so you can choose. Point at a line to copy it; the icon turns into a green tick. ``` location = /old-page/ { return 301 /new-page/; } Redirect 301 /old-page/ /new-page/ ``` **Download the nginx lines** (or **Download the Apache lines**) saves every suggested line for your server as one file, with loose matches marked. Check each one before you add it. Once a line is on your server, select **Mark fixed** on its row; the address moves to **After the fix** on the next data update. ## How the figures are checked Every to-do and every lost-page figure is counted twice: once by the page's own code and once by a separate count straight from the stored days. A figure the two counts disagree on is not shown; the page names what was left out. --- # AI answers: what assistants say about you > Skald docs: https://skaldanalytics.com/docs/ai-answers/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). See what ChatGPT, Perplexity, Gemini and Claude answer when someone asks the questions your buyers ask, whether they name you, which of your pages they cite and which competitors they name. Then follow one citation through what only Skald sees: the assistant reading your page, the visitors it sent, and the leads that followed. This guide covers setting it up, tracking profiles, what each figure means, and what it costs. ## Read this first **These are a sample of API answers, not what every user sees.** Each provider answers people differently by account, history, place and wording, and its API can differ from its app. Read the figures as a direction over weeks, never as a count of people. Every answer is kept, so you can read exactly what was said. ## Where to find it - AI & bots › AI answers: the figures, the chain, the questions and the profile's settings beside them. - Settings › AI answers: the site's questions, brand terms and competitors, and the tracking profiles. - Settings › Integrations: the assistants' API keys (install admins). ## Set it up 1. **Add a key** for at least one assistant: Settings › Integrations › AI answers. Each key is your own account with that provider (OpenAI, Perplexity, Google Gemini, Anthropic). It is write-only: the page only says set or not. 2. **Write the lists**: Settings › AI answers. The questions your buyers ask (at most 50, written as a customer would), your brand terms (your name and your domain) and your competitors (names or domains). 3. **Make a tracking profile**: a place and a language, the assistants to ask, how often, how many tries per question, and a daily spending cap. The panel shows the cost estimate before you save. 4. **Wait for the first run**, from 06:00 in the site's time zone, or choose **Run now**. The figures appear once every answer of the run is in and has been checked. Nothing is asked, and nothing costs, until a key and a profile exist. The questions are sent to the providers you choose: do not write anything private into a question. ## Tracking profiles A profile asks as if from one place, in one language: Toronto in English, Montréal in French, New York. Make one per market you care about; at most 10 per site. | Setting | What it does | |---|---| | Location | City, region and country, with optional coordinates and time zone. ChatGPT, Perplexity and Claude take it in their API's location field (**API location**). Gemini's API has none, so "I am in ..." is written into the question (**In question**). | | Language | Asked for in the question ("Answer in fr-CA."), and passed to Perplexity as its language. Write the questions in that language too. | | Questions | Its own list, or the site's. | | Brand terms, competitors | Its own, or the site's when left empty. | | Assistants | Which to ask. An assistant without a key is left out and said. | | How often | Daily, weekly or monthly. | | Tries per question | 1 to 5. Answers vary from one try to the next: with 5 tries you see "named you in 3 of 5", never a single yes or no. | | Daily spending cap | In US dollars. A run that would pass it stops and goes on the next day. | Pause, resume, duplicate and delete are in Settings › AI answers. Nothing is erased: a deleted profile leaves the list and its results stay; an edit makes a new version and earlier runs keep theirs. ## What the page shows - **From an answer to a lead**: for one assistant and one of your pages, over the last 30 days, four counts side by side: answers that **cited** the page (the sample), the times that assistant's fetcher **read** it for people asking (your server log), the visitors it **sent** to it (your log), and the leads whose first visit came from an AI answer and landed on it (your forms and booking tool). They are counted on their own roads and joined by page and day, not by person: a path, not one buyer. They are never added together. - **How often each assistant names you**: the share of answered tries that named a brand term, per run of the profile, one line per assistant, each named at its end. Hover or focus a run for its figures. - **Every profile, every assistant**: one question across all profiles: "3 of 5" tries, a filled dot for each try that named you. Pick a question in the table below to change it. Select another profile's name to open that profile; **Export what you see** downloads the grid as a CSV. - **Questions**: each question of the profile's last run: tries that named you over all assistants, the pages cited, the competitors named, and a line from one answer. **Full answer** opens the whole answer and its cited links. A cited page opens that page's detail, as anywhere in Skald. - **Competitors in the answers**: how many answers named each competitor, per assistant. The Questions and Competitors tables work like every table in Skald: sort by any column, filter each column, choose the columns, search, and export what you see. A profile's place is shown as its flag and the three-letter country code (CAN, GBR), the full name in the tooltip. The page keeps its own period, each profile's runs, so the date range does not apply here. ## What counts as named and cited - **Named**: a brand term appears as a whole word, any case. "Contoso" counts in "Contoso's plan", not in "Contosoville". - **Cited**: the answer links to a page on your site's hosts (or a brand term written as a domain). Gemini sometimes hides the address behind Google's redirect; Skald reads where it leads, and when it cannot, the page shows "(page not known)". - **Citation rank**: where your first link sits among the answer's links (1 = first). Every figure is counted twice: once when the answer is read, and again by a separate count from the stored answer text and links. A run where the two disagree is **withheld**: the page says so and shows no figures for it. ## What it costs Each answer costs on your provider account: the model's tokens and its web search. The estimate on the profile's panel is questions × tries × assistants × a price an answer × runs a month. The prices are rough defaults (about 1 to 4 US cents an answer) that your server's admin can match to your contract. The estimate is not a bill: check each provider's own usage page. --- # Connections: send, read and connect > Skald docs: https://skaldanalytics.com/docs/connections/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Settings > Connections is where a site's visits reach Skald from another server, a script reads your figures, and an AI assistant connects. This guide covers the three set-up dialogs, how to test each one, your tokens, and adding a site step by step, by one line to your AI agent or by hand. ## Where to find it Open **Settings > Team & access > Connections**, or go straight to Connections. Settings opens from the gear at the foot of the sidebar. The page was called **API & tokens** before. Old links to it still work. ### Set up your site For owners and admins, the page starts with **Set up example.com** (your site's name): the same list as on Overview, with every line of the site's set-up, each with its **Set up** button while it is left. Here it is always shown. Once every line is done it shows every tick and **All set**, so you can check the site's set-up at any time. **Hide on Overview** on Overview's copy of the list hides only that copy: this one stays. See [Set up your site](https://skaldanalytics.com/docs/getting-started.md#set-up-your-site) for every line. The **Lead hook tested** line is ticked only while no hook's last real lead was refused. When one was (say its signature did not match, because the tool signs with an old secret), the line says leads are not arriving, its Set up button opens the lead hook's dialog, and the cause and fix are under **For your server admin**. ### Links to a dialog Each dialog has its own link, with its step: **Read your figures (API)** at step 2 is `#settings/api?conn=api&step=2`, and the checklist's script tag dialog is `#settings/api?setup=script&step=1`. Copy it from the address bar and send it to another owner or admin: it opens the same dialog, at that step or at the first step that still needs a choice. **Back** closes the dialog; closing it takes it out of the address. Someone who may not use the dialog (a viewer) sees the page with one line: "Not available to your role." The link a **Share with your developer** form makes is never in the address. At the top are three cards. Each has a **Set up** button that opens a short dialog with steps. Press **Next** to go on and **Back** to go back. | Card | What it is for | | --- | --- | | Send this site's visits to Skald | A site whose web server is not your Skald server: its log, or its events, must be sent to Skald. **Set up with your AI agent** on this card gives you one line for your agent instead (see [Add a site, step by step](https://skaldanalytics.com/docs/connections.md#add-a-site-step-by-step)). **Test a sample** checks that Skald can read a log before you send it (see [Test a sample of a log](https://skaldanalytics.com/docs/connections.md#test-a-sample-of-a-log)) | | Read your figures (API) | A script or a spreadsheet that reads your figures as JSON | | Connect an AI assistant (MCP) | Claude, ChatGPT or another assistant that answers questions about your traffic | Below the cards is **Your tokens**: every token of your organisation. ### Who can use it - **Owners and admins** use the dialogs and see the tokens. - **Viewers** see what each part is for, and the address of the figures while signed in. Tokens and dialogs are for admins. - **A server with one shared login** has no team accounts. The page says the dialogs need accounts. It keeps the server commands for tokens folded away under one heading. See [The read-only API](https://skaldanalytics.com/docs/api.md), "Servers that use one shared login". ## Send this site's visits to Skald Use this when the site's web server is not the server Skald runs on. If Skald reads the site's log in place already, you need nothing here. The dialog has three steps: **Where it runs**, **Run one command** (or the way's own steps), **Test**. Cloudflare and Vercel or Netlify have a **Sending token** step and an **Install** step in place of **Run one command**; the script tag has **Install** only. ### Step 1: Where it runs Step 1 first shows **What Skald found** for this site (see **Skald checks your site**, below), and chooses the way it suggests. **Save the host names and page assets to this site** puts the found host names (added to the ones the site has) and page assets in the site's settings. Then pick where the site runs, or keep the suggestion: | Choice | When to pick it | | --- | --- | | My own server (nginx or Apache) | Recommended. With or without Docker. One command on the server | | Cloudflare | Cloudflare serves the site. The Skald Worker sends a record of each request. Any plan works, the free one too | | Vercel or Netlify | The platform sends its request logs to Skald (a "log drain"). Nothing to install | | Only the script tag | In-page events only: clicks, forms, scroll. No server log, so no AI crawlers or bots | A server log is the best source. The script tag alone never sees AI crawlers or bots. ### Step 2: Run one command The steps for the way you chose, with the send address and the sending token filled in. You never make the token yourself: Skald makes it when the step opens (a token that can only add log lines to this one site, and cannot read anything). It shows in **Your tokens** as "Sending logs: , ". The send address looks like this: ``` https://analytics.example.com/traffic-ingest/example.com ``` Every command has a **Copy** button. **My own server: one command.** On the web server, run the command the step shows: ``` curl -fsSL https://analytics.example.com/traffic-kit/ | sudo bash ``` The link works once, for 24 hours, and holds this site's sending token. The installer finds the site's access log in nginx's or Apache's settings by itself, and asks only when it cannot tell. It uses Docker when the server has it, and a small service with Python 3.9 or newer when not. Running it again is safe. A used or expired link prints "This install link was used or has expired" and changes nothing: press **Make a new command**. Coming back to the step after the command was used makes a new one by itself. Opening the link in a browser (or a mail's link preview) only shows the command; it does not use the link up. **Download as one file** gives the same installer as one file, `skald-install-.sh`, with the token inside. Run it with `sudo bash skald-install-.sh`. Add `--dry-run` to see what it would do without changing anything, `--log ` to name the log yourself, or `--yes` to answer nothing. It leaves two commands on the server: `sudo skald-ship-check` checks everything and changes nothing; `sudo skald-ship-uninstall` removes only what it installed. **Email it to your server admin** sends the set-up pack (below). **Other ways: by hand with Docker / without Docker** keeps the same steps as plain commands, for a server where you would rather type each one. The installer only reads the log. It never changes nginx, Apache or the website. The command and the file hold the token: send them only to the site's admin. ### Send set-up to someone: the set-up pack **Send set-up to someone** (at the top of the site's **Set up** list, and in the logs and leads dialogs) mails one zip, `skald-setup-.zip`, to up to 3 addresses, or downloads it. It holds: - `START-HERE.md`: every case, step by step (own server, Cloudflare, Vercel or Netlify, only the script tag), then leads, then how to check it works. - `CLAUDE.md` (and `AGENTS.md`): the same, written for an AI coding agent. Give the folder to Claude Code and say *Set up Skald from CLAUDE.md*. It keeps every key in environment variables, never in code or git. - `secrets.env`, the installer, the script tag, and the lead files for PHP, Node, Python and WordPress. **Include the keys** is ticked by default: the pack then holds this site's sending token and its lead secret (the one the site has, or a new one when it has none), so the person can finish without asking you. Treat that mail like a password: anyone with it can send data for this site. Delete it after the set-up. Untick it to send the pack without keys; the files then say where each key goes. Mailing needs mail set up on the server (**Settings > Mail**); without it, download the pack instead. **Cloudflare.** Deploy the Skald Worker, give it the send address and the token (as a secret named `SKALD_TOKEN`), and add your site's route to it. The dialog lists each step. When nothing else has reached Skald for the site yet, Skald reads it as edge records. **Vercel or Netlify.** In the platform's log drain settings, give the address and the header the dialog shows. Vercel needs a Pro or Enterprise plan; Netlify needs Enterprise. **Only the script tag.** The tag goes in the `` of every page; nothing changes on your web server. To serve it from your own domain instead, the step has the lines under **Advanced: serve it from your own domain**. See [The tracking script](https://skaldanalytics.com/docs/tracking-script.md). ### Step 3: Test The Test starts by itself when the step opens: Skald says what it received for the site, or why it refused it. It checks at once, then again every 5 seconds for up to 2 minutes. **Test connection** checks again. | What you see | What it means | | --- | --- | | Waiting for the first lines... | Nothing has reached Skald yet. Open your site in a browser and wait a little | | Connected: 1,240 lines received today, the last at 10:42 (2 min ago). | It works | | Connected before: the last lines came at ... Nothing today yet. | It worked before. Open the site, then test again | | Refused at 10:41: the token is not for this site. | Skald refused the lines. The fix is shown under it | | Lines arrive, but none could be read | The log's format does not match the site's setting. [Test a sample](https://skaldanalytics.com/docs/connections.md#test-a-sample-of-a-log) says which format the lines are in | For your own server, a wrong answer also says: run `sudo skald-ship-check` on the web server, which checks each part and names the fix (an older zip kit has `sudo bash check.sh` in its folder instead), and where to see what the shipper says: `sudo docker logs --tail 20 skald-ship` with Docker, or `journalctl -u skald-ship -n 20` without. A set-up saved before 2.30 gets the same advice. Under the answer is a folded table, **Common problems, and the fix**. Every answer and its fix are in [If a connection does not work](https://skaldanalytics.com/docs/troubleshooting-connections.md). ## Test a sample of a log Can Skald read this log, and what would it count? **Test a sample** on the **Send this site's visits to Skald** card tells you now, not after the next daily run. It is for owners and admins. 1. Paste up to 200 lines of the log: the first lines of your web server's access log (nginx, Apache, Caddy), or of an export from Cloudflare, CloudFront, Fastly, Akamai, Vercel or Netlify. 2. Leave **Format** on **Detect it (auto)**, or choose the one you expect. 3. Press **Next**. Skald reads the lines with the same readers and the same rules as the daily run, with this site's settings. Nothing is stored. The report says: | Part | What it tells you | | --- | --- | | Format | The reader Skald picked, how sure it is, and how many lines each reader could read | | Lines | How many lines it read, and the ones it could not, with the main reasons and up to 3 line numbers each. A line itself is never shown | | Times | The first and last time in the lines, and the time zone they are written in | | Hosts | The host names in the lines, and whether each is one of this site's | | Skald's five groups | Real people, answer engines, search engines, bots and scanners, and your own traffic, for these lines. They add up to the lines read | | Pages | The pages people viewed most | | Warnings | What to fix before you send the log | The warnings, and what to do: | Warning | What to do | | --- | --- | | This looks like an error log | Use the access log, often `access.log` in the same folder | | No line has a referrer | Every visit's source would be Direct. Add the Referer header to the log format | | No line has a user agent | Skald cannot tell people from bots. Add the User-Agent header to the log format | | No line carries a host name | Fine when the server serves one site. Otherwise add `$host` to the log format | | Lines for `www.` are not one of the site's hosts | Add the host name in Settings > Sites, or those lines are counted apart | | This site is set to read another format | Set the site's log format to Detect it (auto) | | No asset request is under the site's page assets | Set the site's page assets, or people show as bots | | More than 1% of the lines cannot be read | A daily run would hold the day back. The reasons above say why | The pasted lines stay in your browser and are sent once for the test. They are not kept, and they never go in the page address. The dialog's link is `#settings/api?conn=sample&step=1`; a link to step 2 opens step 1, since the lines are not in it. On the server, the same test reads a whole file: `docker exec skald traffic-intel try-log /var/lib/traffic-intel/ingest/example.com/access.log --site example.com` (Docker), or `traffic-intel try-log `. It reads the first 500 lines (`--lines` for more), plain or gzip. ## Read your figures (API) The dialog has three steps: **Token**, **Use it**, **Test**. 1. **Token.** Keep or change the name, and press **Make the token**. Copy it: it is shown only once. The box **Can also read visitor addresses** is off by default. Turn it on only if a script truly needs the request log and AI activity, which hold visitors' IP addresses. 2. **Use it.** The base address, a curl command with your token in it, and the list of files this site serves. Add a file's name to the base address, for example `overview`. 3. **Test.** Press **Test**. This browser asks for the site's `overview` file with the token, as a script would. "200: it works." means the token reads, and it names the first fields of the answer. Any other answer is said in words, with what to do. More on the files and the answers: [The read-only API](https://skaldanalytics.com/docs/api.md). ## Connect an AI assistant (MCP) The dialog has three steps: **Token**, **Connect**, **Test**. 1. **Token.** Make a token for the assistant and copy it. If MCP is not switched on for your server, the dialog says so at the top. You can make the token anyway. 2. **Connect.** The MCP address, the Claude Code command, the Claude Desktop config (it uses `mcp-remote`), and what ChatGPT can and cannot do today. Your token is filled in. 3. **Test.** Press **Test**. This browser starts an MCP session with the token and lists the tools, as an assistant would. "Connected:" with the number of tools means it works. Each step in full: [Ask an AI assistant about your traffic](https://skaldanalytics.com/docs/mcp.md). ## Your tokens One table lists every token of your organisation. | Column | What it shows | | --- | --- | | Name | The name given when it was made | | What it is for | In words, from what it may do: Sending logs, Reading figures, Reading figures and addresses, AI assistant (MCP) | | Sites | The sites it may use | | Made | When, and by whom | | Last used | When it was last used | To stop a token, press **Revoke**, then **Yes, revoke**. Whatever uses it stops at once. This cannot be undone. A token is shown only once, when it is made. Skald keeps only a fingerprint of it. If you lose one, revoke it and make a new one. ### Make a token by hand Open **Make a token by hand** under the table. It is for advanced use: any sites, and any mix of what the token may do. 1. Type a **Name**. 2. Under **Sites**, tick the sites it may use. 3. Under **What it may do**, tick one or more: **Reading figures**, **Reading visitor addresses**, **AI assistant (MCP)**, **Sending logs**. 4. Press **Make token**, copy it, then press **I have copied it**. The cards above are simpler for one task. ### A quick look while signed in **Quick look while you are signed in**, folded at the foot of the page, has the address of the figures in this browser, with no token, and the list of files this page serves. ## Add a site, step by step Owners and admins add a site in five short steps. Open **Settings > Organisation > Add or remove sites** (Add or remove sites) and press **Add a site, step by step**. The site picker at `/traffic/` has **+ Add a site** too. The quickest way is to let your AI agent do it. You copy one line from Skald and paste it into your agent. The agent reads Skald's steps, sets up the site, and reports back. You watch each step tick. 1. **Your site.** Type the site's address, as people type it. A whole link works too: `https://www.example.com/pricing` becomes the site `www.example.com`. The page shows the name it will use. Choose the time zone its days are counted in. 2. **Prove it's yours.** Add the DNS TXT record or the meta tag the step shows, then press **Check now**. A new DNS record can take a few minutes to show. Nothing is collected for the site until this is done, but you can go on now. When your server asks for no proof, the step says so. 3. **Your AI agent.** "Which AI agent are you using?" Pick one: **Claude Code**, **Codex**, **Cursor**, **Claude.ai or ChatGPT** (chat only), or **Another agent** (one that can run shell commands). No agent? Press **No AI agent: set it up by hand** (below). 4. **Send this line.** Skald writes one line for your agent, like this: ``` set up Skald for example.com — read https://analytics.example.com/traffic-kit/llms.txt and use code •••••••••••• (works once, 24 hours) ``` The code is hidden until you press **Show code**. Press **Copy** (it turns into a green tick), and paste the line where the step says: | Agent | Where to paste the line | | --- | --- | | Claude Code, Codex | In a terminal in the site's project folder, or on the site's server | | Cursor | In the site's project, in the chat, in Agent mode | | Another agent | In the site's project folder or on its server. It must be able to run shell commands there | | Claude.ai or ChatGPT | A chat in the browser cannot run commands on your server. So this step shows the one command to paste on the server yourself, as in Connections | **Make a new line** gives a new code. **Cancel this line** stops it at once. The step says until when the line works. Opened from a link straight at step 4, it asks you to press **Make the line** first, so an old link never ends a line your agent is already using. 5. **Watch it connect.** A list ticks itself while the step is open: your agent read the steps, it took the keys (with the time), it sent its report, and the first log lines arrived. When the agent says it added the script tag or a lead form, Skald also looks for the tag on your home page and waits for a test lead. When the first log lines arrive it says "Done: example.com is receiving visits", with **Open its dashboard**. **Finish later** closes the dialog. Your agent asks you what it cannot find out alone: how the site is hosted, if the code does not say, and whether to keep visitors' IP addresses, for how many days, and the link to your privacy notice. It asks before it changes a server or your web server's settings. ### Is it safe to paste this into a chat? The same box is under the line in step 4: - Yes. The line holds a one-time code, not a password or a key. - The keys come out once. When your agent takes them, the code can never give them again. A code nobody uses stops after 24 hours. - Your agent is told to save the keys straight into a locked file and never show them in the chat. Skald gives them only to a command-line download such as curl, and turns away web readers and fetch tools, which could pass them into a chat. - The keys work for this one site only. The sending key can only send log lines. The lead secret sends leads and can read back this site's lead list (reference, stage, time), never names, emails, addresses or visitor IDs. Neither can open your dashboard or see your visitors. - Skald keeps only a scrambled copy of the code (a hash), never the code itself, and never writes it to a log. - Until the keys are taken, treat the line like a house key: paste it only into your own agent. Changed your mind? Press Cancel this line and it stops at once. A new line also cancels an older one whose keys were not taken yet. - If your site already has a lead secret, the agent gets that same one (unless your Skald keeps it in the server's settings; then the keys hold none). After the set-up you can remove the agent's sending key in Settings > Connections > Your tokens, and make a new lead secret in Settings > Integrations. Your agent reads Skald's steps with `curl` too, so the code does not pass through a fetch tool. If it tries a web reader or a fetch tool for the keys, Skald refuses it and the keys stay unused. If it fetches the keys twice, the second try never overwrites the keys file it already has. After the keys are taken, the code still opens the steps, the installer and the lead files until it stops: none of them holds a key. ### Your agent took the keys Step 5 says **Your agent took the keys at 10:42. The code is used up: it cannot give the keys again.** If it was not your agent, press **Cancel this line**, remove the key in **Settings > Connections**, under **Your tokens**, and make a new line. ### What your agent reported When the agent is done, it sends Skald a short report, and step 5 shows it in plain words. Only the agent that took the keys can send it. If it sends a newer one (say after a fix), step 5 shows that one: how the site is hosted, the web server, the access log, what the site is built with, its page assets, where the script tag went, its lead forms, the privacy answers, the files it changed, and anything **Left for you**. The report is a suggestion. Skald changes no setting by itself. When the agent found other page assets than the site has, **Use these page assets** saves them. Set the privacy answers yourself, in **Settings > Data retention > People**. ### When the line stops | What step 4 or 5 says | What to do | | --- | --- | | This line ran out. | Nobody took the keys in 24 hours. Press **Make a new line** and send that one | | Cancelled. This line no longer works. | You cancelled it. Make a new line when you are ready | | The old line was cancelled. | You made a new line, so the older one stopped. Send the new one | | This line was made before the page was reloaded, so it is not shown again. | Already sent it? Press **Next** to watch it connect. If not, make a new line, or cancel this one | ### Set it up by hand On step 3, press **No AI agent: set it up by hand**. Steps 3 to 5 then become these. **Use an AI agent instead** takes you back. 3. **How visits reach Skald.** The same choice as in Connections: my own server (with or without Docker), Cloudflare, Vercel or Netlify, or only the script tag. The way Skald suggests is chosen already, marked **Recommended for** your site, with the reason under it. 4. **Install.** As in Connections: one command for your own server, with the token made for you, or the way's own steps. **Download as one file** and **Email it to your server admin** are there too. 5. **Test and finish.** The Test starts by itself. When lines arrive it says "Done: example.com is receiving visits", with **Open its dashboard**. If not, it shows the fix and the common problems. **Finish later** closes the dialog. A site you added already can use its AI agent too: in **Settings > Connections**, press **Set up with your AI agent** on the **Send this site's visits to Skald** card. It opens the site's wizard at step 3. ### Skald checks your site When you type the address in step 1, Skald looks at the site from outside, the way a browser does, and shows **What Skald found**: | Line | What it says | Read from | | --- | --- | --- | | Server | nginx, Apache, LiteSpeed, Caddy or Microsoft IIS, and the system it names (like Ubuntu) | The site's `Server` header | | Platform | Cloudflare (when its proxy is on), Vercel, Netlify, CloudFront, Fastly, GitHub Pages, or a hosted builder: Shopify, Wix, Squarespace, Webflow | Headers such as `cf-ray`, `x-vercel-id`, `x-nf-request-id`, `x-amz-cf-id` | | Built with | Next.js, Nuxt, WordPress, Gatsby, Astro, or plain HTML | `X-Powered-By`, and the page's own folders like `/_next/static/` or `/wp-content/` | | Host names | Both `example.com` and `www.example.com` when they are the same site, and which one sends visitors to the other | Both addresses are asked | Each line has a tick, or says **could not tell**. Under them: the page assets it suggests for **Settings > This site > Sites > Page assets**, and the way it suggests, with one plain reason. For example: "example.com runs on its own nginx server (Ubuntu), so one command on that server sends its log." A Next.js site on its own nginx, Apache or LiteSpeed server also gets one more line: add ` rsc_prefetch=$http_next_router_prefetch` at the end of the site's nginx `log_format` (Apache: ` rsc_prefetch=%{Next-Router-Prefetch}i`), then reload nginx. Without it, a page opened by a click inside the site is not counted, because Next.js loads it without an HTML request. `sudo skald-ship-check` on the server says whether the log has it. For a Next.js site this line is part of setting it up, not an extra. **Is it safe?** Yes. The line only adds one header's value (`1`, or `-` when absent) to what nginx already writes in its own log file. It opens no port, adds no route and gives no one access; nginx escapes an odd value as it does for every other header; and the log already goes to Skald over HTTPS with the site's own token. ### Check a new site end to end A site is ready when a real visit shows up the way you expect: 1. In a private window, open the site and click two links inside it. Within about 2 minutes, Overview shows the visit and Traffic › Visitors lists it with every page you clicked. On a Next.js site, if only the first page shows, the line above is missing. 2. Do the same on a phone on mobile data, in a new private tab (a tab that is already open shows the page from memory and sends nothing). 3. The site's **Set up** list (on Overview, and always at the top of Settings › Connections): every line is ticked, or marked optional on purpose. 4. If the site sends leads: a lead sent with `"test": true` is listed under Left out and not counted. | What Skald sees | The way it suggests | | --- | --- | | nginx, Apache, LiteSpeed or Caddy, nothing in front | My own server (nginx or Apache): one command on that server | | Cloudflare with its proxy on | Cloudflare (the Skald Worker sees every request, cached pages too) | | Vercel or Netlify | Vercel or Netlify (its log drain) | | CloudFront or Fastly in front of nginx or Apache | My own server: one command on the server behind it. Pages the CDN answers from its cache are not in that log | | Shopify, Wix, Squarespace, Webflow, GitHub Pages | Only the script tag: Skald cannot read their server log | | Microsoft IIS | Only the script tag: the one-command install is for Linux servers | | Nothing it can tell | Nothing: you choose | Every line is a suggestion. **Use these** is on at first: the site is added with the found host names and page assets, and step 3 chooses the suggested way. Turn it off to use none of them, or change any of them later: the way in step 3, the host names and page assets in **Settings > Sites**. **Check again** looks once more. When the site cannot be reached (it is not live yet, or it turns automated visitors away), the card says so plainly. Nothing depends on the check: add the site and choose the way by hand. How the check behaves: - It asks only the site's home page, `https:///` and `https://www./` (or without `www.`), with a `HEAD` request, then reads at most 256 KB of the page when the headers do not say enough. Each request waits at most 6 seconds and follows at most 3 redirects. It sends no cookie, and says it is Skald's site check in its user agent. - It fetches only public addresses, over http or https on ports 80 and 443. A name that points to a private, local or cloud-metadata address is refused, and so is a redirect to one. - An admin can ask for about 30 checks an hour, and one address is checked at most 6 times in 10 minutes. The server log keeps only the site's name and the outcome. - The facts are kept with the set-up's progress, so **Setup: step 3 of 5, continue** shows them again. ### Continue later Skald saves how far you got, on the server, for each site: the step, the way chosen, what the site check found, and which sending token was made (its id, never the token itself). Closing the dialog or the browser loses nothing. A site with an unfinished set-up shows a button in **Add or remove sites** and on the site picker: | Button | What it means | | --- | --- | | Setup: step 3 of 5, continue | The set-up stopped at that step. Press it to carry on | | No visits yet: test the connection | Set-up was finished, but nothing has reached Skald in 24 hours. Press it to open the Test step | The wizard is in the address while it is open: `#settings/sitesadmin?setup=new&step=1` for a new site, `setup=example.com&step=3` for one already started. The agent you chose is there too (`agent=claude-code`, or `agent=hand` for by hand), and in step 5 the line being watched (`link=` and 16 letters and digits, a scrambled copy of the code). The line and its code are never in the address. **Back** closes it. Until the site's first log lines arrive, its own address says so: **example.com is waiting for its first log lines** (with its own name), with the step it reached, the same **Continue setup** (or **Test the connection**) button, and a link to [If a connection does not work](https://skaldanalytics.com/docs/troubleshooting-connections.md). A site removed in Add or remove sites says that it was removed instead: nothing is collected for it, and its data stays until an install admin purges it. Each new step, way chosen, and finish is written to the audit log. > If you choose Cloudflare for a new site that has received nothing yet, Skald sets its log format to edge records for you. This is written to the audit log too. ## Share with your developer The form is in the address while it is open (`share=1` beside the dialog's own link), so a reload brings it back. The link it makes is never in the address. Someone else may do the install: your web developer, or whoever runs your server. They need no sign-in. In the send dialog (step **Run one command**, or **Install** for the other ways), in Add a site (step 4) and in the Set up dialogs of the script tag and the lead hook, press **Share with your developer**: 1. Choose how long the link works: 1 day, 7 days (the default) or 30 days. 2. Press **Make the link**. Copy it: it is shown only here. **Send it by mail** opens your mail program with the link in it. 3. Your developer opens the link. It shows only that one task for that one site: the steps with everything filled in, the Copy buttons, the installer to download (for a server, **Download as one file**) and the **Test** button. The token or secret is not in the link. Your developer presses **Show the secret** on the page: the secret is made at that moment and shown once. After that the page says it was shown already, and you make a new link if needed. For the lead hook, a new secret replaces the older one for that tool. The links you made are listed under **Shared links** in Connections, with when they end, whether they were opened and whether the secret was shown. **Revoke** ends a link at once. A token made from a link stays in **Your tokens** (named "Shared link: ..."), where you can revoke it too. Making, opening, showing and revoking a link are written to the audit log. The page keeps no visitor address and writes no secret to any log. ## Related guides - [If a connection does not work](https://skaldanalytics.com/docs/troubleshooting-connections.md): every answer of the Test, and its fix. - [The read-only API](https://skaldanalytics.com/docs/api.md): files, answers and scopes. - [Ask an AI assistant about your traffic](https://skaldanalytics.com/docs/mcp.md): each assistant in full. - [The tracking script](https://skaldanalytics.com/docs/tracking-script.md): the tag, and the two addresses it needs. - [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md): who is an admin. --- # The read-only API > Skald docs: https://skaldanalytics.com/docs/api/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Every figure Skald shows is also a JSON file that a script, a spreadsheet or another tool can read with a token. This guide covers tokens, scopes, the addresses to call, and what the answers look like. ## What the API is The API gives you the same data files the dashboard itself reads. Nothing is counted again, and nothing can be changed through it. It is read-only. The files are rewritten every 2 minutes. Each file says when it was written, in its `generated_at` field. There are two ways in: | Way in | Who it is for | How it proves who you are | | --- | --- | --- | | While signed in | You, in your own browser, for a quick look | Your dashboard sign-in | | With a token | A script, a spreadsheet, another tool | A token in the `Authorization` header | Both ways show exactly the same files. ## Where to find it Open **Settings > Team & access > Connections**, or go straight to Connections. Settings opens from the gear at the foot of the sidebar. The page was called **API & tokens** before; old links still work. For the API, the page has: - the card **Read your figures (API)**: its **Set up** button makes a token, shows the base address, a curl command with your token, and the files this site serves, then tests the token (admins only); - **Your tokens**: every token of your organisation (admins only); - **Quick look while you are signed in**, folded at the foot: the address of the overview file in this browser, and every file name you can put in place of `overview`. The whole page is explained in [Connections](https://skaldanalytics.com/docs/connections.md). ## Tokens A token is a long secret that lets a script read your figures without a password. It looks like this: ``` skald__ ``` Treat it like a password. Keep it in a password manager or your tool's secret store. Never put it in a shared document, a chat, or a code repository. ### Who can make tokens Only an owner or an admin of the organisation can make, see or revoke tokens. A viewer sees a note that tokens are for admins. See [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md) for roles. ### Make a token 1. Open Connections and press **Set up** on **Read your figures (API)**. 2. Keep the **Name** it suggests, or type one that says what it is for, for example "Weekly sheet". 3. Leave **Can also read visitor addresses** off unless the script really needs them. 4. Press **Make the token**. 5. Copy the token from the green box. It is shown only this once. 6. Press **Next** for the address and a curl command, then **Next** again to **Test** it. The token made there reads the site you are on. For a token that reads several sites, or does several things, open **Make a token by hand** under **Your tokens**: tick the **Sites** and what it may do, then press **Make token**. > Skald keeps only a fingerprint of the token, never the token itself. If you lose it, nobody can show it to you again. Revoke it and make a new one. ### The token list **Your tokens** shows each token's **Name**, **What it is for** (in words, such as "Reading figures"), **Sites**, when it was **Made** and by whom, and when it was **Last used**. The token itself is never shown again. ### How long a token lasts A token has no expiry date. It works until one of these happens: - an admin revokes it; - the person who made it is removed, disabled, or is no longer an owner or admin. The second rule means a token stops at once when its maker leaves the team. Make tokens for long-running jobs with that in mind. ### Revoke a token 1. Open Connections. 2. Find the token under **Your tokens** and press **Revoke**. 3. Press **Yes, revoke**. Every script using it stops at once. Revoking cannot be undone. ## Scopes A scope is what a token may do. A token can have more than one. | Scope | Label in the app | What it allows | | --- | --- | --- | | `read` | Reading figures | Read every figure these pages show, as JSON, through the API | | `read_private` | Reading visitor addresses | Also read the two files that hold visitor addresses, `requests` and `ai_activity`, and look a person up | | `mcp` | AI assistant (MCP) | Let an AI assistant ask about your traffic through the MCP server | | `ingest` | Sending logs | Send a site's log lines into Skald. It reads nothing | The box **Can also read visitor addresses** in the API dialog adds **Reading visitor addresses**. A sending token is made in **Send this site's visits to Skald** (see [Connections](https://skaldanalytics.com/docs/connections.md)). > A token with only **AI assistant (MCP)** cannot read the API, and a token with only **Reading figures** cannot be used by an AI assistant. Use **Make a token by hand** and tick both if one token must do both. ## The addresses to call Every address starts with your dashboard's own address. In the examples below it is `https://analytics.example.com`, and the site is `example.com`. With a token: ``` https://analytics.example.com/traffic-api/v1// https://analytics.example.com/traffic-api/v1/// ``` While signed in, in your browser: ``` https://analytics.example.com/traffic//api/v1/ https://analytics.example.com/traffic///api/v1/ ``` - `` is the site's name, for example `example.com`. - `` is a part of the site, such as `us-english`. Leave it out for the whole site. - `` is a file name without `.json`, for example `overview`. The signed-in address also lists the files a page has: end it at `api/v1/`. The token address has no list; use the list in the API dialog's **Use it** step, or under **Quick look while you are signed in**, instead. ## The main files | File | What it holds | | --- | --- | | `overview` | Headline figures per day: visits, pages read, AI and search fetches, 404s, errors | | `live` | Today so far, from the 2-minute snapshot | | `visits` | Visits per day, and today's anonymous journeys | | `hourly` | Figures per hour | | `breakdowns` | Countries, sources, devices, browsers and more | | `pages` | Each page: readers, entries, exits, AI and search fetches | | `campaigns` | Tagged links and campaigns | | `bots` | Crawlers and bots | | `leads` | Leads from the website's forms | | `conversions` | Demos, forms, sign-ups and payments by stage | | `funnels` | Saved funnels and their results | | `quality` | Whether each day was read whole and passed its checks | | `health` | Server health | | `alerts` | Open alerts | | `ours` | Your own traffic and the heartbeat | | `todos` | Overview > Insights' to-do list: each to-do's proof, what to do, and whether it is to do, marked done (with its re-check) or not now | | `lost_pages` | Site > Not found: old addresses still asked for, the closest live page, and the nginx and Apache lines | | `page_changes` | The changes marked on a page, and whether each worked (the days before against the days after) | | `ai_ready` | AI readiness: robots.txt for each main AI crawler, what AI got back by page, the sitemap | | `page_trends` | Each page's visits and AI reads by day, and the pages changed in the last 30 days | | `ai_activity` | What AI assistants did (holds addresses: needs **Reading visitor addresses**) | | `requests` | The recent request log (holds addresses: needs **Reading visitor addresses**) | | `people?q=...` | One person by IP address, email, visitor ID or lead: their visits, leads and conversions, linked. Needs **Reading visitor addresses**; each search goes to the audit log | The Connections page lists every file your site really has. ## Examples with curl Read the overview of the whole site: ``` curl -H "Authorization: Bearer skald__" \ https://analytics.example.com/traffic-api/v1/example.com/overview ``` Read the pages of one view: ``` curl -H "Authorization: Bearer skald__" \ https://analytics.example.com/traffic-api/v1/example.com/us-english/pages ``` The header must be exactly `Authorization: Bearer ` followed by the token. ## What an answer looks like Every file starts with the same few fields, then its own figures. Most files hold one entry per day for the last 180 days, oldest first. A short piece of `overview`: ```json {"schema_version": 1, "generated_at": "2026-10-08T07:16:02-04:00", "site": "example.com", "view": null, "timezone": "America/New_York", "anchor": "2026-10-07", "first_day": "2026-09-01", "days": [ {"day": "2026-10-05", "state": "withheld", "problems": "the audit did not agree"}, {"day": "2026-10-06", "state": "ok", "not_found_people": null, "server_errors": 0, "visits": {"visits": 120, "views": 170, "one_page": 98, "median_seconds": 4}}, {"day": "2026-10-07", "state": "ok", "not_found_people": 9, "server_errors": 0, "visits": {"visits": 140, "views": 190, "one_page": 118, "median_seconds": 3}} ]} ``` ### Read the day's state first Each day has a `state`. Check it before you use any figure. | State | Figures? | What it means | | --- | --- | --- | | `ok` | Yes | A whole day that passed every check | | `partial` | Yes | Checked, but the logs did not cover the whole day. Do not add it to whole days or compare it with them | | `withheld` | No | It failed a check, so Skald shows no figures for it. `problems` says why | | `missing` | No | Not collected | | `before` | No | Before your history starts | ### Not recorded is null, never 0 - A figure that was not recorded is `null`. It never becomes `0`. - `0` means Skald counted and found none. - A withheld or missing day has no figures at all. Leave it out of totals and say so; never count it as zero. - Visits are an upper bound on people, not a count of people. Requests are never people. > In a spreadsheet, keep `null` as an empty cell. If you turn it into 0, your totals and averages will be wrong. ## Errors | Status | Body | When | | --- | --- | --- | | 200 | The file | All went well | | 401 | `{"ok": false, "error": "..."}` | No token, a wrong or revoked token, a token for another site, a token without **Reading figures**, or a private file without **Reading visitor addresses** | | 404 | `{"ok": false, "error": "no such file"}` | The site, view or file does not exist | The `error` text says which problem it was, for example "unrecognised or revoked token" or "this token is not for that site". The **Test** step of the API dialog says each answer in words; see [If a connection does not work](https://skaldanalytics.com/docs/troubleshooting-connections.md). Answers are never cached. Ask again for fresh figures; they change every 2 minutes. ## Rate limits Skald sets no request limit of its own on the API today, so you will not see a 429 answer from Skald. Whoever runs your server may add one. The files only change every 2 minutes, so asking more often than that gains nothing. Once every few minutes is plenty for a live screen; once a day is enough for a report. ## Servers that use one shared login Some Skald servers have no team accounts. Everyone signs in with one shared name and password set on the server. On those servers: - the Connections page says its set-up dialogs need team accounts. Under **Read the figures with the dashboard login, or a token made on the server** it shows the address and curl command with your login, and with a token; - tokens are not made on the page. Whoever runs the server makes them with a command, and the page shows that command; - the signed-in address also works from curl with `curl -u `, using the dashboard password; - the MCP server is not available. See [MCP](https://skaldanalytics.com/docs/mcp.md). Everything else in this guide is the same. ## Related guides - [Connections](https://skaldanalytics.com/docs/connections.md): the page, its dialogs and Your tokens. - [MCP](https://skaldanalytics.com/docs/mcp.md): let an AI assistant read your figures. - [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md): roles, and who may make tokens. - [Privacy](https://skaldanalytics.com/docs/privacy.md): which files hold addresses. --- # Ask an AI assistant about your traffic (MCP) > Skald docs: https://skaldanalytics.com/docs/mcp/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Connect Claude, ChatGPT or another AI assistant to Skald, so you can ask about your traffic in plain words. This guide covers the address to use, the token it needs, setup for each assistant, and what it can and cannot see. ## What MCP is MCP (Model Context Protocol) is a standard way for an AI assistant to use outside tools, such as Skald. Once connected, you can ask "How did last week compare with the week before?" and the assistant reads the answer from Skald. It does not guess. ## Before you start You need three things: - **MCP switched on for your server.** Whoever runs your Skald server turns it on. If it is off, the address below answers "MCP is not switched on here". - **Team accounts.** MCP works only on servers where people sign in with their own accounts. It is not available on servers with one shared login. - **A token for the assistant** (the MCP scope). Only an owner or admin can make one. The quickest way is the card **Connect an AI assistant (MCP)** in Connections. Its **Set up** button makes the token, shows what to paste where, and tests it. This guide has each step in full. ## The address to use The MCP address is your dashboard's address, followed by `/traffic-mcp`: ``` https://analytics.example.com/traffic-mcp ``` Copy yours from **Settings > Team & access > Connections** > **Connect an AI assistant (MCP)**, step **Connect**. That step has the Claude Code command and the Claude Desktop config too, with your token filled in. > The address is the same for every site on your server. The token decides which sites the assistant may read. ## Make a token for the assistant 1. Open Connections and press **Set up** on **Connect an AI assistant (MCP)**. 2. Keep the **Name** it suggests, or type one, for example "Claude". 3. Press **Make the token** and copy the token. It is shown only once. 4. Press **Next** for the address and the set-up of each assistant, then **Next** again to **Test** it. That token reads the site you are on, and sees no visitor addresses. For an assistant that reads several sites, open **Make a token by hand** under **Your tokens**: tick the **Sites**, and **AI assistant (MCP)** under **What it may do**. Tick **Reading visitor addresses** there only if you truly need it. With it, the assistant can see visitor addresses in today's AI activity and look a person up (`find_person`: their visits, emails and names). The token looks like `skald__`. In every step below, put your own token where you see `skald__`. See [Connections](https://skaldanalytics.com/docs/connections.md) and [The read-only API](https://skaldanalytics.com/docs/api.md) for more on tokens. ## Connect Claude Code Run this one command in your terminal: ``` claude mcp add --transport http skald https://analytics.example.com/traffic-mcp \ --header "Authorization: Bearer skald__" ``` Then start Claude Code and type `/mcp` to see that `skald` is connected. ## Connect Claude Desktop Claude Desktop needs a small helper to send the token, because Skald does not offer a sign-in screen (OAuth) yet. The common helper is `mcp-remote`, which needs Node.js on your computer. 1. In Claude Desktop, open **Settings > Developer** and press **Edit Config**. This opens `claude_desktop_config.json`. 2. Add Skald to it as below, with your own address and token. 3. Save the file and restart Claude Desktop. 4. Start a new chat and check that the Skald tools are listed. ```json { "mcpServers": { "skald": { "command": "npx", "args": ["mcp-remote", "https://analytics.example.com/traffic-mcp", "--header", "Authorization:${SKALD_AUTH}"], "env": {"SKALD_AUTH": "Bearer skald__"} } } } ``` > Claude's **Connectors** screen (Add custom connector) asks for a URL and, at most, OAuth details. It has no place for a token, so it cannot connect to Skald yet. Use the config file instead. If you run the Skald server yourself, you can also connect over SSH without a token. The server docs explain how. ## Connect ChatGPT Skald does not offer OAuth sign-in yet. ChatGPT can only use Skald where it lets you give the token yourself. - **ChatGPT app (connectors, developer mode):** these screens ask for OAuth or no sign-in. They cannot send the token, so they cannot connect to Skald yet. - **OpenAI API:** a developer can add Skald as a remote MCP server: set `server_url` to `https://analytics.example.com/traffic-mcp` and put the token, `skald__`, in the MCP tool's `authorization` field. OpenAI's [connectors guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp) (read 24 October 2026) says to "Provide this token using the `authorization` field of the MCP tool", and its example has the token alone, with no `Bearer` in front. The value is not stored, so send it in every request. ## Other MCP clients Any MCP client works if it can do both of these: - connect to a remote server over HTTP (often called "Streamable HTTP"); - send a custom header with every request. Use the address above and the header `Authorization: Bearer skald__`. Skald answers each request with plain JSON and does not keep an event stream open. ## The tools it offers The assistant chooses these tools itself. Dates are in your site's own time zone. With no dates, a tool uses the last 7 whole days: yesterday and the 6 days before it. | Tool | What it answers | | --- | --- | | `list_sites` | The sites you may read, their views, and the days held | | `overview` | Visits, pages read, AI fetches, search fetches, 404s, server errors, and the change against the period before | | `breakdown` | Visits by country, source, campaign, landing page, device, browser, hour, operating system or referrer | | `top_pages` | The pages people read most, with AI fetches of each | | `ai_activity` | Which AI assistants and crawlers came, what they fetched, and visits that followed an assistant's link | | `leads` | Leads from the website's forms, by type and by first source (totals only) | | `funnel` | A saved funnel, or steps you name, over the last 7 days and today | | `compare` | Two date ranges side by side, up to 400 days | | `page_story` | One page since it was published: first Googlebot, first AI crawler, first person, first lead | | `live_today` | Today so far, from the 2-minute snapshot (partial, never compared) | | `data_quality` | Per day: whether the log was read whole and passed its checks | | `site_health` | Open alerts, heartbeat uptime, errors today, late runs, page speed | | `conversions` | Demos, forms, sign-ups and payments by stage: started, completed, abandoned, won, lost. The completed ones and the leads, by source, first touch, landing page, country, campaign, device or tool | | `what_changed` | Summary's **What changed: is it working?**: visits by source and by landing page against the period before, with the biggest rise and drop. Only when both periods are whole | | `todos` | **Overview > Insights**' to-do list: what to fix, its proof, what to do, and whether each is to do, marked done (fixed or not yet) or not now. Give an id for the list behind one to-do | | `lost_pages` | **Site > Not found**: old addresses still asked for, the closest live page and the nginx or Apache line to copy; also the Markdown copies AI asked for, and the addresses marked fixed | | `page_changes` | The changes marked with **+ Mark a change** on a page, and for each the days before against the days after: up, down, no clear change or too early to tell | | `ai_readiness` | **Pages AI can't read well**, worst first; or one page's **AI readiness**: robots.txt for each main AI crawler, errors, the Markdown copy, the sitemap and the last AI read | | `changed_pages` | Summary's **New and updated pages**: pages changed in the last 30 days, their visits and search visits since, and whether Google fetched them | | `find_person` | One person by IP address, email, visitor ID or lead: their visits, leads and conversions. Only with **Reading visitor addresses**; each search goes to the audit log | It also offers a ready prompt, **weekly_report**, which asks the assistant for a short weekly report. ## Questions to try - "Which sites can you see in Skald?" - "How did last week compare with the week before?" - "Where did our visits come from in the last 30 days?" - "Which pages did ChatGPT fetch yesterday?" - "What are our ten most-read pages this month?" - "Was any day withheld last week, and why?" - "How is the pricing page doing since it was published?" - "How many demos were booked last month, and from which sources?" - "Is the site healthy right now?" - "What should we fix first this week, and did last week's fixes work?" - "Which old addresses still get visits, and where should each one redirect?" - "Did the change I made to the pricing page work?" - "Can ChatGPT and Claude read our guides page?" - "Which sources and landing pages rose or fell against the week before?" - "Write a short weekly report for example.com." ## How honest the answers are Skald gives the assistant the same rules the dashboard follows: - Visits are called "visits (an upper bound on people)". A visit count is at most the number of people, never a count of them. - Totals use whole, checked days only. Withheld, partial and missing days are named by date and left out. They are never counted as zero. - A figure that was not recorded is null, never 0. - A change against an earlier period is given only when both periods are whole. > If the assistant says a day was withheld, that day failed a check. See [FAQ](https://skaldanalytics.com/docs/faq.md) for what that means. ## Safety - **Read-only.** Every tool only reads. The assistant cannot change settings, delete data, or add anything. - **Only your sites.** The token reads only your organisation's sites, and only the ones you ticked. Every tool checks this again on each call. - **No addresses by default.** Without **Reading visitor addresses**, no answer carries a visitor's address, and the assistant cannot look a person up. - **Turn it off at once.** To cut off an assistant, revoke its token: open **Settings > Team & access > Connections**, find it under **Your tokens**, press **Revoke**, then **Yes, revoke**. It stops working immediately. - **Tied to the maker.** If the person who made the token leaves the team or stops being an admin, the token stops too. - **Keep the token secret.** It sits in your assistant's settings. Do not paste it into a chat or a shared file. ## If it does not connect First press **Test** in the dialog's last step. It says in words what is wrong. Every answer is in [If a connection does not work](https://skaldanalytics.com/docs/troubleshooting-connections.md). | What you see | What to check | | --- | --- | | "MCP is not switched on here" | Ask whoever runs your server to switch MCP on | | 401, "Unauthorized" | The token is wrong, revoked, or has no MCP scope. Make a new one with **Connect an AI assistant (MCP)** | | "this token is not for that site" | Make the token from that site's page, or tick that site in **Make a token by hand** | | 403, "this Origin may not call the MCP server" | You are calling from a web page. Use an assistant app or command line instead | | 405 | The client tried to open an event stream. Pick the HTTP (Streamable HTTP) transport | ## Related guides - [Connections](https://skaldanalytics.com/docs/connections.md): the page, its dialogs and Your tokens. - [The read-only API](https://skaldanalytics.com/docs/api.md): tokens and scopes in full. - [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md): who is an admin. - [Privacy](https://skaldanalytics.com/docs/privacy.md): what holds visitor addresses. --- # If a connection does not work > Skald docs: https://skaldanalytics.com/docs/troubleshooting-connections/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). What to do when a site's visits do not reach Skald, or a script or an AI assistant cannot connect. This guide covers skald-ship-check, the common problems, and what every answer of a Test means. ## Start with skald-ship-check If the site sends its log from your own server (the one command, or the one file), run this on the web server first: ``` sudo skald-ship-check ``` The installer leaves it there, with Docker or without. An older zip kit (with Docker, or without Docker) has the same checks as `check.sh` in its folder: run `sudo bash check.sh` there. It changes nothing. It checks each part in turn and prints one line for each: - `✓ OK` when that part works; - `✗ PROBLEM`, with a `fix:` line under it that says what to do; - `? CANNOT TELL` when it could not find out. It checks, in this order: Docker (when the shipper runs with Docker), the web server, the access log and who may read it, whether the log is growing, whether www writes to that log, whether Skald's send address can be reached, the saved token file, whether the shipper is running, the shipper's last lines, and whether lines are leaving. Fix the first problem it names, then run it again. At the end is a block that starts **Copy this and send it to us**. Send that block to whoever runs your Skald server, or to us. It holds no token, and every IP address in it is masked. > No `skald-ship-check` on the server? The shipper was not installed, was removed, or came from an older zip kit (use its `check.sh`). Make a new command in Connections > **Send this site's visits to Skald** > **Run one command**, and run it on the server. ## Common problems The same table is under the Test in the dialog, folded as **Common problems, and the fix**. | What you see | Likely cause | Fix | | --- | --- | --- | | Refused: the token is wrong or was revoked (401) | The token was mistyped, cut short, or revoked in Your tokens. | Make a new command and run it again on the server: it holds a new sending token. | | Refused: the token is not for this site (403) | The token was made for another site. | Make a sending token from this site's own page. | | Nothing received: the shipper is not running | The container or service stopped, or the installer did not finish. | Run sudo skald-ship-check on the server. It says what stopped. Then make a new command and run it again. | | Nothing received: a firewall | The server may not open HTTPS connections out to Skald. | Allow outbound HTTPS (port 443) to Skald's address. sudo skald-ship-check tests it: a 401 without a token means the path works. | | Nothing received: the wrong log file | The shipper reads a file the website does not write to. | sudo skald-ship-check shows the file and the server blocks that write to it. Run the installer again with --log and the right path. | | Nothing received: the log is not growing | Nobody visited yet, or the site logs somewhere else. | Open the site in a browser, then test again. If the file still does not grow, find its access_log line. | | Visits from another host name | www and another host share one log file. | Give www its own access_log line in its server block, then run the installer again. | | Refused: too many batches (429) | The shipper sent many batches at once, usually right after it started. | Nothing to do. The shipper slows down and sends the rest. | | Refused: the site is not verified (403) | The site waits for proof that the domain is yours. | Finish step 2 of the site's set-up (Settings > Add or remove sites). | | Docker is missing | The server has no Docker. The one command (or the one file) then runs the shipper as a small system service; only the by-hand way with Docker needs Docker. | Nothing to do. If the installer stops with "this server can run the shipper in no way it knows", install Python 3.9 or newer, then run the command again (make a new one if it was used). | | Pages opened inside a Next.js site are not counted | The log does not record Next.js's prefetch header, so a click cannot be told from a prefetch. Only the first page of each visit counts. | Add one line to the site's log format (below, "Next.js sites"). sudo skald-ship-check and Overview's checklist say when it is missing. | | The site check could not reach the site | The site is not live yet, turns automated visitors away, or its name points to a private address, which Skald never fetches. | Choose the way by hand in step 3: nothing else depends on the check. Press Check again once the site is live. | ## Next.js sites: pages opened inside the site On a Next.js site, a click on a link inside the site loads no new page from your server. The browser asks only for the next page's data, an address that ends in `?_rsc=...`. Next.js asks for the same addresses to get links on screen ready (a prefetch), even when nobody clicks them. A plain log cannot tell the two apart, so Skald counts none of them, and only the first page of each visit shows. Nothing is guessed. Next.js marks a prefetch with a header. Add one field at the very end of the site's nginx `log_format`, inside its quotes: ```nginx rsc_prefetch=$http_next_router_prefetch ``` For Apache, add this to the end of its `LogFormat`: ```apache rsc_prefetch=%{Next-Router-Prefetch}i ``` Then check and reload: ```sh sudo nginx -t sudo systemctl reload nginx ``` From then on, a page opened inside the site counts as a page view, and a prefetch never does. Lines written before stay as they were. **Set up** on Overview's **Pages opened inside the site are not counted yet** has the same steps, a Copy button, **Share with your developer**, and a Test that waits until lines with the field arrive. Caddy already logs the header: nothing to add. The Vercel and Netlify log drains carry no headers, so there these pages are still not counted. ## What the Test of a log says In **Send this site's visits to Skald**, step **Test**, and in the last step of **Add a site**, **Test connection** gives one of these answers. | Answer | What it means | | --- | --- | | Waiting for the first lines. Nothing has reached Skald yet. | No batch has arrived. It checks again every 5 seconds for up to 2 minutes. If nothing comes, see Common problems above | | Connected: N lines received today, the last at 10:42 (2 min ago). | It works | | Connected before: the last lines came at ... Nothing today yet. | Lines came on an earlier day. Open the site in a browser, then test again | | Lines arrive, but none could be read | Batches reach Skald, but no line is in a format the site reads. Set the site's log format to Detect it (auto) in Settings > Add or remove sites. **Test a sample** in Settings > Connections says which format the lines are in and why they fail ([Test a sample of a log](https://skaldanalytics.com/docs/connections.md#test-a-sample-of-a-log)) | | The log endpoint is switched off on this server. | Ask whoever runs Skald's server to switch the log endpoint on (ingest.enabled) | | The site is not verified yet, so nothing is collected for it. | Finish step 2 of the site's set-up (prove the domain is yours) | | Refused at 10:41: ... | Skald refused the last batch. The reason and the fix are in the next table | Skald notes only the newest refusal for each site, and at most one a second. It never keeps an IP address, a token or a log line for this. ### Why a batch was refused | Skald says | What to do | | --- | --- | | No token was sent. | The shipper sent no token. Make a new command and run it again on the server. | | The token is wrong or was revoked. | The token is wrong or was revoked. Make a new command and run it again on the server (Cloudflare, Vercel or Netlify: a new sending token in the platform's setting). | | The token is not for this site. | The token was made for another site. Make a sending token for this site. | | The token is not a sending token (it has no ingest scope). | That token cannot send logs. Make a sending token (Sending logs) for this site. | | The site is not verified yet, so nothing is collected for it. | Finish step 2 of the site's set-up (prove the domain is yours). Nothing is lost: the shipper keeps the lines. | | The site was removed. | The site was removed. Stop the shipper: sudo skald-ship-uninstall on the server (an older zip kit: sudo bash uninstall.sh in its folder). | | This server does not process this site. | This Skald does not process the site. Check the site name in the send address. | | Too many batches at once. The shipper slows down and catches up. | Nothing to do: the shipper waits and sends the lines a little later. | | Today's limit of log data is used up. The shipper sends the rest later. | Nothing to do: the lines are sent after midnight UTC. Ask for a bigger daily limit if this happens every day. | | Over the plan's limit. | Choose a bigger plan in Settings > Billing. Nothing is lost. | | The organisation is read-only (billing). | Settle the bill in Settings > Billing. Nothing is lost. | | Skald's server is short of disk space. The shipper tries again later. | Ask whoever runs Skald's server to free disk space. Nothing is lost. | | The site's log format does not match what was sent. | Set the site's log format to Detect it (auto) in Settings > Add or remove sites. | | The batch was malformed. | Update the shipper: make a new command and run it again on the server. | | The batch was too large. | Update the shipper: make a new command and run it again on the server. | | Accounts are not set up on Skald's server. | Ask whoever runs Skald's server: accounts must be on for log sending. | | The log endpoint is switched off on Skald's server. | Ask whoever runs Skald's server to switch the log endpoint on (ingest.enabled). | | Refused. | Run sudo skald-ship-check on the web server and send us its last block. | The shipper keeps every line it could not send and tries again, so a refusal loses nothing once it is fixed. A refused post from the Cloudflare Worker is not sent again. ## What the Test of the script tag says When you chose **Only the script tag**: | Answer | What to do | | --- | --- | | Connected: N events on a day, the counters last written at ... | It works | | Connected: a page opened with the tag at ... No event is counted yet. | The tag loads. Events follow when people click, scroll or start a form | | Waiting for the first event. | Open a page of the site in a normal browser window, with Do Not Track and Global Privacy Control off | | The script tag is switched off on this server. | Ask whoever runs your Skald server to turn it on | While nothing has arrived, the Test also looks at your home page and says what it found there: | What Skald found on your home page | What to do | | --- | --- | | The tag is on your page, and its events go to ..., which reaches Skald. | The page is right. Test in a normal browser window without an ad blocker. A browser that opened the site in the last day may still run the older script: empty its cache or use a private window | | The tag is not on your home page. | Add the line from step 2 to the `` of every page, then publish | | The tag is not in the HTML ... The page loads Google Tag Manager. | Publish the Tag Manager container, with the tag firing on All Pages | | The tag is on your page but posts to www.example.com/traffic-beacon, which answers 308. | An older tag or script that posts to your own site. Copy the tag again from step 2, or pass `/traffic-beacon` on to Skald from your server | | ... its script answers 404. | You serve the tag from your own domain, and your server does not pass `/traffic-js/` on to Skald. Use the tag from step 2, or add the lines in "Advanced: serve it from your own domain" | | Your page is at ..., which is not one of the site's host names. | Add that host name in Settings > Sites | | ... Content-Security-Policy does not let it send to ... | Add your Skald server's address to `connect-src` | | Skald could not open ... | Check the address opens in a browser | In the browser's developer tools, Network panel, a request to `traffic-beacon` answered 204 or 202 means the tag works. 403 means the page's host is not one of the site's host names. A request to your own site's `/traffic-beacon` answered 308, 404 or 405 means an older tag or script. See [The tracking script](https://skaldanalytics.com/docs/tracking-script.md). ## What the API test says In **Read your figures (API)**, step **Test**: | Answer | What to do | | --- | --- | | 200: it works. | Nothing. It also names the first fields of the answer | | 401: the token is wrong, revoked, or not for this site. | Make a new token for reading figures | | 403: refused. | The token may not read this | | 404: the file is not there yet. | The figures appear after the first daily run | | 429: too many requests. | Wait a minute and test again | | No answer | Skald's address could not be reached from this browser | More: [The read-only API](https://skaldanalytics.com/docs/api.md), "Errors". ## What the MCP test says In **Connect an AI assistant (MCP)**, step **Test**: | Answer | What to do | | --- | --- | | Connected: N tools. | It works | | 401: the token is wrong, revoked, or has no MCP scope. | Make a new token for the AI assistant | | 403: this browser's address is not allowed to call MCP. | The token may still be fine: try it in your assistant | | 404: MCP is not switched on here. | Ask whoever runs your Skald server to switch it on: `mcp.enabled` in the config, or `SKALD_MCP=true` on Docker | | 405: the request was not a POST. | The client must post each request (Streamable HTTP) | | No answer | Skald's address could not be reached from this browser | More: [Ask an AI assistant about your traffic](https://skaldanalytics.com/docs/mcp.md), "If it does not connect". ## Still stuck? - Run `sudo skald-ship-check` again (an older zip kit: `sudo bash check.sh` in its folder) and send its last block. - Say which way you chose (your own server, with Docker or without, Cloudflare, Vercel or Netlify, or the script tag), and the exact words of the Test's answer. - Never send the token itself. ## Related guides - [Connections](https://skaldanalytics.com/docs/connections.md): the dialogs, step by step. - [The tracking script](https://skaldanalytics.com/docs/tracking-script.md): the tag and its two addresses. --- # Settings and retention > Skald docs: https://skaldanalytics.com/docs/settings-and-retention/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). What each settings page does, who may change it, and how long Skald keeps each kind of detail. ## Opening Settings Select the gear at the foot of the sidebar, or press `g` then `,` (comma). The settings menu takes the sidebar's place while a settings page is open. Settings pages come with accounts. On a server with one shared login, most of these values are set in the server's config file by whoever runs it, and the pages say so. A page you may not change is shown read-only, with the reason beside each field. Pages your role cannot use are left out of the menu. ## The settings menu | Group | Pages | | --- | --- | | You | Your account | | Team & access | Team, Connections (Users, on a server with one shared login) | | This site | Sites, Internal traffic, Data retention, Mail, Reports, Integrations, AI answers | | Organisation | Add or remove sites, Security, Audit log, Billing | **Back to the dashboard**, at the top of the settings menu, returns to the page you came from. These pages have guides of their own: - Your account and Team: [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md). - Connections (called API & tokens before): [Connections](https://skaldanalytics.com/docs/connections.md), [API](https://skaldanalytics.com/docs/api.md) and [MCP](https://skaldanalytics.com/docs/mcp.md). - Internal traffic: [Internal traffic](https://skaldanalytics.com/docs/internal-traffic.md). - Integrations: [Tracking script](https://skaldanalytics.com/docs/tracking-script.md) and [Webhooks](https://skaldanalytics.com/docs/webhooks.md). - Reports: [Client reports](https://skaldanalytics.com/docs/client-reports.md). - AI answers, and its API keys in Integrations: [AI answers](https://skaldanalytics.com/docs/ai-answers.md). ## Who may change what "Admin" means an owner or admin of the site's organisation. "Install admin" is a flag the server admin gives to a person, for settings that apply to every site on the server. | Page | Part | Who may change it | | --- | --- | --- | | Data retention | Detail kept for this site | Admin | | Data retention | People (IP addresses, names and emails) | Admin | | Data retention | Leads (how long leads and their pages are kept) | Admin | | Data retention | Backups | Install admin | | Mail | Reports | Admin | | Mail | Mail server (SMTP), Google PageSpeed | Install admin | | Reports | Client reports, agency branding | Admin | | Sites | Host names, Views, Time zone | Admin | | Sites | Log file | Install admin | | Sites | Goals | Admin | | Integrations | Hooks, event types, outgoing webhooks | Admin | | Integrations | AI answers: the assistants' API keys | Install admin | | AI answers | The site's lists, tracking profiles | Admin | | Security | Sign-in rules | Owner | | Audit log | (read only) | Admins may read it | | AI & bots > Crawler rules | Every crawler | Install admin | | Add or remove sites | Add a site | Admin | | Add or remove sites | Remove a site | Owner | | Billing | Plan and payment | Owner | Viewers see the pages read-only. ## Data retention Data retention sets how long the detail is kept. Totals, page milestones and every stored day's figures are kept forever. They are not a setting. | Setting | What it keeps | Range | Default | | --- | --- | --- | --- | | Detail days | Each visit by its labels, and each page's daily figures (used by Compare with… and the page story) | 30 to 3650 days | 400 | | Journeys | Anonymous visits: start time, country, device, source, pages in order | 1 to 90 days | 7 | | Returning-visitor matching | How long a visitor can be recognised as returning | 1 to 90 days | 30 | | In-page events | Events from the script tag | 30 to 3650 days | 400 | | Visits with addresses | Each visit with its IP address and browser (card **People**) | 1 to 3650 days | 395 | | Leads and the pages they read | Each lead, its name and email, and the pages its visitor ID read (card **Leads**) | Kept for good, or 1 to 3650 days | 400 days | | Pages of visitors who have not signed up yet | The pages read with a visitor ID that has no lead yet (card **Leads**) | 1 to 400 days | 30 | | Daily backup copies | Copies of the history on the server, for every site | 1 to 60 | 14 | | Weekly backup copies | Weekly copies of the history | 0 to 52 | 8 | Each field says where its value comes from: this site, the organisation, the whole server, the config file, or the default. ### People The **People** card says what Skald keeps about the people who visit: - **Keep visitors' IP addresses**: each visit is kept with its IP address, browser, times, pages in order, source, referrer, campaign, country and device. Admins can then look a person up with the search at the top. Three choices: - **Full address**: the whole address. - **Shortened (last part removed)**: `203.0.113.57` is kept as `203.0.113.0`. Switching from full shortens the addresses already kept at the next nightly run. Switching back to full keeps only new visits in full. - **Don't keep**: no address is kept, and the visits already kept with one are deleted at the next nightly run. - **Visits with addresses**: how many days those visits are kept. The default is 395. - **Leads' names and emails**: **full** keeps a lead's name and email with its conversions; **anonymous** keeps neither. A new install keeps both. An install that was upgraded from a version before 2.4.0 keeps neither until you turn them on. The field then says "kept off by the upgrade to 2.4.0". Your privacy notice must say what you keep before you turn them on. See [Privacy](https://skaldanalytics.com/docs/privacy.md). Below the card, **Wording for your privacy notice** gives a short text made from these settings, with a **Copy** button. Fill in the parts in [square brackets]. It is a starting point, not legal advice. ### Leads The **Leads** card says how long Skald keeps your leads. A lead is a form sent or a call booked. - **Leads and the pages they read**: each lead, with its name and email (when **Leads' names and emails** is full), where it came from, the pages its visitor ID read, and the leads a sync removed. Two choices: - **Kept for good**: Skald never deletes them. Nothing is removed at any nightly run, however old. - **N days**: each lead is deleted N days after it came, with its pages. The default is 400 days. - **Pages of visitors who have not signed up yet**: the pages read with a visitor ID that has no lead, kept for this many days (1 to 400, default 30). Once that visitor sends a form or books a call, all their pages are kept with the lead, for as long as the lead is kept. What is kept for good: - the leads, every stage of each (sent, booked, paid, won, lost), and the ones marked test; - the leads a sync removed (under **Show left out > Removed**); - the pages read by a visitor ID that became a lead; - the name and email of each lead (when you keep them); - the raw copy of each hook message, which the second count recounts leads from. So old days still add up. What still goes: the log of hook deliveries after 90 days, and the pages of visitors who never became a lead, after the days you set. "Kept for good" means Skald never deletes them by itself. A person's own request to be deleted still works: your server admin runs `ti.py forget --email ...` (or `--lead`, `--vid`), and everything about that person goes. See [Privacy](https://skaldanalytics.com/docs/privacy.md). Your privacy notice must say how long you keep leads: the **Wording for your privacy notice** card says "until you ask us to delete them" when they are kept for good. The server admin can set the same from the shell, for one site: ``` ti.py settings set example.com conversions.retention_days forever ti.py settings set example.com journeys.vid_days 400 ``` `0` means the same as `forever`. ### Keeping less Making a window shorter removes stored data, so Skald asks first. 1. Type the new, smaller number (or pick days instead of **Kept for good**) and select **Save**. 2. The page shows what will go: how many rows and days. 3. Type the site's name to confirm, then select **Yes, keep less**. Or select **Keep it as it is**. Nothing is removed at once. Removal happens at the next daily run. A change to **Returning-visitor matching** also changes who counts as a returning visitor from then on. > If your server sells plans, your plan may cap the detail you can keep. The page then says "Capped by your plan". ## Mail Mail sets the mails Skald sends. ### Reports - **Daily mail**: sent after each morning's run. - **Weekly mail**: sent on Mondays. - **Recipients**: up to 20 email addresses. - **From name**: the name the mail comes from. Each new recipient first gets one mail with a confirm link. They get nothing else until they open the link and select the button on that page. The link works once, for 7 days. Until at least one address is confirmed, the reports are held back, not failed. The mails carry figures only, never an address and never a journey. A mail that would include a withheld or partial day is held back. ### Mail server (SMTP) For install admins. The sender address, server, port, security, user and password apply to every site on the server. The password is write-only: the page only says whether it is set. ### Google PageSpeed A free API key raises Google's daily limit for the tests on Site > Speed. It is write-only too. ### Preview the mail Owners, admins and install admins can see the daily or the weekly mail exactly as this site would send it now: select **Preview the daily mail** or **Preview the weekly mail**. The mail is built from the same checked days, by the same code, and shown on the page with its subject. Nothing is sent, and no mail server is needed. When the mail would not go now (a day held back, a week not whole), the preview says why. ### Send a test mail Owners, admins and install admins can send a short test mail through the settings above. 1. Type the address in **Send to**. It must be your own address or a confirmed recipient. 2. Select **Send test mail**. At most 3 test mails in 10 minutes. ### Alerts and failure mail Alerts always show on Site > Alerts and on the bell in the top bar. They go to confirmed recipients only. Admins and owners switch alerts on or off for this site under **Which alerts are on** on Site > Alerts: - **All alerts**: the switch for every rule. While it is off, nothing is checked, and the page offers **Turn alerts on**. - **Mail alerts**: off, alerts are only shown here, never mailed. - **Each rule** (site down, server errors, a crawl burst, AI started reading, and the rest): on or off by itself. - **Sale** and **Refund on a sale** (off until you turn them on): one alert for each sale or refund the Revenue tab counts, with an optional **Sale alert minimum** per currency. See [Revenue](https://skaldanalytics.com/docs/revenue.md#sale-alerts). A change applies from the next live run, within 2 minutes. How much makes an alert (the thresholds) is set in the server's config. A switch the server's config locks shows its reason and cannot be changed here. Viewers see the switches but cannot change them. If the morning run fails, the server can send one short failure mail. It goes to the same people as the daily mail: the confirmed recipients on this page, or the server's own list when none is set here or none has confirmed yet. It uses the mail server set on this page or on the server. A day held back because it did not add up is not a failure, so it sends no failure mail. ## Sites Sites says what counts as this site, and where its log is. - **Host names**: the names this site answers to, such as `www.example.com` and `example.com`. 1 to 50. - **Views**: page groups, one per line: a name, the path it starts with, and any paths to leave out. Up to 20. They show in the **Page group** list in the top bar, which reads **All pages** until you pick one. See "Views (page groups)" below. - **Time zone**: the site's own zone. Days are counted midnight to midnight in it. - **Log file**: the log folder and file name. For install admins. - **Goals**: the page events that count as this site's goals, such as Sign-up sent or Demo booked. Tick up to 10. The events your site has already counted are listed first; the others are under **Not counted yet**. See [Funnels and goals](https://skaldanalytics.com/docs/funnels-and-goals.md). - **Not pages**: machine addresses that are never a person reading a page, such as feeds and WordPress's API. See "Not pages" below. Changes apply from the next run. Days already stored are not counted again, except a new page group's (below). ### Not pages Some addresses answer like a page but nobody reads them: a feed for feed readers, WordPress's API, a Markdown copy of a page that an AI agent fetches. Skald does not count a request for one as a page view, on every site. The request still counts as a request, in its usual group (people, bots and the rest), so the groups still add up to the log. It is left out of page views, visitors who read a page, landing pages, exits and journeys. The list, each one ticked by default: - **markdown**: a Markdown copy of a page (`/docs/index.md`, `.mdx`, `.markdown`). People read the page itself; tools and AI agents fetch the copy. AI fetches of it still show under **What they fetch** on AI & bots, as Markdown. If you publish `.md` copies for AI on purpose and want them in the pages' AI columns, untick it. - **wp_rest**: WordPress's API, `/wp-json/` and `?rest_route=`. - **wp_files**: WordPress's machine files, `xmlrpc.php`, `wp-cron.php` and `admin-ajax.php`. - **feeds**: feeds, `/feed/`, `/comments/feed/`, `/feed/atom/` and `?feed=`. - **oembed**: `/oembed/`, what another site asks for to embed a link. - **trackback**: `/trackback/` and `wp-trackback.php`, notices one blog sends another. - **author_number**: `?author=2`, a lookup that lists a WordPress site's user names. - **reply_to_comment**: `?replytocom=`, WordPress's reply link for browsers without scripts. - **sitemap_page**: an HTML sitemap page, `/sitemap/`, `/site-map/`, `/html-sitemap/` and `/sitemap.html`. Crawlers, link checkers and test browsers walk it; people find pages through the menu and search. Its visits show in Site health > Data quality, not in the pages. **Count one as pages again.** If your site serves a real page at one of these, for example a page called `/feed/`, untick it and press **Save**. **Add your own.** Under **Also not pages**, one per line, up to 20: - `/status`: that path, with or without a slash at the end. - `/preview/*`: every path that starts with `/preview/`. - `?preview`: every address with `preview` in its query, such as `/?preview=1`. Capital letters do not matter. `/` alone is refused: it would take every page out. **What stays a page.** Pages a person opens are never on the list: search results, a cart or checkout, a sign-in page, an app's dashboard, a thank-you page. If one of them is filled by a bot, that is a question of who visited, which Skald answers by itself; add it under **Also not pages** only if you never want it counted. **Where you see it.** Site health > Data quality shows each day's **Not counted as pages**: the requests, how many were people's, and which entry (hover the number). In Requests such a request says "a machine address, not a page" and names the entry. A change counts from the next run. Days already stored keep their figures until the server recounts them (`backfill --days N --force`). ### Views (page groups) A page group is a part of your site you want to see on its own, such as the blog or the Canada pages. Each one gets its own pages, with the same figures as the whole site, counted the same way. Pick it in the **Page group** list in the top bar (it reads **All pages** until you do). Write one group per line: its name, the path it starts with, and any paths to leave out, separated by `|`. Several paths to leave out are separated by spaces. For example: - `Blog | /blog/`: every page under /blog/, and /blog itself. - `Canada | /ca/ | /ca/fr/`: the Canada pages, but not the French ones under /ca/fr/. - `Security pages | /security/`: every page under /security/. - `Rest of the site | | /blog/ /ca/`: every page except the blog and the Canada pages. Paths start with `/` and match the start of the address's path, capital letters counted, never the part after `?`. Up to 20 groups. **What fills automatically.** A new group counts from the next run. Its past days are filled from the server's raw log: under the Views box, **Fill past days for the new groups** is ticked. Press **Save** and Skald counts the new or changed groups again for every whole day the raw log still holds, in the background. The box below shows how far it is: "Filling 9 days… 4 of 9", then "Done: 9 days filled, all audited". Each day is checked by a second, separate count, as every stored day is; a day where the two disagree is held back and not shown. Untick the box and Skald offers the same with a **Fill past days for the new groups** button instead. One fill runs at a time on a server; owners and admins can start one. **What cannot be filled.** The raw log keeps only the last days, 14 on most servers. The box says so: "Days before 13 Oct can't be filled: the raw log is kept 14 days." In the group's pages those earlier days show as **not recorded for this group**, never as 0. A renamed group counts as a new one. A removed group leaves the **Page group** list; nothing needs counting again. ### Logo Each site shows its own logo: on the page where you choose a site, in the menu's site button, in the site switcher and on the browser tab. The card shows it at those three sizes, and what Skald found on the site. Choose where it comes from: - **From the site (automatic)**: Skald asks your site itself for its app icon, then its favicon. It never asks an outside icon service. It asks again once a week. **Check the site again now** asks at once. - **Upload my own**: a PNG, SVG or ICO file, square, at least 64 x 64 pixels, at most 256 KB. An SVG that could run a script or load something from elsewhere is refused; save it again as a plain drawing, or upload a PNG. - **A letter on a colour**: the site's first letter on one of four colours. Skald also shows this while there is no logo. Press **Save**. Owners and admins can change the logo; everyone else sees it. ## Add or remove sites Add or remove sites lists your organisation's sites. If your server offers it, an admin adds a site here with **Add a site, step by step**: the site's address, proof it is yours, how its visits reach Skald, the install, and a test. You can stop at any step and continue later. A site not finished shows **Setup: step 3 of 5, continue**; a finished one that has received nothing for a day shows **No visits yet: test the connection**. See [Connections](https://skaldanalytics.com/docs/connections.md), "Add a site, step by step". A new site gets a page at once, with a short "Getting ready" note until its first figures arrive. Only an owner can remove a site. A removed site stops being processed. Its data is kept until the server admin deletes it. ## Crawler rules The rules for AI crawlers are not in Settings any more. They are on AI & bots > Crawler rules, next to the crawlers they apply to. Old links to Settings > AI crawlers open that tab. Crawler rules lets each AI company's crawlers in, or keeps them out. If your server offers it, each company has three choices: - **Allow**: every crawler of that company may read the site. - **Block training only**: crawlers that collect for training are kept out. AI search and answers to people's questions still reach the site. - **Block**: every crawler of that company is kept out. Each crawler can also be set on its own: **As** the company, **Always allow** or **Always block**. Beside each one is how often it was refused in the last 30 days. Select **Save**. The tab says "Saved. Being applied to the site..." and then "Live on the site" within a few seconds. **Undo changes** puts back what was saved before. A blocked crawler gets a refusal before the page is served. Some crawlers cannot be told apart by the server: they are marked "robots.txt only", and only a line in your robots.txt can ask them to stay away. The tab suggests the lines to use, with a **Copy** button. Under the rules, your site's robots.txt is shown beside each crawler. Skald never writes your robots.txt. These rules apply to every site on the server, so only an install admin may change them. ### Suggested llms.txt AI & bots > llms.txt shows **Suggested llms.txt**: a file you can publish at `/llms.txt` on your site. It follows the format at llmstxt.org: your site's name, a one-line summary, then links to your pages, grouped by section, so an AI assistant knows which pages matter. Skald picks the pages people and AI assistants read most over the last 90 days. Only recorded days count; any day left out is named. It leaves out private pages (sign-in, account, cart, search, admin), pages your robots.txt keeps crawlers from, redirects, and pages that are gone. With your website's page list, each link has the page's title; without it, the link is named by its path. Write the summary line yourself and check the links. Then select **Copy** or **Download** and publish it on your site. Skald never writes it to your website. Your server admin can also print it with `ti.py llms-txt`. ## Security Security sets sign-in rules for everyone in the organisation. Only an owner may change them. - **Require two-step sign-in**: members without it set it up the next time they sign in. Turn it on for your own account first. - **Session length**: how long a sign-in lasts at most, 1 to 90 days. The default is 14. ## Audit log Audit log shows who changed what, and when, newest first: settings, the team, tokens, sign-ins and failed sign-ins. Admins may read it. Viewers cannot. - Pick one kind of change in **Action**, or keep **Every action**. - Select **Download CSV** to keep a copy. No password, token, link or address is ever written to the log. Each person search is written too (`people.search`): what kind of value was searched and a one-way code of it, never the value. So is each conversion marked test or not (`conversions.test`, `conversions.untest`). Install admins see every organisation's rows. ## Billing Billing shows only on servers that sell plans, and only to owners. Other servers say "This server does not sell plans." - **Your plan**: its name, status and trial days left. - A table of use against the plan's limits: sites, requests this month, members and days of detail kept. - **Plans**: select **Choose** and a plan's name to buy it. - **Manage billing**: card, invoices and cancelling, on the payment provider's own page. "Requests this month" counts every request the server logged for your sites, people and machines alike. If your organisation goes over a limit, every page shows a banner. Nothing is deleted. If a payment is overdue, the organisation becomes read-only: you can still read every page, change your own account and pay, but not change settings or the team. ## Related guides - [Getting started](https://skaldanalytics.com/docs/getting-started.md) - [Privacy](https://skaldanalytics.com/docs/privacy.md) - [FAQ](https://skaldanalytics.com/docs/faq.md) --- # Client reports > Skald docs: https://skaldanalytics.com/docs/client-reports/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Branded reports for your clients: a mail with a PDF attached, sent on a schedule you set, with your agency's name, colour and logo. Client reports come with accounts. Owners and admins of the site's organisation set them up in Settings > Reports. ## Make a report 1. Open **Settings > Reports** for the site and select **New report**. 2. Give it a **name**. The client sees it at the top of the mail and the PDF. 3. Add the **recipients**, one address per line (20 at most). 4. Choose **when it goes**: weekly on a weekday, monthly on a day of the month, or every few days from a start date, at a time of day. Times are in the site's time zone (Settings > Sites). 5. Choose the **period** it covers. 6. Tick the **sections** to include, and write any **notes** and a **footer** line. 7. Select **Preview** to see the mail, or **Download PDF** to see the attachment. Nothing is sent. 8. Select **Create the report**. ## Recipients confirm first Every address you add gets one short mail with a confirm link, and nothing else until its owner follows the link. This is the rule for every mail Skald sends, so nobody can be put on a list without agreeing. With no mail server set up yet (Settings › Mail), no link goes out and the page says so: save the report again once mail is set up. The list says how many recipients have confirmed and who is still waiting. A report goes to the confirmed ones only; the send record says how many were left out. ## When it goes and what it covers | Schedule | Goes | | --- | --- | | Weekly | Every chosen weekday, at the time | | Monthly | On the chosen day. Day 29, 30 or 31 in a shorter month: its last day | | Every few days | Every 1 to 92 days from the start date | | Period | Covers | | --- | --- | | Last week | Monday to Sunday of the week before | | Last month | The whole calendar month before | | Month to date | The 1st of this month to yesterday. Sent on the 1st, the whole month before | The list shows, for each report, when it goes next and which days that send will cover. ## What a report shows | Section | Shows | | --- | --- | | Summary | Visitors, pages read, visitors from AI, leads and returning visitors; visitors per day as a bar chart; which days are in the figures | | Where visitors came from | Each source's visitors and share | | Top pages | The 10 most-read pages | | Countries and cities | The top countries and cities by visitors | | AI and bots | Fetches because someone asked an AI, training crawlers, search engines; the busiest crawlers; the AI assistants that sent visitors | | Leads | Leads in the period by kind: the dashboard's Leads tile (forms sent and demos booked, completed; test rows left out), counted a second time. A period that starts before leads were recorded gets no sum, and says so | | Search Console | Google's clicks, impressions, CTR and average position for the period, with the top queries and pages, from the synced Search Console figures | | Notes | Your own words, as you typed them | **Search Console** needs the site connected first (Settings › Integrations › Search); until then the section says it is not connected. Only days Google calls final are counted. The newest days, which Google may still change, are listed as provisional and left out, and so are days not synced yet. Search Console's days run on Pacific Time, and a click is not a visit, so these figures are never added to Skald's own. ## Figures you can stand behind A report only shows figures Skald has already stored and checked: - Only **verified, whole days** are counted. A day whose figures did not reconcile (withheld), a day the logs did not wholly cover (partial) and a day not recorded are listed by date under the summary, drawn as a pale stub in the chart, and **never counted as zero**. - A change against the period before is shown only when both periods are whole. - Before it is sent, a report's sums are counted a second time from the stored days. If they disagree, or no day of the period is verified, the report is not sent and the reason is recorded. - If a breakdown (sources, countries, cities) was not recorded on every day of the period, that section says so instead of showing part of it. ## Your branding The **Agency branding** card on the same page is for every report of every site in your organisation: - **Agency name**: in each report's header, and as the mail's From name. The mail is still sent from the server's own address. - **Brand colour**: the band at the top of each report. Text on it is white or dark, whichever reads better. - **Agency logo**: a PNG or JPEG of at most 256 KB, shown on the right of the header. Untick **Show the agency logo** in a report to leave it out there. The site's own logo (Settings > Sites > Logo) is shown on the left. An SVG logo cannot travel in a mail or a PDF, so the site's letter is shown instead. ## Send now, history and pausing - **Send now** sends the report at once to its confirmed recipients, for the period it would cover today. At most 3 sends by hand in 10 minutes. - **History** lists every send: when, who started it (the schedule or a person), the period, the addresses it went to, and whether it went or why not. - **Paused** keeps a report but stops its schedule. Send now still works. ## When a send fails A scheduled send that fails (the mail server is down, say) is tried once more 30 minutes later. After that it waits for its next scheduled day. If the period's last day is not stored yet when the report is due, it waits for the morning run until 18:00, then goes with that day listed as not recorded. A report needs a mail server (Settings > Mail). Without one, reports can still be made, previewed and downloaded, and the page says why they are not sent. --- # Privacy > Skald docs: https://skaldanalytics.com/docs/privacy/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). What Skald keeps about the people who visit, who can see it, for how long, and how a person is removed when they ask. ## The short version - Skald can keep each visit with its IP address, so you can look a person up and follow them across days. You choose: the full address, a shortened one with its last part removed, or none. A new install keeps the full address. - It can keep the name and email of each lead with its conversions. Also a switch, on by default for a new install. - An install upgraded from before version 2.4.0 keeps neither until you turn them on. - Only owners and admins can look a person up. Every search is written to the audit log. - Only owners and admins see the leads' names and emails on Leads & sales > Leads. Each time the list shows them, the audit log gets one line, "lead list viewed", with the period and the number of rows: never a name or an email. Other members see the same list without them. - Stored history (the totals) never holds an address. - Nothing is hidden: spam, duplicate and test leads, and cancelled, removed and test conversions, are listed, not only counted. - One command on the server removes a person everywhere Skald keeps them. - **Settings > Data retention** gives you wording for your privacy notice, made from what this site keeps. > Your privacy notice must say what you keep before you turn these on. Some places also need the visitor's consent. ## What is kept about a visit **Keep visitors' IP addresses** (**Settings > Data retention > People**) has three choices. Only owners and admins can change it. - **Full address**: each visit is kept with its whole IP address. - **Shortened (last part removed)**: each visit is kept with a shortened address. It no longer points to one device, but visits can still be followed, more loosely. - **Don't keep**: no address is kept with the visits. With the full or the shortened address, each visit is kept with: - its IP address (full or shortened) and browser string - when it started and ended - the pages it read, in order - where it came from: source, referring site, campaign - its country and device A visit here is a visit that read at least one page, the same visit Traffic > Visitors counts. Visits are kept for 395 days by default (**Visits with addresses**). ### How an address is shortened - An IPv4 address loses its last part: `203.0.113.57` is kept as `203.0.113.0`. - An IPv6 address keeps its first 48 bits, and the rest is zeroed: `2001:db8:85a3:8d3::` is kept as `2001:db8:85a3::`. - An IPv4 address written the IPv6 way (`::ffff:203.0.113.57`) is treated as IPv4. The country and city are looked up from the full address before it is shortened. Everything else Skald works out from the raw log is the same in every choice: which visits are bots, which are your own team's, and every total. A shortened address can be several people: everyone on the same network, or neighbours at the same internet provider. So with shortened addresses, visits linked "by address" are a weaker link, and the person panel says so. ### Changing the choice - **Full to shortened**: the addresses already kept are shortened at the next nightly run. - **Shortened to full**: only new visits are kept in full. The visits kept before stay shortened. - **To Don't keep**: no address is kept from then on. The visits already kept with an address are deleted at the next nightly run. These visits are kept apart from the history, in the conversions file on your server. They are counted twice like everything else: each morning Skald counts every address's visits again from the raw log, by separate code. A day that does not match is withheld. ## What is kept about a lead When **Leads' names and emails** is **full**, the name and email of each lead are kept with its conversions. With **anonymous**, neither is kept. Skald reads your website's lead database without changing it. Leads marked spam or duplicate there, and leads marked test, are left out of every figure. They are listed in full: on Leads & sales > Leads, open **More** and turn on **Show left out**. ## Looking a person up Owners and admins see a **Find a person** box at the top of every page; on a narrower screen it is a search icon. The same search is on Leads & sales > Find a person, and **Ctrl K** (or **Cmd K**) also offers **Find a person**. Type one of these: - an IP address - an email - a lead's number or reference - a visitor ID (`skv_...`) The person opens in a panel on the right: - **Linked**: the IP addresses, emails, visitor IDs and leads tied to them, and what this site keeps. - **Visits**: every visit kept with that address, with its pages in order, source, referring site, country and device. A day whose figures did not add up is marked "withheld day". - **Requests in the last 6 hours**: the address's requests from Site > Requests. - **Leads**: their leads, spam and test ones too. - **Leads and the visits that sent a form at their time**: visits that sent a form from 2 minutes before a lead to 30 seconds after it. This is a match by time only. It is likely the same person, never certain: the lead database keeps no address. - **Conversions**: every conversion, cancelled and test ones too, and the rows removed by a sync. - **Pages seen by its visitor IDs**, when the site has visitor IDs on. Every address, email, lead and visitor ID in the panel is a link. Select it to open that one. When the site keeps shortened addresses, type the full address: it finds the visits kept under its shortened form. The panel says the address is shortened, and that those visits may be several people's. Each search is written to the audit log: who searched, when, and what kind of value. Never the value itself. Viewers cannot look a person up. An API token or an AI assistant can, only with the **Reading visitor addresses** scope (read_private). See [Connections](https://skaldanalytics.com/docs/connections.md), [API](https://skaldanalytics.com/docs/api.md) and [MCP](https://skaldanalytics.com/docs/mcp.md). ## Where addresses appear Addresses show only behind sign-in: - The person search, for owners and admins. - Site > Requests: the last 6 hours of the request log. - Today's AI activity, on AI & bots > Assistants when the period is Today (or **Today · Live** is on): today only. - The internal-address list in Internal traffic, so your team can manage it. They never appear in a stored day's totals, a CSV export of history, or a mail. ## How long things are kept | What | Kept | | --- | --- | | Raw access logs, with addresses | 14 days on your server | | Each visit with its address (when kept) | 395 days, or what you set | | Leads, with their names and emails (when kept) | 400 days, or for good: Settings › Data retention › Leads | | Conversions removed by a sync, whole | As long as the leads | | Pages read with a visitor ID that became a lead | As long as the lead | | Pages read with a visitor ID that has no lead | 30 days, or what you set (1 to 400) | | The Requests list, with addresses | Its 6-hour window | | Today's AI activity, with addresses | Today only | | Returning-visitor codes | 30 days | | Anonymous journeys | 7 days | | Per-visit and per-page detail | 400 days | | In-page events from the script tag | 400 days | | Totals and labels | For good | | Backups of the history and the conversions file (the conversions file holds the kept visits, names and emails) | 14 daily and 8 weekly copies | Admins can change most of these. See [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md). ## Turning it on after an upgrade An install that ran a version before 2.4.0 keeps working as before: no address and no email is kept. The **People** card says "kept off by the upgrade to 2.4.0". To turn them on: 1. Change your privacy notice first, so it says what Skald keeps. 2. Open **Settings > Data retention**. 3. Under **People**, choose **Full address** or **Shortened (last part removed)** for **Keep visitors' IP addresses**, and set **Leads' names and emails** to **full**. 4. Select **Save**. It applies from the next run. On a server without the Settings pages, the server admin sets `privacy: keep_ips: full` (or `short`) and `conversions: identity: full` in the config file. With Docker, `SKALD_KEEP_IPS=1` (or `short`) and `SKALD_IDENTITY=full`. The older `true` and `false` still work, as full and off. ## Wording for your privacy notice **Settings > Data retention** has a card, **Wording for your privacy notice**. It is a short text you can paste into your privacy page, with a **Copy** button. It is made from this site's own settings, so it says exactly what is kept: - that Skald reads your server's log to count visits, and that this sets no cookie and stores nothing on the visitor's device; - that the raw log, the 6-hour Requests list and today's AI activity hold full addresses for that short time, in every choice; - whether IP addresses are kept in full, shortened or not at all, and for how long; - whether leads' names and emails are kept; - if the site uses the script tag: whether it keeps a visitor ID and where, that it keeps the visit's source in the tab's session storage, that Do Not Track and Global Privacy Control are respected, and that it can wait for consent; - how a person can ask for their data to be deleted. Skald does not know some things, so they are left in [square brackets] for you to fill in or delete: your contact for deletion requests, how long your web server keeps its logs, and the script tag lines that depend on how you added the tag. Save a change on the page and the wording is made again. > A starting point, not legal advice. Your notice must match your own situation. ## The script tag The optional `skald.js` tag counts what happens inside a page. As Skald gives it, it loads from your Skald server and posts its events there. If your web server passes `/traffic-js/` and `/traffic-beacon` on to Skald, it loads from your own site and posts there instead. No third-party code. See [Tracking script](https://skaldanalytics.com/docs/tracking-script.md). - **The visit's source, for the tab only.** With visitor IDs on or off, the tag keeps the site a visit came from and its `utm_source` tag in the tab's session storage, which the browser deletes when the tab is closed. See [Tracking script](https://skaldanalytics.com/docs/tracking-script.md), "What the tag keeps in the browser". - **No cookie by default.** A cookie is set only if your site turns on visitor IDs and also asks to share them across subdomains (on the tag, or an admin switches it on in Skald: Settings > Sites > Script tag). When it is switched on in Skald, the privacy notice wording says so plainly. - **Do Not Track and Global Privacy Control are respected.** A browser that sends either is not counted at all: no ID, no storage, no event. - **Consent first, if you need it.** The tag can wait and send nothing until your consent banner says yes. When a visitor says no, the ID, the cookie and the stored source are deleted. - **Never a field's value.** Form events say which form was used, never what was typed. A click on a phone or email link never keeps the number or the address. Visitor IDs are off by default, and set per site. With them on, the browser keeps a random ID. Turn them on for a site only when that site's privacy notice says so, and let its consent banner gate the tag where consent is needed. ## Webhooks and conversions When a booking, form or payment tool sends Skald a webhook, Skald keeps only the fields it needs. Everything else is dropped before anything is written. Always dropped: - addresses and browser strings - phone numbers - answers to form questions and any free text - cancel and reschedule links Kept: the action (for example, a demo or a sign-up), its stage, the time, the tool, the tool's own reference, and the source, campaign, landing page, country and device. The name and email too, when **Leads' names and emails** is **full**. Skald's outgoing webhooks never send a name or an email. See [Webhooks](https://skaldanalytics.com/docs/webhooks.md) and [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). ## Removing a person on request If someone asks you to delete their data, the server admin removes it. It cannot be done from the dashboard. Use the search first to see what is kept about them. 1. Ask the person for their IP address or email (or a lead's reference). 2. Send it to the person who runs your Skald server. 3. The server admin runs `ti.py forget`, first with `--dry-run` to see what it would remove. `ti.py forget --ip` with the address: - Deletes every visit kept with that address. On a site that keeps shortened addresses, it also deletes the visits kept under the shortened form, since they cannot be told apart. Those may include other people's visits. - Replaces it with "(removed)" in the Requests list and today's AI activity. - Deletes its returning-visitor codes. - Keeps it hidden for 15 days, longer than the raw logs last. Visits in that time are kept without the address. - Writes a line to a private log with the counts, never the address. `ti.py forget --email` (or `--lead`, `--vid`) deletes that person's conversions, the rows removed by a sync, their name and email, and the page views of their visitor IDs. The raw access logs rotate away after 14 days. The server admin can also scrub the address from them sooner. The lead itself, in your website's own database, is yours to delete there. > Backups made before the request still hold what was kept then: the 30-day codes, and the copies of the conversions file, with the visits kept with addresses and the names and emails. They stay on your server, readable only by Skald, and age out on their own (14 daily and 8 weekly copies). ## Related guides - [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md) - [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md) - [Internal traffic](https://skaldanalytics.com/docs/internal-traffic.md) - [FAQ](https://skaldanalytics.com/docs/faq.md) --- # FAQ > Skald docs: https://skaldanalytics.com/docs/faq/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Short answers to the questions people ask most about Skald's figures, privacy and data. ## Why do my numbers differ from Google Analytics? They count different things, in different ways. - Google Analytics counts only browsers that run its script and accept it. Ad blockers, privacy browsers and refused consent banners hide many visits from it. Skald reads the server's own log, so it sees those visits. - Google Analytics cannot see AI assistants, search engines or bots. Skald counts them, but in their own groups, never as people. - Skald is careful about who is a person: when in doubt, a request is not a person. Its people figure is an upper bound. - On sites that change pages without loading a new one, the log sees fewer page views than a script does. Skald says so where it matters. - Days may be cut at different times if the two tools use different time zones. ## Why does a day say withheld? Every figure is counted twice, by two separate roads. If the two counts do not agree, the day is withheld rather than shown wrong. It is never shown as zero. The server admin can see why on Site > Data quality. ## Why is a figure blank and not zero? Blank means not known. Zero means Skald counted and found nothing. A figure that was not recorded, for example a breakdown from before Skald began collecting it, stays blank. What was not recorded is never guessed. ## Why do my countries say Not recorded? Skald takes a visit's country from your server's log. Some logs carry it (Cloudflare's country, or nginx's own `cc=` field). Others do not, for example Caddy's log or nginx's stock log. For those, whoever runs your Skald server can set a GeoIP country file (`logs.country_db`, or `SKALD_COUNTRY_DB` in Docker; DB-IP's free file works). Skald then looks up each visit's address in it. Without a file, the country stays Not recorded. It is never guessed. See [Docker: first install](https://skaldanalytics.com/docs/docker-first-install.md). ## Is AI traffic counted as people? No. AI crawlers and fetches are machines, and show under AI & bots (Assistants and Crawlers) in their own units. A person who clicks a link in an AI answer is a person, and the AI assistant is shown as their source on Traffic > Sources, and in full on AI & bots > Assistants. ## What does "likely from ChatGPT" mean? It is an estimate, not a count. A person who clicks a link in an AI answer often arrives with no referrer, so the visit shows as Direct. But when they asked, the assistant fetched your page from your server. When a direct visit with no campaign tag lands on a page an assistant fetched in the same minute or up to 15 minutes before, Skald says the visit was *likely* sent by that assistant. Not when chance explains it: if an assistant fetched a page so often that day that the page's ordinary direct visits would land after its fetches anyway, those fetches say nothing, and no visit to that page is credited to it that day. Your home page is the usual case: people type it in all day, and assistants fetch it over and over. It shows these on AI & bots > Assistants, on Leads & sales > Sources (with either Count from) and in a visitor's details, always marked "likely" with an (i) that says how. They are never added to the AI assistant source or to any other figure. See [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). ## What did the AI read before it sent this visitor? Open a visit on Traffic > Visitors. If the visit followed a link in an AI answer (ChatGPT, Perplexity, Claude, Gemini, Copilot, Mistral, Meta AI), or is likely from one, **What the AI read before this visit** lists the pages that assistant's user fetcher fetched in the 15 minutes before the visit: "Fetched by ChatGPT in the 15 minutes before", nearest first, at most 10 pages, with the page the visit landed on marked. It is context, not a count of people: a fetch can be for someone else. Training crawlers (GPTBot, ClaudeBot, ...) are never listed: nobody was waiting on them. On AI & bots > Assistants, **What the AI read before it sent a visitor** shows, per assistant, the visits from its answers, how many came after it fetched pages, and how many landed on a page it had just fetched (fetch, then a visit to the same page). Likely visits are shown apart: they always land on a page the assistant fetched, because that is how they are inferred. Assistants with no known user fetcher (You.com, Phind, Poe, DeepSeek, Grok) are not in that table. Days before this feature read "not recorded". The window is the same setting as for "likely" (`attribution.ai_window_minutes`). ## What are the AI alerts? Three alerts watch how AI assistants treat your site. They show on Site > Alerts (press **AI** to see only them) and in **AI reading changes** on AI & bots > Assistants: - **AI started reading**: an assistant fetched a page yesterday that it had not fetched in the 28 days before. - **AI stopped reading**: an assistant that fetched a page in at least 3 of the 4 weeks before has not fetched it for 7 days. - **AI sending people**: the visits likely sent by an assistant jumped (3 times its 28-day average, and at least 10) or vanished. With fewer visits Skald says "too few to judge" and does not alert. An assistant means its company: GPTBot, OAI-SearchBot and ChatGPT-User are all OpenAI. Each alert names the assistant, the page, the days and the numbers. They use whole days only, and only figures Skald has already counted twice. If a day in the window is withheld or missing, the alert waits; it never fires on a day it cannot trust. The server admin can change the thresholds or switch each one off (`alerts.ai_*` in the server's config). If alert mail is on, the weekly mail lists the week's changes, and an outgoing webhook can receive each one as `ai.reading_changed` ([Webhooks](https://skaldanalytics.com/docs/webhooks.md)). Four more alerts watch AI citations and crawlers: - **First AI citation**: the first visit ever that followed a link in an assistant's answer (ChatGPT, Claude, Perplexity, ...). The alert gives the time, the page it landed on and that day's visits from the assistant. - **A page's first AI citation**: the first visit ever that landed on a page from an AI answer. More than 5 pages on one day make one alert that lists them. - **Visits from AI answers**: yesterday's visits from AI answers compared with the usual for that weekday. The usual is the median of the same weekday over the 6 weeks before (at least 4 of them counted). The alert fires when the day has at least 2 times the usual and at least 10 visits, or at most half of a usual of at least 10. With fewer visits Skald says "too few to judge" and does not alert. The alert lists each week's figure, so you can check it. - **Crawler gone silent**: an AI or search crawler that came on at least 7 of 28 days has made no request for 7 days. Check robots.txt and any firewall or CDN rule. These count visits (a person who clicked a link) or requests (a crawler), never both in one figure. The first-citation alerts start once Skald has 28 days of history, so a new install does not call every citation a first. The morning run checks each alert's figures again against the stored days, with separate code. The server admin sets these in the server's config too (`alerts.first_citation`, `alerts.cited_weeks`, `alerts.silent_days`, ...). To get any alert in Slack or another tool, add an outgoing webhook with the event `alert.opened` ([Webhooks](https://skaldanalytics.com/docs/webhooks.md)). ## What do AI assistants and crawlers fetch? **What they fetch** on AI & bots > Assistants (fetches for a question) and Crawlers (fetches for training) splits every AI fetch by what its address names: a page, your llms.txt (`/llms.txt` or `/llms-full.txt`), a Markdown file (`.md`), another text file, robots.txt, a sitemap, or another file (images, scripts, feeds, PDFs). A second table gives the same split per assistant or crawler; each day keeps the 20 with the most fetches and sums the rest in "(all others)". The address decides, not the answer: a fetch of an llms.txt you do not have still counts as llms.txt. Days counted before version 2.10.0 have no split, and a range that holds one shows none. ## Can Skald see Google's AI Mode? Only when Google says so. A click from Google whose address carries `udm=50` (Google's AI Mode) is still a search engine visit, but its referring site reads **google.com (AI Mode)** on Traffic > Sources, so you can see it apart. Most clicks from AI Mode and AI Overviews arrive as a plain `google.com`, the same as any search click; Skald never guesses which those were. ## Can Skald write an llms.txt for my site? It suggests one; it never writes it to your website. Open AI & bots > llms.txt and see **Suggested llms.txt**: links to the pages people and AI assistants read most over the last 90 days, grouped by section, in the llmstxt.org format. Write the one-line summary yourself, check the links, then copy or download it and publish it at `/llms.txt`. See [Settings and retention](https://skaldanalytics.com/docs/settings-and-retention.md). ## Why are pages opened inside my Next.js site not counted? A click on a link inside a Next.js site loads no new page from your server: the browser fetches only the next page's data. Next.js fetches the same data for links on screen that nobody clicked (a prefetch). Unless the server's log records the header Next.js sends with a prefetch, Skald cannot tell a click from a prefetch, so it counts neither. Only the first page of each visit shows, and a visit from a tab left open may count as no visit at all. One line in the web server's log format fixes it: see [Troubleshooting connections](https://skaldanalytics.com/docs/troubleshooting-connections.md), "Next.js sites". Overview's checklist shows **Pages opened inside the site are not counted yet** while it is missing. ## Why is my own visit not counted? Your team's addresses are probably listed as internal. Visits from them go to the "Our own" group, kept apart from people, so your own testing does not inflate the figures. See [Internal traffic](https://skaldanalytics.com/docs/internal-traffic.md) and Internal traffic settings. ## Does Skald use cookies? Not to count visits. Skald reads the server's log, which needs nothing in the browser. The optional script tag sets no cookie by default; a cookie is used only if your site turns on anonymous visitor IDs shared across subdomains (on the tag, or in Skald's settings). Signing in to the dashboard uses one cookie, for your own session. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## Does Skald count visitors who decline or ignore the cookie banner? Yes. Skald counts from your web server's own access log, and the server writes a line for every request, whatever the visitor chose on the banner. Skald sets no cookie and stores nothing on the visitor's device to count them. The optional script tag is the one part that follows the banner. It can wait for consent (`data-consent="required"`, then your banner calls `skald('consent', true)`), it does nothing when Do Not Track or Global Privacy Control is on, and it keeps no visitor ID unless you turn IDs on for the site. The log holds IP addresses, which are personal data in places like the EU. You choose whether Skald keeps them in full, shortened (the last part removed) or not at all, and for how long (Data retention). The same page writes the wording for your privacy notice from those settings. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## How fresh is today? Today's figures are recounted about every two minutes. The foot of every page says when they were last updated and when the next update is due. If your server allows it, **Refresh** in the top bar recounts at once. Yesterday becomes a whole, checked day after the morning's run. ## Why does a whole day appear only the next morning? The daily run checks each finished day by both counts before it is stored. Until then, today's figures are a live count, and can change. ## Where are my leads? On Leads & sales > Leads: every lead of the period, one row each, with its name and email for owners and admins (when your site keeps them), what they did, its stage now and where it came from. Summary has how many against the period before, and Sources where they came from; both count from your lead database by default; switch **Count from** to **Tools** for bookings, forms and payments from your tools. Leads and demos booked also show beside the visitors on Overview > Summary, Traffic > Sources, Pages and Places & devices, and Overview > Insights. Spam, duplicate and test leads are listed on Leads with **More > Show left out**. Owners and admins can find one lead, with its email and the visits around it, on Leads & sales > Find a person or with the search at the top, when your site keeps names and emails. See [Leads and conversions](https://skaldanalytics.com/docs/leads-and-conversions.md). ## How do I get data out? - **Export what you see** above any table: its matching rows, in their order, with the columns shown, as a CSV. - **Export** in the top bar: every table on the tab as CSV files in a ZIP, the tab as JSON, a printable report, or the report builder for several tabs at once. - **API**: every figure as JSON, for a script. Tokens are made in **Settings > Team & access > Connections**. See [API](https://skaldanalytics.com/docs/api.md) and [Connections](https://skaldanalytics.com/docs/connections.md). - **MCP**: let an AI assistant read your figures. See [MCP](https://skaldanalytics.com/docs/mcp.md). ## Who can see visitor addresses? Only people signed in to your organisation: Site > Requests (the last 6 hours), today's AI activity (on AI & bots > Assistants, when the period is Today), and the internal-address list (admins). When your site keeps IP addresses, owners and admins also see each kept visit's address in the person search; every search goes to the audit log. Stored history never holds an address. API tokens see addresses only with **Reading visitor addresses**. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## How do I delete someone's data on request? Send the address or the email address to the person who runs your Skald server. They run Skald's forget command, which deletes the visits, conversions, name and email kept for it. See [Privacy](https://skaldanalytics.com/docs/privacy.md). ## Why are my filters greyed out? The log keeps each breakdown on its own, so the counted tabs apply one value per breakdown and cannot cross them (for example, Canada AND Chrome). Rules over single visits (several values, OR, groups, pages read, time on site, first came from) apply in full on Traffic > Visitors and Leads & sales > Funnels, and a crawler rule on Site > Requests. A greyed-out chip is one the tab cannot apply; a line at the top of the tab says which, and links to where it does apply. ## Can I see times in my own time zone? Yes. Times show in the site's own zone unless you choose otherwise. Select the round **You** button at the foot of the sidebar and choose **Time zone**: the site's, your own, or UTC. Days are still counted midnight to midnight in the site's own zone. ## How do I invite a colleague? Admins and owners go to **Settings > Team & access > Team** and use **Invite someone**. See [Sign-in and team](https://skaldanalytics.com/docs/sign-in-and-team.md). ## My site is behind Cloudflare: will cached pages be missed? Only if Skald reads nothing but your server's log. A page Cloudflare serves from its cache never reaches your server, so your server's log does not see that visit. Two fixes, set up by whoever runs your Skald server: Cloudflare's own request log (Logpush), if your Cloudflare plan offers it, or the small Skald Worker on any plan. The Worker sends Skald one line per request after the page has gone out, so it never slows the page. When Skald reads both the Worker and your server's log, it takes only the cached pages from the Worker, so no visit is counted twice. ## What does "A newer Skald is available" mean? A newer release of Skald is out than the one your server runs. Only owners and admins see this line at the top of the dashboard, and the daily and weekly mail show it too. **What's new** opens the release notes. **×** hides the line in your browser until the next release. A red line is a security release: ask whoever runs your Skald server to upgrade soon. Nothing changes until they do (or until they switch on automatic upgrades). To check, your server asks GitHub once a day which release is the latest; it sends only Skald's version, never your figures or visitors. Your server admin can turn this off (`updates.check: false`). See [Upgrading](https://github.com/spcaeo/skald/blob/main/docs/UPGRADING.md). ## How is Skald upgraded? Whoever runs your Skald server upgrades it with one command. It checks the download, copies every database first, refuses to jump to a new major version or to run during the morning run, and puts the old version back if anything fails. Your figures and settings stay. Automatic upgrades for fixes (or fixes and features) can be switched on; they are off unless your admin turns them on. With Docker, the admin pulls the new image. See [Upgrading](https://github.com/spcaeo/skald/blob/main/docs/UPGRADING.md). --- # Search: queries, impressions and position from Google and Bing > Skald docs: https://skaldanalytics.com/docs/search/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). Your server log cannot see a search. It never knows what someone typed, how often your page was shown, or where it ranked. Only the search engine knows. Skald syncs these figures from Google Search Console and Bing Webmaster Tools, every 6 hours, and shows them on **Traffic › Search**, beside the visits Skald counted itself. This guide covers connecting them, what the tab shows, and why Google's clicks and Skald's visits never quite match. ## Where to find it - Traffic › Search: the queries, pages, countries, devices and search appearance, a trend chart, and the cross-check with Skald's own visits. - **Settings › This site › Integrations › Search**, or Integrations: connect, test, sync now, and the sync's status. Until a site is connected, the Search tab shows the steps to connect it. ## Connect Search Console Skald reads Search Console with a **service account**: a robot Google account that can only read. You make it once for the whole Skald install. **Once, by an install admin:** 1. In Google Cloud console, make a project (or pick one). 2. In APIs & Services › Library, find **Google Search Console API** and choose **Enable**. 3. In IAM & Admin › Service accounts, choose **Create service account**. Give it a name, such as skald-search. It needs no role. 4. Open it, then Keys › Add key › Create new key › **JSON**. A `.json` file downloads. Keep it safe: it is the robot's password. 5. In Skald, **Settings › Integrations › Search › Upload service account key**, and pick the file. Skald checks it, keeps it on the server only, and from then on shows only the robot's email. **For each site, by a site admin:** 1. **Give the robot access.** Copy the robot's email. In Search Console › Settings › Users and permissions, choose **Add user**, paste the email, set **Restricted** and choose **Add**. Restricted can only read: it cannot change anything. 2. **Find my properties.** Skald lists the Search Console properties the robot can see for this site. Pick one. Only a property that covers this site's host names is accepted. 3. **Test connection.** Skald reads the last 3 days and shows, for example, "OK: 412 rows, clicks 37". Nothing is saved yet. 4. **Connect.** Choose how much history to fetch: none, 3 months, or 16 months (all Search Console keeps). The history comes in the background, with its progress on the page. **A client's site:** your client does step 1 in their own Search Console. **Copy text to send** gives you a short message with the steps and the robot's email, ready to paste into an email. ## Connect Bing 1. In Bing Webmaster Tools, go to Settings › API access and make an **API key**. 2. An install admin types it in **Settings › Integrations › Search › Bing API key**. Skald shows only its last 4 characters after that. 3. For each site: **Find my sites**, pick the site, **Test connection**, **Connect**. ## What the tab shows | Figure | What it is | |---|---| | Clicks | Clicks on your site in the search results. | | Impressions | How often a page of yours was shown in the results. | | CTR | Clicks divided by impressions. | | Position | Your average place in the results, weighted by impressions. 1 is the top. | Pick **7 days**, **28 days**, **3 months** or **16 months**. Sort any table by a column; click a row for its detail. - **Queries**: what people searched for. Google leaves out rare queries to protect searchers, so the queries add up to less than the total. The table shows that gap as **Not given by Google**, never hidden. - **Pages**: your pages in the results. Google counts these per page, so this table has its own total. - **Countries**, **Devices**, **Search appearance**: how and where you were shown, under Google's own names. - **(all others)**: the rows past the top of each day's table, added together, so every table still adds up to its total. **AI Overviews and AI Mode:** Google's API does not give them apart yet. If Google adds them, they appear under **Search appearance** with Google's own name. Skald never guesses them. **Bing** gives fewer figures: clicks and impressions per day, and its top queries and pages, updated weekly. Bing gives no total beside its top tables, so a Bing table is only what Bing returned. ## How fresh it is - Skald syncs **every 6 hours** by itself. **Sync now** asks for a sync at once (admins, at most once in 10 minutes). - Google's figures arrive **2 to 3 days late**. The newest days can still change: they are marked **provisional**, and each sync fetches the last 4 days again until Google says they are final. - Google's days are **Pacific Time**. Skald keeps them as Google gives them. - The status line says **Last synced 13:40 · next ~19:40**, the days stored, and any error. ## When something goes wrong If a sync fails, the Search tab and Integrations say why, in plain words, and what to do. The most common cause: someone removed the robot from Search Console. Add it back (step 1 above); the next sync clears the error. The first failure sends **one mail** to the site's mail recipients. No more mails come until it works again. ## Why Google's clicks and Skald's visits differ The **Cross-check** panel puts Search Console's clicks beside Skald's visits from Google search, by day and by landing page. They never match exactly, and Skald never forces them to: - A click is not a visit. Two clicks by one person can be one visit. A click closed before the page drew is no visit in Skald. - Some browsers and apps send no referrer. Skald counts those visits as Direct, not Google. - Google's days are Pacific Time; yours may not be. One day differs more than a week. - Search Console here is web results only. Ad clicks are in neither side. What matters is the ratio over time. When it moves sharply, the panel says so: that is usually a tracking break, such as a page that stopped loading or a redirect that drops the referrer. ## How Skald checks these figures These figures are Google's and Bing's, not Skald's count, so Skald cannot recount them from the log. Instead, every day it adds up every stored table again by a separate road, checks the tables against the day's total, and asks Google again for one day's total. If they disagree, the tab says so. Skald's own side of the cross-check (visits from search) is counted twice, like every Skald figure. --- # Security: scanners, exposures and attack attempts > Skald docs: https://skaldanalytics.com/docs/security/ . Every guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0). See who probes your server for files and panels it does not have, whether the server ever handed one of them a real file, what else they tried, and which addresses to block. All of it comes from your server log, the same lines every other figure is counted from. ## Where to find it Open **Security** in the menu (on a phone: **Menu**, then **Security**). It has seven tabs, each answering one question. The date range in the top bar applies to every tab but Alerts. Press `g` then `y` to jump here. Security is a view across your traffic, never a group of its own: a scanner's requests stay in **bots**, where they always were, so every other figure is unchanged. ## What counts as a scanner A **probe** is a request for a path no real site serves, or with an attack string in the address. For example: - secret files: `/.env`, `/.git/config`, `/.aws/credentials`, private keys; - cloud and deploy files: Terraform (`.tf`, `.tfstate`), `serverless.yml`, `appspec.yml`, `docker-compose.yml`; - WordPress: `/wp-login.php`, `/xmlrpc.php`, `/wp-config.php`; - database dumps and backups: `backup.sql`, `site.zip`; - admin panels and consoles: `/phpmyadmin/`, `/admin`, `/owa/`, Jenkins, Solr; - PHP scripts: `/phpinfo.php`, `/config/auth.php`, `/storage/logs/laravel.log`; - path tricks and injection in the address: `../`, `/etc/passwd`, `${jndi:`, `