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.
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.
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:
npm install -g get-graphql-schema
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 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 forpermanentId
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 [ 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:
- To decide if you need to request assets to be rendered, look at the document fields (see 2.7): if
assetStatus
isAVAILABLE
, butdownloadableAssets
is an empty array, it probably means you need to initiate the rendering process yourself. - Use the following GraphQL mutation to trigger the rendering job:
mutation {
renderDownloadableAssets(input: {
documentIdentifier:"$DOCUMENT_ID"
}) {
successful,
downloadableAsset {
status, path
},
errors {
code, message
}
}
}
- 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 ), 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
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.
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 optionalbefore
andafter
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!
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
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