The REST surface of the CodePen API v2: plain JSON over HTTP, for scripts, CI jobs and any HTTP client. It offers the same operations as the GraphQL API v2, and both run the same logic, so a change made through one is visible through the other. Keys, scope, limits and error codes are shared and described on the API v2 page.
Requests
Every request goes to https://api.codepen.io/v2/ and carries your key in the Authorization header (or an X-API-Key header).
GET https://api.codepen.io/v2/pens
Authorization: Bearer cp_user_your_key_here
Content-Type: application/jsonCode language: HTTP (http)
Write endpoints take a JSON body, or a multipart/form-data body when they upload files (see Uploads below). Unknown keys are ignored. Responses are JSON objects, or JSON arrays for lists. Endpoints that create answer 201; endpoints that remove something and have nothing to return answer 204.
Conventions
- Keys are snake_case in requests and responses.
- Enum values are accepted in any case and returned in PascalCase:
accessisPublic,Private,ProtectedorCollaborators;roleisEditororViewer;editorisV2orClassic. - Classic Pens (
"editor": "Classic") are read-only and read as files. Versions, builds and deployments exist for 2.0 Pens only. - Every write to a Pen’s files makes a new version and builds it, and the compiler may change your files along the way. Wait for the build and read the files again before writing again (see Builds).
- Timestamps are RFC 3339 in UTC. IDs are the ones in CodePen URLs.
- There is no endpoint for the key owner: the key says who that is, and every list (
/v2/pens,/v2/collections,/v2/assets) is theirs.
Endpoints
Pens
| Method and path | What it does |
|---|---|
GET /v2/pens | Your Pens, paginated and filterable (see Filters). |
POST /v2/pens | Creates a 2.0 Pen and builds it. JSON, or a multipart form with file parts. Optional wait. 201 with the Pen and its build. |
GET /v2/pens/:id | One Pen. |
PATCH /v2/pens/:id | Updates title, description, tags, template, access, password. Returns the Pen. |
DELETE /v2/pens/:id | Moves the Pen to the trash. It’s listed under Deleted Items on the site and can be restored for 3 days. 204. |
POST /v2/pens/:id/fork | Copies one of your 2.0 Pens and builds the copy. Optional title, description, tags, access, password, wait. 201 with the Pen and its build. |
PATCH /v2/pens/:id/settings | Updates auto_deploy, auto_run, auto_save, indent_with, tab_size. Returns the settings. |
Files
| Method and path | What it does |
|---|---|
GET /v2/pens/:id/files | The saved head’s files, with content. |
PUT /v2/pens/:id/files | Applies a list of files changes as one new version and builds it. JSON or multipart. Optional expected_version_id, wait. Returns { pen, version, build }. |
GET /v2/pens/:id/files/*path | One file, with content and url. |
PUT /v2/pens/:id/files/*path | Writes one file: JSON content, or a multipart file part. Optional expected_version_id, wait. Returns { pen, version, build }. |
DELETE /v2/pens/:id/files/*path | Deletes one file, or every file under a directory. Optional ?expected_version_id=, ?wait=. Returns { pen, version, build }. |
Versions, Builds and Deployments
| Method and path | What it does |
|---|---|
GET /v2/pens/:id/versions | Saved versions, paginated, each with its build. |
GET /v2/versions/:id | One version with its files (the source) and its build, including the build’s files (the compiled output). |
GET /v2/builds/:id | A version’s build, by the version’s ID. This is the endpoint to poll while a build is running (see Builds). |
POST /v2/pens/:id/versions/:version_id/restore | Copies an older version to the head. Returns the Pen. |
PUT /v2/pens/:id/deployment | Deploys. Optional version_id, default the saved head. Returns the deployment. |
DELETE /v2/pens/:id/deployment | Takes the deployment down. 204. |
Collaborators
| Method and path | What it does |
|---|---|
GET /v2/pens/:id/collaborators | The Pen’s collaborators. |
POST /v2/pens/:id/collaborators | Invites someone: email_or_username and role. 201 with the pending invites. |
PATCH /v2/pens/:id/collaborators/:username | Changes a collaborator’s role. Returns the collaborators. |
DELETE /v2/pens/:id/collaborators/:username | Removes a collaborator. 204. |
GET /v2/pens/:id/invites | Pending invitations. |
Collections
| Method and path | What it does |
|---|---|
GET /v2/collections | Your Collections, paginated; access filter. |
POST /v2/collections | Creates one: title, description, access. 201. |
GET /v2/collections/:id | One Collection. |
PATCH /v2/collections/:id | Updates title, description, access. |
DELETE /v2/collections/:id | Moves the Collection to the trash; its Pens are untouched. Like a Pen, it’s listed under Deleted Items on the site and can be restored for 3 days. 204. |
GET /v2/collections/:id/pens | Your Pens in it, paginated. |
PUT /v2/collections/:id/pens/:pen_id | Adds a Pen. Returns the Collection. |
DELETE /v2/collections/:id/pens/:pen_id | Removes a Pen. 204. |
Assets
| Method and path | What it does |
|---|---|
GET /v2/assets | Your Assets, paginated. |
POST /v2/assets | Adds a file: a multipart file part (and filename), or JSON filename and text content. 201. |
GET /v2/assets/:id | One Asset, with content for text files. |
PATCH /v2/assets/:id | Renames (filename) and/or replaces the file (JSON content, or a multipart file part). |
DELETE /v2/assets/:id | Deletes the Asset from storage. 204. |
Objects
Pen
{
"id": "01a08738-73f0-7847-b6a8-5789d289c158",
"title": "Hello",
"description": "",
"tags": ["api"],
"access": "Private",
"password": null,
"template": false,
"popularity": 12,
"editor": "V2",
"created_at": "2026-09-09T22:23:39Z",
"updated_at": "2026-09-09T22:23:40Z",
"urls": {
"editor": "https://codepen.io/editor/yourname/pen/01a0.../89bb...",
"preview": "https://89bb....codepen.dev/"
},
"screenshot_urls": {
"small": "https://89bb....codepen.dev/.codepen/screenshots/acb9.../320.jpg",
"medium": "https://89bb....codepen.dev/.codepen/screenshots/acb9.../512.jpg",
"large": "https://89bb....codepen.dev/.codepen/screenshots/acb9.../800.jpg",
"xlarge": "https://89bb....codepen.dev/.codepen/screenshots/acb9.../1280.jpg"
},
"saved_version": {
"id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"pen_id": "01a08738-73f0-7847-b6a8-5789d289c158",
"hash": "acb9...",
"created_at": "2026-09-09T22:23:39Z",
"deployed": false,
"build": {
"id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"status": "Success",
"...": "..."
},
"preview_url": "https://pv-01a0....codepen.dev/"
},
"deployment": null,
"settings": {
"auto_deploy": false,
"auto_run": true,
"auto_save": true,
"indent_with": "spaces",
"tab_size": 2
}
}Code language: JSON / JSON with Comments (json)
idis the cpid for 2.0 Pens and the hashid for Classic Pens.passwordis write-only and always null.urls.previewrenders the saved head on its own. It 404s for Protected and Collaborators Pens, which need a browser session. A deployed Pen’s public URL isdeployment.url.screenshot_urlsare JPEG screenshots of the saved head, 16:9, by width:small320,medium512,large800 andxlarge1280 pixels. A screenshot is rendered the first time it is requested, so the first fetch can take a few seconds. Protected and Collaborators Pens answer a not-found image, as withurls.preview. Null for a 2.0 Pen with no saved version.saved_versionis null for Classic Pens and carries nofiles; read them fromGET /v2/pens/:id/filesorGET /v2/versions/:id.deployment, when set, is{ id, version_id, url, custom_domains, created_at, updated_at }.- The Pen returned by
POST /v2/pensandPOST /v2/pens/:id/forkalso has a top-levelbuild: the build of the new Pen’s newest version.
File
{
"id": "01a0884b-3177-71c3-a1c2-9452bfb9a3fb",
"path": "/src/app.js",
"name": "app.js",
"content_hash": "0a28...",
"is_binary": false,
"content": "console.log(1)",
"url": "https://01a08738-73f0-7847-b6a8-5789d289c158.codepenusercontent.com/src/app.js"
}Code language: JSON / JSON with Comments (json)
id is stable across versions, renames and edits. content is null for binary files. url serves any 2.0 file raw and is null for a Classic Pen’s files, which are built on read.
Version
{
"id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"pen_id": "01a08738-73f0-7847-b6a8-5789d289c158",
"hash": "acb9...",
"created_at": "2026-09-09T22:23:39Z",
"deployed": false,
"build": { "...": "..." },
"preview_url": "https://pv-01a0....codepen.dev/",
"files": [{ "...": "..." }]
}Code language: JSON / JSON with Comments (json)
files are the version’s source files. They are included by GET /v2/versions/:id and by the file-write endpoints, and left out of lists and of saved_version. build is the version’s Build.
Build
{
"id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"status": "Success",
"started_at": "2026-09-09T22:23:40Z",
"ended_at": "2026-09-09T22:23:43Z",
"rounds": 1,
"diagnostics": [],
"errors": [],
"applied_patches": [],
"patches": [],
"files": [{ "...": "..." }],
"version_id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"next_id": null,
"previous_id": null
}Code language: JSON / JSON with Comments (json)
idis the ID of the version the build belongs to.statusisQueued,Running,Success,FailedorCanceled.Canceledmeans the compiler asked for changes (see Builds).roundsis how many compiler builds it took to get here, counted from the version you wrote. It is 1 when the compiler needed no changes.diagnosticsare everything the final build reported.errorsare the ones withtypeError, as a shortcut.applied_patchesare the changes the API made to your files on the compiler’s behalf. The version’s files already include them.patchesare changes the compiler still requires. Empty unlessstatusisCanceled.filesare the compiled output, File objects whoseurlis under the version’spreview_url. They are included byGET /v2/versions/:id,GET /v2/builds/:idand the write endpoints, only when the build succeeded, and left out of lists and ofsaved_version.version_idis null while that version is still being put together.next_idandprevious_idlink the two versions one write can produce: the version you wrote, and the version with the compiler’s changes applied.
A diagnostic:
{
"type": "Error",
"scope": "File",
"message": "Unexpected token",
"code": null,
"url": null,
"block_name": "typescript",
"path": "/src/app.ts",
"file_id": "01a0...",
"range": { "start": { "line": 3, "column": 12 }, "end": null }
}Code language: JSON / JSON with Comments (json)
type is Error, Warning, Information or Hint. scope is Build (about the whole build), BuildStep (about one step of it) or File. Lines and columns start at 1.
A patch:
{
"type": "create",
"path": "/tsconfig.json",
"file_id": null,
"content": "{...}"
}Code language: JSON / JSON with Comments (json)
type is create, update or delete. An update or delete carries the file_id of the file it changes; a delete has no content.
Collaborator and Invite
{
"username": "teammate",
"name": "Team Mate",
"role": "Editor",
"is_team_member": false
}Code language: JSON / JSON with Comments (json)
{
"id": "01a0...",
"email": null,
"username": "teammate",
"role": "Editor",
"created_at": "2026-09-09T22:23:39Z"
}Code language: JSON / JSON with Comments (json)
role is Owner, Editor or Viewer. One of an invite’s email and username is set.
Collection
{
"id": "QbaoZL",
"title": "Demos",
"description": "",
"access": "Public",
"items_count": 3,
"popularity": 4,
"created_at": "2026-09-09T22:23:39Z",
"updated_at": "2026-09-09T22:23:40Z",
"url": "https://codepen.io/collection/QbaoZL"
}Code language: JSON / JSON with Comments (json)
Asset
{
"id": "k7Ym0R",
"filename": "logo.png",
"url": "https://assets.codepen.io/1/logo.png",
"is_binary": true,
"created_at": "2026-09-09T22:23:39Z",
"updated_at": "2026-09-09T22:23:40Z"
}Code language: JSON / JSON with Comments (json)
GET /v2/assets/:id adds content for text Assets (SVG, HTML, CSS, Sass, JS, TS, JSON, Markdown, plain text and similar); it is null for binary ones, whose bytes are at url. Lists and the write endpoints leave content out.
Privacy and Passwords
POST /v2/pens, POST /v2/pens/:id/fork and PATCH /v2/pens/:id take access and password. Anything but Public needs a PRO plan (otherwise 403). Protected needs a password of at least 8 characters; a fork of a Protected Pen is Protected too, so it needs one as well. On a Pen that is already Protected, a PATCH with password alone replaces the password. A password with any other access is a 400.
curl -X PATCH https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "access": "Protected", "password": "correct horse battery" }'Code language: Bash (bash)
Files
Files belong to 2.0 Pens; a Classic Pen’s files can be read but not written. Paths are full paths like /src/app.js (a leading / is added if missing). PUT /v2/pens/:id/files takes a list of files changes, applied in order. Each change addresses a file by path or by id and says what to do with it:
| Change | Effect |
|---|---|
{ "path", "content" } | Writes the file at path, creating it if needed. |
{ "id", "content" } | Writes the file with that ID, wherever it is. |
{ "id", "path" } | Moves the file with that ID to path. It keeps its ID. Add content to write there too. |
{ "path", "delete": true } | Deletes the file, or every file under a directory path. |
{ "id", "delete": true } | Deletes the file with that ID. |
content is text. A binary file is sent as a part of a multipart form instead (see Uploads). Reading back, content is null for binary files, and every 2.0 file, text or binary, is served at its url.
Pass expected_version_id (the saved_version.id your changes were built on) and the write is refused with a 409 if the Pen’s head has moved since. The API derives content hashes and the version hash itself; you never send them.
Every write is built, and the build may change the files you wrote. Read the next section before making more than one write to a Pen.
Builds
Pens are compiled, so a write works like a push to a repository with CI attached. Every write (PUT /v2/pens/:id/files, PUT and DELETE /v2/pens/:id/files/*path, POST /v2/pens, POST /v2/pens/:id/fork) saves your files as a new version and builds it. The response carries the newest version the write produced and its build.
When the Compiler Asks for Changes
The compiler sometimes needs the source files to change before it can build them. Adding a .ts file for the first time calls for a /tsconfig.json. An index.html that links to a JavaScript file using ES Modules needs type="module" on its <script> tag. An imported package needs to be in package.json. The Pen’s config file has to match the kinds of files in the Pen. A Pen that formats on save gets formatted.
These changes are patches. The API applies them for you, builds again, and saves the result as one more version, however many builds that takes (at most five). So a write produces one version, or two when the compiler asked for changes:
- The version holding exactly the files you sent. Its build ends
Canceled, withnext_idset. - The version holding your files with the patches applied. It becomes the Pen’s head. Its build’s
applied_patcheslist what changed, and itsprevious_idpoints back to the first.
A patch can create a file, and it can also change or delete a file you just wrote. After a write, the Pen’s files may not be the files you sent.
Waiting and Polling
A write waits for its build before answering. wait is the number of seconds: 45 by default, at most 75, and 0 to answer as soon as your files are saved. Send it in the JSON body or as ?wait=. The build carries on whether or not the request is still waiting for it.
When the response comes back, look at build:
build.status | What to do |
|---|---|
Success | Done. version.files are the source files and build.files the compiled output. |
Failed | Done, with errors. Your files are saved; build.errors say why they don’t build. Nothing is retried. Fix the files and write again. |
Queued, Running | Not done. Poll GET /v2/builds/:id with build.id. |
Canceled with next_id | The API is applying the compiler’s changes. Poll GET /v2/builds/:id with next_id; that build is Running until its version is saved. |
Canceled without next_id | Done, but changes are still required: a patch could not be applied, or five builds weren’t enough. build.patches lists them, and build.diagnostics says what happened. Apply them yourself and write again. |
Writing More Than Once
Before you write to the same Pen again:
- Wait until the build is done, following
next_idif there is one. - Read the files again, from
GET /v2/pens/:id/filesor fromGET /v2/versions/:idwith the finished build’sversion_id. Don’t reuse your own copy of what you sent. - Send that version’s ID as
expected_version_idwith the next write.
A write made while the previous one is still building is based on files that are about to change. With expected_version_id it is refused with a 409 once the head has moved. Without it, the write can undo a change the compiler made, and the next build has to make it again.
Source Files and Output Files
version.filesare the source files (thinksrc): what you read, edit and write back. Theirurlserves the file raw.build.filesare the compiled output (thinkdist): whatpreview_urland a deployment serve. Theirurlis under the version’spreview_url. They exist only when the build succeeded.
The Examples below include a full write, poll and re-read.
Uploads
Binary files go up as multipart/form-data (curl -F). The JSON that would have been the body goes in a text field beside them. The whole request is capped at 120 MB and one file at 30 MB; your plan’s own limit applies on save. An empty part answers 400. On a multipart request, set the build wait with ?wait=.
| Endpoint | Fields |
|---|---|
POST /v2/pens | pen: the JSON body (optional). One part per uploaded file, named by the file’s path. Text files may still go in pen.files with content. |
PUT /v2/pens/:id/files | changes: the JSON body (optional), expected_version_id. One part per uploaded file, named by the file’s path; each is one more change, applied after those in changes. |
PUT /v2/pens/:id/files/*path | file: the part to write at path; expected_version_id. |
POST /v2/assets | file: the part; filename (default: the part’s own filename). |
PATCH /v2/assets/:id | file and/or filename. |
A text file can go either way: content in JSON, or a part. A JSON body is capped at 16 MB.
Filters
GET /v2/pens and GET /v2/collections take filters as query params beside the paging ones. Every filter given must match.
| Param | Lists | Values |
|---|---|---|
access | Pens, Collections | Comma list of Public, Private and, for Pens, Protected, Collaborators. |
deployed | Pens | true or false. |
template | Pens | true or false. |
forks | Pens | All (default), Forked or NotForked. |
editor | Pens | Classic or V2. |
tags | Pens | Comma list of up to three tags; matches Pens tagged with any of them. |
search_term | Pens | Matches the title or description. Web search syntax, up to 200 characters. |
Tags are matched the way they are saved: lowercased, spaces as dashes, punctuation dropped (Two Words! is two-words). In search_term, all words have to appear, "quoted words" stay together and -word excludes one.
Pagination
List endpoints page the way GitHub’s REST API does:
per_pagesets the page size (default 30, maximum 100) andpagepicks the page, starting at 1.sortiscreated(default),updatedor, for/v2/pensand/v2/collections,popularity.directionisascordesc(default), so lists come newest first.- The body is a JSON array of the page’s items.
- A
Linkheader carries the other pages. Follow its URLs rather than building your own; they keep the request’s other query params.
link: <https://api.codepen.io/v2/pens?page=1&per_page=30>; rel="prev",
<https://api.codepen.io/v2/pens?page=3&per_page=30>; rel="next",
<https://api.codepen.io/v2/pens?page=1&per_page=30>; rel="first",
<https://api.codepen.io/v2/pens?page=21&per_page=30>; rel="last"Code language: HTTP (http)
rel="prev" is absent on the first page and rel="next" on the last. rel="first" and rel="last" are always present, and rel="last" counts under the same filters as the request.
Here is the whole loop in JavaScript:
async function fetchAllPens(apiKey) {
const pens = [];
let url = "https://api.codepen.io/v2/pens?per_page=100";
while (url) {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!response.ok) throw new Error(`CodePen API: ${response.status}`);
pens.push(...(await response.json()));
const link = response.headers.get("link") || "";
const next = link.match(/<([^>]+)>;\s*rel="next"/);
url = next ? next[1] : null;
}
return pens;
}Code language: JavaScript (javascript)
Errors
Every error body is { "message": "...", "code": "..." } with a matching HTTP status. The codes are listed on the API v2 page. A 409 also carries expected_version_id and current_version_id:
{
"message": "the Pen's head is 01a0...c5, not 01a0...99; read it again and retry",
"code": "CONFLICT",
"expected_version_id": "01a0...99",
"current_version_id": "01a0...c5"
}Code language: JSON / JSON with Comments (json)
A build that fails is not an error response: the write answers 200 (or 201) with your files saved and build.status Failed.
Examples
IDs are illustrative. $KEY is your API key.
Read a Pen and Its Files
curl https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158 \
-H "Authorization: Bearer $KEY"
curl https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
List Pens
Public 2.0 Pens that are not forks, tagged gsap or svg, mentioning “cards” in their title or description, oldest first:
curl -i "https://api.codepen.io/v2/pens?access=Public&editor=V2&forks=NotForked&tags=gsap,svg&search_term=cards&sort=created&direction=asc&per_page=50" \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Then follow the rel="next" URL from the Link header for the next page.
Create a Pen
You get starter files when files is omitted. The title defaults to “Untitled” and access to your default privacy setting.
curl -X POST https://api.codepen.io/v2/pens \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"title": "Hello",
"description": "Made with the API",
"tags": ["api", "demo"],
"access": "Private",
"files": [
{ "path": "index.html", "content": "<h1>Hello</h1>" },
{ "path": "style.css", "content": "h1 { color: teal }" }
]
}'Code language: Bash (bash)
201 with the Pen and its build. The new Pen is built like any other write, so the files in it may be more than the files you sent (see Builds). wait, in the body or as ?wait=, sets how long the request waits for the build.
Update a Pen
Only the keys given change. tags replaces the whole list. Changing these details doesn’t touch the Pen’s files, so it makes no version and no build.
curl -X PATCH https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "title": "Hello, world", "tags": ["api", "demo", "v2"], "access": "Public" }'Code language: Bash (bash)
Write Several Files as One Version
Move script.js to src/app.js by its ID and give it new content, rewrite index.html by path, and delete style.css, all in one saved version.
curl -X PUT https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"expected_version_id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2",
"files": [
{ "id": "01a0884b-3177-71c3-a1c2-9452bfb9a3fb", "path": "src/app.js", "content": "console.log(\"moved\")" },
{ "path": "index.html", "content": "<script src=\"src/app.js\"></script>" },
{ "path": "style.css", "delete": true }
]
}'Code language: Bash (bash)
The response is { "pen": ..., "version": ..., "build": ... }: the newest version with its source files, and that version’s build with the compiled output. A stale expected_version_id answers 409.
{
"pen": { "...": "..." },
"version": {
"id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"files": [{ "...": "..." }],
"...": "..."
},
"build": {
"id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"status": "Success",
"started_at": "2026-09-09T22:25:02Z",
"ended_at": "2026-09-09T22:25:05Z",
"rounds": 1,
"diagnostics": [],
"errors": [],
"applied_patches": [],
"patches": [],
"files": [
{
"id": "01a0...",
"path": "/index.html",
"name": "index.html",
"content_hash": "9f2e...",
"is_binary": false,
"content": "...",
"url": "https://pv-01a0....codepen.dev/index.html"
},
{
"id": "01a0...",
"path": "/src/app.js",
"name": "app.js",
"content_hash": "71c0...",
"is_binary": false,
"content": "...",
"url": "https://pv-01a0....codepen.dev/src/app.js"
}
],
"version_id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"next_id": null,
"previous_id": null
}
}Code language: JSON / JSON with Comments (json)
Write, Then Wait for the Build
Add a TypeScript file to a Pen that has none. With wait=0 the write answers as soon as the file is saved, with the build still to run:
curl -X PUT "https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files/src/app.ts?wait=0" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "content": "const n: number = 1;", "expected_version_id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2" }'Code language: Bash (bash)
{
"pen": { "...": "..." },
"version": { "id": "01a08853-f1ab-7967-812e-eda134bd9e61", "...": "..." },
"build": {
"id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"status": "Queued",
"rounds": 1,
"...": "..."
}
}Code language: JSON / JSON with Comments (json)
Poll the build:
curl https://api.codepen.io/v2/builds/01a08853-f1ab-7967-812e-eda134bd9e61 \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
The compiler asked for a /tsconfig.json, so this build ends Canceled, and next_id says the API is applying the change:
{
"id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"status": "Canceled",
"rounds": 1,
"patches": [
{
"type": "create",
"path": "/tsconfig.json",
"file_id": null,
"content": "{...}"
}
],
"next_id": "01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d",
"previous_id": null,
"...": "..."
}Code language: JSON / JSON with Comments (json)
Poll GET /v2/builds/01a08854-... next. It answers Running until the second version is saved, then:
{
"id": "01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d",
"status": "Success",
"rounds": 2,
"applied_patches": [
{
"type": "create",
"path": "/tsconfig.json",
"file_id": null,
"content": "{...}"
}
],
"files": [{ "...": "..." }],
"version_id": "01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d",
"next_id": null,
"previous_id": "01a08853-f1ab-7967-812e-eda134bd9e61",
"...": "..."
}Code language: JSON / JSON with Comments (json)
Now read the source files again, and use that version’s ID as expected_version_id for the next write:
curl https://api.codepen.io/v2/versions/01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Here is the whole loop in JavaScript:
const API = "https://api.codepen.io/v2";
async function api(apiKey, path, options = {}) {
const response = await fetch(`${API}${path}`, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
});
if (!response.ok) throw new Error(`CodePen API: ${response.status}`);
return response.json();
}
// Follows a build until it is done, through next_id when the API is
// applying the compiler's changes.
async function settle(apiKey, build) {
for (;;) {
const running = build.status === "Queued" || build.status === "Running";
const next = build.status === "Canceled" ? build.next_id : null;
if (!running && !next) return build;
await new Promise((resolve) => setTimeout(resolve, 2000));
build = await api(apiKey, `/builds/${next || build.id}`);
}
}
async function writeFiles(apiKey, penId, expectedVersionId, files) {
const written = await api(apiKey, `/pens/${penId}/files`, {
method: "PUT",
body: JSON.stringify({ expected_version_id: expectedVersionId, files })
});
const build = await settle(apiKey, written.build);
// The source files as they are now, which may not be the files sent.
const version = await api(apiKey, `/versions/${build.version_id}`);
return { version, build };
}Code language: JavaScript (javascript)
Pass the returned version.id as expectedVersionId the next time, and build the next changes from version.files.
Write One File
curl -X PUT https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files/src/app.js \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "content": "console.log(1)", "expected_version_id": "01a08738-73f0-7848-b8bf-e8e7e91b1ae2" }'Code language: Bash (bash)
Delete One File
A directory path deletes every file under it.
curl -X DELETE "https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files/src/app.js?expected_version_id=01a08738-73f0-7848-b8bf-e8e7e91b1ae2" \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Upload a Binary File
The single-file PUT takes the file as a part named file. The bulk PUT and POST /v2/pens take one part per file, named by its path.
curl -X PUT https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files/img/logo.png \
-H "Authorization: Bearer $KEY" \
-F file=@logo.png -F expected_version_id=01a08738-73f0-7848-b8bf-e8e7e91b1ae2
curl -X PUT https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files \
-H "Authorization: Bearer $KEY" \
-F changes='{ "files": [{ "path": "index.html", "content": "<img src=\"img/logo.png\">" }] }' \
-F /img/logo.png=@logo.pngCode language: Bash (bash)
Read it back and you get the file’s metadata, with content null and the bytes at url:
curl https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/files/img/logo.png \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
{
"id": "01a08853-f1aa-7966-82f1-6d37d7140e34",
"path": "/img/logo.png",
"name": "logo.png",
"content_hash": "3c1b...",
"is_binary": true,
"content": null,
"url": "https://01a08738-73f0-7847-b6a8-5789d289c158.codepenusercontent.com/img/logo.png"
}Code language: JSON / JSON with Comments (json)
Deploy and Undeploy
version_id defaults to the saved head.
curl -X PUT https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/deployment \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{}'Code language: Bash (bash)
{
"id": "kRxnnr",
"version_id": "01a08845-072e-71fb-80ec-82b53a7fa110",
"url": "https://cheerful-term-baboon.codepen.app/",
"custom_domains": [],
"created_at": "2026-09-09T22:23:40Z",
"updated_at": "2026-09-09T22:23:40Z"
}Code language: JSON / JSON with Comments (json)
curl -X DELETE https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/deployment \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Update Settings
curl -X PATCH https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/settings \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "auto_deploy": true, "indent_with": "tabs", "tab_size": 4 }'Code language: Bash (bash)
Fork and Delete a Pen
A fork copies one of your 2.0 Pens and keeps the original’s title, description, tags and access unless given. The copy is built, and the response is the new Pen with its build. A delete moves the Pen to the trash. You can restore it from Deleted Items on the site for 3 days.
curl -X POST https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/fork \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "title": "My copy" }'
curl -X DELETE https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158 \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Versions: List, Inspect, Restore
curl -i "https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/versions?per_page=10" \
-H "Authorization: Bearer $KEY"
curl https://api.codepen.io/v2/versions/01a08738-73f0-7848-b8bf-e8e7e91b1ae2 \
-H "Authorization: Bearer $KEY"
curl -X POST https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/versions/01a08738-73f0-7848-b8bf-e8e7e91b1ae2/restore \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
The restore answers with the Pen, whose saved_version is the new head. A restored version keeps the build of the version it copies; it is not built again.
Collaborators
curl -X POST https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/collaborators \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "email_or_username": "teammate", "role": "Editor" }'
curl -X PATCH https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/collaborators/teammate \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "role": "Viewer" }'
curl -X DELETE https://api.codepen.io/v2/pens/01a08738-73f0-7847-b6a8-5789d289c158/collaborators/teammate \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Collections
curl -X POST https://api.codepen.io/v2/collections \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "title": "Demos", "access": "Public" }'
curl -X PUT https://api.codepen.io/v2/collections/QbaoZL/pens/01a08738-73f0-7847-b6a8-5789d289c158 \
-H "Authorization: Bearer $KEY"
curl -i "https://api.codepen.io/v2/collections/QbaoZL/pens?per_page=20" \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Deleting a Collection moves the Collection itself to the trash; the Pens in it are untouched. Like a Pen, you can restore it from Deleted Items on the site for 3 days.
curl -X DELETE https://api.codepen.io/v2/collections/QbaoZL \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
204 No Content.
Assets
Add a file to your Asset Hosting as a multipart file part. filename defaults to the part’s own filename.
curl -X POST https://api.codepen.io/v2/assets \
-H "Authorization: Bearer $KEY" \
-F file=@logo.png -F filename=logo.pngCode language: Bash (bash)
A text Asset may be sent as JSON instead:
curl -X POST https://api.codepen.io/v2/assets \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "filename": "theme.css", "content": "body { color: red }" }'Code language: Bash (bash)
PATCH renames, replaces the file, or both. Read, list and delete:
curl -X PATCH https://api.codepen.io/v2/assets/k7Ym0R \
-H "Authorization: Bearer $KEY" \
-F file=@logo-v2.png
curl https://api.codepen.io/v2/assets/k7Ym0R \
-H "Authorization: Bearer $KEY"
curl -i "https://api.codepen.io/v2/assets?sort=created&per_page=50" \
-H "Authorization: Bearer $KEY"
curl -X DELETE https://api.codepen.io/v2/assets/k7Ym0R \
-H "Authorization: Bearer $KEY"Code language: Bash (bash)
Filenames must be unique among your Assets and cannot contain / or \. A rename changes the Asset’s url. New content counts against your plan’s per-file size and total storage (403 when over). Assets are not part of any Pen, so writing one makes no version and no build.