Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Webhooks allow Scope Customers and 3rd party integrators to build Applications that import data and/or execute custom code after execute whenever specific events occur in the Scopear.com ecosystem.

For example , as a Scope Customer , you might want to use leverage a webhook to trigger a custom action that downloads and stores ScenarioSession data in the Customer’s system of record whenever a User completes a session in the Scope Worklink app.

...

What is a webhook?

  • Webhook is a single HTTP POST request that is

Use cases

Common webhook use cases include the following:

  • Collecting data for data-warehousing

  • Sending notifications to IM clients and pagers (e.g. when new Scenarios become available)

  • Updating state in your system-of-record (e.g. when users complete Scope Scenarios)

What is a webhook?

  • Webhook is a single HTTP POST request that is sent by a web server to a url that you specify, and is typically triggered by the occurrence of a specific, predefined type of event. Scope Webhooks contain a JSON payload in the body and metadata in the headers.

  • Webhook Topic is a persisted data object that can be queried in the Scope GraphQL API. Each record describes a specific type of event that triggers a webhook, and is uniquely identified by a key (e.g. “user/created”) and version (e.g. “1.1”).

  • Webhook Subscription is also a persisted data object that can be queried in the Scope GraphQL API. Each record describes a webhook topic that you want to receive notifications about, a callback url, and an ( optional ) GraphQL query. (to be executed during delivery).

Info

If a valid query is supplied with a webhook subscription, the default JSON payload it will be replaced with the result of the executed query during the delivery of each subsequent webhook notification, and the result will replace the default JSON payload. See customizing the payload for more information.

Use cases

Common webhook use cases include the following:

  • Collecting data for data-warehousing

  • Sending notifications to IM clients and pagers (e.g. when new Scenarios become available)

  • Updating user status in your system-of-record (e.g. when users complete Scope Scenarios)

Anatomy of a webhook

...

What does a webhook look like?

After you subscribe to a webhook topic, Scope sends a webhook notification as an HTTP POST to the specified callback url each time an event occurs pertaining that pertains to the specified topic. This notification is an HTTP POST request that contains a JSON payload, and HTTP headers that provide context.For example, the 

By way of example, the default scenario_session/create webhook notification includes the following:

...

Some HTTP headers are particularly useful for your app. For example, X-Scope-Hmac-Sha256 is used to verify the authenticity of webhooks, X-Scope-Webhook-Id is used to troubleshoot webhooks, and X-Scope-Domain is used to identify the system that sent the webhook.

At this time, the value of Currently, all Scope webhook notifactions include a value of node for the X-Scope-Webhook-Type is always node header. However, in the future we hope to introduce additional webhook types that pertain to events that do not involve GraphQL nodes (e.g. a value of service might indicate that a particular service has become available.

Info

If your organization is equipped with an “on-prem” install of the Scope System, your domain value is likely to be different from than the example provided value above.

Body

Code Block
languagejson
{
  "data": {
    "id": "t5V3S3X8Trt/weWu0A2YHb0cRFJfcRA2rXT3vAGTz1A="
    "url": "https://cms.scopear.com/api/v3/graphql/object/t5V3S3X8Trt/weWu0A2YHb0cRFJfcRA2rXT3vAGTz1A="
    }
  }
}

...

  • id is a globally unique descriptor that can be used to query the Scope GraphQL API for more information.

  • url is a reference to a simple REST representation of the object that can be accessed by HTTP GET.

Note

Only use the provided url if you are unable to use The url provided should only be used as a fallback if you are unable to leverage GraphQL to gather additional information.

REST endpoints are only capable of delivering 1st-class attributes for any given resource (e.g. they can provide a User’s id, name, & email… but cannot provide the scenario_sessions associated with the a User).

...

What can I do with the information that a webhook provides?

By design, the default JSON payloads provide very limited information in order to encourage integrators to query the Scope GraphQL API for the specific information that they require (i.e. information about a ScenarioSession in this example).

See Acting on webhook data to learn more.

In lieu of querying the Scope GraphQL API for additional data, you can supply a valid GraphQL query (for the server to execute on your behalf) when creating or updating a webhook subscription.

If a query is supplied to a webhook subscription, it will be executed by the server when preparing each relevant webhook notification for delivery, and the result of that execution will replace the default JSON payload.

Info

Queries are executed within the context of the user & organization that created the subscription; as if the user had authenticated and executed the query via the API.

By way of example, to customize the payload for a scenario_session/completed webhook subscription, you could supply the following query:

...

languagegraphql

...

Using Global Node Ids and Exploring the Graph to learn how to use the Scope GraphQL API.

Info

Most relationships (aka “Connections”) are bidirectional in the Scope GraphQL API. This means that given a node in the graph, you can usually to navigate “up” to find parent-like objects (such as organization, viewer, or scenario) as well as as “down” to find child-like objects (such as scenarioSessionEvents, or scenarioSessionCheckboxes).

Examples

Given the following webhook:

Code Block
{
  "id": "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=",
  "url": "https://cms.scopear.com/api/v3/graphql/object/w4Aa1bjiiNSt1StG11n40b4dYU8lAigrCalO8r6cYwYchxAsWytj-F6wojhI_CsN"
}

An integrator could answer the question “what Scenario is associated with this ScenarioSession?” by querying the Scope GraphQL API as follows:

Code Block
query {
  node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") {
    ... on ScenarioSession {
      scenario {
        nodesid
{      }
    id}
     }
}

Or, an integrator could answer the question “what ScenarioSessionEvents are associated with this ScenarioSession?” by querying the Scope GraphQL API as follows:

Code Block
query {
  node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") duration{
    ... on ScenarioSession {
      scenarioSessionEvents {
  }      nodes }{      }  
}
}

See Sample Queries for more complicated query examples.

List of available webhook topics

Every GraphQL Object Type that implements the Node Interface triggers created, updated, and deleted events. Additionally, the following special topics are available:

  • call/completed

  • call_recording/completed

  • scenario_session/paused

  • scenario_session/resumed

  • scenario_session/completed

  • scenario_session_part/completed

  • scenario_session_step/completed

Alternatively, you can retrieve the list of currently available topics (at any time) by executing the following query against the Scope GraphQL API:

Code Block
languagegraphql
query {
  webhookTopics {
    nodes {
      key
      version
      disabled_at
    }
  }
}

Configuring your app

To receive webhooks, register a HTTPS endpoint with your app as a webhook receiver. Then, create a webhook subscription specifying said endpoint and the webhook topic that you wish to receive webhook notifications about. Scope will send your app a JSON payload whenever an event for the subscribed topic occurs. The default payload contains the object id that triggered the event.

Webhook subscriptions are scoped by the user and organization that they're registered to. This means that when a webhook subscription is registered: a) other users & organizations can't view, modify, or delete it; and b) other subscriptions will not be triggered by events pertaining to your data.

Versioning

The header X-Scope-API-Version specifies the version of the Scope GraphQL API that was used to serialize the webhook event payload.

The headers X-Scope-Topic & X-Scope-Topic-Version specify the topic that triggered the webhook.

If multiple versions are available for a given topic, each may be triggered by different circumstances.

Subscribing to a webhook topic

You can subscribe to a webhook topic by running the createWebhookSubscription mutation in GraphQL.

The following example shows how to subscribe for the scenario_session/create webhook topic.

GraphQL Query

Code Block
languagegraphql
mutation {
  createWebhookSubscription(    
    webhookSubscription: {
      topic: { key: 'scenario_session/create' }
      callbackUrl: "https://my-custom-application.com/hooks/scopear"
      sharedSecret: "SOME SUPER SECRET STRING"
    }
  ) {
    errors {
      field
      message
    }
    webhookSubscription {
      id
    }
  }
}

JSON response

Code Block
{
  "data": {
    "createWebhookSubscription": {
      "errors": [],
      "webhookSubscription": {
        "id": "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      }
    }
  }
}

Testing webhooks

When testing webhooks, you can run a local server or use a publicly available service such as Beeceptor. If you decide to run a server locally, then you need to make it publicly available using a service such as Pagekite or ngrok. The following URLS do not work as endpoints for webhooks:

  • Localhost

  • Domains like www.example.com

Creating an endpoint for webhooks

Your endpoint needs to be an HTTPS webhook address with a valid SSL certificate that can correctly process event notifications as described below. You also need to implement verification to make sure webhook requests originate from Scope.

Receiving a webhook

After you register a webhook URL, Scope issues an HTTP POST request to the URL specified every time that event occurs. The request's POST parameters contain JSON data relevant to the event that triggered the request.

Responding to a webhook

Your webhook acknowledges that it received data by sending a 200 OK response. Any response outside of the 200 range, including 3XX HTTP redirection codes, indicates that you did not receive the webhook. Scope does not follow redirects for webhook notifications and considers them to be an error response.

Frequency

Scope has implemented a five second timeout period and a retry period for subscriptions. Scope waits five seconds for a response to each webhook request. If there is no response, or an error is returned, then Scope retries the connection 25 retries over the next 21 days.

Note

If there are 25 consecutive failures, or more than 100 errors within a 1 hour period, then the webhook subscription is automatically disabled.

To avoid timeouts and errors, consider deferring app processing until after the webhook response has been successfully sent.

Verifying webhooks

Each webhook request includes a base64-encoded X-Shopify-Hmac-SHA256 header, which is generated using the shared secret you provide when creating a webhook subscription along with the data sent in the request.

Info

If you're using a Rack based framework such as Ruby on Rails or Sinatra, then the header you are looking for is HTTP_X_SHOPIFY_HMAC_SHA256.

To verify that the request came from Shopify, compute the HMAC digest according to the following algorithm and compare it to the value in the X-Shopify-Hmac-SHA256 header. If they match, then you can be sure that the webhook was sent from Shopify. As a best practice, the HMAC digest should be verified before the app responds to the webhook.

The following Ruby example uses the Sinatra web framework to verify a webhook request:

Code Block
languageruby
require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'
require 'active_support/security_utils'

# The Shopify app's shared secret, viewable from the Partner dashboard
SHARED_SECRET = 'my_shared_secret'



helpers do
  # Compare the computed HMAC digest based on the shared secret and the request contents
  # to the reported HMAC in the headers
  def verify_webhook(data, hmac_header)
    calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', SHARED_SECRET, data))
    ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
  end
end



# Respond to HTTP POST requests sent to this web service
post '/' do
  request.body.rewind
  data = request.body.read
  verified = verify_webhook(data, env["HTTP_X_SHOPIFY_HMAC_SHA256"])

  # Output 'true' or 'false'
  puts "Webhook verified: #{verified}"
end

Filtering webhooks

If you use a single endpoint (aka “callback_url”) to receive multiple subscriptions, you will need to filter requests using the headers.

The following Ruby example assumes you are only interested in handling updated events for ScenarioSession resources:

Code Block
languageruby
#!/usr/bin/ruby

# @payload: hash
def handleWebook(headers, body)
  return unless headers['X-Scope-Topic'] == 'scenario_session/updated'
    
  # business logic goes here...
end

Best practices

This section contains some procedures to ensure your webhook integration functions as seamlessly as possible.

Recovering webhooks

In the event that your app goes offline for an extended period of time, you can recover your webhook subscriptions by re-registering (or re-enabling) your webhook subscriptions and importing the missing data.

Re-registering webhook subscriptions

To re-register disabled webhook subscriptions, consult the app's code that initially registered the webhooks. You can add a check that fetches all your active webhook subscriptions and only registers the ones that you need.

Re-enabling webhook subscriptions

To re-enable disabled webhook subscriptions, execute the following query:

GraphQL Query

Code Block
languagegraphql
mutation {
  updateWebhookSubscription(
    webhookSubscription: {
      id: "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      disabled: false
    }
  ) {
    errors {
      field
      message
    }
    webhookSubscription {
      id
    }
  }
}

JSON response

Code Block
{
  "data": {
    "updateWebhookSubscription": {
      "errors": [],
      "webhookSubscription": {
        "id": "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      }
    }
  }
}

Importing missing data

To import the missing data, you can fetch data from the outage period and feed it into your webhook processing code.

Note

Your app should not rely solely on receiving data from Shopify webhooks. Since webhook delivery is not always guaranteed, you should implement reconciliation jobs to periodically fetch data from Scope. Most query endpoints support both the created_at_min and updated_at_min filter parameters. These filters can be used to build a job that fetches all resources that have been created or updated since the last time the job ran.

Acting on webhook data

Upon receiving an HTTP POST to your webhook URL, you may want to query the API for additional information associated with a resource.

See Using Global Node Ids and Exploring the Graph.

Most relationships (aka “Connections”) are bidirectional in the Scope GraphQL API. This means that given a node in the graph, you can usually to navigate “up” to find parent-like objects (such as organization, viewer, or scenario) as well as as “down” to find child-like objects (such as scenarioSessionEvents, or scenarioSessionCheckboxes).

Examples

Given the following webhook:

Code Block
{
  "time": "2020-10-20T18:06:02Z",
  "event": "create",
  "type": "resource",
  "data": {
    "resource-id": "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=",
    "resource-url": "https://cms.scopear.com/api/v3/graphql/object/w4Aa1bjiiNSt1StG11n40b4dYU8lAigrCalO8r6cYwYchxAsWytj-F6wojhI_CsN",
    "resource-type": "ScenarioSession"
  }
}

An integrator could query the API to answer the question “what Scenario is associated with this ScenarioSession?”:

Code Block
query {
  node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") {
    ... on ScenarioSession {
      scenario {
        id
      }
    }
  }
}

Or, an integrator could query the API as follows to answer the question “what ScenarioSessionEvents are associated with this ScenarioSession?”:

Code Block
query {
  node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") {
    ... on ScenarioSession {
      scenarioSessionEvents {
        nodes {        
          id
        }
      }
    }
  }
}          id
        }
      }
    }
  }
}

Customizing the payload

In lieu of querying the Scope GraphQL API for additional data after receiving a webhook, you can supply a valid GraphQL query when subscribing to a webhook.

When a query is supplied for a webhook subscription, it will be executed by the server when preparing each relevant webhook notification for delivery, and the result of that execution will replace the default JSON payload.

Info

Subscription queries are executed with the context of the user & organization that created the subscription; as if the user had authenticated and executed the query via the API.

By way of example, to customize the payload for a scenario_session/completed webhook subscription, you could supply the following query when creating or updating the subscription:

Code Block
languagegraphql
query {
  node(id: "t5V3S3X8Trt/weWu0A2YHb0cRFJfcRA2rXT3vAGTz1A=")
    ... on ScenarioSession
      startedAt
      endedAt
      duration
      state
      parts {
        nodes {
          id
          duration        
        }
      }
    }
  }
}

See Sample Queries for more complicated query examples.

...

List of available webhook topics

Created, updated, and deleted topics are available for every GraphQL Object Type that implements the Node Interface (e.g. scenario_session/created, scenario_session/updated, and scenario_session/deleted).

Note

Although deleted topics are available for all Node types, not all Node types can be deleted (e.g. scenario_session). This means that you might never receive a webhook notification for a topic that you have subscribed to.

Additionally, the following special use topics are available:

  • call/completed

  • call_recording/completed

  • scenario_session/paused

  • scenario_session/resumed

  • scenario_session/completed

  • scenario_session_part/completed

  • scenario_session_step/completed

Alternatively, you can retrieve the list of currently available topics at any time by executing the following query against the Scope GraphQL API:

Code Block
languagegraphql
query {
  webhookTopics {
    nodes {
      key
      version
      disabled_at
    }
  }
}

A topic is disabled if the disable_at field is not null.

Configuring your app

To receive webhooks, register a HTTPS endpoint with your app to act as a webhook receiver.

Then, subscribe to a webhook topic and supply said endpoint as the callback_url value. Scope will immediately begin sending your app webhook notifications whenever events that pertain to the subscribed topic occur.

Webhook subscriptions are scoped to the user and organization that they're registered with.

This means that: a) other users & organizations can't view, modify, or delete the subscriptions you create; and b) subscriptions registered for other organizations will not be triggered by events that pertain to your organization’s data.

Versioning

The header X-Scope-API-Version specifies the version of the Scope GraphQL API that was used to serialize the webhook event payload.

The headers X-Scope-Topic & X-Scope-Topic-Version specify the topic that triggered the webhook.

If multiple versions are available for a given topic, each may be triggered by different circumstances.

Subscribing to a webhook topic

You can subscribe to a webhook topic by running the createWebhookSubscription mutation in GraphQL.

The following example shows how to subscribe for the scenario_session/create webhook topic.

GraphQL Query

Code Block
languagegraphql
mutation {
  createWebhookSubscription(    
    webhookSubscription: {
      topic: { key: 'scenario_session/create' }
      callbackUrl: "https://my-custom-application.com/hooks/scopear"
      sharedSecret: "SOME SUPER SECRET STRING"
    }
  ) {
    errors {
      field
      message
    }
    webhookSubscription {
      id
    }
  }
}

JSON response

Code Block
{
  "data": {
    "createWebhookSubscription": {
      "errors": [],
      "webhookSubscription": {
        "id": "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      }
    }
  }
}

Testing webhooks

When testing webhooks, you can run a local server or use a publicly available service such as Beeceptor. If you decide to run a server locally, then you need to make it publicly available using a service such as Pagekite or ngrok. The following URLS do not work as endpoints for webhooks:

  • Localhost

  • Domains like www.example.com

Creating an endpoint for webhooks

Your endpoint needs to be an HTTPS webhook address with a valid SSL certificate that can correctly process event notifications as described below. You also need to implement verification to make sure webhook requests originate from Scope.

Receiving a webhook

After you register a webhook URL, Scope issues an HTTP POST request to the URL specified every time that event occurs. The request's POST parameters contain JSON data relevant to the event that triggered the request.

Responding to a webhook

Your webhook acknowledges that it received data by sending a 200 OK response. Any response outside of the 200 range, including 3XX HTTP redirection codes, indicates that you did not receive the webhook. Scope does not follow redirects for webhook notifications and considers them to be an error response.

Frequency

Scope has implemented a five second timeout period and a retry period for subscriptions. Scope waits five seconds for a response to each webhook request. If there is no response, or an error is returned, then Scope retries the connection 25 retries over the next 21 days.

Note

If there are 25 consecutive failures, or more than 100 errors within a 1 hour period, then the webhook subscription is automatically disabled.

To avoid timeouts and errors, consider deferring app processing until after the webhook response has been successfully sent.

Verifying webhooks

Each webhook request includes a base64-encoded X-Scope-Hmac-SHA256 header, which is generated using the shared secret you provide when creating a webhook subscription along with the data sent in the request.

Info

If you're using a Rack based framework such as Ruby on Rails or Sinatra, then the header you are looking for is HTTP_X_Scope_HMAC_SHA256.

To verify that the request came from Scope, compute the HMAC digest according to the following algorithm and compare it to the value in the X-Scope-Hmac-SHA256 header. If they match, then you can be sure that the webhook was sent from Scope. As a best practice, the HMAC digest should be verified before the app responds to the webhook.

The following Ruby example uses the Sinatra web framework to verify a webhook request:

Code Block
languageruby
require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'
require 'active_support/security_utils'

# The Scope app's shared secret, viewable from the Partner dashboard
SHARED_SECRET = 'my_shared_secret'

helpers do
  # Compare the computed HMAC digest based on the shared secret and the request contents
  # to the reported HMAC in the headers
  def verify_webhook(data, hmac_header)
    calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', SHARED_SECRET, data))
    ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
  end
end

# Respond to HTTP POST requests sent to this web service
post '/' do
  request.body.rewind
  data = request.body.read
  verified = verify_webhook(data, env["HTTP_X_Scope_HMAC_SHA256"])

  # Output 'true' or 'false'
  puts "Webhook verified: #{verified}"
end

Best practices

This section contains some procedures to ensure your webhook integration functions as seamlessly as possible.

Filtering webhooks

If you use a single endpoint (aka “callback_url”) to receive multiple subscriptions, you will need to filter requests using the headers.

The following Ruby example assumes you are only interested in handling updated events for ScenarioSession resources:

Code Block
languageruby
#!/usr/bin/ruby

# @payload: hash
def handleWebook(headers, body)
  return unless headers['X-Scope-Topic'] == 'scenario_session/updated'
    
  # business logic goes here...
end

Recovering webhooks

In the event that your app goes offline for an extended period of time, you can recover your webhook subscriptions by re-registering (or re-enabling) your webhook subscriptions and importing the missing data.

Re-registering webhook subscriptions

To re-register disabled webhook subscriptions, consult the app's code that initially registered the webhooks. You can add a check that fetches all your active webhook subscriptions and only registers the ones that you need.

Re-enabling webhook subscriptions

To re-enable disabled webhook subscriptions, execute the following query:

GraphQL Query

Code Block
languagegraphql
mutation {
  updateWebhookSubscription(
    webhookSubscription: {
      id: "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      disabled: false
    }
  ) {
    errors {
      field
      message
    }
    webhookSubscription {
      id
    }
  }
}

JSON response

Code Block
{
  "data": {
    "updateWebhookSubscription": {
      "errors": [],
      "webhookSubscription": {
        "id": "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk="
      }
    }
  }
}

Importing missing data

To import the missing data, you can fetch data from the outage period and feed it into your webhook processing code.

Note

Your app should not rely solely on receiving data from Scope webhooks. Since webhook delivery is not always guaranteed, you should implement reconciliation jobs to periodically fetch data from Scope. Most query endpoints support both the created_at_min and updated_at_min filter parameters. These filters can be used to build a job that fetches all resources that have been created or updated since the last time the job ran.