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: access is Public, Private, Protected or Collaborators; role is Editor or Viewer; editor is V2 or Classic.
  • 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 pathWhat it does
GET /v2/pensYour Pens, paginated and filterable (see Filters).
POST /v2/pensCreates 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/:idOne Pen.
PATCH /v2/pens/:idUpdates title, description, tags, template, access, password. Returns the Pen.
DELETE /v2/pens/:idMoves 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/forkCopies 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/settingsUpdates auto_deploy, auto_run, auto_save, indent_with, tab_size. Returns the settings.

Files

Method and pathWhat it does
GET /v2/pens/:id/filesThe saved head’s files, with content.
PUT /v2/pens/:id/filesApplies 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/*pathOne file, with content and url.
PUT /v2/pens/:id/files/*pathWrites one file: JSON content, or a multipart file part. Optional expected_version_id, wait. Returns { pen, version, build }.
DELETE /v2/pens/:id/files/*pathDeletes one file, or every file under a directory. Optional ?expected_version_id=, ?wait=. Returns { pen, version, build }.

Versions, Builds and Deployments

Method and pathWhat it does
GET /v2/pens/:id/versionsSaved versions, paginated, each with its build.
GET /v2/versions/:idOne version with its files (the source) and its build, including the build’s files (the compiled output).
GET /v2/builds/:idA 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/restoreCopies an older version to the head. Returns the Pen.
PUT /v2/pens/:id/deploymentDeploys. Optional version_id, default the saved head. Returns the deployment.
DELETE /v2/pens/:id/deploymentTakes the deployment down. 204.

Collaborators

Method and pathWhat it does
GET /v2/pens/:id/collaboratorsThe Pen’s collaborators.
POST /v2/pens/:id/collaboratorsInvites someone: email_or_username and role. 201 with the pending invites.
PATCH /v2/pens/:id/collaborators/:usernameChanges a collaborator’s role. Returns the collaborators.
DELETE /v2/pens/:id/collaborators/:usernameRemoves a collaborator. 204.
GET /v2/pens/:id/invitesPending invitations.

Collections

Method and pathWhat it does
GET /v2/collectionsYour Collections, paginated; access filter.
POST /v2/collectionsCreates one: title, description, access. 201.
GET /v2/collections/:idOne Collection.
PATCH /v2/collections/:idUpdates title, description, access.
DELETE /v2/collections/:idMoves 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/pensYour Pens in it, paginated.
PUT /v2/collections/:id/pens/:pen_idAdds a Pen. Returns the Collection.
DELETE /v2/collections/:id/pens/:pen_idRemoves a Pen. 204.

Assets

Method and pathWhat it does
GET /v2/assetsYour Assets, paginated.
POST /v2/assetsAdds a file: a multipart file part (and filename), or JSON filename and text content. 201.
GET /v2/assets/:idOne Asset, with content for text files.
PATCH /v2/assets/:idRenames (filename) and/or replaces the file (JSON content, or a multipart file part).
DELETE /v2/assets/:idDeletes 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)
  • id is the cpid for 2.0 Pens and the hashid for Classic Pens.
  • password is write-only and always null.
  • urls.preview renders the saved head on its own. It 404s for Protected and Collaborators Pens, which need a browser session. A deployed Pen’s public URL is deployment.url.
  • screenshot_urls are JPEG screenshots of the saved head, 16:9, by width: small 320, medium 512, large 800 and xlarge 1280 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 with urls.preview. Null for a 2.0 Pen with no saved version.
  • saved_version is null for Classic Pens and carries no files; read them from GET /v2/pens/:id/files or GET /v2/versions/:id.
  • deployment, when set, is { id, version_id, url, custom_domains, created_at, updated_at }.
  • The Pen returned by POST /v2/pens and POST /v2/pens/:id/fork also has a top-level build: 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)
  • id is the ID of the version the build belongs to.
  • status is Queued, Running, Success, Failed or Canceled. Canceled means the compiler asked for changes (see Builds).
  • rounds is how many compiler builds it took to get here, counted from the version you wrote. It is 1 when the compiler needed no changes.
  • diagnostics are everything the final build reported. errors are the ones with type Error, as a shortcut.
  • applied_patches are the changes the API made to your files on the compiler’s behalf. The version’s files already include them.
  • patches are changes the compiler still requires. Empty unless status is Canceled.
  • files are the compiled output, File objects whose url is under the version’s preview_url. They are included by GET /v2/versions/:id, GET /v2/builds/:id and the write endpoints, only when the build succeeded, and left out of lists and of saved_version.
  • version_id is null while that version is still being put together.
  • next_id and previous_id link 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:

ChangeEffect
{ "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:

  1. The version holding exactly the files you sent. Its build ends Canceled, with next_id set.
  2. The version holding your files with the patches applied. It becomes the Pen’s head. Its build’s applied_patches list what changed, and its previous_id points 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.statusWhat to do
SuccessDone. version.files are the source files and build.files the compiled output.
FailedDone, with errors. Your files are saved; build.errors say why they don’t build. Nothing is retried. Fix the files and write again.
Queued, RunningNot done. Poll GET /v2/builds/:id with build.id.
Canceled with next_idThe 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_idDone, 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:

  1. Wait until the build is done, following next_id if there is one.
  2. Read the files again, from GET /v2/pens/:id/files or from GET /v2/versions/:id with the finished build’s version_id. Don’t reuse your own copy of what you sent.
  3. Send that version’s ID as expected_version_id with 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.files are the source files (think src): what you read, edit and write back. Their url serves the file raw.
  • build.files are the compiled output (think dist): what preview_url and a deployment serve. Their url is under the version’s preview_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=.

EndpointFields
POST /v2/penspen: 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/fileschanges: 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/*pathfile: the part to write at path; expected_version_id.
POST /v2/assetsfile: the part; filename (default: the part’s own filename).
PATCH /v2/assets/:idfile 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.

ParamListsValues
accessPens, CollectionsComma list of Public, Private and, for Pens, Protected, Collaborators.
deployedPenstrue or false.
templatePenstrue or false.
forksPensAll (default), Forked or NotForked.
editorPensClassic or V2.
tagsPensComma list of up to three tags; matches Pens tagged with any of them.
search_termPensMatches 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_page sets the page size (default 30, maximum 100) and page picks the page, starting at 1.
  • sort is created (default), updated or, for /v2/pens and /v2/collections, popularity. direction is asc or desc (default), so lists come newest first.
  • The body is a JSON array of the page’s items.
  • A Link header 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.

Was this article helpful?
YesNo
Last updated: October 1, 2026