Your first request, and the ids everything else needs.
Getting started
The NodePen API lets a script read what a team holds. Everything lives under /api/v1 on
https://app.nodepen.io. Anything outside that prefix is the web app talking to itself — those
endpoints change with the UI and will refuse an API token.
The whole surface is read-only. Every endpoint is a GET, and no scope changes anything.
The shape of a document in particular is the app's own internal type and will change. Nothing here is deleted or renamed without warning, but do not assume the stability you would expect of a frozen API yet. Tell us what you are building and it will shape what stabilises first.
Your first request
Create a token in Team settings → API tokens, then start here. Everything else needs an id, and this is where you get the first one. The token already says which team, so there is nothing to pass.
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
https://app.nodepen.io/api/v1/team
{ "team": { "name": "My Team", "rootFolderId": "O7VSE6B0KZE2" } }
A folder, and what is in it
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
https://app.nodepen.io/api/v1/folders/O7VSE6B0KZE2
root works in place of an id and gives you the same folder /api/v1/team points at, so a script
that only wants to walk the tree can start here and skip the call above.
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
https://app.nodepen.io/api/v1/folders/root
{
"folder": {
"id": "O7VSE6B0KZE2",
"name": "Root Folder",
"parentId": null,
"path": [],
"folders": [{ "id": "K2P0LMN4QRST", "name": "Studies" }],
"documents": [
{
"id": "PH1J4SA5JBEK",
"name": "Bridge study",
"visibility": "private",
"createdAt": "2026-09-04T09:21:11.972Z",
"updatedAt": "2026-09-04T09:21:12.402Z"
}
]
}
}
path is the ancestors of this folder, root first, and is empty at the root. folders and
documents are only what is directly inside — walk folders to go deeper.
Finding a document
Walking folders only finds a file if you already know which folder it is in. This is the other way round — the same search the search bar on the team page runs, over everything the team keeps:
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
"https://app.nodepen.io/api/v1/documents?q=bridge"
q matches anywhere in a name and ignores case, so bridge finds Bridge study and
Cable-stayed bridge alike. Names the query starts come first, and the rest are ordered by when
they were last changed. % and _ are matched as themselves rather than as wildcards, so
searching for 50% finds 50% scale.
Leave q off and you get the team's documents, newest first — a bare /api/v1/documents is the
"what has this team got" call rather than an error.
folderId narrows it to one folder and everything inside it, at any depth. root works in place
of an id here just as it does above. A folder that is not there and a folder that is not yours are
both 404, in the same words.
folder is where each document is kept: hand its id to /api/v1/folders/{folderId} to see what
else is in there. isRoot marks the folder a file sits in when nobody has filed it anywhere — that
folder's name is an internal placeholder, so show your team's own name against it rather than what
is in name.
At most 50 documents come back and there is no paging yet, so narrow with q or folderId rather
than expecting to walk the whole team.
Reading a document
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
https://app.nodepen.io/api/v1/documents/PH1J4SA5JBEK
The nested document is the script itself, and it is the same live state the editor is showing —
edits made a second ago are in it. document.version is the number to branch on if this shape
changes. It is 1 and has never been anything else.
Within a node, inputs and outputs map port ids to their order; sources is the wiring, mapping
one of your input ports to the upstream { nodeInstanceId, portInstanceId } feeding it; and
values holds anything typed in by hand rather than computed.
Reading a solution
A solution is what the solver worked out for a document: a tree of values per node, and optionally the geometry that goes with them.
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
https://app.nodepen.io/api/v1/documents/PH1J4SA5JBEK/solution
data is the solve as the solver produced it, keyed by node instance id — the same values the
editor draws. data.solutionId names this solve, so you can tell one from the next.
Geometry is off by default, because reading and parsing the model file is by far the most expensive part of this endpoint and most callers only want the values:
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
"https://app.nodepen.io/api/v1/documents/PH1J4SA5JBEK/solution?includeGeometry=true"
Asking for geometry is a request, not a guarantee. A solve can have no model file, and reading
one can fail — both still answer 200 with the values, because those are the real result.
includesGeometry is what tells you which you got, so check it rather than assuming.
?format=bimz answers with the same solve as a BIM object document instead: a flat store of objects
keyed by platform id, one per value in a data tree branch, each carrying its geometry and pointing
at the node, port and branch it was computed for. It always attempts geometry, so includeGeometry
does not apply to it.
Exporting a document
Grasshopper (.gh) and Rhino (.3dm), the same two formats the editor offers.
curl -H "Authorization: Bearer $NODEPEN_TOKEN" \
"https://app.nodepen.io/api/v1/documents/PH1J4SA5JBEK/export?format=.3dm&rhinoVersion=8"
The response is a link, not the file. Follow it to download:
curl -o nodepen-export.3dm "$(curl -s -H "Authorization: Bearer $NODEPEN_TOKEN" \
"https://app.nodepen.io/api/v1/documents/PH1J4SA5JBEK/export?format=.gh" | jq -r .export.url)"
format is required and takes the leading dot. rhinoVersion is 7, 8 or 9, defaults to 9,
and is only accepted alongside format=.3dm — sending it with .gh is an error rather than being
quietly ignored. The url stops working at expiresAt, an hour out.
Writing
Not yet, and not by accident. A NodePen document is a CRDT that several people and the agent may have open at once, so "replace this with that JSON" is last-writer-wins against whoever is editing right now — which is precisely what the editor's own save path goes out of its way not to be. Rather than ship a write endpoint that quietly loses work, there is none. If you need one, say what you are trying to do; it changes what the right shape is.