Python Support and GraphQL API

Hello, everyone!
I would like to know if Sketch Cloud has its own API (preferably in GraphQL) and if it is possible to write a plugin in Python (or is it limited to JS only)

It has API and it’s GraphQL. But it’s not open. You can read the schema, but as far as I understand, it’s limited to be used only internally in Web App (requests should come withing same origin).

@vitlayozza After digging a bit, I figured it’s possible to use GraphQL API for publicly shared library (via secret link). So, you can get tokens, and all the data that is available there.

1 Like

That’s not entirely true from what I was able to gather recently: the GraphQL API, while not publicly documented, is available to anyone with an access token: for both shared and private documents and libraries. Sketch.app also relies on this same API to work with Workspace-related features so it’s pretty stable at this point.

:thinking: Some GraphQL mutation don’t work as expected for me when called directly while Sketch Web App has no problems running them (e.g. renderDownloadableAssets) .

I’ve had some downtime today and decided to go down the API rabbit hole a bit, with a goal of composing some sort of a publicly available guide with usage examples where possible. So here goes:

A short (unofficial) guide to Sketch GraphQL API

0. Downloading the schema


Sketch GraphQL schema describes what kind of data is available to you via queries, and what kind of actions (mutations) are allowed. It is public and available to inspect, here’s how to download it:

  1. npm install -g get-graphql-schema
  2. get-graphql-schema https://graphql.sketch.cloud/api > sketch-api-schema.graphql

1. Authorization


1.1 Acquiring an OAuth token

curl -X "POST" "https://auth.sketch.cloud/oauth/token" \
     -H 'Content-Type: application/json' \
     -d $'{
  "email": "$EMAIL",
  "password": "$PASSWORD",
  "grant_type": "password"
}'

where $EMAIL and $PASSWORD are your personal credentials from sketch.com. You’ll receive something like this in response:

{
  "access_token": "xxxx-yyyy-zzzz",
  "expires_in": 3600,
  "refresh_token": "xxxx-yyyy-zzzz",
  "token_type": "bearer"
}

There’s also a few ways to authenticate with 2FA, but I don’t have it enabled on my account so not covering these here.

1.2 Updating the expired OAuth token

When the access token expires eventually, use the refresh_token to obtain a new one:

curl -X "POST" "https://auth.sketch.cloud/oauth/token" \
     -H 'Content-Type: application/json' \
     -H 'Authorization: bearer $EXPIRED_ACCESS_TOKEN' \
     -d $'{
          "refresh_token": "$REFRESH_TOKEN",
          "grant_type": "refresh_token"
     }'

2. Making requests


I assume that readers are already familiar with GraphQL and know how to send queries using their tech stack of choice.

You may’ve noticed that all auth requests above were presented as curl commands – so you can paste them directly into your terminal and run those requests right away. Let’s keep it this way and use curl for all GraphQL request examples below as well.

I’m personally a fan of using RapidAPI (ex-Paw) app as a playground for network requests including the ones in this guide. I guess Postman would also work.

In a nutshell, the following GraphQL query:

query {
  me {
    name, email
  }
}

might be sent to the API with the following curl invocation:

curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query { me { name,email } }"
     }'

I’m going to collapse these curl commands and their output by default like this :point_down: so they won’t occupy your entire screen real estate:

A collapsed curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query { me { name,email } }"
     }'
Example output of such command
{
  "data": {
    "me": {
      "email": "i.am.rodionovd@gmail.com",
      "name": "Dmitry Rodionov"
    }
  }
}

Now, back to the useful queries:

2.1. Listing all available Workspaces

query {
  me {
    workspaceMemberships {
      entries {
        workspace {
          name, identifier
        }
      }
    }
  }
}
curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{me{workspaceMemberships{entries{workspace{name,identifier}}}}}"
     }'
Example output
{
  "data": {
    "me": {
      "workspaceMemberships": {
        "entries": [
          {
            "workspace": {
              "identifier": "d1185cd7-XXXX-YYYY-ZZZZ-80a3821e9251",
              "name": "My Personal Workspace"
            }
          }
        ]
      }
    }
  }
}

2.2. Listing all Projects in a given Workspace

query {
  workspace(identifier:"$WORKSPACE_UUID") {
    projects {
      entries { name, identifier, type }
    }
  }
}
curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{workspace(identifier:\\"$WORKSPACE_UUID\\"){projects{entries{name,identifier}}}}"
     }'
Example output
{
  "data": {
    "workspace": {
      "projects": {
        "entries": [
          {
            "identifier": "7a6f8845-XXXX-YYYY-ZZZZ-65316032b0f8",
            "name": "My Drafts",
            "type": "PERSONAL_DRAFTS"
          },
          {
            "identifier": "56dc8f03-XXXX-YYYY-ZZZZ-8b7e060f9da0",
            "name": "My iOS Project",
            "type": "STANDARD"
          }
        ]
      }
    }
  }
}

2.3. Listing all Documents in a given Project

Documents (aka “shares” in API terms) includes regular documents, templates and libraries.

query {
  project(identifier:"$PROJECT_UUID") {
    shares {
      entries { name, identifier, type }
    }
  }
}
curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{project(identifier:\\"$PROJECT_UUID\\"){shares{entries{name,identifier,type}}}}"
     }'

Example output
{
  "data": {
    "project": {
      "shares": {
        "entries": [
          {
            "identifier": "8e0f1c4d-XXXX-YYYY-ZZZZ-fcabc18241a8",
            "name": "iOS Designs",
            "type": "STANDARD"
          }
        ]
      }
    }
  }
}

2.4. Listing all Pages and Artboards in given a Document (aka Share)

query {
  share(id: "$SHARE_ID") {
    version {
      document {
        pages {
          entries {
            name, identifier, artboards {
              entries { name, identifier, permanentArtboardShortId, uuid }
            }
          }
        }
      }
    }
  }
}
curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{share(id:\\"$SHARE_ID\\"){version{document{pages{entries{name,identifier,artboards{entries{name,identifier,permanentArtboardShortId,uuid}}}}}}}}"
     }'
Example output
{
  "data": {
    "share": {
      "version": {
        "document": {
          "pages": {
            "entries": [
              {
                "artboards": {
                  "entries": [
                    {
                      "identifier": "756ab676-XXXX-YYYY-ZZZZ-9f4cdfa80be1",
                      "name": "iOS Homescreen",
                      "permanentArtboardShortId": "xXxxXxY",
                      "uuid": "EC448142-XXXX-YYYY-ZZZZ-61487AA618FF"
                    },
                    {
                      "identifier": "c3348c9c-XXXX-YYYY-ZZZZ-87c0374441cd",
                      "name": "Twitter Profile",
                      "permanentArtboardShortId": "xXxxXxY",
                      "uuid": "D9923398-XXXX-YYYY-ZZZZ-CD610E756D9B"
                    }
                  ]
                },
                "identifier": "c9017152-XXXX-YYYY-ZZZZ-c5af073fb678",
                "name": "Page 1"
              }
            ]
          }
        }
      }
    }
  }
}

2.5. Downloading previews for a given Artboard

Lists URLS of all available thumbnails of the given artboard. Note that we use permanentArtboardShortId from the previews request’s output to identify a specific artboard within a document:

query {
	artboard(shareIdentifier: "$SHARE_ID", permanentArtboardShortId: "$PERMANENT_ARTBOARD_SHORT_ID") {
    files {
      scale, thumbnails { type, url }
    }
  }
}
curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{artboard(shareIdentifier:\\"$SHARE_ID\\",permanentArtboardShortId:\\"$PERMANENT_ARTBOARD_SHORT_ID\\"){files{scale,thumbnails{type,url}}}}"
     }'

Example output
{
  "data": {
    "artboard": {
      "files": [
        {
          "scale": 1,
          "thumbnails": [
            {
              "type": "L",
              "url": "https://graphql.sketch.cloud/assets/.../xxx.png"
            },
            {
              "type": "M",
              "url": "https://graphql.sketch.cloud/assets/../x.m.png"
            },
            {
              "type": "W400P",
              "url": "https://graphql.sketch.cloud/assets/.../x.w400p.png"
            }
          ]
        },
        {
          "scale": 2,
          "thumbnails": [
            {
              "type": "L",
              "url": "https://graphql.sketch.cloud/assets/.../xxx.png"
            },
            {
              "type": "M",
              "url": "https://graphql.sketch.cloud/assets/../x.m.png"
            },
            {
              "type": "W400P",
              "url": "https://graphql.sketch.cloud/assets/.../x.w400p.png"
            }
          ]
        }
      ]
    }
  }
}

2.6. Listing all annotations and their comments for a given Artboard or Page

Please note that we’re using $PERMANENT_ARTBOARD_UUID to identify an artboard in this query instead of $PERMANENT_ARTBOARD_SHORT_ID we provided for other requests. The former is available as uuid field of an Artboard (see the output of 2.4).

query {
  annotations(shareIdentifier:"$SHARE_ID", subject: {
    permanentId:"$PERMANENT_ARTBOARD_UUID", type:ARTBOARD
  }) {
    entries {
      identifier, updatedAt, resolution { resolvedAt }, comments {
        entries {
          body, identifier, user { identifier, name }
        }
      }
    }	    
  }
}

You may also specify type:PAGE and provide a page identifier for permanentId to receive all annotations for the given page instead of an artboard.

curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          query": "query{annotations(shareIdentifier:\\"$SHARE_ID\\",subject:{permanentId:\\"$PERMANENT_ARTBOARD_UUID\\",type:ARTBOARD}){entries{identifier,updatedAt,resolution{resolvedAt},comments{entries{body,identifier,user{identifier,name}}}}}}"
     }'
Example output
{
  "data": {
    "annotations": {
      "entries": [
        {
          "comments": {
            "entries": [
              {
                "body": "Noted 👍🏼 ",
                "identifier": "e040979b-XXXX-YYYY-ZZZZ-2ba77ee83104",
                "user": {
                  "identifier": "96f3569f-XXXX-YYYY-ZZZZ-2107857ce3c8",
                  "name": "Certainly Not Dmitry Rodionov"
                }
              },
              {
                "body": "This color rocks, let's turn it in into a Color Variable with a cool name",
                "identifier": "e1a1d976-XXXX-YYYY-ZZZZ-9641cabeccd3",
                "user": {
                  "identifier": "96f3569f-XXXX-YYYY-ZZZZ-2107857ce3c8",
                  "name": "Dmitry Rodionov"
                }
              }
            ]
          },
          "identifier": "3c7c5516-XXXX-YYYY-ZZZZ-e2501322b030",
          "resolution": null,
          "updatedAt": "2023-05-12T12:25:56Z"
        }
      ]
    }
  }
}

2.7 Downloading pre-generated assets for a given Document

Requests a list of downloadable assets for the given document, which may come in handy for developer handoff:

query {
  share(id:"$SHARE_ID") {
    version {
      document {
        identifier, assetStatus, downloadableAssets {
          path, status
        }
      }
    }
  }
}

A note from Sketch GraphQL schema: “these assets may be individual files or a zip file of all files in this document”.

curl command
curl 'https://graphql.sketch.cloud/api' \
     -H "authorization: bearer $ACCESS_TOKEN" \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json' \
     -d $'{
          "query": "query{share(id:\\"$SHARE_ID\\"){version{document{identifier,assetStatus,downloadableAssets{path,status}}}}}"
     }'
Example output
{
  "data": {
    "share": {
      "version": {
        "document": {
          "assetStatus": "AVAILABLE",
          "downloadableAssets": [
            {
              "path": "https://resources-live.sketch.cloud/downloadable_assets/.../my_assets_2023-05-12_v42.zip",
              "status": "AVAILABLE"
            }
          ],
          "identifier": "178883c4-XXXX-YYYY-ZZZZ-db332666df91"
        }
      }
    }
  }

2.8 [:construction_worker_woman::construction: Work-in-Progress] Rendering downloadable assets on demand

Since Sketch doesn’t pre-render assets for a newly uploaded document automatically, you usually have to press the “Export Assets” button in the web app to prepare them for download.

Now, here’s my journey with rendering assets on demand so far:

  1. To decide if you need to request assets to be rendered, look at the document fields (see 2.7): if assetStatus is AVAILABLE, but downloadableAssets is an empty array, it probably means you need to initiate the rendering process yourself.
  2. Use the following GraphQL mutation to trigger the rendering job:
mutation {
  renderDownloadableAssets(input: {
    documentIdentifier:"$DOCUMENT_ID"
  }) {
    successful,
    downloadableAsset {
      status, path
    },
    errors {
      code, message
    }
  }
}
  1. Now, Sketch always responds with “Assets already requested for rendering” error to this mutation even if nobody has requested those assets before. I’m probably doing something obviously wrong here ¯\(ツ)/¯ so please feel free to point me to the right direction!

I guess that’s all I wanted to cover this time, but there are certainly other things in the schema that caught my attention (mentions of design system components, collections and even some AI stuff :eyes:), but since those features are not publicly available yet I’m not sure if those are actually useful or not.

Thanks for reading by the way and let’s hope Sketch folks won’t be mad at me for exposing all this private API stuff here :sweat_smile:

1 Like

I see. I didn’t noticed the ability to renew the token. I’m pretty sure I tried (also. long time ago) to use token outside of same origin, and wasn’t working.

I don’t see this could be considered as exposure, as everything is open in browser and schema is freely accessible.

1 Like

Yep, I remember playing with this in early 2020 and I’m almost 100% certain things didn’t go as smoothly back then as they do now.