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/ and GET /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 GET requests, no authentication or CSRF token required;
  • return 404 if graph_id does not match a post;
  • send X-Robots-Tag: noindex so 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.

formatWhat 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"
}
FieldTypeRequiredDescription
codestringYes The graph, in the language named by format.
formatstringYes "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();