API
Send a project's events, costs and payments to Sumly, and read its numbers back. The whole of it is one OpenAPI 3.1 file, for Postman, an SDK generator or an agent.
Keys
Each call names one project by its key: Authorization: Bearer <key>, at https://sumly.dev/api.
- Site key. The project's public site key (sl_…), from its tracking line. Events only.
- Secret key. The project's secret key (sk_sumly_…), made under Visitors › App. Server only.
- Read key. The project's read key (rk_sumly_…), made by the owner under Connect an assistant. Reads only.
Money is in the currency's main unit (29.00, not 2900). Batches take up to 1000 items; each is checked on its own and the answer lists the refused ones by index. Up to 600 calls a minute.
Read a project's numbers
curl https://sumly.dev/api/v1/stats/overview \ -H "Authorization: Bearer rk_sumly_…"
SDK
One package, no dependencies: npm i @sumly/sdk. @sumly/sdk/server is a typed client for everything below (Node, Bun, Deno, edge); @sumly/sdk/react-native is the app SDK; @sumly/sdk/web loads the site script in React or Next.js and gives typed track and identify.
import { Sumly } from '@sumly/sdk/server';
const sumly = new Sumly({ key: process.env.SUMLY_KEY });
await sumly.track('u_1842', 'trial_started', { plan: 'pro' });
const { mrr } = await sumly.stats.overview();
The app SDKs are also single files to copy: sumly.ts for React Native and Expo, Sumly.swift and Sumly.kt. The guide.
For AI assistants
ChatGPT, Claude and other MCP clients reach the same numbers, and more, at https://sumly.dev/mcp, signed in with OAuth: no key to copy. See llms.txt and skill.md.
Read
A project's numbers, the same answers the MCP tools give.
/api/v1/stats/overview
Overview
MRR, ARR, paying customers, trials, churn, the last 30 days of money, costs and profit, and the next milestone. In euros.
Key: read key or secret key.
/api/v1/stats/visitors
Visitors
Visitors, page views, bounce and visit length against the period before, by day, and the top sources, pages, places and devices.
Key: read key or secret key.
range(query, 24h | 7d | 30d | 90d) How far back from now.filter(query, object) Only visits matching these, as on the Visitors page:filter[source]=Google,filter[country]=SI.
/api/v1/stats/sources
Revenue by source
Net revenue in the range by the source of each buyer's first visit, with buyers and visitors per source. In euros.
Key: read key or secret key.
range(query, 24h | 7d | 30d | 90d) How far back from now.
Events
Page views, screens and actions from a server or an app without the script.
/api/v1/events
Send events
One event, or {"events": [...]} with up to 1000. page, screen and track count as visits; identify (a user's name and e-mail) and forget need the secret key and are never visits.
The app SDKs send here with the public key: sumly.ts for React Native and Expo, Sumly.swift and Sumly.kt. They send screens, and track app_installed once per install, with props.install_referrer on Android, which ties the install to the web visit behind it. A server sending app_installed for a phone passes the phone's address as ip; it is only hashed for the match, never kept.
Key: site key or secret key.
Fields
type* page | screen | track | identify | forgetuser_id* string Your id for the user; RevenueCat's app user id ties their payments to them.urlstring A page event's full URL.titlestringscreenstring A screen event's name.namestring A track event's name.propsobject A track event's properties.platformweb | ios | android | mac | windows | watch | otherapp_versionstringcountrystring Where the user is. From a server, send it: the request says where the server is.referrerstringutmobjectatdate-time or ms When, up to a week back. Defaults to now.ipstring Only on app_installed from a server: the phone's address, hashed for the match with the web visit and never kept.traitsobject For identify: an empty one clears it, one left out stays.
{
"sdk": "react-native 1.2.0",
"events": [
{
"type": "screen",
"user_id": "u_1842",
"screen": "Paywall",
"platform": "ios",
"country": "SI"
},
{
"type": "track",
"user_id": "u_1842",
"name": "trial_started",
"props": {
"plan": "pro"
}
},
{
"type": "identify",
"user_id": "u_1842",
"traits": {
"name": "Ana Novak",
"email": "[email protected]"
}
}
]
}
/api/v1/active
Who is using the app now
The first way Sumly took app users: each becomes a screen event on "App". /v1/events does the same and more.
Key: site key or secret key.
Fields
id* stringplatformweb | ios | android | mac | windows | watch | othercountrystring
/api/v1/bots
Send crawler hits
Requests your server saw, by user agent: the bots and AI crawlers among them are counted under Visitors › Bots. Others are ignored. One hit or {"hits": [...]}.
Key: site key or secret key.
Fields
user_agent* stringpathstringatstring
Money
Costs and payments Sumly does not read from a provider.
/api/v1/costs
Send costs
One cost, or {"costs": [...]}. Each has your own id: sending it again updates it, never counts it twice. A cost with a creator, campaign or channel counts against that on ROI.
Key: secret key.
Fields
id* string Your own id for it.date* stringname* stringamount* numbercurrencystringcategorystringcreatorstringcampaignstringchannelstring
{
"costs": [
{
"id": "payout-2026-10-tiktok-ana",
"date": "2026-10-31",
"name": "TikTok creator payout",
"amount": 250,
"currency": "EUR",
"creator": "@ana"
}
]
}
/api/v1/costs/{id}
Take back a cost
By the id you sent it with: a cancelled payout.
Key: secret key.
id(path, string)
/api/v1/payments
Send payments
Payments, refunds and cancelled subscriptions from a system Sumly does not read. They land in the same ledger as a provider's: MRR, revenue, customers and attribution count them. A payment with an interval starts or renews a subscription paid until the end of it; one neither renewed nor cancelled lapses three days later. Name the buyer with visitor_id (the sumly_vid cookie) or user_id to trace them to their first visit.
Key: secret key.
Fields
typepayment | refund | cancelid* string Your own id for it.amountnumbercurrencystringfeenumberatstring When; not in the future. Defaults to now.intervalday | week | month | year Left out for a one-off payment.interval_countintegercustomer_idstringemailstring Kept only as a hash.customer_namestringsubscription_idstringproductstringplanstringdescriptionstringcountrystringvisitor_idstring The sumly_vid cookie's value, to trace the payment to its first visit.user_idstring The app user id sent with events.payment_idstring For a refund: the id of the payment it refunds.ends_atstring For a cancel: when access ends.reasonstring
{
"payments": [
{
"id": "inv_1042",
"amount": 29,
"currency": "EUR",
"interval": "month",
"customer_id": "cust_77",
"product": "Pro",
"visitor_id": "3f2c9a1e-7b44-4d0a-9e1f-2a6b8c0d5e11",
"country": "SI"
},
{
"type": "refund",
"id": "ref_88",
"payment_id": "inv_1001",
"amount": 29
}
]
}
/api/v1/payments/{id}
Take back a payment
A payment or refund sent by mistake, by the id you sent it with.
Key: secret key.
id(path, string)
Script
What the site script (s.js) sends. You do not call this yourself.
/api/e
The script's collector
Where s.js sends page views and events, in short field names. Shown for completeness: use the script, or /v1/events from a server. The answer is always an empty 204, so nobody learns whether a key is real. No IP address is kept.
No key: the address is the secret.
Fields
s* string The site key.vstring Visitor id from the sumly_vid cookie.tpageview | event | identify | checkoutustring The page's full URL.rstring The referrer.nstring An event's name.pobject An event's properties.winteger Screen width.c0 | 1 Cookie consent, where it is asked.tsinteger When, in milliseconds.batcharray
Other
/api/v1/ping
Check a key
Says which project a key belongs to and whether it is the secret key.
Key: site key or secret key.
/api/v1/marks/{token}
Mark the charts
A release or deploy as a mark on the charts. Takes GitHub's and Vercel's webhooks as they are, or {"label": "…"} from any script. The address, made under Visitors › Insights › Marks, is the secret; no key is sent.
No key: the address is the secret.
token(path, string)
Fields
labelstringdaystring Defaults to today.
{
"label": "v2.4: new paywall"
}