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:

2 Likes

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.

Adding one more API request to help with generating changelogs:

2.9 Listing all versions for a given Document

query {
  share(id: "$SHARE_ID") {
    versionHistory(include: [VERSION]) {
      entries {
        ... on Version {
          kind        # starred revision will have "PUBLISHED" here
          description # will be non-null for a starred revision
          updatedAt
        }
      }
    }
  }
}

According to the the schema versionHistory() also supports optional before and after arguments (e.g. to skip older versions when generating a changelog) but I couldnā€™t get them to work.

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\\"){versionHistory(include:[VERSION]){entries{... on Version{kind  description  order  updatedAt}}}}}"
}'
Example output
{
  "data": {
    "share": {
      "versionHistory": {
        "entries": [
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:47:47Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:43:00Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:40:48Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:02:39Z"
          },
          {
            "description": "Another milestone for this design",
            "kind": "PUBLISHED",
            "updatedAt": "2023-06-14T10:35:21Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:02:01Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T13:01:32Z"
          },
          {
            "description": "The second update with āœØ",
            "kind": "PUBLISHED",
            "updatedAt": "2023-06-14T10:34:53Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T12:53:10Z"
          },
          {
            "description": null,
            "kind": "DRAFT",
            "updatedAt": "2023-05-12T12:42:29Z"
          },
          {
            "description": "The first revision ever",
            "kind": "PUBLISHED",
            "updatedAt": "2023-06-14T10:33:45Z"
          }
        ]
      }
    }
  }
}

Hey folks!

Firstly, I just want to say that we appreciate your enthusiasm and community spirit! :smiley:

However, I want to take a moment to highlight that the GraphQL API is intended for private first-party use only, and so is not designed for use by third parties. We can (and do) make breaking changes without prior warning.

Obviously this means we also donā€™t provide any official documentation or support for our web APIs either, so please bear this in mind :pray:

1 Like

Thatā€™s a totally fair policy, and frankly weā€™re just glad this API exists at all and open to third parties!

Some of us have been in the game of (ab)using private Sketch APIs long before Sketch had a web app, so weā€™ll manage :sweat_smile:

3 Likes