API reference

API reference

A small, read-only REST API for pulling your numbers into scripts, spreadsheets or dashboards. Everything is JSON over HTTPS, authenticated with a personal API key.

Get a key#

  1. Create it

    Settings, API & MCP, give the key a name, click create. The key starts with pk_ and is shown once. Pulse stores only a hash of it, so a lost key cannot be recovered, only replaced.

  2. Send it as a bearer token
    bash
    curl -H "Authorization: Bearer pk_YOUR_KEY" "https://stats.shedi.app/api/v1/stats?site=all&period=30"
  3. Revoke it any time

    Revoking in Settings stops the key at once. You can have up to 10 keys.

Basics#

Base URLhttps://stats.shedi.app/api/v1
AuthAuthorization: Bearer pk_…
MethodGET only. The API cannot change anything.
Rate limit120 requests a minute per account. Over it you get 429 with “Rate limited: 120 requests a minute”.
MoneyUS dollars, net of tax, as decimal numbers (49 means $49.00), not cents.
Text valuesNames that come from visitors (pages, referrers, campaigns) are cleaned: control characters removed, then cut to 80 characters in <code>/stats</code> and 120 in <code>/revenue</code>.

Common parameters#

ParameterMeaning
siteA site id, a site domain such as example.com, or all. Default all.
periodEither a number of days, 1 to 365 (counting today), or a date range YYYY-MM-DD..YYYY-MM-DD in the site's time zone, at most two years long and with the start before the end. Anything invalid returns 400, as the MCP server does; Pulse never guesses. days is accepted as an alias.

Errors#

StatusBodyWhen
400{"error":"period must be a number of days from 1 to 365, or YYYY-MM-DD..YYYY-MM-DD"}The period isn’t one Pulse understands (a typo, a reversed range, a date that doesn’t exist). Pulse never guesses.
401{"error":"Send Authorization: Bearer <your pk_ key>"}Missing, malformed or revoked key.
404{"error":"site not found"}The site is not on your account. An unknown path under /api/v1 also answers 404, as {"error":"not found"}.
405Empty body, with an Allow headerAny method other than GET on the endpoints above.
429{"error":"Rate limited: 120 requests a minute"}Too many requests, or too many invalid keys from one address (60 a minute). The Retry-After header says how many seconds to wait.
500{"error":"Could not load stats"}Something failed on the server; retry shortly.

GET /api/v1/sites#

The sites on your account. Use the id or domain in other calls.

curl -H "Authorization: Bearer pk_YOUR_KEY" https://stats.shedi.app/api/v1/sites

source is pulse for sites tracked by the Pulse script and umami for sites imported from Umami.

GET /api/v1/stats#

Headline numbers, top lists and a daily series for a period, with the period before for comparison. Parameters: site, period.

curl -H "Authorization: Bearer pk_YOUR_KEY" "https://stats.shedi.app/api/v1/stats?site=example.com&period=7"

The lists in this example are shortened. Real responses return up to 10 entries per list.

FieldMeaning
periodExact start and end (UTC) and the time zone the days follow.
visitorsDistinct visitors. visitorsBefore is the same for the previous period of equal length.
pageviewsPage loads and single-page-app route changes.
visitsSessions (30-minute inactivity gap).
bounceRateVisits with exactly one pageview divided by all visits, from 0 to 1.
salesPaid, non-refunded orders; salesBefore for the previous period.
revenueTheir net-of-tax total in dollars; revenueBefore for the previous period.
conversionSales per visitor, from 0 to 1. rpv is revenue per visitor in dollars.
topSourcesReadable source names by visitors (Threads, Google; an unknown referrer stays its host name). Visits with no referrer are not listed here; they show up as Direct in topChannels.
topChannelsChannels by visitors. name is the readable label (Organic social) and id the stable value: direct, organicSearch, organicSocial, paidSearch, paidSocial, email, referral, llm, organicVideo. The same names and ids appear in /revenue.
topPagesPaths by distinct visitors.
topCountriesTwo-letter country codes by visitors.
devicesdesktop, mobile, tablet.
topEventsCustom event names by number of events (up to 30).
topTagsTagged links by visitors: the kept query string, such as utm_source=threads or ref=app.
dailyOne entry per bucket with visitors and revenue. The bucket is YYYY-MM-DDTHH for period=1, YYYY-MM-DD up to 120 days, and YYYY-MM beyond.

GET /api/v1/revenue#

Revenue broken down by where the paying visitors first came from. Parameters: site, period. See revenue attribution for the rules.

curl -H "Authorization: Bearer pk_YOUR_KEY" "https://stats.shedi.app/api/v1/revenue?site=all&period=2026-10-01..2026-10-15"
FieldMeaning
currencyUSD always. revenueNote says the amounts are net of tax.
revenueTotal net revenue in the period, all orders.
ordersNumber of paid orders.
attributedOrders tracked to the visit that made them.
likelyOrders without tracking that Pulse matched to a visit from the same country just before the sale. They are an estimate, and they appear in the dims rows too: each row's likely says how many of its orders are estimates. estimateNote repeats this in one sentence.
dimsSeven lists, each sorted by revenue: source, channel, campaign, landing (the visitor's landing page), country, device and post (the exact post or URL a visit came from, or Source · utm_content when the link carried one). Each row has x (the value, a readable name for source and channel), revenue, orders, customers (distinct buyers), visitors (people whose first visit in the period had that value), conversion (orders / visitors, 0 to 1) and rpv (revenue per visitor). conversion and rpv are null without visitors. channel rows also carry the stable id (organicSocial).
mixnew, renewal and refunds, each { orders, revenue }, plus mrr: subscription and renewal payments of the last 30 days, or null when the site has no subscriptions. It is cash, not a smoothed MRR: a yearly plan counts in full in the month it is paid.
ltvLifetime value by first-touch source: x (source), customers, revenue (everything those customers ever paid, refunds excluded) and ltv (revenue per customer).
currencyNoteA sentence when some orders were left out because their currency has no USD rate, otherwise null.

A dimension is empty when no traced visit carried it, for example campaign when none of the buyers arrived with a utm_source. Orders with no traced visit are counted in revenue and orders but appear in no list except, where the checkout carried one, source and country.

More ways to read your data#

Common issues#

401 even though I pasted my key
Send the whole key including pk_, as Authorization: Bearer pk_… (not as a query parameter), with no quotes or trailing spaces. A revoked key also returns 401.
404 site not found
Use an id or domain from /api/v1/sites. A domain must match exactly (without www. or https://).
400 period must be…
Send a number of days from 1 to 365, or YYYY-MM-DD..YYYY-MM-DD with real dates, start first, at most two years.
Totals do not match the dashboard
The dashboard follows the site's time zone and your current filter; the API uses the time zone too but never filters.