Overview
This page is the contract. The auth tiers, the 1,000-row cap, the unadjusted-price rule, the no-fill rule and the FIDC regime break are written here and nowhere else; other pages link to these anchors. The machine-readable twin isPOST /rpc/catalog.
Base URL
api). Views are GET /<view>; functions are POST /rpc/<function>.
Note: /v1/* paths belong to serve/app.py, a local Flask adapter that is not the public API.
Authentication
Send the project’s publishable key (sb_publishable_…) on the apikey header.
Authorization: Bearer. That header is for a user token, which moves you to the authenticated tier. Never use the Secret / service_role key.
Sign in with GitHub at silo-bz-deloslabs.vercel.app/signin.html and send the token as Authorization: Bearer <token> alongside apikey:
- Rows per response never change.
db-max-rowsis server-wide; no tier raises it. - Exceeding an id ceiling is an error, not a trim.
22023(HTTP 400) naming the limit. - The token lasts about an hour, and expiry is silent. No
401: you drop back to the anonymous tier, and a four-idpanelstarts raising22023.
cvm_*, b3_cotahist, cia_*, bacen_*) carry no grant for either role (Accept-Profile: public returns 401). Schema api is the whole public surface; the list for your server is catalog().postgrest. If api is not the default schema, add Accept-Profile: api to GET and Content-Profile: api to POST.
Two shapes of endpoint
Filter syntax
select=a,b,c · col=eq.V · gt / gte / lt / lte / neq · col=in.(A,B) · col=is.null (not.is.null) · order=col.desc (.asc, .nullslast) · limit=100&offset=100 · header Prefer: count=exact adds the total to Content-Range. The same operators work on an RPC result (POST /rpc/fund_nav?select=period,nav&order=period.desc), applied after the function’s own cap.
Function arguments
- Names are the SQL parameter names (
p_ticker,p_from,p_metrics). An unknown name failsPGRST202(functions match by argument set, so a typo looks like a missing function). - Omitting an argument is not the same as sending
null. Defaults apply only when absent."p_from": nullmakesBETWEEN null AND …, which matches nothing. The one exception isp_board, where explicitnullmeans “all boards”. - An unrecognised
panelmetric is ignored, not rejected, so the panel comes back smaller and plausible. Take names fromcatalog().metrics.
Dates, ids, and case
- Dates are ISO
YYYY-MM-DD. Ranges useBETWEEN, both ends inclusive. - Tickers are upper-cased and trimmed server-side. CNPJs are stripped to 14 digits.
- In
panel, an id with exactly 14 digits after stripping is a CNPJ; anything else is a ticker. - Monthly grains key on the first day of the month in
panel.fund_navreturns CVM’s filed period, whose convention is per family (fidcfiles month-end,fipfiles 31-December annually).
Row caps
The refusal says why and how:<function>: refused, this request would return more than 1000 rows., the reason, then To fix: with the remedy for that function. PostgREST also returns them separately as details (why) and hint (how). Example hint: This function has no cursor. Narrow the window with p_from/p_to, or ask explicitly for the newest N rows with p_limit (1..1000).
Note: fidc_cedentes, fidc_sacados and fidc_portfolio trimmed silently before catalog v34 and now refuse like the rest. Their p_limit is an explicit newest-first head: 1..1000 returns exactly the newest N rows; null (or anything above one page) is the whole window, served whole or refused; below 1 is 22023.
Refuse and page with p_after:
Refuse, no cursor: narrow the request:
Fixed caps:
The cursor protocol
A page shorter than 1,000 rows is the last. The key is
'<date>\|<id>\|<metric>\|<asset_class>' for panel, and the bare date for quote_history, fund_nav, index_history and trade_consolidated_history.
fund_navrequires a family before it will page. Its cursor is a bare period, unique only within a family (385 CNPJs file under two in the same month). Withoutp_afteryou get every family at once, each row labelled with itsentity_type.Rangepaging does not work on RPC. Rows 1000–1999 of an/rpc/call re-run the function and return the first page again.p_afteris the RPC cursor;Range/limit/offsetare the view cursor.
Reading Content-Range
On GET views it is the only truthful signal: 0-999/* means you hit the cap and there is more; 0-41/* means you got everything; with Prefer: count=exact you get 0-999/1906 (the true total). RPC functions raise 22023 instead, so 0-999/* means you are on a view. The SDK raises SiloTruncated.
Statement timeouts
3 seconds foranon, 8 seconds for authenticated. Exceeding it fails with SQLSTATE 57014, never a partial result: narrow the window, ids, or metrics. The first call after idle can be slow; retry once before concluding the API is down.
Nulls and gaps
- Absent row vs null. In
panel, a metric with no observation for that(id, date)produces no row. In views and series functions, a column the source did not publish is JSONnull(panel’scloseandvolumecan be too). close_returnacross a gap is null, hence absent. Monthly: only when the previous row is the immediately preceding calendar month. Daily: only when the previous session is within 7 calendar days. A quotation-factor change also nulls it (a fatcot flip rescales the quote ±99.9% with no market move), and so does a split, grouping or bonus between the two prints (DESDOBRAMENTO,GRUPAMENTO,BONIFICACAO; monthly: anywhere between the two month-end prints), which changes the share count, not the value.- Prices are unadjusted. Every quote row carries
adjusted: false; a 2:1 split reports roughly −50% in the rawclose(panel’sclose_returnadjusts the previous close across it).closeis the price as published (for a paper quoted per lot, 1,000 shares);close_unitdivides byquotation_factorso levels compare across papers. Neither is corporate-action adjusted. The one adjusted price isquote_history’sclose_adj(refused, never served raw, where it cannot be stood behind; see Quotes), andquote_historyrefuses a window that starts before an instrument’s first session (the tape starts 2019-01-02). - Not applicable is not missing.
fund_navreturns eleven columns for every family but each files only some. A null outside a family’s list is by construction; inside it, a blank in that filing. Map:catalog().applicability. delinquencyis a value, not a rate (BRL). Ask fornavin the same call and divide yourself.freq=dayis quotes only. No fund fundamental is daily; mix asset classes atfreq=month.panelgrain is(id, asset_class, date, metric). Passp_entity_typeor keepasset_classin your pivot key.- Identifier matches are never invented.
lookupcompanytickerscome from CVM’s published FCA registry. - Exercises and auctions are events, not quotes.
option_exercises(tpmerc 012/013) and theauctionsview (017) carry one print per event. Never compute a return over either.
Regime breaks
Where a source format changed so one column means different things on either side of a date, the boundary is published, not smoothed:catalog().regime_breaks and that dataset’s coverage row (notes). Read it before differencing anything.
Default windows serve only complete periods
With no explicitto, fund metrics end at each family’s latest COMPLETE period: a month CVM has only fractionally published is not served as if it were the industry, and FIP’s early-filed year-end row waits for its year to close. An explicit to serves the window verbatim.
Freshness
Callcoverage before you claim anything is current. Only the first two columns below are freshness:
metric_coverage says which (family, metric) pairs are actually filed and over what span. A pair absent from it is one the family never files, not one whose data is late.
Note: landed_at is our health; complete_through is the source’s. A stale complete_through with a fresh landed_at is Brazil’s filing calendar, not an outage.
Errors and empty results
PostgREST returns a JSON array on success: no envelope, nocount, no kind.
An empty array does not say why it is empty. Confirm the identifier with
lookup and the range with coverage.
What is live
SQL underapi reaches production only when the analytical layer is applied (the nightly ingest, or an operator’s analytics-only run), so a merge and a deploy are not the same moment. The truth is POST /rpc/catalog:
catalog().versionis the contract the database is serving. The SDK warnsSiloCatalogDrifton a mismatch.catalog().postgrestlists the functions and views that exist on that server.
404 PGRST202 has not been applied yet; it was not removed. The reverse also happens, so treat catalog().postgrest as a floor and a 200 as the last word.