Forming Calls with GraphQL

Authenticating with GraphQL

To communicate with the GraphQL server, you'll need an API access token.

To create an Access Token:

  • Go to Settings > API Access Tokens.
  • Click on "Add API Access Key" and choose the appropriate key type.
    Read-only keys cannot perform operations that change data (mutations) while Read-Write keys allow those operations to be executed.

The API notifies you if you try to execute a mutation with a read-only key.

The GraphQL HTTP endpoint

While a traditional REST API has numerous endpoints – or URLs – the GraphQL API has a single endpoint:

https://api.onsign.tv/graphql

The endpoint remains constant no matter what operation you perform. The one exception is uploading files. Check our file upload guide for more info.

Communicating with GraphQL

Because GraphQL operations consist of multiline JSON, it is recommended using the Playground to familiarize yourself with GraphQL calls. You can also use cURL or any other HTTP-speaking library.

In REST, HTTP verbs determine the operation performed. In GraphQL, you'll provide a JSON-encoded body whether you're performing a query or a mutation, so the HTTP verb is POST. The exception is an introspection query, which is a simple GET to the endpoint.

To query GraphQL using cURL, make a POST request with a JSON payload. The payload must contain a string called query:

curl https://api.onsign.tv/graphql -H 'Content-Type: application/json' -H 'Authorization: token YOUR-TOKEN' --data-binary '
{
  "query": "query { organization { id, name }}"
}
'
Note: The string value of "query" must escape newline characters or the schema will not parse it correctly. For the POST body, use outer single quotes so you don't have to escape inner double quotes.

About query and mutation operations

The two types of allowed operations in the GraphQL API are queries and mutations. Comparing GraphQL to REST, queries operate like GET requests, while mutations operate like POST/PATCH/DELETE requests. The mutation name determines which modification is executed.

Queries and mutations share similar forms, with some important differences.

About queries

GraphQL queries return only the data you specify. To form a query, you must specify fields within fields (also known as nested subfields) until you return only values without subfields.

Queries are structured like this:

query {
  JSON objects to return
}

For a real-world example, see this Example query.

About mutations

To form a mutation, you must specify three things:

  • Mutation name. The type of modification you want to perform.
  • Input object. The data you want to send to the server, composed of input fields. Pass it as an argument to the mutation name.
  • Payload object. The data you want to return from the server, composed of return fields. Pass it as the body of the mutation name.

Mutations are structured like this:

mutation {
  mutationName(input: {MutationNameInput!}) {
    MutationNamePayload
  }
}

The input object in this example is MutationNameInput, and the payload object is MutationNamePayload.

In the mutations reference, the listed input fields are what you pass as the input object. The listed return fields are what you pass as the payload object.

For a real-world example, see this Example mutation.

There is a limit to the number of calls you can make or nested fields you can request. For information about rate limiting, see "GraphQL resource limitations."

Example queries

Let's walk through a more complex query and put this information in context.

The following query looks up the organization, finds 5 directories in ascending order, and returns each directory's id, name and the first 10 children, along with their id, name and kind:

query {
    organization {
      contents(kind: FOLDER, first: 5) {
        nodes {
          id
          name

          children(first: 10) {
            nodes {
              id
              name
              kind
            }
          }
        }
      }
    }
}

Looking at the query line by line:

  • query {

    Because we want to read data from the server, not modify it, query is the root operation. (If you don't specify an operation, query is also the default.)

  • organization {

    To begin the query, we must provide the root object, organization, since all data to be returned will belong to the API key's organization.

  • contents(kind: FOLDER, first: 5) {

    To account for all the fileds, folders and apps in the organization, we call the contents connection.

    Some details about the contents object:

    • The docs tell us this object has the type ContentConnection.
    • Schema validation indicates this object requires a parameter called first, to limit the number of results, so we provide 5.
    • The docs also tell us this object accepts a kind argument, which is an ContentKind enum that accepts APP, FOLDER, etc. as values. To find only content that are folders, we give the kind key a value of FOLDER.
  • nodes {

    We know contents is a connection because it has the ContentConnection type. So here we retrieve the individual items belonging to the connection.

  • Now that we know we're retrieving an Content object, we can look at the docs and specify the fields we want to return:

    id
    name
    
    children(first: 10) {
      nodes {
        id
        name
        kind
      }
    }
    

    Here we specify the id, name, and children fields of the Content object. The children field is of the type ContentConnection as well, so we can query the same fields we have queried previously. We also add the kind field to return the kind of the content, which is of the type ContentKind, the same enum we mentioned previously.

Further query examples:

List the first 10 players and the items being played for the primary loop:

{
  organization {
    players(first: 10) {
      nodes {
        id
        name
        tags

        loop(name: PRIMARY) {
          items {
            edges {
              id
              position
            }
          }
        }
      }
    }
  }
}

Listing the 100 players that connected to the server the last:

{
  organization {
    players(first: 10, orderBy: {field: LAST_SEEN_AT}) {
      nodes {
        id
        name
        lastSeen
        isConnected
      }
    }
  }
}

List campaigns inside a playlist:

{
  organization {
    playlists(first: 10) {
      nodes {
        id
        name

        items(first: 10) {
          edges {
            node {
              ... on Campaign {
                id
                name
              }
            }
          }
        }
      }
    }
  }
}

Example mutations

Mutations often require information that you can only find out by performing a query first. This example shows two operations:

  1. A query to get a player ID.
  2. A mutation to add a tag to the player.
query FindPlayerID {
  organization {
    players(first: 1) {
      nodes {
        id
      }
    }
  }
}

mutation UpdatePlayerTag {
  updatePlayerTags(input: { id: "ID from query above", tags: ["tag one"]}) {
    player {
      id
      name
      tags
    }
  }
}

Although you can include a query and a mutation in the same Explorer window if you give them names (FindPlayerID and UpdatePlayerTag in this example), the operations will be executed as separate calls to the GraphQL endpoint. It's not possible to perform a query at the same time as a mutation, or vice versa.

Let's walk through the example. The task sounds simple: update the list of tags given to a player.

So how do we know to begin with a query? We don't, yet.

Because we want to modify data on the server (update the tags of a player), we begin by searching the schema for a helpful mutation. The reference docs show the updatePlayerTags mutation, with this description: Updates the player tags. That's exactly what we want!

The docs for the mutation list three input fields:

  • clientMutationId (String)
  • id (ID!)
  • tags ([String!]!)

The !s indicate that id and tags are required fields. A required tags makes sense: we want to update the player tags, so we'll need to specify which tags we want to be set.

But why is id required? It's because the id is the only way to identify which player we want to update.

This is why we start this example with a query: to get the player ID.

Let's examine the query line by line:

  • query FindPlayerID {

    Here we're performing a query, and we name it FindPlayerID. Note that naming a query is optional; we give it a name here so that we can include it in same Explorer window as the mutation.

  • players(first: 1) {

    We specify the players connection, fetching the first player.

  • id

    This is where we retrieve the id of the player to pass as the id input to the mutation.

When we run the query, we get the player id, which should be a string containing a series of alphanumeric characters such as "AuCj".

Note: The id returned in the query is the value we'll pass as the id in the mutation. Neither the docs nor schema introspection will indicate this relationship; you'll need to understand the concepts behind the names to figure this out.

With the ID known, we can proceed with the mutation:

  • mutation UpdatePlayerTags {

    Here we're performing a mutation, and we name it UpdatePlayerTags. As with queries, naming a mutation is optional; we give it a name here so we can include it in the same Explorer window as the query.

  • updatePlayerTags(input: { id: "AuCj", tags: ["tag one", "tag two"] }) {

    Let's examine this line:

    • updatePlayerTag is the name of the mutation.

    • input is the required argument key. There will always be a mandatory input type for each mutation.

    • { id: "AuCJ", tags: ["tag one", "tag two"]} is the required argument value. This will always be an input object (hence the curly braces) composed of input fields (id and tags in this case) for a mutation.

    • The rest of the call is composed of the payload object. This is where we specify the data we want the server to return after we've performed the mutation. These lines come from the updatePlayerTags docs, which specifies two possible return fields:

      clientMutationId (String)
      player (Player!)
      

    In this example, we return the required field (player), from which we select the id, name and tags fields.

When we run the mutation, this is the response:

{
  "data": {
    "updatePlayerTags": {
      "player": {
        "id": "AuCj",
        "name": "Sample",
        "tags": ["tag one", "tag two"]
      }
    }
  }
}

That's it! Check out that the player tags has been updated by going to the player page on the main website and checking the player tags there.

One final note: when you pass multiple fields in an input object, the syntax can get unwieldy. Moving the fields into a variable can help. Here's how you could rewrite the original mutation using a variable:

mutation($myVar:UpdatePlayerTagsInput!) {
  updatePlayerTags(input:$myVar) {
    player {
      id
      name
      tags
    }
  }
}
variables {
  "myVar": {
    "id": "o2CA",
    "tags": ["tag one", "tag two"]
  }
}

Further reading

There is a lot more you can do when forming GraphQL calls. Here are some places to look next:

Overview