Open Causal API
This page covers every HTTP endpoint for getting a graph into or out of Open Causal in machine-readable form.
- Export:
GET /graph/<id>/json/,GET /graph/<id>/csv/andGET /graph/<id>/dagitty/each return one published post's graph in a different format. - Share:
POST /share/turns a graph you already have into a prefilled submission page, without publishing anything.
Export endpoints
Given the graph_id of a published post, each export endpoint
returns one of the fields Django derives from the submission's raw input
when it is saved. All three:
- are plain
GETrequests, no authentication or CSRF token required; - return
404ifgraph_iddoes not match a post; - send
X-Robots-Tag: noindexso search engines don't index the raw data file itself, only the post page that links to it; - return the field's raw text as-is — no wrapping, no pretty-printing beyond whatever was already stored.
GET /graph/<id>/json/ — CausalJSON
GET /graph/smoking-cancer-abc123/json/
200, Content-Type: application/json —
the graph as an Open Causal Graph Format (CausalJSON)
document, including graph metadata (title, description,
research question, etc.) as well as nodes and edges:
{
"graph": {"title": "Smoking and cancer", "type": "dag"},
"nodes": [{"id": "smoking", "label": "smoking", "role": "covariate"},
{"id": "cancer", "label": "cancer", "role": "covariate"}],
"edges": [{"source": "smoking", "target": "cancer", "type": "directed"}]
}
This field is rewritten on every save that changes the submission, and is
refreshed in bulk for older records by manage.py rebuild_graph_json.
GET /graph/<id>/csv/ — edge list
GET /graph/smoking-cancer-abc123/csv/
200, Content-Type: text/plain — the
graph's edges as CSV, one row per edge with source,
target, type and, where present,
sign and justification columns.
source,target,type
smoking,cancer,directed
A second endpoint, GET /graph/<id>/csv/download, returns
the identical content with Content-Type: text/csv and
Content-Disposition: attachment; filename="opencausal_<id>.csv",
so a browser downloads it as a file instead of displaying it inline.
GET /graph/<id>/dagitty/ — DAGitty
GET /graph/smoking-cancer-abc123/dagitty/
200, Content-Type: text/plain — the
graph in DAGitty syntax, the same format accepted by the Share Graph page's
DAGitty option:
dag {
smoking [pos="0,0"]
cancer [pos="1,1"]
smoking -> cancer
}
Share Graph API
POST /share/ with a JSON body prefills the
Share Graph page with a graph, ready for a title, description, licence and
authors. It does not save anything: no post is created and no files are
written.
It accepts the same three formats read by the same code that reads a submission when it is published, so what a caller sends here is what the record will contain.
format | What code holds |
|---|---|
causaljson |
A CausalJSON document. Its
nodes and edges are used;
graph metadata is ignored except for type,
because the record's own metadata comes from the form. |
csv |
An edge list. A header row is matched case-insensitively with
aliases (source/from/cause,
target/to/effect, plus
optional type, sign and
justification); the delimiter is sniffed; a
two-column paste with no header works and says so in
warnings. |
dagitty |
DAGitty syntax: dag { A -> B; C <-> D },
with optional pos="x,y", label="…"
and per-node exposure, outcome or
latent. The circle-marked operators @->,
@-@ and @-- are accepted too, for graphs
from a discovery algorithm. |
Authentication
The endpoint requires a logged-in session (@login_required);
unauthenticated requests are redirected to the login page (302)
rather than processed.
Because it is a same-origin, cookie-authenticated POST, it is also subject
to Django's standard CSRF protection. Callers must send the current session's
CSRF token in the X-CSRFToken header (read from the
csrftoken cookie), the same way any other authenticated
fetch/AJAX call to this app would.
POST /share/ — prefill the page
Returns the Share Graph page as HTML with the Code field already filled in. The page previews it itself on load, so no image is rendered server-side.
This is separate from the page's own HTML <form>, which
POSTs multipart/form-data and is what actually publishes a
graph. The two are distinguished by request Content-Type: send
application/json to use the prefill API described here.
Request
POST /share/
Content-Type: application/json
X-CSRFToken: <csrf token>
Cookie: <session cookie>
{
"code": "dag { X [pos=\"0,0\"] Y [pos=\"1,1\"] X -> Y }",
"format": "dagitty"
}
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The graph, in the language named by format. |
format | string | Yes | "dagitty", "causaljson" or "csv". |
Response
Success (200) — the full Share Graph page, with the
Code field set to code and the Format field set to
format. If the graph cannot be read, the page still loads with
the field prefilled and a page-level message saying what is wrong: the
graph is not discarded, and it can be fixed inline.
If a CausalJSON code carries its own graph.title,
graph.description, graph.research_question,
graph.statistical_unit, graph.population or
graph.provenance, those seed the form's separate Title,
Description, Research question, Statistical unit, Population and Provenance
fields too — the same fields a submitter fills in by hand further down
the page — rather than leaving the submitter to retype what was already
written. DAGitty and CSV have nowhere to carry these, so this never applies
to them.
Client error (400) — a JSON body describing the problem, for example:
{"error": "Both 'code' and 'format' fields are required."}
Cases that return 400: the body is not valid JSON or not a
JSON object; code or format is missing or not a
string; format is not one of the three.
Example
This is what the Graph Builder's “Share on Open Causal” button does, handing the page over to whatever it just built:
const res = await fetch("/share/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"),
},
body: JSON.stringify({
code: 'dag { X [pos="0,0"] Y [pos="1,1"] X -> Y }',
format: "dagitty",
}),
});
history.pushState({}, "", "/share/"); // the address bar still said the old page
document.open();
document.write(await res.text());
document.close();