[{"data":1,"prerenderedAt":3765},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fapi":3,"docs-nav":3543},{"id":4,"title":5,"body":6,"description":3535,"extension":3536,"meta":3537,"navigation":329,"path":3539,"seo":3540,"stem":3541,"__hash__":3542},"docs\u002Fdocs\u002Fapi.md","Tabularia API",{"type":7,"value":8,"toc":3512},"minimark",[9,13,32,37,44,73,80,109,112,173,184,187,191,194,255,262,267,273,281,288,349,361,365,376,679,690,695,698,941,944,947,972,975,1228,1235,1249,1284,1288,1359,1369,1372,1380,1388,1538,1555,1559,1583,1993,2002,2067,2071,2155,2170,2173,2191,2195,2201,2211,2215,2257,2388,2481,2550,2606,2625,2629,2679,2745,2769,2825,2834,2838,2844,2848,2914,2917,2921,2927,3143,3157,3206,3210,3229,3421,3424,3428,3508],[10,11,5],"h1",{"id":12},"tabularia-api",[14,15,16,17,21,22,25,26,31],"p",{},"For whoever connects another system to Tabularia: a scheduler such as Airflow, a\nCI job, a department script. Everything the interface does goes through this\nAPI; the complete, always-current reference is the OpenAPI page the gateway\nserves itself, at ",[18,19,20],"code",{},"https:\u002F\u002F\u003Cyour host>\u002Fapi\u002Fdocs"," (",[18,23,24],{},"http:\u002F\u002Flocalhost:8000\u002Fdocs"," in\ndevelopment). This guide covers the part an integration needs and promises to\nkeep stable. The other integrations are listed in ",[27,28,30],"a",{"href":29},"\u002Fdocs\u002Fintegrations","Integrations",".",[33,34,36],"h2",{"id":35},"signing-in-api-tokens","Signing in: API tokens",[14,38,39,40,43],{},"A script authenticates with a token in the ",[18,41,42],{},"Authorization"," header:",[45,46,51],"pre",{"className":47,"code":48,"language":49,"meta":50,"style":50},"language-bash shiki shiki-themes github-dark-dimmed github-light","curl -H \"Authorization: Bearer tab_…\" https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fauth\u002Fme\n","bash","",[18,52,53],{"__ignoreMap":50},[54,55,58,62,66,70],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"sqRhv","curl",[54,63,65],{"class":64},"swcJU"," -H",[54,67,69],{"class":68},"sXfbr"," \"Authorization: Bearer tab_…\"",[54,71,72],{"class":68}," https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fauth\u002Fme\n",[14,74,75,76,79],{},"Tokens start with ",[18,77,78],{},"tab_",". They come in two kinds:",[81,82,83,99],"ul",{},[84,85,86,90,91,94,95,98],"li",{},[87,88,89],"strong",{},"Personal tokens",", which anyone creates in ",[87,92,93],{},"Settings → API tokens"," for their\nown scripts. They act with that person's permissions. An administrator can turn\nthem off for the whole installation (",[18,96,97],{},"AUTH__PERSONAL_TOKENS=false","): from then\non, personal tokens already given out stop working too, while service account\ntokens keep working. Observer accounts (such as a shared demo guest) have no\npersonal tokens: an account made an observer loses the ones it had, and a\npersonal token never works for an account that is only an observer, however it\nbecame one (for example by losing its administrator role).",[84,100,101,104,105,108],{},[87,102,103],{},"Service account tokens",", created by an administrator in ",[87,106,107],{},"Administration →\nService accounts",". A service account is an integration rather than a person: it has\nno password and cannot sign in through SSO. It gets permissions like anyone else,\nfrom the groups it belongs to and from the folders it is granted. Use one per\nintegration, so that revoking one integration's access does not affect the others.",[14,110,111],{},"Every token has:",[81,113,114,157,167],{},[84,115,116,119,120,124,125,128,129,132,133,136,137,140,141,144,145,148,149,152,153,156],{},[87,117,118],{},"An access level."," ",[121,122,123],"em",{},"Read only"," refuses every request that changes something\n(any method other than GET\u002FHEAD\u002FOPTIONS) with ",[18,126,127],{},"403","; the POSTs that only read,\n",[18,130,131],{},"POST \u002Ftasks\u002Fexport"," (with ",[18,134,135],{},"POST \u002Ftasks\u002Fexports","), ",[18,138,139],{},"POST \u002Ftasks\u002Fpreview",",\n",[18,142,143],{},"POST \u002Freports\u002F{id}\u002Frender",", ",[18,146,147],{},"POST \u002Fdashboards\u002F{id}\u002Fdata"," and\n",[18,150,151],{},"POST \u002Fdashboards\u002F{id}\u002Frender",", are allowed. ",[121,154,155],{},"Read and write"," can do whatever its\naccount can do. A token never adds permissions: it\nis at most as powerful as its account.",[84,158,159,162,163,166],{},[87,160,161],{},"An expiry"," of at most ",[18,164,165],{},"AUTH__API_TOKEN_MAX_DAYS"," days, one year by default.",[84,168,169,172],{},[87,170,171],{},"A secret that is shown once",", at creation. Tabularia keeps only its SHA-256\nfingerprint and its first characters, so you can recognise it in a list.",[14,174,175,176,179,180,183],{},"Tokens are revoked from the same screens: the owner revokes their own, an\nadministrator revokes any token from ",[87,177,178],{},"Administration → Service accounts → All API\ntokens",". A revoked or expired token gets ",[18,181,182],{},"401"," on the next request, and so does\nany token of a deleted account. Deactivating an account or setting a new password\nfor it revokes its tokens along with its sessions; reactivating the account does\nnot bring them back. Every action records in the audit log the name of the token\nthat did it, never the token itself. Tokens travel only in the header; one placed\nin a cookie is ignored.",[14,185,186],{},"A token cannot create other tokens, create users, set passwords, reactivate an\naccount, or make an account an administrator or an observer (by its flag, a group's\nflag or joining such a group). So revoking a leaked token is enough to close the\ndoor: it cannot have left behind another way in. What it did with its permissions\nstays done, as with any account: after revoking a leaked administrator token, look\nin the audit log for the actions carrying its name (a webhook it created, a\nschedule it changed).",[33,188,190],{"id":189},"example-a-token-for-a-scheduler","Example: a token for a scheduler",[14,192,193],{},"An external scheduler (Airflow, a CI job) runs the sample flow «Top 20 Products by\nRevenue» every night. As an administrator:",[195,196,197,214,236],"ol",{},[84,198,199,202,203,144,206,209,210,213],{},[87,200,201],{},"Administration → Service accounts",", name ",[18,204,205],{},"Airflow production",[87,207,208],{},"Create account",".\nThe account gets an address of its own (",[18,211,212],{},"airflow-production@service.invalid",") and\nno password.",[84,215,216,217,220,221,224,225,220,228,231,232,235],{},"Give it what the job needs, as to a person: ",[87,218,219],{},"RUN"," on ",[18,222,223],{},"Sample \u002F Flows",", where the\nflow is, and ",[87,226,227],{},"EDIT",[18,229,230],{},"Sample \u002F Private tables",", where its Output writes (in each\nfolder's ",[87,233,234],{},"Folder settings",", or through a group). Without EDIT there the run starts\nand then fails with «Missing 'edit' permission on folder …».",[84,237,238,239,144,242,119,245,140,247,250,251,254],{},"Open the account, name the token ",[18,240,241],{},"nightly runs",[87,243,244],{},"Access",[121,246,155],{},[87,248,249],{},"Expires"," 90 days, ",[87,252,253],{},"Create token",". Copy the secret now: it is shown once.",[14,256,257],{},[258,259],"img",{"alt":260,"src":261},"The service account Airflow production opened, with the form for a new token: name, access Read and write, expiry 90 days","\u002Fmedia\u002Fguide\u002Fapi\u002Fservice-account-token-form.png",[14,263,264],{},[121,265,266],{},"A token is created on the account it acts as; its access and its expiry are chosen here.",[14,268,269],{},[258,270],{"alt":271,"src":272},"The account with its token nightly runs, read and write, expiring in 90 days, never used yet","\u002Fmedia\u002Fguide\u002Fapi\u002Fservice-account-tokens.png",[14,274,275],{},[121,276,277,278,280],{},"After ",[87,279,253],{}," the list keeps only the first characters of the token, to recognise it, and when it was last used.",[14,282,283,284,287],{},"Then, from the scheduler (the flow's id is in its editor address, ",[18,285,286],{},"\u002Feditor?flow=6","):",[45,289,291],{"className":47,"code":290,"language":49,"meta":50,"style":50},"curl -X POST -H \"Authorization: Bearer tab_…\" \\\n  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fflows\u002F6\u002Frun-now\n# 202 {\"status\": \"started\", \"flow_id\": 6, \"run_id\": 1318}\n\ncurl -H \"Authorization: Bearer tab_…\" https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fruns\u002F1318\n# {\"id\": 1318, \"status\": \"SUCCESS\", \"flow_id\": 6, \"launched_by\": 206, …}\n",[18,292,293,311,317,324,331,343],{"__ignoreMap":50},[54,294,295,297,300,303,305,307],{"class":56,"line":57},[54,296,61],{"class":60},[54,298,299],{"class":64}," -X",[54,301,302],{"class":68}," POST",[54,304,65],{"class":64},[54,306,69],{"class":68},[54,308,310],{"class":309},"sxsTv"," \\\n",[54,312,314],{"class":56,"line":313},2,[54,315,316],{"class":68},"  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fflows\u002F6\u002Frun-now\n",[54,318,320],{"class":56,"line":319},3,[54,321,323],{"class":322},"sgHix","# 202 {\"status\": \"started\", \"flow_id\": 6, \"run_id\": 1318}\n",[54,325,327],{"class":56,"line":326},4,[54,328,330],{"emptyLinePlaceholder":329},true,"\n",[54,332,334,336,338,340],{"class":56,"line":333},5,[54,335,61],{"class":60},[54,337,65],{"class":64},[54,339,69],{"class":68},[54,341,342],{"class":68}," https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fruns\u002F1318\n",[54,344,346],{"class":56,"line":345},6,[54,347,348],{"class":322},"# {\"id\": 1318, \"status\": \"SUCCESS\", \"flow_id\": 6, \"launched_by\": 206, …}\n",[14,350,351,352,355,356,358,359,31],{},"The run appears in ",[87,353,354],{},"Runs"," started by ",[18,357,205],{},", and the audit log names the\ntoken. To cut the job off, revoke the token: the next call gets ",[18,360,182],{},[33,362,364],{"id":363},"stable-endpoints","Stable endpoints",[14,366,367,368,371,372,375],{},"The paths below are a contract: within a major version they keep their meaning\nand their fields. New fields can appear, so ignore the ones you do not know.\nAnything else in ",[18,369,370],{},"\u002Fdocs"," may change between minor versions. Breaking changes are\nannounced in ",[18,373,374],{},"CHANGELOG.md"," one minor release in advance.",[377,378,379,395],"table",{},[380,381,382],"thead",{},[383,384,385,389,392],"tr",{},[386,387,388],"th",{},"What",[386,390,391],{},"Request",[386,393,394],{},"Notes",[396,397,398,412,425,440,468,500,515,540,555,567,583,612,637,649,662],"tbody",{},[383,399,400,404,409],{},[401,402,403],"td",{},"Who am I",[401,405,406],{},[18,407,408],{},"GET \u002Fauth\u002Fme",[401,410,411],{},"the account behind the token",[383,413,414,417,422],{},[401,415,416],{},"Folders",[401,418,419],{},[18,420,421],{},"GET \u002Fprojects",[401,423,424],{},"those you can see",[383,426,427,430,438],{},[401,428,429],{},"Flows",[401,431,432,144,435],{},[18,433,434],{},"GET \u002Fflows",[18,436,437],{},"GET \u002Fflows\u002F{id}",[401,439],{},[383,441,442,445,450],{},[401,443,444],{},"Run a flow",[401,446,447],{},[18,448,449],{},"POST \u002Fflows\u002F{id}\u002Frun-now",[401,451,452,455,456,459,460,463,464,467],{},[18,453,454],{},"202"," with ",[18,457,458],{},"{\"run_id\": …}","; ",[18,461,462],{},"409"," if it is already running. ",[18,465,466],{},"?mode=production"," uses the flow's production engine, as a schedule would",[383,469,470,473,478],{},[401,471,472],{},"A run",[401,474,475],{},[18,476,477],{},"GET \u002Fruns\u002F{id}",[401,479,480,483,484,144,487,144,490,144,493,459,496,499],{},[18,481,482],{},"status",": ",[18,485,486],{},"PENDING",[18,488,489],{},"STARTED",[18,491,492],{},"SUCCESS",[18,494,495],{},"FAILURE",[18,497,498],{},"error"," when it failed",[383,501,502,504,509],{},[401,503,354],{},[401,505,506],{},[18,507,508],{},"GET \u002Fruns?q=…&status=…&limit=…&offset=…",[401,510,511,512],{},"a page: ",[18,513,514],{},"{\"items\": […], \"total\": n}",[383,516,517,520,525],{},[401,518,519],{},"Datasources",[401,521,522],{},[18,523,524],{},"GET \u002Fdatasources",[401,526,527,528,531,532,535,536,539],{},"each with ",[18,529,530],{},"bucket"," and ",[18,533,534],{},"key",", which the export needs; ",[18,537,538],{},"GET \u002Fdatasources\u002F{id}\u002Fruns"," for its refreshes",[383,541,542,545,550],{},[401,543,544],{},"Refresh a datasource",[401,546,547],{},[18,548,549],{},"POST \u002Fdatasources\u002F{id}\u002Frefresh",[401,551,552,554],{},[18,553,454],{}," with the run; database datasources only",[383,556,557,560,564],{},[401,558,559],{},"Export",[401,561,562],{},[18,563,131],{},[401,565,566],{},"CSV (streamed) or Excel; see below",[383,568,569,572,577],{},[401,570,571],{},"Download a report",[401,573,574],{},[18,575,576],{},"GET \u002Freports\u002F{id}\u002Frender?format=pdf&\u003Cparameter>=\u003Cvalue>",[401,578,579,580],{},"the file, waited for; see ",[27,581,571],{"href":582},"#download-a-report",[383,584,585,588,599],{},[401,586,587],{},"Reports",[401,589,590,144,593,144,596],{},[18,591,592],{},"GET \u002Freports",[18,594,595],{},"GET \u002Freports\u002Fsearch?q=…&sort=trending",[18,597,598],{},"GET \u002Freports\u002F{id}\u002Fparameters",[401,600,601,602,144,605,144,608,611],{},"those you can see (all, or a page searched and ordered as the Reports tab: ",[18,603,604],{},"trending",[18,606,607],{},"name",[18,609,610],{},"updated","); a report's parameters with their defaults and choices",[383,613,614,617,631],{},[401,615,616],{},"Dashboards",[401,618,619,144,622,144,625,144,628],{},[18,620,621],{},"GET \u002Fdashboards",[18,623,624],{},"GET \u002Fdashboards\u002Fsearch?q=…&sort=trending",[18,626,627],{},"GET \u002Fdashboards\u002F{id}",[18,629,630],{},"GET \u002Fdashboards\u002F{id}\u002Fparameters",[401,632,633,634],{},"those you can see, and as the Dashboards tab; see ",[27,635,616],{"href":636},"#dashboards",[383,638,639,642,646],{},[401,640,641],{},"A dashboard's numbers",[401,643,644],{},[18,645,147],{},[401,647,648],{},"the numbers of its tiles, ready to draw",[383,650,651,654,659],{},[401,652,653],{},"Download a dashboard",[401,655,656],{},[18,657,658],{},"GET \u002Fdashboards\u002F{id}\u002Frender?format=pdf&\u003Cparameter>=\u003Cvalue>&click.\u003Ctile>=\u003Cvalue>",[401,660,661],{},"the file, waited for",[383,663,664,667,672],{},[401,665,666],{},"Lineage",[401,668,669],{},[18,670,671],{},"GET \u002Flineage",[401,673,674,675,678],{},"the graph of flows, datasources, connections, scripts, SQL scripts, notebooks and flow groups; ",[18,676,677],{},"?type=flow|datasource|connection|script|sql|notebook|group&id=…"," centres it",[14,680,681,682,685,686,689],{},"All paths are relative to the gateway: ",[18,683,684],{},"https:\u002F\u002F\u003Chost>\u002Fapi"," behind the reference\ningress or compose file, ",[18,687,688],{},"http:\u002F\u002Flocalhost:8000"," in development.",[691,692,694],"h3",{"id":693},"recipes","Recipes",[14,696,697],{},"Run a flow and wait for the result:",[45,699,703],{"className":700,"code":701,"language":702,"meta":50,"style":50},"language-python shiki shiki-themes github-dark-dimmed github-light","import time, requests\n\nAPI = \"https:\u002F\u002Ftabularia.example.com\u002Fapi\"\nH = {\"Authorization\": \"Bearer tab_…\"}\n\nrun_id = requests.post(f\"{API}\u002Fflows\u002F42\u002Frun-now\", headers=H).json()[\"run_id\"]\nwhile True:\n    run = requests.get(f\"{API}\u002Fruns\u002F{run_id}\", headers=H).json()\n    if run[\"status\"] in (\"SUCCESS\", \"FAILURE\"):\n        break\n    time.sleep(5)\nif run[\"status\"] == \"FAILURE\":\n    raise SystemExit(run[\"error\"])\n","python",[18,704,705,715,719,730,752,756,800,812,854,885,891,903,923],{"__ignoreMap":50},[54,706,707,711],{"class":56,"line":57},[54,708,710],{"class":709},"s6PUj","import",[54,712,714],{"class":713},"ssh_m"," time, requests\n",[54,716,717],{"class":56,"line":313},[54,718,330],{"emptyLinePlaceholder":329},[54,720,721,724,727],{"class":56,"line":319},[54,722,723],{"class":64},"API",[54,725,726],{"class":709}," =",[54,728,729],{"class":68}," \"https:\u002F\u002Ftabularia.example.com\u002Fapi\"\n",[54,731,732,735,738,741,744,746,749],{"class":56,"line":326},[54,733,734],{"class":713},"H ",[54,736,737],{"class":709},"=",[54,739,740],{"class":713}," {",[54,742,743],{"class":68},"\"Authorization\"",[54,745,483],{"class":713},[54,747,748],{"class":68},"\"Bearer tab_…\"",[54,750,751],{"class":713},"}\n",[54,753,754],{"class":56,"line":333},[54,755,330],{"emptyLinePlaceholder":329},[54,757,758,761,763,766,769,772,775,777,780,783,785,789,791,794,797],{"class":56,"line":345},[54,759,760],{"class":713},"run_id ",[54,762,737],{"class":709},[54,764,765],{"class":713}," requests.post(",[54,767,768],{"class":709},"f",[54,770,771],{"class":68},"\"",[54,773,774],{"class":309},"{",[54,776,723],{"class":64},[54,778,779],{"class":309},"}",[54,781,782],{"class":68},"\u002Fflows\u002F42\u002Frun-now\"",[54,784,144],{"class":713},[54,786,788],{"class":787},"sNjOc","headers",[54,790,737],{"class":709},[54,792,793],{"class":713},"H).json()[",[54,795,796],{"class":68},"\"run_id\"",[54,798,799],{"class":713},"]\n",[54,801,803,806,809],{"class":56,"line":802},7,[54,804,805],{"class":709},"while",[54,807,808],{"class":64}," True",[54,810,811],{"class":713},":\n",[54,813,815,818,820,823,825,827,829,831,833,836,838,841,843,845,847,849,851],{"class":56,"line":814},8,[54,816,817],{"class":713},"    run ",[54,819,737],{"class":709},[54,821,822],{"class":713}," requests.get(",[54,824,768],{"class":709},[54,826,771],{"class":68},[54,828,774],{"class":309},[54,830,723],{"class":64},[54,832,779],{"class":309},[54,834,835],{"class":68},"\u002Fruns\u002F",[54,837,774],{"class":309},[54,839,840],{"class":713},"run_id",[54,842,779],{"class":309},[54,844,771],{"class":68},[54,846,144],{"class":713},[54,848,788],{"class":787},[54,850,737],{"class":709},[54,852,853],{"class":713},"H).json()\n",[54,855,857,860,863,866,869,872,874,877,879,882],{"class":56,"line":856},9,[54,858,859],{"class":709},"    if",[54,861,862],{"class":713}," run[",[54,864,865],{"class":68},"\"status\"",[54,867,868],{"class":713},"] ",[54,870,871],{"class":709},"in",[54,873,21],{"class":713},[54,875,876],{"class":68},"\"SUCCESS\"",[54,878,144],{"class":713},[54,880,881],{"class":68},"\"FAILURE\"",[54,883,884],{"class":713},"):\n",[54,886,888],{"class":56,"line":887},10,[54,889,890],{"class":709},"        break\n",[54,892,894,897,900],{"class":56,"line":893},11,[54,895,896],{"class":713},"    time.sleep(",[54,898,899],{"class":64},"5",[54,901,902],{"class":713},")\n",[54,904,906,909,911,913,915,918,921],{"class":56,"line":905},12,[54,907,908],{"class":709},"if",[54,910,862],{"class":713},[54,912,865],{"class":68},[54,914,868],{"class":713},[54,916,917],{"class":709},"==",[54,919,920],{"class":68}," \"FAILURE\"",[54,922,811],{"class":713},[54,924,926,929,932,935,938],{"class":56,"line":925},13,[54,927,928],{"class":709},"    raise",[54,930,931],{"class":64}," SystemExit",[54,933,934],{"class":713},"(run[",[54,936,937],{"class":68},"\"error\"",[54,939,940],{"class":713},"])\n",[14,942,943],{},"Polling is fine for a script. For anything long-lived, have Tabularia call you\ninstead (webhooks, below).",[14,945,946],{},"Refresh a database datasource:",[45,948,950],{"className":47,"code":949,"language":49,"meta":50,"style":50},"curl -X POST -H \"Authorization: Bearer tab_…\" https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdatasources\u002F7\u002Frefresh\n# 202 {\"id\": 1234, \"status\": \"PENDING\", …}: then GET \u002Fruns\u002F1234 as above\n",[18,951,952,967],{"__ignoreMap":50},[54,953,954,956,958,960,962,964],{"class":56,"line":57},[54,955,61],{"class":60},[54,957,299],{"class":64},[54,959,302],{"class":68},[54,961,65],{"class":64},[54,963,69],{"class":68},[54,965,966],{"class":68}," https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdatasources\u002F7\u002Frefresh\n",[54,968,969],{"class":56,"line":313},[54,970,971],{"class":322},"# 202 {\"id\": 1234, \"status\": \"PENDING\", …}: then GET \u002Fruns\u002F1234 as above\n",[14,973,974],{},"Download a datasource as CSV:",[45,976,978],{"className":700,"code":977,"language":702,"meta":50,"style":50},"ds = next(d for d in requests.get(f\"{API}\u002Fdatasources\", headers=H).json() if d[\"id\"] == 7)\nbody = {\"bucket\": ds[\"bucket\"], \"input_key\": ds[\"key\"], \"operations\": [], \"format\": \"csv\", \"filename\": f\"{ds['name']}.csv\"}\nwith requests.post(f\"{API}\u002Ftasks\u002Fexport\", headers=H, json=body, stream=True) as r:\n    r.raise_for_status()\n    with open(body[\"filename\"], \"wb\") as f:\n        for chunk in r.iter_content(1 \u003C\u003C 20):\n            f.write(chunk)\n",[18,979,980,1042,1115,1169,1174,1199,1223],{"__ignoreMap":50},[54,981,982,985,987,990,993,996,999,1001,1003,1005,1007,1009,1011,1013,1016,1018,1020,1022,1025,1027,1030,1033,1035,1037,1040],{"class":56,"line":57},[54,983,984],{"class":713},"ds ",[54,986,737],{"class":709},[54,988,989],{"class":64}," next",[54,991,992],{"class":713},"(d ",[54,994,995],{"class":709},"for",[54,997,998],{"class":713}," d ",[54,1000,871],{"class":709},[54,1002,822],{"class":713},[54,1004,768],{"class":709},[54,1006,771],{"class":68},[54,1008,774],{"class":309},[54,1010,723],{"class":64},[54,1012,779],{"class":309},[54,1014,1015],{"class":68},"\u002Fdatasources\"",[54,1017,144],{"class":713},[54,1019,788],{"class":787},[54,1021,737],{"class":709},[54,1023,1024],{"class":713},"H).json() ",[54,1026,908],{"class":709},[54,1028,1029],{"class":713}," d[",[54,1031,1032],{"class":68},"\"id\"",[54,1034,868],{"class":713},[54,1036,917],{"class":709},[54,1038,1039],{"class":64}," 7",[54,1041,902],{"class":713},[54,1043,1044,1047,1049,1051,1054,1057,1059,1062,1065,1067,1070,1072,1075,1078,1081,1083,1086,1088,1091,1093,1095,1097,1099,1102,1105,1108,1110,1113],{"class":56,"line":313},[54,1045,1046],{"class":713},"body ",[54,1048,737],{"class":709},[54,1050,740],{"class":713},[54,1052,1053],{"class":68},"\"bucket\"",[54,1055,1056],{"class":713},": ds[",[54,1058,1053],{"class":68},[54,1060,1061],{"class":713},"], ",[54,1063,1064],{"class":68},"\"input_key\"",[54,1066,1056],{"class":713},[54,1068,1069],{"class":68},"\"key\"",[54,1071,1061],{"class":713},[54,1073,1074],{"class":68},"\"operations\"",[54,1076,1077],{"class":713},": [], ",[54,1079,1080],{"class":68},"\"format\"",[54,1082,483],{"class":713},[54,1084,1085],{"class":68},"\"csv\"",[54,1087,144],{"class":713},[54,1089,1090],{"class":68},"\"filename\"",[54,1092,483],{"class":713},[54,1094,768],{"class":709},[54,1096,771],{"class":68},[54,1098,774],{"class":309},[54,1100,1101],{"class":713},"ds[",[54,1103,1104],{"class":68},"'name'",[54,1106,1107],{"class":713},"]",[54,1109,779],{"class":309},[54,1111,1112],{"class":68},".csv\"",[54,1114,751],{"class":713},[54,1116,1117,1120,1122,1124,1126,1128,1130,1132,1135,1137,1139,1141,1144,1147,1149,1152,1155,1157,1160,1163,1166],{"class":56,"line":319},[54,1118,1119],{"class":709},"with",[54,1121,765],{"class":713},[54,1123,768],{"class":709},[54,1125,771],{"class":68},[54,1127,774],{"class":309},[54,1129,723],{"class":64},[54,1131,779],{"class":309},[54,1133,1134],{"class":68},"\u002Ftasks\u002Fexport\"",[54,1136,144],{"class":713},[54,1138,788],{"class":787},[54,1140,737],{"class":709},[54,1142,1143],{"class":713},"H, ",[54,1145,1146],{"class":787},"json",[54,1148,737],{"class":709},[54,1150,1151],{"class":713},"body, ",[54,1153,1154],{"class":787},"stream",[54,1156,737],{"class":709},[54,1158,1159],{"class":64},"True",[54,1161,1162],{"class":713},") ",[54,1164,1165],{"class":709},"as",[54,1167,1168],{"class":713}," r:\n",[54,1170,1171],{"class":56,"line":326},[54,1172,1173],{"class":713},"    r.raise_for_status()\n",[54,1175,1176,1179,1182,1185,1187,1189,1192,1194,1196],{"class":56,"line":333},[54,1177,1178],{"class":709},"    with",[54,1180,1181],{"class":64}," open",[54,1183,1184],{"class":713},"(body[",[54,1186,1090],{"class":68},[54,1188,1061],{"class":713},[54,1190,1191],{"class":68},"\"wb\"",[54,1193,1162],{"class":713},[54,1195,1165],{"class":709},[54,1197,1198],{"class":713}," f:\n",[54,1200,1201,1204,1207,1209,1212,1215,1218,1221],{"class":56,"line":345},[54,1202,1203],{"class":709},"        for",[54,1205,1206],{"class":713}," chunk ",[54,1208,871],{"class":709},[54,1210,1211],{"class":713}," r.iter_content(",[54,1213,1214],{"class":64},"1",[54,1216,1217],{"class":709}," \u003C\u003C",[54,1219,1220],{"class":64}," 20",[54,1222,884],{"class":713},[54,1224,1225],{"class":56,"line":802},[54,1226,1227],{"class":713},"            f.write(chunk)\n",[14,1229,1230,1231,1234],{},"CSV exports stop at ",[18,1232,1233],{},"EXPORT__MAX_ROWS"," (5,000,000 rows by default) and Excel\nexports at 1,048,575 rows; the response says so instead of cutting silently.",[14,1236,1237,1238,1241,1242,1245,1246,1248],{},"A person, or a service account, has at most ",[18,1239,1240],{},"EXPORT__MAX_RUNNING_PER_PERSON"," (3 by\ndefault) exports and documents being prepared at once. A request with an API token past\nit does not fail: it waits for one of them to finish, up to ",[18,1243,1244],{},"REPORTS__API_WAIT_SECONDS","\n(",[18,1247,131],{}," as long as it waits for a worker), and answers 429 only when none\ndid. A script that sends ten reports by email, one after the other or a few at a time,\ngets them all.",[14,1250,1251,1252,1254,1255,1257,1258,1261,1262,1265,1266,144,1269,1272,1273,132,1276,1279,1280,1283],{},"A worker writes the file, and ",[18,1253,131],{}," keeps the request open until it\nhas. For a long export, start it and come back for it instead, as the web app does:\n",[18,1256,135],{}," with the same body answers ",[18,1259,1260],{},"{\"task_id\": …}",";\n",[18,1263,1264],{},"GET \u002Ftasks\u002Fexports\u002F{task_id}"," says ",[18,1267,1268],{},"running",[18,1270,1271],{},"ready"," or ",[18,1274,1275],{},"failed",[18,1277,1278],{},"detail",");\nonce ready, ",[18,1281,1282],{},"GET \u002Ftasks\u002Fexports\u002F{task_id}\u002Ffile"," sends the file, once: it is deleted\nas soon as it has been sent, and one never taken is deleted after an hour. Only the\naccount that started an export can see it.",[691,1285,1287],{"id":1286},"errors","Errors",[377,1289,1290,1300],{},[380,1291,1292],{},[383,1293,1294,1297],{},[386,1295,1296],{},"Code",[386,1298,1299],{},"Meaning",[396,1301,1302,1311,1320,1330,1339,1349],{},[383,1303,1304,1308],{},[401,1305,1306],{},[18,1307,182],{},[401,1309,1310],{},"no token, unknown token, expired or revoked token, deactivated account",[383,1312,1313,1317],{},[401,1314,1315],{},[18,1316,127],{},[401,1318,1319],{},"the account lacks the permission, or the token is read only",[383,1321,1322,1327],{},[401,1323,1324],{},[18,1325,1326],{},"404",[401,1328,1329],{},"not found, or not visible to this account",[383,1331,1332,1336],{},[401,1333,1334],{},[18,1335,462],{},[401,1337,1338],{},"conflict: the flow is already running, the datasource is already refreshing",[383,1340,1341,1346],{},[401,1342,1343],{},[18,1344,1345],{},"422",[401,1347,1348],{},"the request is malformed: the body says which field",[383,1350,1351,1356],{},[401,1352,1353],{},[18,1354,1355],{},"503",[401,1357,1358],{},"the engine or the database is not answering: try again in a moment",[14,1360,1361,1362,1364,1365,1368],{},"Error bodies are JSON with a ",[18,1363,1278],{}," field. Its language follows the\n",[18,1366,1367],{},"Accept-Language"," header where a translation exists.",[33,1370,571],{"id":1371},"download-a-report",[14,1373,1374,1375,1379],{},"A ",[27,1376,1378],{"href":1377},"\u002Fdocs\u002Fcapabilities\u002Freports","pixel-perfect report"," (a page designed in the report designer,\nkept in a folder) can be downloaded from any script, with its parameters, as the web app\nrenders it:",[45,1381,1386],{"className":1382,"code":1384,"language":1385},[1383],"language-text","GET \u002Freports\u002F{id}\u002Frender?format=pdf&region=North&since=2026-09-01\n","text",[18,1387,1384],{"__ignoreMap":50},[81,1389,1390,1413,1477,1494,1510],{},[84,1391,1392,483,1397,1400,1401,1404,1405,1408,1409,1412],{},[87,1393,1394],{},[18,1395,1396],{},"format",[18,1398,1399],{},"pdf"," (the default), ",[18,1402,1403],{},"xlsx"," (the report's tables, groups and subtotals as\nrows) or ",[18,1406,1407],{},"html"," (the summary an email carries, as an ",[18,1410,1411],{},".mhtml"," file: the HTML with its\ncharts as images).",[84,1414,1415,1418,1419,1422,1423,1426,1427,1430,1431,1434,1435,144,1438,1272,1441,1444,1445,1449,1450,1453,1454,1457,1458,1461,1462,21,1465,1468,1469,1472,1473,1476],{},[87,1416,1417],{},"The parameters"," go in the query string by name. A parameter that takes several values\nrepeats its name: ",[18,1420,1421],{},"?region=North&region=South",". One left out, or empty, takes the\nreport's default; ",[18,1424,1425],{},"{none}"," says no value for an optional one, whatever its default\n(",[18,1428,1429],{},"region=%7Bnone%7D","), and the viewer's links do the same. A value is text, as typed in the report's form: a number with a dot for decimals\n(",[18,1432,1433],{},"1234.5","), a date as ",[18,1436,1437],{},"yyyy-mm-dd",[18,1439,1440],{},"true",[18,1442,1443],{},"false",". A date may also be a\n",[27,1446,1448],{"href":1447},"\u002Fdocs\u002Fplaceholders","placeholder",", resolved in the report's time zone when it is rendered:\n",[18,1451,1452],{},"since={start_of_month-1m}"," is the first day of last month. Encode the values: in a\nquery string ",[18,1455,1456],{},"+"," is a space, so ",[18,1459,1460],{},"{today+7}"," is written ",[18,1463,1464],{},"%7Btoday%2B7%7D",[18,1466,1467],{},"requests","'\n",[18,1470,1471],{},"params"," and curl's ",[18,1474,1475],{},"--data-urlencode"," do it for you). A value repeated for a parameter\nthat takes several counts once.",[84,1478,1479,1486,1487,1490,1491,1493],{},[87,1480,1481,1482],{},"An ",[27,1483,1485],{"href":1484},"\u002Fdocs\u002Fcapabilities\u002Freports#ai-comments","AI comment"," in the report: a token that may write makes\nthe call it needs, on its account; a ",[121,1488,1489],{},"read only"," one causes none, and gets what is kept, or a line\nin its place. ",[18,1492,1403],{}," has no place for one.",[84,1495,1496,483,1499,1501,1502,1505,1506,1509],{},[87,1497,1498],{},"The token",[121,1500,1489],{}," is enough. The report reads with the rights of the token's\naccount: it needs ",[87,1503,1504],{},"VIEW"," on the report's folder, and every dataset the report reads\nmust be one that account may read (the datasources, the views, the SQL scripts, and\n",[87,1507,1508],{},"CONNECT"," on a database connection).",[84,1511,1512,1515,1516,21,1519,1522,1523,1526,1527,1529,1530,1533,1534,1537],{},[87,1513,1514],{},"The answer"," is the file, with its name in ",[18,1517,1518],{},"Content-Disposition",[18,1520,1521],{},"Monthly sales.pdf",";\na name with accents comes twice, as written and without them, so ",[18,1524,1525],{},"curl -OJ"," keeps it).\nThe request stays open while a worker renders it, up to ",[18,1528,1244],{},"\n(120 seconds by default); past that the render is stopped, and so it is when the script\nstops waiting first (its own timeout): a script that tries again does not pile renders\nup. Every download is in the audit log (",[18,1531,1532],{},"report.render",", from ",[18,1535,1536],{},"api",").",[14,1539,1540,1541,1544,1545,1548,1549,1551,1552,1554],{},"The report's id is in its address (",[18,1542,1543],{},"\u002Freport?id=12","), and ",[87,1546,1547],{},"Copy API link"," in the\nreport's toolbar gives the address with the values on screen. ",[18,1550,592],{}," lists the\nreports the account sees; ",[18,1553,598],{}," lists a report's parameters, with\ntheir types, their defaults as they read today and their choices.",[691,1556,1558],{"id":1557},"example-last-months-pdf-every-month","Example: last month's PDF, every month",[14,1560,1561,1562,1565,1566,1568,1569,1571,1572,1575,1576,531,1579,1582],{},"A script run on the first of each month downloads the PDF of the month before for one\nregion. As an administrator, create a service account (see\n",[27,1563,190],{"href":1564},"#example-a-token-for-a-scheduler",") with ",[87,1567,1504],{}," on the\nreport's folder and on the data it reads, and give it a token with access ",[121,1570,123],{},".\nThe report here has the parameters ",[18,1573,1574],{},"region"," (text) and ",[18,1577,1578],{},"since",[18,1580,1581],{},"until"," (dates):",[45,1584,1586],{"className":700,"code":1585,"language":702,"meta":50,"style":50},"import datetime as dt\nimport os\n\nimport requests\n\nAPI = \"https:\u002F\u002Ftabularia.example.com\u002Fapi\"\nTOKEN = os.environ[\"TABULARIA_TOKEN\"]  # never in the script itself\nREPORT_ID = 12\n\nfirst_of_this_month = dt.date.today().replace(day=1)\nlast_day = first_of_this_month - dt.timedelta(days=1)\nfirst_day = last_day.replace(day=1)\n\nresponse = requests.get(\n    f\"{API}\u002Freports\u002F{REPORT_ID}\u002Frender\",\n    params={\"format\": \"pdf\", \"region\": \"North\", \"since\": first_day.isoformat(), \"until\": last_day.isoformat()},\n    headers={\"Authorization\": f\"Bearer {TOKEN}\"},\n    timeout=180,  # longer than REPORTS__API_WAIT_SECONDS\n)\nif response.status_code == 400:\n    detail = response.json()[\"detail\"]\n    raise SystemExit(f\"{detail['parameter']}: {detail['message']}\")\nresponse.raise_for_status()\nwith open(f\"sales_north_{first_day:%Y-%m}.pdf\", \"wb\") as f:\n    f.write(response.content)\n",[18,1587,1588,1600,1607,1611,1618,1622,1630,1649,1659,1663,1682,1707,1725,1729,1740,1768,1809,1839,1856,1861,1876,1892,1935,1941,1987],{"__ignoreMap":50},[54,1589,1590,1592,1595,1597],{"class":56,"line":57},[54,1591,710],{"class":709},[54,1593,1594],{"class":713}," datetime ",[54,1596,1165],{"class":709},[54,1598,1599],{"class":713}," dt\n",[54,1601,1602,1604],{"class":56,"line":313},[54,1603,710],{"class":709},[54,1605,1606],{"class":713}," os\n",[54,1608,1609],{"class":56,"line":319},[54,1610,330],{"emptyLinePlaceholder":329},[54,1612,1613,1615],{"class":56,"line":326},[54,1614,710],{"class":709},[54,1616,1617],{"class":713}," requests\n",[54,1619,1620],{"class":56,"line":333},[54,1621,330],{"emptyLinePlaceholder":329},[54,1623,1624,1626,1628],{"class":56,"line":345},[54,1625,723],{"class":64},[54,1627,726],{"class":709},[54,1629,729],{"class":68},[54,1631,1632,1635,1637,1640,1643,1646],{"class":56,"line":802},[54,1633,1634],{"class":64},"TOKEN",[54,1636,726],{"class":709},[54,1638,1639],{"class":713}," os.environ[",[54,1641,1642],{"class":68},"\"TABULARIA_TOKEN\"",[54,1644,1645],{"class":713},"]  ",[54,1647,1648],{"class":322},"# never in the script itself\n",[54,1650,1651,1654,1656],{"class":56,"line":814},[54,1652,1653],{"class":64},"REPORT_ID",[54,1655,726],{"class":709},[54,1657,1658],{"class":64}," 12\n",[54,1660,1661],{"class":56,"line":856},[54,1662,330],{"emptyLinePlaceholder":329},[54,1664,1665,1668,1670,1673,1676,1678,1680],{"class":56,"line":887},[54,1666,1667],{"class":713},"first_of_this_month ",[54,1669,737],{"class":709},[54,1671,1672],{"class":713}," dt.date.today().replace(",[54,1674,1675],{"class":787},"day",[54,1677,737],{"class":709},[54,1679,1214],{"class":64},[54,1681,902],{"class":713},[54,1683,1684,1687,1689,1692,1695,1698,1701,1703,1705],{"class":56,"line":893},[54,1685,1686],{"class":713},"last_day ",[54,1688,737],{"class":709},[54,1690,1691],{"class":713}," first_of_this_month ",[54,1693,1694],{"class":709},"-",[54,1696,1697],{"class":713}," dt.timedelta(",[54,1699,1700],{"class":787},"days",[54,1702,737],{"class":709},[54,1704,1214],{"class":64},[54,1706,902],{"class":713},[54,1708,1709,1712,1714,1717,1719,1721,1723],{"class":56,"line":905},[54,1710,1711],{"class":713},"first_day ",[54,1713,737],{"class":709},[54,1715,1716],{"class":713}," last_day.replace(",[54,1718,1675],{"class":787},[54,1720,737],{"class":709},[54,1722,1214],{"class":64},[54,1724,902],{"class":713},[54,1726,1727],{"class":56,"line":925},[54,1728,330],{"emptyLinePlaceholder":329},[54,1730,1732,1735,1737],{"class":56,"line":1731},14,[54,1733,1734],{"class":713},"response ",[54,1736,737],{"class":709},[54,1738,1739],{"class":713}," requests.get(\n",[54,1741,1743,1746,1748,1750,1752,1754,1757,1759,1761,1763,1766],{"class":56,"line":1742},15,[54,1744,1745],{"class":709},"    f",[54,1747,771],{"class":68},[54,1749,774],{"class":309},[54,1751,723],{"class":64},[54,1753,779],{"class":309},[54,1755,1756],{"class":68},"\u002Freports\u002F",[54,1758,774],{"class":309},[54,1760,1653],{"class":64},[54,1762,779],{"class":309},[54,1764,1765],{"class":68},"\u002Frender\"",[54,1767,140],{"class":713},[54,1769,1771,1774,1776,1778,1780,1782,1785,1787,1790,1792,1795,1797,1800,1803,1806],{"class":56,"line":1770},16,[54,1772,1773],{"class":787},"    params",[54,1775,737],{"class":709},[54,1777,774],{"class":713},[54,1779,1080],{"class":68},[54,1781,483],{"class":713},[54,1783,1784],{"class":68},"\"pdf\"",[54,1786,144],{"class":713},[54,1788,1789],{"class":68},"\"region\"",[54,1791,483],{"class":713},[54,1793,1794],{"class":68},"\"North\"",[54,1796,144],{"class":713},[54,1798,1799],{"class":68},"\"since\"",[54,1801,1802],{"class":713},": first_day.isoformat(), ",[54,1804,1805],{"class":68},"\"until\"",[54,1807,1808],{"class":713},": last_day.isoformat()},\n",[54,1810,1812,1815,1817,1819,1821,1823,1825,1828,1830,1832,1834,1836],{"class":56,"line":1811},17,[54,1813,1814],{"class":787},"    headers",[54,1816,737],{"class":709},[54,1818,774],{"class":713},[54,1820,743],{"class":68},[54,1822,483],{"class":713},[54,1824,768],{"class":709},[54,1826,1827],{"class":68},"\"Bearer ",[54,1829,774],{"class":309},[54,1831,1634],{"class":64},[54,1833,779],{"class":309},[54,1835,771],{"class":68},[54,1837,1838],{"class":713},"},\n",[54,1840,1842,1845,1847,1850,1853],{"class":56,"line":1841},18,[54,1843,1844],{"class":787},"    timeout",[54,1846,737],{"class":709},[54,1848,1849],{"class":64},"180",[54,1851,1852],{"class":713},",  ",[54,1854,1855],{"class":322},"# longer than REPORTS__API_WAIT_SECONDS\n",[54,1857,1859],{"class":56,"line":1858},19,[54,1860,902],{"class":713},[54,1862,1864,1866,1869,1871,1874],{"class":56,"line":1863},20,[54,1865,908],{"class":709},[54,1867,1868],{"class":713}," response.status_code ",[54,1870,917],{"class":709},[54,1872,1873],{"class":64}," 400",[54,1875,811],{"class":713},[54,1877,1879,1882,1884,1887,1890],{"class":56,"line":1878},21,[54,1880,1881],{"class":713},"    detail ",[54,1883,737],{"class":709},[54,1885,1886],{"class":713}," response.json()[",[54,1888,1889],{"class":68},"\"detail\"",[54,1891,799],{"class":713},[54,1893,1895,1897,1899,1902,1904,1906,1908,1911,1914,1916,1918,1920,1922,1924,1927,1929,1931,1933],{"class":56,"line":1894},22,[54,1896,928],{"class":709},[54,1898,931],{"class":64},[54,1900,1901],{"class":713},"(",[54,1903,768],{"class":709},[54,1905,771],{"class":68},[54,1907,774],{"class":309},[54,1909,1910],{"class":713},"detail[",[54,1912,1913],{"class":68},"'parameter'",[54,1915,1107],{"class":713},[54,1917,779],{"class":309},[54,1919,483],{"class":68},[54,1921,774],{"class":309},[54,1923,1910],{"class":713},[54,1925,1926],{"class":68},"'message'",[54,1928,1107],{"class":713},[54,1930,779],{"class":309},[54,1932,771],{"class":68},[54,1934,902],{"class":713},[54,1936,1938],{"class":56,"line":1937},23,[54,1939,1940],{"class":713},"response.raise_for_status()\n",[54,1942,1944,1946,1948,1950,1952,1955,1957,1960,1963,1966,1969,1972,1974,1977,1979,1981,1983,1985],{"class":56,"line":1943},24,[54,1945,1119],{"class":709},[54,1947,1181],{"class":64},[54,1949,1901],{"class":713},[54,1951,768],{"class":709},[54,1953,1954],{"class":68},"\"sales_north_",[54,1956,774],{"class":309},[54,1958,1959],{"class":713},"first_day:",[54,1961,1962],{"class":709},"%",[54,1964,1965],{"class":713},"Y",[54,1967,1968],{"class":709},"-%",[54,1970,1971],{"class":713},"m",[54,1973,779],{"class":309},[54,1975,1976],{"class":68},".pdf\"",[54,1978,144],{"class":713},[54,1980,1191],{"class":68},[54,1982,1162],{"class":713},[54,1984,1165],{"class":709},[54,1986,1198],{"class":713},[54,1988,1990],{"class":56,"line":1989},25,[54,1991,1992],{"class":713},"    f.write(response.content)\n",[14,1994,1995,1996,455,1999,2001],{},"The same with curl, two regions in Excel; ",[18,1997,1998],{},"-G",[18,2000,1475],{}," puts the values in\nthe query string, encoded:",[45,2003,2005],{"className":47,"code":2004,"language":49,"meta":50,"style":50},"curl -fG -H \"Authorization: Bearer $TABULARIA_TOKEN\" -o sales.xlsx \\\n  --data-urlencode format=xlsx --data-urlencode region=North --data-urlencode region=South \\\n  --data-urlencode \"since={start_of_month-1m}\" \\\n  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Freports\u002F12\u002Frender\n",[18,2006,2007,2032,2053,2062],{"__ignoreMap":50},[54,2008,2009,2011,2014,2016,2019,2022,2024,2027,2030],{"class":56,"line":57},[54,2010,61],{"class":60},[54,2012,2013],{"class":64}," -fG",[54,2015,65],{"class":64},[54,2017,2018],{"class":68}," \"Authorization: Bearer ",[54,2020,2021],{"class":713},"$TABULARIA_TOKEN",[54,2023,771],{"class":68},[54,2025,2026],{"class":64}," -o",[54,2028,2029],{"class":68}," sales.xlsx",[54,2031,310],{"class":309},[54,2033,2034,2037,2040,2043,2046,2048,2051],{"class":56,"line":313},[54,2035,2036],{"class":64},"  --data-urlencode",[54,2038,2039],{"class":68}," format=xlsx",[54,2041,2042],{"class":64}," --data-urlencode",[54,2044,2045],{"class":68}," region=North",[54,2047,2042],{"class":64},[54,2049,2050],{"class":68}," region=South",[54,2052,310],{"class":309},[54,2054,2055,2057,2060],{"class":56,"line":319},[54,2056,2036],{"class":64},[54,2058,2059],{"class":68}," \"since={start_of_month-1m}\"",[54,2061,310],{"class":309},[54,2063,2064],{"class":56,"line":326},[54,2065,2066],{"class":68},"  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Freports\u002F12\u002Frender\n",[691,2068,2070],{"id":2069},"when-it-fails","When it fails",[377,2072,2073,2081],{},[380,2074,2075],{},[383,2076,2077,2079],{},[386,2078,1296],{},[386,2080,1299],{},[396,2082,2083,2097,2106,2123,2133,2142],{},[383,2084,2085,2090],{},[401,2086,2087],{},[18,2088,2089],{},"400",[401,2091,2092,2093,2096],{},"a value does not fit a parameter: ",[18,2094,2095],{},"{\"detail\": {\"message\": \"Parameter «since»: '2026-02-30' is not a date: write it as yyyy-mm-dd\", \"parameter\": \"since\"}}",". The same for a required parameter without a value, a value outside its choices, a name the report does not have",[383,2098,2099,2103],{},[401,2100,2101],{},[18,2102,1326],{},[401,2104,2105],{},"no report with that id, or one in a folder the account cannot see",[383,2107,2108,2112],{},[401,2109,2110],{},[18,2111,1345],{},[401,2113,2114,2115,2118,2119,2122],{},"the report cannot be rendered: a dataset the account may not read, said with its name as one that does not exist (",[18,2116,2117],{},"The dataset «orders»: there is no datasource «Salaries» you can read","), a dataset over the row limit (",[18,2120,2121],{},"REPORTS__MAX_ROWS",", 3,000,000 by default: aggregate in SQL, or filter), too many pages",[383,2124,2125,2130],{},[401,2126,2127],{},[18,2128,2129],{},"502",[401,2131,2132],{},"the engine could not render it (the chart service down, for a report with charts)",[383,2134,2135,2139],{},[401,2136,2137],{},[18,2138,1355],{},[401,2140,2141],{},"the engine is not answering (restarting, its node drained): try again in a moment",[383,2143,2144,2149],{},[401,2145,2146],{},[18,2147,2148],{},"504",[401,2150,2151,2152,2154],{},"not ready within ",[18,2153,1244],{},": the render was stopped",[14,2156,2157,2158,2162,2163,2169],{},"To send a report by email instead, on a schedule and right after the data is refreshed,\nput it in the ",[27,2159,2161],{"href":2160},"\u002Fdocs\u002Fpushmail#reports","Pushmail cell of a flow group",". In a marimo notebook,\n",[27,2164,2166],{"href":2165},"\u002Fdocs\u002Fcapabilities\u002Fnotebooks#reports",[18,2167,2168],{},"tabularia.report()"," does the same as this API.",[33,2171,616],{"id":2172},"dashboards",[14,2174,1374,2175,2179,2180,2182,2183,2187,2188,1537],{},[27,2176,2178],{"href":2177},"\u002Fdocs\u002Fcapabilities\u002Fdashboards","dashboard"," answers scripts with its numbers and with its files.\nA token that may only read is enough for all of this. The reads are with the rights of the\ntoken's account: ",[87,2181,1504],{}," on the dashboard's folder, and the datasets it reads must be ones that\naccount may read. A tile on a dataset it may not read says so in its place, and the others answer.\nThe examples read «Sales overview» of the ",[27,2184,2186],{"href":2185},"\u002Fdocs\u002Fcapabilities\u002Fdashboards#example-sales-overview","sample data",";\nits id is in its address (",[18,2189,2190],{},"\u002Fdashboard?id=12",[691,2192,2194],{"id":2193},"its-parameters","Its parameters",[45,2196,2199],{"className":2197,"code":2198,"language":1385},[1383],"GET \u002Fdashboards\u002F12\u002Fparameters?area=Nord-Ovest\n",[18,2200,2198],{"__ignoreMap":50},[14,2202,2203,2204,144,2207,2210],{},"The parameters as the form shows them: types, defaults as they read today, choices. The values\nin the query string are those the other lists follow: with ",[18,2205,2206],{},"area=Nord-Ovest",[18,2208,2209],{},"agent"," has 13\nchoices instead of 90.",[691,2212,2214],{"id":2213},"its-numbers","Its numbers",[45,2216,2218],{"className":47,"code":2217,"language":49,"meta":50,"style":50},"curl -s -H \"Authorization: Bearer $TABULARIA_TOKEN\" -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"params\": {\"area\": \"Nord-Ovest\"}, \"tiles\": [\"revenue\"]}' \\\n  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdashboards\u002F12\u002Fdata\n",[18,2219,2220,2242,2252],{"__ignoreMap":50},[54,2221,2222,2224,2227,2229,2231,2233,2235,2237,2240],{"class":56,"line":57},[54,2223,61],{"class":60},[54,2225,2226],{"class":64}," -s",[54,2228,65],{"class":64},[54,2230,2018],{"class":68},[54,2232,2021],{"class":713},[54,2234,771],{"class":68},[54,2236,65],{"class":64},[54,2238,2239],{"class":68}," \"Content-Type: application\u002Fjson\"",[54,2241,310],{"class":309},[54,2243,2244,2247,2250],{"class":56,"line":313},[54,2245,2246],{"class":64},"  -d",[54,2248,2249],{"class":68}," '{\"params\": {\"area\": \"Nord-Ovest\"}, \"tiles\": [\"revenue\"]}'",[54,2251,310],{"class":309},[54,2253,2254],{"class":56,"line":319},[54,2255,2256],{"class":68},"  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdashboards\u002F12\u002Fdata\n",[45,2258,2261],{"className":2259,"code":2260,"language":1146,"meta":50,"style":50},"language-json shiki shiki-themes github-dark-dimmed github-light","{\"tiles\": {\"revenue\": {\"type\": \"kpi\", \"label\": \"Revenue\", \"value\": 2818062.7190681,\n                       \"text\": \"€ 2,818,063\", \"change\": null, \"spark\": null, \"title\": \"Revenue\",\n                       \"computed_at\": \"2026-11-01T20:03:24.145747+00:00\", \"cached\": false}},\n \"truncated\": {\"agents\": false}}\n",[18,2262,2263,2309,2349,2371],{"__ignoreMap":50},[54,2264,2265,2267,2271,2274,2277,2279,2282,2284,2287,2289,2292,2294,2297,2299,2302,2304,2307],{"class":56,"line":57},[54,2266,774],{"class":713},[54,2268,2270],{"class":2269},"sJK54","\"tiles\"",[54,2272,2273],{"class":713},": {",[54,2275,2276],{"class":2269},"\"revenue\"",[54,2278,2273],{"class":713},[54,2280,2281],{"class":2269},"\"type\"",[54,2283,483],{"class":713},[54,2285,2286],{"class":68},"\"kpi\"",[54,2288,144],{"class":713},[54,2290,2291],{"class":2269},"\"label\"",[54,2293,483],{"class":713},[54,2295,2296],{"class":68},"\"Revenue\"",[54,2298,144],{"class":713},[54,2300,2301],{"class":2269},"\"value\"",[54,2303,483],{"class":713},[54,2305,2306],{"class":64},"2818062.7190681",[54,2308,140],{"class":713},[54,2310,2311,2314,2316,2319,2321,2324,2326,2329,2331,2334,2336,2338,2340,2343,2345,2347],{"class":56,"line":313},[54,2312,2313],{"class":2269},"                       \"text\"",[54,2315,483],{"class":713},[54,2317,2318],{"class":68},"\"€ 2,818,063\"",[54,2320,144],{"class":713},[54,2322,2323],{"class":2269},"\"change\"",[54,2325,483],{"class":713},[54,2327,2328],{"class":64},"null",[54,2330,144],{"class":713},[54,2332,2333],{"class":2269},"\"spark\"",[54,2335,483],{"class":713},[54,2337,2328],{"class":64},[54,2339,144],{"class":713},[54,2341,2342],{"class":2269},"\"title\"",[54,2344,483],{"class":713},[54,2346,2296],{"class":68},[54,2348,140],{"class":713},[54,2350,2351,2354,2356,2359,2361,2364,2366,2368],{"class":56,"line":319},[54,2352,2353],{"class":2269},"                       \"computed_at\"",[54,2355,483],{"class":713},[54,2357,2358],{"class":68},"\"2026-11-01T20:03:24.145747+00:00\"",[54,2360,144],{"class":713},[54,2362,2363],{"class":2269},"\"cached\"",[54,2365,483],{"class":713},[54,2367,1443],{"class":64},[54,2369,2370],{"class":713},"}},\n",[54,2372,2373,2376,2378,2381,2383,2385],{"class":56,"line":326},[54,2374,2375],{"class":2269}," \"truncated\"",[54,2377,2273],{"class":713},[54,2379,2380],{"class":2269},"\"agents\"",[54,2382,483],{"class":713},[54,2384,1443],{"class":64},[54,2386,2387],{"class":713},"}}\n",[377,2389,2390,2399],{},[380,2391,2392],{},[383,2393,2394,2397],{},[386,2395,2396],{},"In the request",[386,2398],{},[396,2400,2401,2410,2432,2442,2456,2469],{},[383,2402,2403,2407],{},[401,2404,2405],{},[18,2406,1471],{},[401,2408,2409],{},"the parameters' values, by name, as text",[383,2411,2412,2417],{},[401,2413,2414],{},[18,2415,2416],{},"clicks",[401,2418,2419,2420,2423,2424,2427,2428,2431],{},"what is chosen on a tile: ",[18,2421,2422],{},"[{\"tile\": \"by_area\", \"values\": [\"Centro\"]}]",", and ",[18,2425,2426],{},"\"on\": \"columns\""," for a matrix's columns. ",[87,2429,2430],{},"A tile's id and values, never an expression",": the expression is the saved one",[383,2433,2434,2439],{},[401,2435,2436],{},[18,2437,2438],{},"tiles",[401,2440,2441],{},"only these tiles; none, every one",[383,2443,2444,2449],{},[401,2445,2446],{},[18,2447,2448],{},"page",[401,2450,2451,2452,2455],{},"a table's page, from 0: ",[18,2453,2454],{},"{\"table\": 1}","; 50 rows a page",[383,2457,2458,2463],{},[401,2459,2460],{},[18,2461,2462],{},"state",[401,2464,2465,2466],{},"a table's sort and closed groups, as on the screen: ",[18,2467,2468],{},"{\"items\": {\"table\": {\"sort\": {\"column\": 1, \"dir\": \"asc\"}, \"collapsed\": [[\"Centro\"]]}}}",[383,2470,2471,2476],{},[401,2472,2473],{},[18,2474,2475],{},"refresh",[401,2477,2478,2480],{},[18,2479,1440],{}," reads the source again, at most once every 10 seconds per dashboard, for everyone: past that it answers with what is kept, without an error",[377,2482,2483,2492],{},[380,2484,2485],{},[383,2486,2487,2490],{},[386,2488,2489],{},"In the answer",[386,2491],{},[396,2493,2494,2536],{},[383,2495,2496,2500],{},[401,2497,2498],{},[18,2499,2438],{},[401,2501,2502,2503,144,2506,2509,2510,531,2513,459,2515,459,2518,136,2521,2524,2525,2528,2529,2532,2533],{},"by tile id. The numbers: ",[18,2504,2505],{},"type",[18,2507,2508],{},"title",", what the type draws (",[18,2511,2512],{},"value",[18,2514,1385],{},[18,2516,2517],{},"data",[18,2519,2520],{},"rows",[18,2522,2523],{},"computed_at"," (when the rows were read from their source) and ",[18,2526,2527],{},"cached"," (this answer did not read it). A tile clicked adds ",[18,2530,2531],{},"chosen",", what stayed of its values. A tile that cannot answer: only ",[18,2534,2535],{},"{\"error\": \"a sentence\", \"code\": …}",[383,2537,2538,2543],{},[401,2539,2540],{},[18,2541,2542],{},"truncated",[401,2544,2545,2546,2549],{},"by dataset read, whether it is past ",[18,2547,2548],{},"DASHBOARDS__MAX_ROWS",". Of a dataset you may not read, nothing",[377,2551,2552,2563],{},[380,2553,2554],{},[383,2555,2556,2560],{},[386,2557,2558],{},[18,2559,18],{},[386,2561,2562],{},"Means",[396,2564,2565,2575,2587,2597],{},[383,2566,2567,2572],{},[401,2568,2569],{},[18,2570,2571],{},"no_access",[401,2573,2574],{},"it reads a dataset the account may not read",[383,2576,2577,2581],{},[401,2578,2579],{},[18,2580,2542],{},[401,2582,2583,2584,2586],{},"it reads a dataset past ",[18,2585,2548],{},": the sentence says how to aggregate. No number on part of the rows",[383,2588,2589,2594],{},[401,2590,2591],{},[18,2592,2593],{},"dataset",[401,2595,2596],{},"its dataset cannot be read for another reason (no data yet, a parameter its SQL needs)",[383,2598,2599,2603],{},[401,2600,2601],{},[18,2602,498],{},[401,2604,2605],{},"anything else in this tile",[14,2607,2608,2609,2612,2613,531,2616,2618,2619,2621,2622,31],{},"A value of a click is the raw one the tile gave in ",[18,2610,2611],{},"keys"," (or ",[18,2614,2615],{},"path",[18,2617,534],{}," in a table): the\nnumber or the date, not the text on the screen; ",[18,2620,2328],{}," for an empty group; a list for several levels,\n",[18,2623,2624],{},"[\"Nord\", 2023]",[691,2626,2628],{"id":2627},"its-files","Its files",[45,2630,2632],{"className":47,"code":2631,"language":49,"meta":50,"style":50},"curl -fG -H \"Authorization: Bearer $TABULARIA_TOKEN\" -o sales.pdf \\\n  --data-urlencode format=pdf --data-urlencode area=Nord-Est --data-urlencode click.by_area=Nord-Est \\\n  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdashboards\u002F12\u002Frender\n",[18,2633,2634,2655,2674],{"__ignoreMap":50},[54,2635,2636,2638,2640,2642,2644,2646,2648,2650,2653],{"class":56,"line":57},[54,2637,61],{"class":60},[54,2639,2013],{"class":64},[54,2641,65],{"class":64},[54,2643,2018],{"class":68},[54,2645,2021],{"class":713},[54,2647,771],{"class":68},[54,2649,2026],{"class":64},[54,2651,2652],{"class":68}," sales.pdf",[54,2654,310],{"class":309},[54,2656,2657,2659,2662,2664,2667,2669,2672],{"class":56,"line":313},[54,2658,2036],{"class":64},[54,2660,2661],{"class":68}," format=pdf",[54,2663,2042],{"class":64},[54,2665,2666],{"class":68}," area=Nord-Est",[54,2668,2042],{"class":64},[54,2670,2671],{"class":68}," click.by_area=Nord-Est",[54,2673,310],{"class":309},[54,2675,2676],{"class":56,"line":319},[54,2677,2678],{"class":68},"  https:\u002F\u002Ftabularia.example.com\u002Fapi\u002Fdashboards\u002F12\u002Frender\n",[81,2680,2681,2694,2706,2730],{},[84,2682,2683,483,2687,2689,2690,2693],{},[87,2684,2685],{},[18,2686,1396],{},[18,2688,1399],{}," (the default) or ",[18,2691,2692],{},"png",", the same document on one long page.",[84,2695,2696,2698,2699,2702,2703,2705],{},[87,2697,1417],{}," go in the query string by name, as for ",[27,2700,2701],{"href":582},"a report",": one left\nout takes the default, ",[18,2704,1425],{}," says no value.",[84,2707,2708,2711,2712,2715,2716,2719,2720,459,2723,2725,2726,2729],{},[87,2709,2710],{},"The clicks"," are written as in the viewer's address: ",[18,2713,2714],{},"click.by_area=Nord-Est",", repeated for several\nvalues; ",[18,2717,2718],{},"click.cross.columns=…"," for the columns of a matrix ",[18,2721,2722],{},"cross",[18,2724,1425],{}," for an empty group; ",[18,2727,2728],{},"\\","\nbefore a text that would read as something else; JSON for several levels.",[84,2731,2732,1515,2734,2736,2737,2739,2740,1533,2743,1537],{},[87,2733,1514],{},[18,2735,1518],{},", as a report's, and the same wait\n(",[18,2738,1244],{},"). Every download is in the audit log (",[18,2741,2742],{},"dashboard.render",[18,2744,1536],{},[14,2746,2747,2749,2750,2753,2754,2757,2758,2760,2761,2763,2764,2766,2767,31],{},[18,2748,151],{},", with ",[18,2751,2752],{},"{\"format\": \"pdf\", \"params\": {…}, \"clicks\": […], \"state\": {…}}",", is what\nthe viewer's button does: it answers ",[18,2755,2756],{},"{\"task_id\", \"filename\"}"," at once, followed at\n",[18,2759,1264],{}," until ",[18,2762,482],{}," is ",[18,2765,1271],{},", and taken at ",[18,2768,1282],{},[377,2770,2771,2780],{},[380,2772,2773],{},[383,2774,2775,2778],{},[386,2776,2777],{},"Status",[386,2779,2562],{},[396,2781,2782,2794,2803],{},[383,2783,2784,2788],{},[401,2785,2786],{},[18,2787,2089],{},[401,2789,2092,2790,2793],{},[18,2791,2792],{},"{\"detail\": {\"message\": \"Parameter «area»: 'Atlantis' is not one of its choices\", \"parameter\": \"area\"}}",". The same for a name the dashboard does not have",[383,2795,2796,2800],{},[401,2797,2798],{},[18,2799,1326],{},[401,2801,2802],{},"no dashboard with that id, or one in a folder the account cannot see",[383,2804,2805,2809],{},[401,2806,2807],{},[18,2808,1345],{},[401,2810,2811,2812,144,2815,144,2818,2821,2822,2824],{},"a click that cannot be: ",[18,2813,2814],{},"The dashboard has no tile «nope»",[18,2816,2817],{},"The tile «revenue» cannot filter the others",[18,2819,2820],{},"The click on «by_area» has 101 values: at most 100","; an unknown ",[18,2823,1396],{},"; a file that cannot be made, saying why",[14,2826,2827,2828,2830,2831,31],{},"A link to the viewer with a click on a tile that is gone opens without it; the API says ",[18,2829,1345],{},". To send a\ndashboard by email, on a schedule, put it in the ",[27,2832,2161],{"href":2833},"\u002Fdocs\u002Fpushmail#dashboards",[33,2835,2837],{"id":2836},"webhooks","Webhooks",[14,2839,2840,2841,1537],{},"Instead of polling, an administrator can have Tabularia call an address when\nsomething finishes (",[87,2842,2843],{},"Administration → Webhooks",[691,2845,2847],{"id":2846},"events","Events",[377,2849,2850,2860],{},[380,2851,2852],{},[383,2853,2854,2857],{},[386,2855,2856],{},"Event",[386,2858,2859],{},"When",[396,2861,2862,2887,2900],{},[383,2863,2864,2873],{},[401,2865,2866,2869,2870],{},[18,2867,2868],{},"flow.run.succeeded"," \u002F ",[18,2871,2872],{},"flow.run.failed",[401,2874,2875,2876,2879,2880,2883,2884,2886],{},"a flow execution finished: from a schedule, from ",[87,2877,2878],{},"Run"," in the interface, from ",[18,2881,2882],{},"run-now",", or inside a flow group (the group itself sends no event). A run closed because its process died, because the gateway shut down, or because it waited in the queue too long is a ",[18,2885,2872],{}," too",[383,2888,2889,2897],{},[401,2890,2891,2869,2894],{},[18,2892,2893],{},"datasource.refresh.succeeded",[18,2895,2896],{},"datasource.refresh.failed",[401,2898,2899],{},"the refresh of a database datasource finished",[383,2901,2902,2907],{},[401,2903,2904],{},[18,2905,2906],{},"ping",[401,2908,2909,2910,2913],{},"the ",[87,2911,2912],{},"Send a test call"," button",[14,2915,2916],{},"A webhook can be limited to one folder and its subfolders.",[691,2918,2920],{"id":2919},"the-call","The call",[14,2922,1374,2923,2926],{},[18,2924,2925],{},"POST"," with a JSON body:",[45,2928,2930],{"className":2259,"code":2929,"language":1146,"meta":50,"style":50},"{\n  \"id\": \"5f0c1c8e-8a2e-4b8f-9a55-2a8f4b1d2c3e\",\n  \"event\": \"flow.run.failed\",\n  \"created_at\": \"2026-10-12T08:30:12.401Z\",\n  \"data\": {\n    \"run\": {\n      \"id\": 2212, \"kind\": \"orchestration\", \"status\": \"FAILURE\", \"trigger\": \"schedule\",\n      \"started_at\": \"2026-10-12T08:29:50.112Z\", \"finished_at\": \"2026-10-12T08:30:11.700Z\",\n      \"duration_seconds\": 21.6, \"rows_written\": null, \"error\": \"ClickHouse: no response from …\"\n    },\n    \"flow\": {\"id\": 14, \"name\": \"Margin by Category and Month\"},\n    \"folder\": {\"id\": 3, \"name\": \"Reports\"}\n  }\n}\n",[18,2931,2932,2937,2949,2961,2973,2981,2988,3028,3050,3078,3083,3109,3134,3139],{"__ignoreMap":50},[54,2933,2934],{"class":56,"line":57},[54,2935,2936],{"class":713},"{\n",[54,2938,2939,2942,2944,2947],{"class":56,"line":313},[54,2940,2941],{"class":2269},"  \"id\"",[54,2943,483],{"class":713},[54,2945,2946],{"class":68},"\"5f0c1c8e-8a2e-4b8f-9a55-2a8f4b1d2c3e\"",[54,2948,140],{"class":713},[54,2950,2951,2954,2956,2959],{"class":56,"line":319},[54,2952,2953],{"class":2269},"  \"event\"",[54,2955,483],{"class":713},[54,2957,2958],{"class":68},"\"flow.run.failed\"",[54,2960,140],{"class":713},[54,2962,2963,2966,2968,2971],{"class":56,"line":326},[54,2964,2965],{"class":2269},"  \"created_at\"",[54,2967,483],{"class":713},[54,2969,2970],{"class":68},"\"2026-10-12T08:30:12.401Z\"",[54,2972,140],{"class":713},[54,2974,2975,2978],{"class":56,"line":333},[54,2976,2977],{"class":2269},"  \"data\"",[54,2979,2980],{"class":713},": {\n",[54,2982,2983,2986],{"class":56,"line":345},[54,2984,2985],{"class":2269},"    \"run\"",[54,2987,2980],{"class":713},[54,2989,2990,2993,2995,2998,3000,3003,3005,3008,3010,3012,3014,3016,3018,3021,3023,3026],{"class":56,"line":802},[54,2991,2992],{"class":2269},"      \"id\"",[54,2994,483],{"class":713},[54,2996,2997],{"class":64},"2212",[54,2999,144],{"class":713},[54,3001,3002],{"class":2269},"\"kind\"",[54,3004,483],{"class":713},[54,3006,3007],{"class":68},"\"orchestration\"",[54,3009,144],{"class":713},[54,3011,865],{"class":2269},[54,3013,483],{"class":713},[54,3015,881],{"class":68},[54,3017,144],{"class":713},[54,3019,3020],{"class":2269},"\"trigger\"",[54,3022,483],{"class":713},[54,3024,3025],{"class":68},"\"schedule\"",[54,3027,140],{"class":713},[54,3029,3030,3033,3035,3038,3040,3043,3045,3048],{"class":56,"line":814},[54,3031,3032],{"class":2269},"      \"started_at\"",[54,3034,483],{"class":713},[54,3036,3037],{"class":68},"\"2026-10-12T08:29:50.112Z\"",[54,3039,144],{"class":713},[54,3041,3042],{"class":2269},"\"finished_at\"",[54,3044,483],{"class":713},[54,3046,3047],{"class":68},"\"2026-10-12T08:30:11.700Z\"",[54,3049,140],{"class":713},[54,3051,3052,3055,3057,3060,3062,3065,3067,3069,3071,3073,3075],{"class":56,"line":856},[54,3053,3054],{"class":2269},"      \"duration_seconds\"",[54,3056,483],{"class":713},[54,3058,3059],{"class":64},"21.6",[54,3061,144],{"class":713},[54,3063,3064],{"class":2269},"\"rows_written\"",[54,3066,483],{"class":713},[54,3068,2328],{"class":64},[54,3070,144],{"class":713},[54,3072,937],{"class":2269},[54,3074,483],{"class":713},[54,3076,3077],{"class":68},"\"ClickHouse: no response from …\"\n",[54,3079,3080],{"class":56,"line":887},[54,3081,3082],{"class":713},"    },\n",[54,3084,3085,3088,3090,3092,3094,3097,3099,3102,3104,3107],{"class":56,"line":893},[54,3086,3087],{"class":2269},"    \"flow\"",[54,3089,2273],{"class":713},[54,3091,1032],{"class":2269},[54,3093,483],{"class":713},[54,3095,3096],{"class":64},"14",[54,3098,144],{"class":713},[54,3100,3101],{"class":2269},"\"name\"",[54,3103,483],{"class":713},[54,3105,3106],{"class":68},"\"Margin by Category and Month\"",[54,3108,1838],{"class":713},[54,3110,3111,3114,3116,3118,3120,3123,3125,3127,3129,3132],{"class":56,"line":905},[54,3112,3113],{"class":2269},"    \"folder\"",[54,3115,2273],{"class":713},[54,3117,1032],{"class":2269},[54,3119,483],{"class":713},[54,3121,3122],{"class":64},"3",[54,3124,144],{"class":713},[54,3126,3101],{"class":2269},[54,3128,483],{"class":713},[54,3130,3131],{"class":68},"\"Reports\"",[54,3133,751],{"class":713},[54,3135,3136],{"class":56,"line":925},[54,3137,3138],{"class":713},"  }\n",[54,3140,3141],{"class":56,"line":1731},[54,3142,751],{"class":713},[14,3144,3145,3146,3148,3149,3152,3153,3156],{},"For datasource events, ",[18,3147,2517],{}," carries ",[18,3150,3151],{},"datasource"," in place of ",[18,3154,3155],{},"flow",". Headers:",[377,3158,3159,3168],{},[380,3160,3161],{},[383,3162,3163,3166],{},[386,3164,3165],{},"Header",[386,3167],{},[396,3169,3170,3180,3194],{},[383,3171,3172,3177],{},[401,3173,3174],{},[18,3175,3176],{},"X-Tabularia-Event",[401,3178,3179],{},"the event",[383,3181,3182,3187],{},[401,3183,3184],{},[18,3185,3186],{},"X-Tabularia-Delivery",[401,3188,3189,3190,3193],{},"the delivery id, the same as ",[18,3191,3192],{},"id"," in the body",[383,3195,3196,3201],{},[401,3197,3198],{},[18,3199,3200],{},"X-Tabularia-Signature",[401,3202,3203],{},[18,3204,3205],{},"t=\u003Cunix seconds>,v1=\u003Chex HMAC-SHA256>",[691,3207,3209],{"id":3208},"verifying-the-signature","Verifying the signature",[14,3211,3212,3213,3216,3217,3220,3221,3224,3225,3228],{},"Each webhook has a secret (",[18,3214,3215],{},"whsec_…","), shown when it is created or regenerated.\n",[18,3218,3219],{},"v1"," is the HMAC-SHA256 of ",[18,3222,3223],{},"\"\u003Ct>.\u003Craw body>\""," keyed with that secret. Check it\nagainst the ",[87,3226,3227],{},"raw"," body, before parsing it, and reject old timestamps:",[45,3230,3232],{"className":700,"code":3231,"language":702,"meta":50,"style":50},"import hashlib, hmac, time\n\ndef verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:\n    parts = dict(p.split(\"=\", 1) for p in header.split(\",\"))\n    t, v1 = parts.get(\"t\", \"\"), parts.get(\"v1\", \"\")\n    expected = hmac.new(secret.encode(), f\"{t}.\".encode() + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) \u003C= tolerance\n",[18,3233,3234,3241,3245,3290,3328,3358,3390],{"__ignoreMap":50},[54,3235,3236,3238],{"class":56,"line":57},[54,3237,710],{"class":709},[54,3239,3240],{"class":713}," hashlib, hmac, time\n",[54,3242,3243],{"class":56,"line":313},[54,3244,330],{"emptyLinePlaceholder":329},[54,3246,3247,3250,3254,3257,3260,3263,3266,3269,3271,3274,3277,3279,3282,3285,3288],{"class":56,"line":319},[54,3248,3249],{"class":709},"def",[54,3251,3253],{"class":3252},"saVmf"," verify",[54,3255,3256],{"class":713},"(secret: ",[54,3258,3259],{"class":64},"str",[54,3261,3262],{"class":713},", raw_body: ",[54,3264,3265],{"class":64},"bytes",[54,3267,3268],{"class":713},", header: ",[54,3270,3259],{"class":64},[54,3272,3273],{"class":713},", tolerance: ",[54,3275,3276],{"class":64},"int",[54,3278,726],{"class":709},[54,3280,3281],{"class":64}," 300",[54,3283,3284],{"class":713},") -> ",[54,3286,3287],{"class":64},"bool",[54,3289,811],{"class":713},[54,3291,3292,3295,3297,3300,3303,3306,3308,3310,3312,3314,3317,3319,3322,3325],{"class":56,"line":326},[54,3293,3294],{"class":713},"    parts ",[54,3296,737],{"class":709},[54,3298,3299],{"class":64}," dict",[54,3301,3302],{"class":713},"(p.split(",[54,3304,3305],{"class":68},"\"=\"",[54,3307,144],{"class":713},[54,3309,1214],{"class":64},[54,3311,1162],{"class":713},[54,3313,995],{"class":709},[54,3315,3316],{"class":713}," p ",[54,3318,871],{"class":709},[54,3320,3321],{"class":713}," header.split(",[54,3323,3324],{"class":68},"\",\"",[54,3326,3327],{"class":713},"))\n",[54,3329,3330,3333,3335,3338,3341,3343,3346,3349,3352,3354,3356],{"class":56,"line":333},[54,3331,3332],{"class":713},"    t, v1 ",[54,3334,737],{"class":709},[54,3336,3337],{"class":713}," parts.get(",[54,3339,3340],{"class":68},"\"t\"",[54,3342,144],{"class":713},[54,3344,3345],{"class":68},"\"\"",[54,3347,3348],{"class":713},"), parts.get(",[54,3350,3351],{"class":68},"\"v1\"",[54,3353,144],{"class":713},[54,3355,3345],{"class":68},[54,3357,902],{"class":713},[54,3359,3360,3363,3365,3368,3370,3372,3374,3377,3379,3382,3385,3387],{"class":56,"line":345},[54,3361,3362],{"class":713},"    expected ",[54,3364,737],{"class":709},[54,3366,3367],{"class":713}," hmac.new(secret.encode(), ",[54,3369,768],{"class":709},[54,3371,771],{"class":68},[54,3373,774],{"class":309},[54,3375,3376],{"class":713},"t",[54,3378,779],{"class":309},[54,3380,3381],{"class":68},".\"",[54,3383,3384],{"class":713},".encode() ",[54,3386,1456],{"class":709},[54,3388,3389],{"class":713}," raw_body, hashlib.sha256).hexdigest()\n",[54,3391,3392,3395,3398,3401,3404,3407,3409,3412,3415,3418],{"class":56,"line":802},[54,3393,3394],{"class":709},"    return",[54,3396,3397],{"class":713}," hmac.compare_digest(expected, v1) ",[54,3399,3400],{"class":709},"and",[54,3402,3403],{"class":64}," abs",[54,3405,3406],{"class":713},"(time.time() ",[54,3408,1694],{"class":709},[54,3410,3411],{"class":64}," int",[54,3413,3414],{"class":713},"(t)) ",[54,3416,3417],{"class":709},"\u003C=",[54,3419,3420],{"class":713}," tolerance\n",[14,3422,3423],{},"Regenerating the secret takes effect at once, including for calls already queued.",[691,3425,3427],{"id":3426},"delivery","Delivery",[81,3429,3430,3440,3449,3462,3468,3480,3498],{},[84,3431,3432,3435,3436,3439],{},[87,3433,3434],{},"Any 2xx answer counts as delivered."," Anything else, or no answer within\n",[18,3437,3438],{},"WEBHOOKS__TIMEOUT_SECONDS"," (10), is retried after 30 s, 2 min, 10 min, 1 h and\n6 h; after the sixth attempt the delivery is marked failed. Redirects are not\nfollowed, and only the first few kilobytes of the answer are read.",[84,3441,3442,3445,3446,3448],{},[87,3443,3444],{},"At least once."," A call can, rarely, arrive twice: deduplicate on\n",[18,3447,3186],{},". A delivery sent again by hand from the history gets a new id.",[84,3450,3451,3454,3455,3458,3459,31],{},[87,3452,3453],{},"Order is not guaranteed"," across events: use ",[18,3456,3457],{},"created_at"," and the run's\n",[18,3460,3461],{},"finished_at",[84,3463,3464,3467],{},[87,3465,3466],{},"One slow receiver does not hold up the others."," Each Tabularia process keeps\nup to 16 calls in flight, at most 2 of them to the same webhook, and starts the\nnext as soon as one finishes. A webhook whose receiver is down retries on its own\nschedule while the others go out as usual.",[84,3469,3470,3473,3474,2612,3477,1537],{},[87,3471,3472],{},"HTTPS."," The certificate is checked against the name in the URL. Tabularia\ntrusts the usual public certificate authorities; for a receiver with a\ncertificate from your company's own authority, give the gateway its bundle with\n",[18,3475,3476],{},"SSL_CERT_FILE",[18,3478,3479],{},"SSL_CERT_DIR",[84,3481,3482,3485,3486,3489,3490,3493,3494,3497],{},[87,3483,3484],{},"Allowed addresses."," Only ",[18,3487,3488],{},"http","\u002F",[18,3491,3492],{},"https",". Never loopback, link-local\n(including the cloud metadata address 169.254.169.254), multicast or reserved\naddresses, nor the other clouds' metadata addresses (100.100.100.200,\n168.63.129.16, fd00:ec2::254). The name is resolved on every call, checked, and\nthe connection goes to the address that was checked, so a name cannot resolve to\nsomething else in between. Private networks are allowed, since an integration on\nthe same network is the normal case. A forbidden address fails at once and is not\nretried; a name that does not resolve is retried like any other failure. An\naddress that cannot be read (",[18,3495,3496],{},"http:\u002F\u002F0177.0.0.1\u002F",", an empty label, a port above\n65535) is refused when the webhook is saved.",[84,3499,3500,3503,3504,3507],{},[87,3501,3502],{},"The history"," of every call (status, attempts, HTTP code, error, the body\nsent) is kept for ",[18,3505,3506],{},"WEBHOOKS__RETENTION_DAYS"," (30) and shown under each webhook,\nwhere a call can be sent again.",[3509,3510,3511],"style",{},"html pre.shiki code .sqRhv, html code.shiki .sqRhv{--shiki-default:#F69D50;--shiki-light:#6F42C1}html pre.shiki code .swcJU, html code.shiki .swcJU{--shiki-default:#6CB6FF;--shiki-light:#005CC5}html pre.shiki code .sXfbr, html code.shiki .sXfbr{--shiki-default:#96D0FF;--shiki-light:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sxsTv, html code.shiki .sxsTv{--shiki-default:#F47067;--shiki-light:#005CC5}html pre.shiki code .sgHix, html code.shiki .sgHix{--shiki-default:#768390;--shiki-light:#6A737D}html pre.shiki code .s6PUj, html code.shiki .s6PUj{--shiki-default:#F47067;--shiki-light:#D73A49}html pre.shiki code .ssh_m, html code.shiki .ssh_m{--shiki-default:#ADBAC7;--shiki-light:#24292E}html pre.shiki code .sNjOc, html code.shiki .sNjOc{--shiki-default:#F69D50;--shiki-light:#E36209}html pre.shiki code .sJK54, html code.shiki .sJK54{--shiki-default:#8DDB8C;--shiki-light:#005CC5}html pre.shiki code .saVmf, html code.shiki .saVmf{--shiki-default:#DCBDFB;--shiki-light:#6F42C1}",{"title":50,"searchDepth":313,"depth":319,"links":3513},[3514,3515,3516,3520,3524,3529],{"id":35,"depth":313,"text":36},{"id":189,"depth":313,"text":190},{"id":363,"depth":313,"text":364,"children":3517},[3518,3519],{"id":693,"depth":319,"text":694},{"id":1286,"depth":319,"text":1287},{"id":1371,"depth":313,"text":571,"children":3521},[3522,3523],{"id":1557,"depth":319,"text":1558},{"id":2069,"depth":319,"text":2070},{"id":2172,"depth":313,"text":616,"children":3525},[3526,3527,3528],{"id":2193,"depth":319,"text":2194},{"id":2213,"depth":319,"text":2214},{"id":2627,"depth":319,"text":2628},{"id":2836,"depth":313,"text":2837,"children":3530},[3531,3532,3533,3534],{"id":2846,"depth":319,"text":2847},{"id":2919,"depth":319,"text":2920},{"id":3208,"depth":319,"text":3209},{"id":3426,"depth":319,"text":3427},"For whoever connects another system to Tabularia: a scheduler such as Airflow, a\nCI job, a department script. Everything the interface does goes through this\nAPI; the complete, always-current reference is the OpenAPI page the gateway\nserves itself, at https:\u002F\u002F\u003Cyour host>\u002Fapi\u002Fdocs (http:\u002F\u002Flocalhost:8000\u002Fdocs in\ndevelopment). This guide covers the part an integration needs and promises to\nkeep stable. The other integrations are listed in Integrations.","md",{"source":3538},"docs\u002Fapi.md","\u002Fdocs\u002Fapi",{"title":5,"description":3535},"docs\u002Fapi","UrocTPP75l67DAQvLVzIMtUzH91ISBCMOa-bJ89tuFU",[3544,3563,3615,3647,3679,3705,3731,3757],{"title":3545,"icon":3546,"intro":3547,"items":3548},"Getting started","start","Install it on one machine, learn the few ideas everything else is built on, and run a first flow.",[3549,3551,3554,3557,3560],{"path":3550,"title":3545},"\u002Fdocs\u002Fgetting-started",{"path":3552,"title":3553},"\u002Fdocs\u002Fgetting-started-docker-compose","Walkthrough with Docker Compose",{"path":3555,"title":3556},"\u002Fdocs\u002Fdeploy\u002Fminikube","Try it on your computer (minikube)",{"path":3558,"title":3559},"\u002Fdocs\u002Fconcepts","Concepts",{"path":3561,"title":3562},"\u002Fdocs\u002Foverview","Overview (README)",{"title":3564,"icon":3565,"intro":3566,"items":3567},"Capabilities","capabilities","What you can build: visual flows, analysis on any step, AI questions, pixel-perfect reports, dashboards, Python, R and SQL scripts, containers and the orchestration that ties them together.",[3568,3571,3574,3577,3580,3582,3585,3586,3589,3592,3595,3598,3601,3604,3607,3609,3612],{"path":3569,"title":3570},"\u002Fdocs\u002Fcapabilities","All capabilities",{"path":3572,"title":3573},"\u002Fdocs\u002Fcapabilities\u002Fflows","Low-code flows",{"path":3575,"title":3576},"\u002Fdocs\u002Fcapabilities\u002Fpreview-and-analysis","Preview and analysis",{"path":3578,"title":3579},"\u002Fdocs\u002Fcapabilities\u002Fask-your-data","Ask your data with AI",{"path":1377,"title":3581},"Pixel-perfect reports",{"path":3583,"title":3584},"\u002Fdocs\u002Fcapabilities\u002Freport-functions","Report functions",{"path":2177,"title":616},{"path":3587,"title":3588},"\u002Fdocs\u002Fscripting","Python and R",{"path":3590,"title":3591},"\u002Fdocs\u002Fsql-scripts","SQL scripts",{"path":3593,"title":3594},"\u002Fdocs\u002Fcapabilities\u002Fnotebooks","marimo notebooks as apps",{"path":3596,"title":3597},"\u002Fdocs\u002Fcontainer-node","Docker images in flows",{"path":3599,"title":3600},"\u002Fdocs\u002Fcapabilities\u002Fflow-groups","Flow groups: orchestration",{"path":3602,"title":3603},"\u002Fdocs\u002Fcapabilities\u002Fscheduling","Scheduling",{"path":3605,"title":3606},"\u002Fdocs\u002Fpushmail","Pushmail: datasources, reports and dashboards by email",{"path":1447,"title":3608},"Dynamic dates",{"path":3610,"title":3611},"\u002Fdocs\u002Fcapabilities\u002Fengines","Engines",{"path":3613,"title":3614},"\u002Fdocs\u002Fengines\u002Fengine-differences","Engine differences",{"title":3616,"icon":3617,"intro":3618,"items":3619},"Sources","sources","Every way data comes in: files, databases, buckets, SFTP folders, HTTP APIs, SharePoint, Google Sheets, and the results of your own flows.",[3620,3623,3626,3629,3632,3635,3638,3641,3644],{"path":3621,"title":3622},"\u002Fdocs\u002Fsources","All sources",{"path":3624,"title":3625},"\u002Fdocs\u002Fsources\u002Ffiles","Uploaded files",{"path":3627,"title":3628},"\u002Fdocs\u002Fsources\u002Fdatabases","Database tables and queries",{"path":3630,"title":3631},"\u002Fdocs\u002Fs3-datasource","S3 buckets",{"path":3633,"title":3634},"\u002Fdocs\u002Fsftp","SFTP folders",{"path":3636,"title":3637},"\u002Fdocs\u002Fapi-datasource","HTTP APIs",{"path":3639,"title":3640},"\u002Fdocs\u002Fsources\u002Fsharepoint","Excel on SharePoint",{"path":3642,"title":3643},"\u002Fdocs\u002Fsources\u002Fgoogle-sheets","Google Sheets",{"path":3645,"title":3646},"\u002Fdocs\u002Fsources\u002Fflow-outputs","Results of flows and scripts",{"title":3648,"icon":3649,"intro":3650,"items":3651},"Connections","connections","The systems Tabularia reads from and writes to, how each connection is set up, and who may use it.",[3652,3655,3658,3661,3664,3666,3667,3669,3670,3673,3676],{"path":3653,"title":3654},"\u002Fdocs\u002Fconnections","All connections",{"path":3656,"title":3657},"\u002Fdocs\u002Fconnections\u002Fdatabases","Databases",{"path":3659,"title":3660},"\u002Fdocs\u002Fsqlserver","Microsoft SQL Server driver",{"path":3662,"title":3663},"\u002Fdocs\u002Fconnections\u002Fs3","S3 and compatible storage",{"path":3633,"title":3665},"SFTP servers",{"path":3636,"title":3637},{"path":3639,"title":3668},"SharePoint",{"path":3642,"title":3643},{"path":3671,"title":3672},"\u002Fdocs\u002Fconnections\u002Fsmtp","SMTP email",{"path":3674,"title":3675},"\u002Fdocs\u002Fcontainer-registries","Container registries",{"path":3677,"title":3678},"\u002Fdocs\u002Fconnections\u002Fdestinations","Writing data out",{"title":30,"icon":3680,"intro":3681,"items":3682},"integrations","How Tabularia fits your stack: AI providers, the REST API and webhooks, dbt, OpenLineage, single sign-on, SIEM and monitoring.",[3683,3685,3688,3690,3693,3696,3699,3702],{"path":29,"title":3684},"All integrations",{"path":3686,"title":3687},"\u002Fdocs\u002Fintegrations\u002Fai","AI: where it is used",{"path":3539,"title":3689},"API, tokens and webhooks",{"path":3691,"title":3692},"\u002Fdocs\u002Fdbt-export","Export to dbt",{"path":3694,"title":3695},"\u002Fdocs\u002Fopenlineage","OpenLineage",{"path":3697,"title":3698},"\u002Fdocs\u002Fsso-keycloak","Single sign-on with Keycloak",{"path":3700,"title":3701},"\u002Fdocs\u002Fsiem-splunk","Audit log to Splunk",{"path":3703,"title":3704},"\u002Fdocs\u002Fintegrations\u002Fmonitoring","Monitoring",{"title":3706,"icon":3707,"intro":3708,"items":3709},"Security & governance","governance","Who may see and do what, what is recorded, and how data quality and personal data are kept in check.",[3710,3713,3716,3719,3722,3725,3728],{"path":3711,"title":3712},"\u002Fdocs\u002Fgovernance","Overview",{"path":3714,"title":3715},"\u002Fdocs\u002Fgovernance\u002Fpermissions","Permissions",{"path":3717,"title":3718},"\u002Fdocs\u002Fgovernance\u002Faudit-log","Audit log",{"path":3720,"title":3721},"\u002Fdocs\u002Fgovernance\u002Fdata-contracts","Data contracts",{"path":3723,"title":3724},"\u002Fdocs\u002Fdesign\u002Fsso-group-mapping","SSO and group mapping",{"path":3726,"title":3727},"\u002Fdocs\u002Fexamples\u002Fkeycloak","Keycloak example on a laptop",{"path":3729,"title":3730},"\u002Fdocs\u002Fengines\u002Fclickhouse-ai-user","A ClickHouse user for the AI assistant",{"title":3732,"icon":3733,"intro":3734,"items":3735},"Install & operate","operate","Run it in production on Kubernetes, configure it, upgrade it and keep it healthy.",[3736,3739,3742,3745,3748,3751,3754],{"path":3737,"title":3738},"\u002Fdocs\u002Fdeploy\u002Fkubernetes","Kubernetes and Helm",{"path":3740,"title":3741},"\u002Fdocs\u002Fdeploy\u002Fkind","A company-like cluster on one computer (kind)",{"path":3743,"title":3744},"\u002Fdocs\u002Fdeploy\u002Fupgrading","Upgrading, backups and restore",{"path":3746,"title":3747},"\u002Fdocs\u002Fdeploy\u002Frelease-checklist","Install and upgrade checklist",{"path":3749,"title":3750},"\u002Fdocs\u002Fconfiguration","Configuration reference",{"path":3752,"title":3753},"\u002Fdocs\u002Fengine-settings","Settings from the panel",{"path":3755,"title":3756},"\u002Fdocs\u002Fdatabase-migrations","Database migrations",{"title":3758,"icon":3759,"intro":3760,"items":3761},"Reference","reference","Release process and the remaining reference pages.",[3762],{"path":3763,"title":3764},"\u002Fdocs\u002Frelease-process","Release process",1793656371140]