The GraphQL surface of the CodePen API v2: one endpoint, and you ask for exactly the fields you want. It offers the same operations as the REST 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.

Endpoint

POST https://api.codepen.io/v2/graphql
Authorization: Bearer cp_user_your_key_here
Content-Type: application/json

{ "query": "...", "variables": { ... } }Code language: HTTP (http)

A request carries one operation. A JSON array of bodies (batching) is refused, so one request is one tick of the rate limit. A request that uploads a file is a multipart form instead of JSON; see Uploads below.

Playground

We have a GraphiQL Playground here:

https://api.codepen.io/v2/graphiql

Set the Authorization header in its settings, and you can explore the schema and run operations from the browser.

Query Complexity

Queries are limited by complexity, the number of fields they can touch. Each list costs its page limit (default 20, at most 100) times the fields selected under it, and a query is refused past 10,000. A page of 100 Pens with their titles costs 200. 100 Pens each with 100 versions and their files does not fit, so page the inner lists.

Conventions

  • Every mutation takes one input object and returns a payload object: PenPayload { pen build }, PenFilesWritePayload { pen version build }, CollectionPayload { collection }, AssetPayload { asset }.
  • 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).
  • Every list is paginated with the same PaginationInput and returns nodes, pageInfo and totalCount, the usual connection shape without edges. Top-level lists take it as input: { pagination: {...} } beside their filters; Collection.pens and Pen.versions take it as input directly.
  • Enum values are PascalCase: Private, Editor, CreatedAt, Classic.
  • access is Public, Private, Protected or Collaborators for Pens and Public or Private for Collections.
  • editor says which editor a Pen belongs to: V2 or Classic. Classic Pens are read-only and read as files; versions, builds and deployments exist for 2.0 Pens only.
  • Timestamps are RFC 3339 (Time). IDs are the ones in CodePen URLs.
  • There is no query for the key owner: the key says who that is, and every list (pens, collections, assets) is theirs.

Operations

QueryReturns
pen(id)One of your Pens, by cpid or hashid.
pens(input)Your Pens, filtered (see Filters).
penVersion(id)A saved version of one of your Pens.
build(id)A version’s build, by the version’s ID. This is the query to poll while a build is running (see Builds).
collection(id)One of your Collections.
collections(input)Your Collections; access filter.
asset(id)An Asset, a file in your Asset Hosting.
assets(input)Your Assets.
MutationEffect
penCreateA new 2.0 Pen; starter files unless files are given; access with password. Builds it.
penForkCopies one of your 2.0 Pens into a new Pen; access with password. Builds it.
penUpdatetitle, description, tags (replaced), template, access with password.
penDeleteMoves the Pen to the trash. It’s listed under Deleted Items on the site and can be restored for 3 days.
penFilesWriteApplies file changes as one new saved version and builds it (see Builds).
penFileWrite, penFileDeleteOne-file shorthands for penFilesWrite.
penVersionRestoreCopies an older version to the head. Not built again.
penDeploy, penUndeployDeploys a version (default: the saved head); takes the deployment down.
penSettingsUpdateautoDeploy, autoRun, autoSave, indentWith, tabSize.
collaboratorInviteInvites a User by username or email as an Editor or Viewer.
collaboratorUpdateRole, collaboratorRemoveChanges or removes a collaborator by username.
collectionCreate, collectionUpdatetitle, description, access.
collectionDeleteMoves 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.
collectionAddPen, collectionRemovePenMembership.
assetCreateAdds a file to your Asset Hosting: filename, then content (text) or upload.
assetUpdateRenames (filename), replaces the file (content or upload), or both.
assetDeleteDeletes an Asset for good.

Types

Comments give the value set or a note where it is not obvious.

type Pen {
  id: ID! # cpid for 2.0 Pens, hashid for Classic Pens
  title: String!
  description: String!
  tags: [String!]!
  access: AccessLevel! # Public | Private | Protected | Collaborators
  password: String # write-only: always null
  template: Boolean!
  popularity: Int! # the site's score; lists can sort by it
  editor: PenEditor! # Classic | V2
  createdAt: Time!
  updatedAt: Time!
  urls: PenUrls!
  screenshotURLs: PenScreenshotURLs # null for a V2 Pen with no saved version
  files: [PenFile!]! # the saved head's files; built for Classic Pens
  savedVersion: PenVersion # null for Classic Pens
  version(id: ID!): PenVersion
  versions(input: PaginationInput): PenVersionPage! # empty for Classic Pens
  deployment: PenDeployment # null while not deployed
  collaborators: [Collaborator!]!
  collaboratorInvites: [CollaboratorInvite!]!
  settings: PenSettings!
}

type PenUrls {
  editor: String! # the Pen in the editor
  preview: String! # the rendered saved head on its own; 404s for Protected and Collaborators Pens
}

# Screenshots of the saved head: JPEGs, 16:9, rendered on first request (so
# the first fetch can take a few seconds). Like urls.preview, Protected and
# Collaborators Pens need a browser session and answer a not-found image.
type PenScreenshotURLs {
  small: String! # 320 pixels wide
  medium: String! # 512
  large: String! # 800
  xlarge: String! # 1280
}

type PenFile {
  id: ID! # stable across versions, renames and edits
  path: String! # "/src/index.html"
  name: String!
  contentHash: String!
  isBinary: Boolean!
  content: String # null for binary files
  url: String # the file served raw; null for Classic Pens
}

type PenVersion {
  id: ID!
  penID: ID!
  hash: String!
  createdAt: Time!
  build: Build! # status, diagnostics, patches, output; see Builds
  deployed: Boolean!
  files: [PenFile!]! # the source files
  previewUrl: String # the rendered version on its own
}

type Build {
  id: ID! # the ID of the version it belongs to
  status: BuildStatus! # Queued | Running | Success | Failed | Canceled
  startedAt: Time
  endedAt: Time
  rounds: Int! # compiler builds it took; 1 when no changes were needed
  diagnostics: [BuildDiagnostic!]! # everything the final build reported
  errors: [BuildDiagnostic!]! # the diagnostics with type Error
  appliedPatches: [BuildPatch!]! # changes the API made to your files for the compiler
  patches: [BuildPatch!]! # changes still required; empty unless Canceled
  files: [PenFile!]! # the compiled output; empty unless Success
  version: PenVersion # null while that version is still being put together
  next: Build # the build of the version applying this one's patches, if any
  previous: Build # the build of the version you wrote, if this one came from it
}

type BuildDiagnostic {
  type: DiagnosticType! # Error | Warning | Information | Hint
  scope: DiagnosticScope! # Build | BuildStep | File
  message: String!
  code: String
  url: String # documentation for the diagnostic, when there is some
  blockName: String
  path: String # the file, when the diagnostic is about one
  fileID: ID
  range: FileRange # start and end { line column }, starting at 1
}

union BuildPatch = BuildPatchCreate | BuildPatchUpdate | BuildPatchDelete

type BuildPatchCreate {
  path: String!
  content: String!
}

type BuildPatchUpdate {
  fileID: ID!
  path: String!
  content: String!
}

type BuildPatchDelete {
  fileID: ID!
  path: String!
}

type PenDeployment {
  id: ID!
  versionID: ID!
  url: String!
  customDomains: [String!]!
  createdAt: Time!
  updatedAt: Time!
}

type PenSettings {
  autoDeploy: Boolean!
  autoRun: Boolean!
  autoSave: Boolean!
  indentWith: String! # "tabs" | "spaces"
  tabSize: Int! # 1 to 6
}

type Collaborator {
  username: String!
  name: String!
  role: Role! # Owner | Editor | Viewer
  isTeamMember: Boolean!
}

type CollaboratorInvite {
  id: ID!
  email: String # one of email and username is set
  username: String
  role: Role!
  createdAt: Time!
}

type Collection {
  id: ID!
  title: String!
  description: String!
  access: AccessLevel! # Public | Private
  itemsCount: Int!
  popularity: Int!
  createdAt: Time!
  updatedAt: Time!
  url: String!
  pens(input: PaginationInput): PenPage! # your own Pens in it
}

type Asset {
  id: ID!
  filename: String!
  url: String! # where the file is served
  isBinary: Boolean!
  content: String # a text Asset's content; null for binary Assets
  createdAt: Time!
  updatedAt: Time!
}Code language: GraphQL (graphql)

PenFile.content and Asset.content are read from storage when selected, so lists are cheapest without them. A binary file has no content; its bytes are served at its url.

The payloads of the mutations that build:

type PenFilesWritePayload { # penFilesWrite, penFileWrite, penFileDelete
  pen: Pen!
  version: PenVersion! # the newest version the write produced
  build: Build! # that version's build
}

type PenPayload {
  pen: Pen!
  build: Build # set by penCreate and penFork
}Code language: GraphQL (graphql)

Privacy and Passwords

penCreate, penFork and penUpdate take access and password. Anything but Public needs a PRO plan (otherwise FORBIDDEN). 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, penUpdate with password alone replaces the password. A password with any other access is BAD_USER_INPUT. The password is write-only: Pen.password exists so the field reads naturally in a client’s types, but it is always null.

mutation {
  penUpdate(
    input: {
      id: "01a08738-73f0-7847-b6a8-5789d289c158"
      access: Protected
      password: "correct horse battery"
    }
  ) {
    pen {
      access
      password # null
    }
  }
}Code language: GraphQL (graphql)

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). penFilesWrite takes files: [PenFileChangeInput!]!, 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.

A change writes either content (text) or upload (a file sent as a multipart part, see Uploads), not both. Reading back, PenFile.content is null for binary files, and every 2.0 file, text or binary, is served at its url.

Pass expectedVersionID (the savedVersion.id your changes were built on) and the write is refused with CONFLICT 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 (penFilesWrite, penFileWrite, penFileDelete, penCreate, penFork) saves your files as a new version and builds it. The payload 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 pointing at the second version’s build.
  2. The version holding your files with the patches applied. It becomes the Pen’s head. Its build’s appliedPatches list what changed, and its previous 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, a field of each of those mutations’ input, is the number of seconds: 45 by default, at most 75, and 0 to answer as soon as your files are saved. The build carries on whether or not the request is still waiting for it.

When the payload 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 build(id) with build.id.
Canceled with nextThe API is applying the compiler’s changes. Poll build(id) with next.id; that build is Running until its version is saved.
Canceled without nextDone, 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 if there is one.
  2. Read the files again, from the finished build’s version { id files { ... } } or from pen(id) { savedVersion { id } files { ... } }. Don’t reuse your own copy of what you sent.
  3. Send that version’s ID as expectedVersionID with the next write.

A write made while the previous one is still building is based on files that are about to change. With expectedVersionID it is refused with CONFLICT 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

  • PenVersion.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 previewUrl and a deployment serve. Their url is under the version’s previewUrl. 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 parts, following the GraphQL multipart request spec that most GraphQL clients implement. Any input with an upload: Upload field takes one: PenFileInput (penCreate), PenFileChangeInput (penFilesWrite), PenFileWriteInput (penFileWrite), AssetCreateInput and AssetUpdateInput. The request is multipart/form-data with three kinds of field:

  • operations: the JSON body, with null where each file goes.
  • map: which part fills which variable path.
  • One part per file, named as in map.
curl https://api.codepen.io/v2/graphql \
  -H "Authorization: Bearer $KEY" \
  -F operations='{ "query": "mutation ($id: ID!, $file: Upload!) { penFileWrite(input: { penID: $id, path: \"img/logo.png\", upload: $file }) { version { id } } }", "variables": { "id": "01a08738-73f0-7847-b6a8-5789d289c158", "file": null } }' \
  -F map='{ "0": ["variables.file"] }' \
  -F 0=@logo.pngCode language: Bash (bash)

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, or content and upload together, answer BAD_USER_INPUT. Text may go either way: as content in a plain JSON request, or as an upload.

Filters

pens and collections take filters beside pagination. Every filter given must match, and totalCount counts under the same filters.

input PensInput {
  access: [AccessLevel!] # any of Public | Private | Protected | Collaborators
  deployed: Boolean
  template: Boolean
  forks: PenForks # All (default) | Forked | NotForked
  editor: PenEditor # Classic | V2
  tags: [String!] # tagged with any of these; up to three
  searchTerm: String # matches the title or description
  pagination: PaginationInput
}

input CollectionsInput {
  access: [AccessLevel!] # any of Public | Private
  pagination: PaginationInput
}Code language: GraphQL (graphql)

Tags are matched the way they are saved: lowercased, spaces as dashes, punctuation dropped ("Two Words!" is two-words). searchTerm uses web search syntax: all words have to appear, "quoted words" stay together and -word excludes one. It is at most 200 characters.

Pagination

Every list takes the same input and answers the same page shape: the connection shape of GitHub’s GraphQL API, without edges.

input PaginationInput {
  cursor: String # a page's endCursor (next page) or startCursor (previous page)
  limit: Int # default 20, maximum 100
  sortBy: SortBy # CreatedAt (default) | UpdatedAt | Popularity (Pens and Collections)
  sortOrder: SortOrder # Asc | Desc, default Desc
}

type PenPage { # likewise CollectionPage, PenVersionPage, AssetPage
  nodes: [Pen!]!
  pageInfo: PageInfo!
  totalCount: Int! # the whole list, not only this page
}

type PageInfo {
  startCursor: String
  endCursor: String
  hasPreviousPage: Boolean!
  hasNextPage: Boolean!
}Code language: GraphQL (graphql)

Pass a page’s endCursor as cursor for the page after it, or its startCursor for the page before it. A limit over 100 is refused. Lists sort by CreatedAt descending (newest first) unless told otherwise; pens and collections also sort by Popularity. Keep the same sortBy and sortOrder while following a list’s cursors, since a cursor only makes sense in the sort that produced it.

query {
  pens(input: { pagination: { limit: 20, cursor: "<endCursor>" } }) {
    totalCount
    nodes {
      id
      title
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}Code language: GraphQL (graphql)

Here is the whole loop in JavaScript:

async function fetchAllPens(apiKey) {
  const query = `query ($cursor: String) {
    pens(input: { pagination: { limit: 100, cursor: $cursor } }) {
      nodes { id title }
      pageInfo { endCursor hasNextPage }
    }
  }`;

  const pens = [];
  let cursor = null;

  do {
    const response = await fetch("https://api.codepen.io/v2/graphql", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ query, variables: { cursor } })
    });
    if (!response.ok) throw new Error(`CodePen API: ${response.status}`);

    const { data, errors } = await response.json();
    if (errors) throw new Error(errors[0].message);

    pens.push(...data.pens.nodes);
    cursor = data.pens.pageInfo.hasNextPage ? data.pens.pageInfo.endCursor : null;
  } while (cursor);

  return pens;
}Code language: JavaScript (javascript)

Errors

Errors come back in errors[] with the code in extensions.code. The codes are listed on the API v2 page. A refused key (401) and the rate limit (429) are answered before the query runs, with the body { "message", "code" }. A CONFLICT also carries expectedVersionID and currentVersionID:

{
  "errors": [
    {
      "message": "the Pen's head is 01a0...c5, not 01a0...99; read it again and retry",
      "path": ["penFilesWrite"],
      "extensions": {
        "code": "CONFLICT",
        "expectedVersionID": "01a0...99",
        "currentVersionID": "01a0...c5"
      }
    }
  ],
  "data": null
}Code language: JSON / JSON with Comments (json)

An invalid query itself, including an unknown enum value, answers GRAPHQL_VALIDATION_FAILED.

A build that fails is not an error: the mutation succeeds with your files saved, and the payload’s build.status is Failed.

Examples

Every example is a complete operation for POST /v2/graphql. IDs are illustrative.

Read a Pen and Its Files

query ($id: ID!) {
  pen(id: $id) {
    id
    title
    access
    editor
    urls {
      editor
      preview
    }
    screenshotURLs {
      medium
    }
    savedVersion {
      id
      build {
        status
      }
    }
    files {
      id
      path
      content
    }
  }
}Code language: GraphQL (graphql)
{ "id": "01a08738-73f0-7847-b6a8-5789d289c158" }Code language: JSON / JSON with Comments (json)

List Pens

Public 2.0 Pens that are not forks, tagged gsap or svg, mentioning “cards” in their title or description:

query {
  pens(
    input: {
      access: [Public]
      editor: V2
      forks: NotForked
      tags: ["gsap", "svg"]
      searchTerm: "cards"
      pagination: { limit: 50 }
    }
  ) {
    totalCount
    nodes {
      id
      title
      updatedAt
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}Code language: GraphQL (graphql)

Create a Pen

You get starter files when files is omitted. The title defaults to “Untitled” and access to your default privacy setting.

mutation {
  penCreate(
    input: {
      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 }" }
      ]
    }
  ) {
    pen {
      id
      urls {
        editor
        preview
      }
      savedVersion {
        id
      }
      files {
        id
        path
      }
    }
    build {
      id
      status
      rounds
      errors {
        path
        message
      }
    }
  }
}Code language: GraphQL (graphql)

build is the build of the new Pen’s newest version. The new Pen is built like any other write, so the files in it may be more than the files you sent (see Builds).

Update a Pen

Only the fields given change. tags replaces the whole list.

mutation {
  penUpdate(
    input: {
      id: "01a08738-73f0-7847-b6a8-5789d289c158"
      title: "Hello, world"
      tags: ["api", "demo", "v2"]
      access: Public
    }
  ) {
    pen {
      id
      title
      tags
      access
    }
  }
}Code language: GraphQL (graphql)

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.

mutation {
  penFilesWrite(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      expectedVersionID: "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 }
      ]
    }
  ) {
    version {
      id
      files {
        id
        path
        content
      }
    }
    build {
      id
      status
      rounds
      errors {
        path
        message
      }
      appliedPatches {
        ... on BuildPatchCreate {
          path
        }
        ... on BuildPatchUpdate {
          path
        }
        ... on BuildPatchDelete {
          path
        }
      }
      next {
        id
        status
      }
    }
  }
}Code language: GraphQL (graphql)

A stale expectedVersionID answers CONFLICT. version is the newest version the write produced, and build is its build. When build.status is still Queued or Running, or Canceled with a next, poll until it is done (next example).

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:

mutation {
  penFileWrite(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      path: "src/app.ts"
      content: "const n: number = 1;"
      expectedVersionID: "01a08738-73f0-7848-b8bf-e8e7e91b1ae2"
      wait: 0
    }
  ) {
    build {
      id
      status
    }
  }
}Code language: GraphQL (graphql)
{
  "data": {
    "penFileWrite": {
      "build": {
        "id": "01a08853-f1ab-7967-812e-eda134bd9e61",
        "status": "Queued"
      }
    }
  }
}Code language: JSON / JSON with Comments (json)

Poll the build:

query {
  build(id: "01a08853-f1ab-7967-812e-eda134bd9e61") {
    status
    rounds
    next {
      id
      status
    }
  }
}Code language: GraphQL (graphql)

The compiler asked for a /tsconfig.json, so this build ends Canceled, and next says the API is applying the change:

{
  "data": {
    "build": {
      "status": "Canceled",
      "rounds": 1,
      "next": {
        "id": "01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d",
        "status": "Running"
      }
    }
  }
}Code language: JSON / JSON with Comments (json)

Poll next.id until it is done. One query then gives you what was changed, the compiled output, and the source files to base the next write on:

query {
  build(id: "01a08854-2c10-7c7e-9b1a-3f0d5a6b7c8d") {
    status
    rounds
    errors {
      path
      message
    }
    appliedPatches {
      ... on BuildPatchCreate {
        path
      }
      ... on BuildPatchUpdate {
        path
      }
      ... on BuildPatchDelete {
        path
      }
    }
    files {
      # the compiled output
      path
      url
    }
    version {
      id # the next write's expectedVersionID
      files {
        # the source files, as they are now
        id
        path
        content
      }
    }
    previous {
      id
    }
  }
}Code language: GraphQL (graphql)

Here is the whole loop in JavaScript:

async function graphql(apiKey, query, variables) {
  const response = await fetch("https://api.codepen.io/v2/graphql", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ query, variables })
  });
  if (!response.ok) throw new Error(`CodePen API: ${response.status}`);

  const { data, errors } = await response.json();
  if (errors) throw new Error(errors[0].message);
  return data;
}

const BUILD = `
  id
  status
  errors { path message }
  next { id }
  version { id files { id path content } }
`;

async function writeFiles(apiKey, penID, expectedVersionID, files) {
  const written = await graphql(
    apiKey,
    `mutation ($input: PenFilesWriteInput!) {
      penFilesWrite(input: $input) { build { ${BUILD} } }
    }`,
    { input: { penID, expectedVersionID, files } }
  );
  let build = written.penFilesWrite.build;

  // Follow the build until it is done, through next when the API is
  // applying the compiler's changes.
  for (;;) {
    const running = build.status === "Queued" || build.status === "Running";
    const next = build.status === "Canceled" ? build.next : null;
    if (!running && !next) break;

    await new Promise((resolve) => setTimeout(resolve, 2000));
    const polled = await graphql(
      apiKey,
      `query ($id: ID!) { build(id: $id) { ${BUILD} } }`,
      { id: next ? next.id : build.id }
    );
    build = polled.build;
  }

  // build.version.files are the source files as they are now, which may
  // not be the files sent.
  return 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

mutation {
  penFileWrite(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      path: "src/app.js"
      content: "console.log(1)"
      expectedVersionID: "01a08738-73f0-7848-b8bf-e8e7e91b1ae2"
    }
  ) {
    version {
      id
      files {
        id
        path
      }
    }
    build {
      id
      status
    }
  }
}Code language: GraphQL (graphql)

Delete One File

By ID, or by path (a directory path deletes every file under it).

mutation {
  penFileDelete(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      id: "01a0884b-3177-71c3-a1c2-9452bfb9a3fb"
    }
  ) {
    version {
      id
      files {
        path
      }
    }
    build {
      id
      status
    }
  }
}Code language: GraphQL (graphql)

Upload a Binary File

Send the file as a multipart part (see Uploads) into the upload field of penFilesWrite, penFileWrite or penCreate. The operation, as it appears in the operations field:

mutation ($id: ID!, $file: Upload!) {
  penFileWrite(input: { penID: $id, path: "img/logo.png", upload: $file }) {
    version {
      id
      files {
        id
        path
        isBinary
        url
      }
    }
  }
}Code language: GraphQL (graphql)
{ "id": "01a08738-73f0-7847-b6a8-5789d289c158", "file": null }Code language: JSON / JSON with Comments (json)

with map { "0": ["variables.file"] } and the file as part 0. Reading it back, content is null and the bytes are at url:

{
  "path": "/img/logo.png",
  "isBinary": 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

versionID defaults to the saved head.

mutation {
  penDeploy(input: { penID: "01a08738-73f0-7847-b6a8-5789d289c158" }) {
    pen {
      deployment {
        id
        versionID
        url
        customDomains
      }
    }
  }
}Code language: GraphQL (graphql)
mutation {
  penUndeploy(input: { penID: "01a08738-73f0-7847-b6a8-5789d289c158" }) {
    pen {
      deployment {
        # null once undeployed
        id
      }
    }
  }
}Code language: GraphQL (graphql)

Update Settings

mutation {
  penSettingsUpdate(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      autoDeploy: true
      indentWith: "tabs"
      tabSize: 4
    }
  ) {
    pen {
      settings {
        autoDeploy
        autoRun
        autoSave
        indentWith
        tabSize
      }
    }
  }
}Code language: GraphQL (graphql)

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 payload’s build is that build. A delete moves the Pen to the trash. You can restore it from Deleted Items on the site for 3 days.

mutation {
  penFork(
    input: { id: "01a08738-73f0-7847-b6a8-5789d289c158", title: "My copy" }
  ) {
    pen {
      id
      title
      access
    }
    build {
      id
      status
    }
  }
}Code language: GraphQL (graphql)
mutation {
  penDelete(input: { id: "01a08738-73f0-7847-b6a8-5789d289c158" }) {
    pen {
      id
      title
    }
  }
}Code language: GraphQL (graphql)

Versions: List, Inspect, Restore

query {
  pen(id: "01a08738-73f0-7847-b6a8-5789d289c158") {
    versions(input: { limit: 10 }) {
      nodes {
        id
        createdAt
        build {
          status
        }
        deployed
        previewUrl
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}Code language: GraphQL (graphql)

One version, with its build, its compiled output and its source files:

query {
  penVersion(id: "01a08738-73f0-7848-b8bf-e8e7e91b1ae2") {
    id
    penID
    build {
      status
      startedAt
      endedAt
      diagnostics {
        type
        path
        message
      }
      files {
        path
        url
      }
    }
    files {
      path
      content
    }
  }
}Code language: GraphQL (graphql)
mutation {
  penVersionRestore(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      versionID: "01a08738-73f0-7848-b8bf-e8e7e91b1ae2"
    }
  ) {
    pen {
      savedVersion {
        id
      }
    }
  }
}Code language: GraphQL (graphql)

A restored version keeps the build of the version it copies; it is not built again.

Collaborators

mutation {
  collaboratorInvite(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      emailOrUsername: "teammate"
      role: Editor
    }
  ) {
    pen {
      collaborators {
        username
        role
        isTeamMember
      }
      collaboratorInvites {
        id
        username
        email
        role
      }
    }
  }
}Code language: GraphQL (graphql)
mutation {
  collaboratorUpdateRole(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      username: "teammate"
      role: Viewer
    }
  ) {
    pen {
      collaborators {
        username
        role
      }
    }
  }
}Code language: GraphQL (graphql)
mutation {
  collaboratorRemove(
    input: {
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
      username: "teammate"
    }
  ) {
    pen {
      collaborators {
        username
      }
    }
  }
}Code language: GraphQL (graphql)

Collections

mutation {
  collectionCreate(input: { title: "Demos", access: Public }) {
    collection {
      id
      title
      url
    }
  }
}Code language: GraphQL (graphql)
mutation {
  collectionAddPen(
    input: {
      collectionID: "QbaoZL"
      penID: "01a08738-73f0-7847-b6a8-5789d289c158"
    }
  ) {
    collection {
      itemsCount
      pens(input: { limit: 20 }) {
        nodes {
          id
          title
        }
      }
    }
  }
}Code language: GraphQL (graphql)

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.

mutation {
  collectionDelete(input: { id: "QbaoZL" }) {
    collection {
      id
      title
    }
  }
}Code language: GraphQL (graphql)

Assets

Assets are the files in your Asset Hosting, served from their url. A file goes up as an upload (a multipart part, see Uploads). A text Asset (SVG, HTML, CSS, Sass, JS, TS, JSON, Markdown, plain text and similar) may be sent as content instead. Send one or the other, not both.

mutation ($file: Upload!) {
  assetCreate(input: { filename: "logo.png", upload: $file }) {
    asset {
      id
      isBinary
      url
    }
  }
}Code language: GraphQL (graphql)

with variables { "file": null }, map { "0": ["variables.file"] } and the file as part 0.

query {
  assets(input: { pagination: { sortBy: CreatedAt, limit: 50 } }) {
    nodes {
      id
      filename
      url
      isBinary
      updatedAt
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}Code language: GraphQL (graphql)
mutation {
  assetUpdate(
    input: {
      id: "k7Ym0R"
      filename: "theme.css"
      content: "body { color: red }"
    }
  ) {
    asset {
      filename
      url
    }
  }
}Code language: GraphQL (graphql)

A rename changes the Asset’s url. The new name must be unique among your Assets and cannot contain / or \. New content counts against your plan’s per-file size and total storage (FORBIDDEN when over), and content is only for text Assets. assetDelete(input: { id }) removes the file from storage and returns the Asset as it was.

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