Read API
Silo exposes its event store to other apps over HTTPS, read-only, at
https://event-store.silo.adpharm.digital. There are two ways in, and one key
works for both:
GET /v1/events— raw event rows, keyset-paginated. For syncing events into your own system.POST /v1/query— any SQLSELECTagainst the same data, JSON back. For answering a question (counts by day, top pages, a funnel) without pulling everything first.GET /v1/schematells you what to query.
Every key is scoped to specific sources (sites), and that scope is enforced by the database itself: a query that names a source outside the key’s scope gets zero rows, not an error.
Get a key
Section titled “Get a key”Keys are minted in the Silo console under API keys. A workspace admin picks a name, the sources the key may read, and an optional expiry. The token is shown once — copy it then. Lost it? Revoke and mint a new one; there is no way to see it again.
Send it on every request as a bearer token. Clients that can’t set an
Authorization header may use X-Api-Key instead; both carry the same secret.
-H "Authorization: Bearer silo_sk_…"Ask a question — POST /v1/query
Section titled “Ask a question — POST /v1/query”Send one SELECT or WITH statement (no semicolons, no multiple statements)
and, optionally, a row limit.
curl -s https://event-store.silo.adpharm.digital/v1/query \ -H "Authorization: Bearer silo_sk_…" \ -H "content-type: application/json" \ -d '{ "sql": "select date_trunc('"'"'day'"'"', created_at) as day, count(*) as events from organic_events group by 1 order by 1 desc", "limit": 90 }'{ "data": [{ "day": "2026-09-09T00:00:00.000Z", "events": "1432" }], "total_rows": 1, "returned": 1, "capped": false, "limit": 90, "max_limit": 1000, "scope": { "source_write_keys": ["my-site"] }}limit is a ceiling, not a page: the query is wrapped in LIMIT limit + 1, so
capped: true means there was more, and total_rows is then the true count.
Page in SQL (ORDER BY … OFFSET, or keyset on id) rather than asking for
everything. The default limit is 200 and the maximum is 1000.
The query runs read-only, with a statement timeout, as a database role that can see only your key’s sources. Aggregate in SQL — it is faster and you keep the rows you don’t need on the server.
What you can query — GET /v1/schema
Section titled “What you can query — GET /v1/schema”The schema reference for the query API, as Markdown, behind the same key:
curl -s https://event-store.silo.adpharm.digital/v1/schema \ -H "Authorization: Bearer silo_sk_…"It lists the two views you can read (organic_events for real people,
all_events for everything including bots and internal traffic), every column,
the shape of the payload JSON, and the rules that matter — for example that
unique users are counted by anonymous_id, and that created_at is the event
time. It is generated from the live database definition, so read it from the
endpoint rather than copying it; if you are handing SQL-writing to a model,
give it this document first.
Pull rows — GET /v1/events
Section titled “Pull rows — GET /v1/events”curl -sG https://event-store.silo.adpharm.digital/v1/events \ -H "Authorization: Bearer silo_sk_…" \ -d source_write_key=my-site \ -d event="Purchase" \ -d from=2026-08-01T00:00:00Z \ -d limit=100Filters, all optional: source_write_key (narrows within the key’s scope),
event, type, from / to (ISO 8601 on created_at; to is exclusive),
prop.<path> for a payload property (prop.checkout.step=3), include
(organic by default, all for bots and internal traffic too), order
(desc by default), and limit (1–1000, default 100).
{ "data": [{ "id": 12345, "created_at": "…", "source_write_key": "my-site", "payload": { } }], "page": { "limit": 100, "order": "desc", "include": "organic", "has_more": true, "next_cursor": "ZGVzYzoxMjM0NQ", "max_limit": 1000 }, "scope": { "source_write_keys": ["my-site"] }}Follow next_cursor until has_more is false, keeping order the same
while you page. Consecutive from / to windows tile without double-counting.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
400 | Bad request. The body carries code (invalid_body or query_rejected) and, for a rejected query, the database’s own message — a misspelt column, a statement timeout, a non-SELECT. |
401 | Missing, malformed, revoked, or expired key. All four answer identically. |
403 | The key is valid but has no sources assigned. |
500 | Something on Silo’s side failed. Logged with the key id, never the token. |
503 | Either that surface isn’t configured on the deployment (code says which part), or Silo is briefly at capacity (code: "over_capacity", with a Retry-After header). Back off for the number of seconds it names and retry the same request. |
Good to know
Section titled “Good to know”- Revoking or rescoping a key in the console takes effect on the next request.
- A key never sees raw IP addresses or any table beyond the two views.
- Keep keys server-side. They are read-only, but they are also the only thing standing between the internet and your event data.