JSON API
Any public or keyed HTTPS JSON or CSV URL. A spreadsheet starts from the Google Sheets KPI template (Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy).
What you can track
Whatever the URL returns, as dated numbers:
- One JSON value (extract.value path). Omit extract.date and tlly dates it the fetch day. A missing or empty date path does the same. A date that is present but not a date is an error.
- A JSON array (extract.items) with a date field and a value field, or aggregate count, sum, mean, min or max by day, month (M) or quarter (Q). Omit extract.date on an aggregate to snapshot every matching record as one point on the fetch day (a sum or a count of the whole list). An empty match is 0.
- json_columns: two parallel arrays (Yahoo chart style).
- jsonstat2: one series (SSB, Eurostat).
- CSV: see the Google Sheets playbook. extract.date may be omitted. Unlike a JSON aggregate, a date-less CSV does not sum the rows: the last row wins that day. For numbers already in a spreadsheet, start from the KPI template. Make a copy: https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV: https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads: /templates/google-sheets.csv and /templates/google-sheets.xlsx. The MCP resource is tlly://templates/google-sheets.
Credential
None for a public URL. For a private API, the owner stores a secret bound to that host and the job uses {{secret:NAME}} in the URL, a header or a string body. The value is filled in at fetch time and never shown.
A stored secret may be sent on GET, on POST when the path contains graphql and the body is a query with no mutation, or on POST https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search (HubSpot's read-only CRM search). Any other POST with a secret is refused.
Where to get it
The owner adds the key under Data sources. list_secrets shows names and hosts. If it is missing, call ask_for_key. Never ask anyone to paste a key into the chat.
tlly job
{"series":"price","cron":"0 7 * * *","request":{"url":"https://example.com/quote.json"},"extract":{"format":"json","value":"price"}}
Paging: request.pages is {"param":"page","start":0,"max":10} (increment that query parameter until items is empty) or {"cursor":"paging.next.after","into":"after","max":10} (copy that JSON path into a JSON body field until it is missing). Max 10 pages. A cursor that remains after max pages is an error. An aggregate of items with no request.pages fails when the body still has a next page (paging.next.after, has_more true, links.next, next, pageInfo.hasNextPage) instead of saving a partial total. A response with no next marker is one complete page and is stored. Link headers are not read. {today}, {yesterday}, {tomorrow}, {year}, {month} and {day} work in the URL and in a string body (UTC).
Gotchas
- The URL must be HTTPS on the public internet. Private networks are refused. Redirects to another host do not keep the secret.
- tlly spaces requests to the same host by about 2 seconds, across every workspace.
- HTTP 429 means the source rate-limited the call. Widen the cron. Do not retry in a tight loop.
- Response cap is the job body limit (the error says when it is too large).
- One value per series per day.
- Preview with test_job before create_job.
Alert ideas
Red above a ceiling the user names. Yellow when the series stops moving (the job still returns 200). Say that a quiet series is not the same as a failed job unless they have set a stale alert.
Verification
This playbook describes the extractor and the secret rules in this repo (extract.go, extract_more.go, secrets.go, jobs.go). It does not claim a particular vendor’s URL. Cursor paging and a date-less JSON aggregate are what this server does. A date-less CSV still keeps only the last row for the fetch day.