Webhooks allow integrators to build Applications that execute after specific events occur in the Scopear.com ecosystem.
For example, a Scope Customer might leverage a webhook to trigger a custom action that imports ScenarioSession
data into the Customer’s system of record whenever a User completes a session in the Scope Worklink app.
This guide introduces how webhooks work, including how to configure a webhook for your app.
Also known as “Service Hooks”.
In this article
Requirements
Your organization must be an active Scope customer.
Your organization must have provisioned at least one Scopear.com
User
account.You must have previously authenticated with the Scope GraphQL API.
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 a system-of-record (e.g. when users complete Scope Scenarios)
See Orchestrating Common Use Cases to see a visualization of webhooks used in connection with other services and components.
What is a webhook?
A Webhook is a single HTTP POST request that is sent to a url that you specify, and is typically triggered by the occurrence of a specific, predefined event. Scope Webhooks contain a JSON payload in the body and metadata in the headers.
A 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”) andversion
(e.g. “1.1”).A 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, acallback url
, and an optional GraphQLquery
(to be executed during delivery).
If a valid query is supplied with a webhook subscription, it will be executed during the delivery of each subsequent webhook notification, and the result will replace the default JSON payload. See customizing the payload for more information.
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 for the specified topic.
By way of example, the default scenario_session/created
webhook notification includes the following:
Headers
X-Scope-Topic:
scenario_session/created
X-Scope-Topic-Version:
1.1
X-Scope-API-Version:
3
X-Scope-Domain:
scopear.com
X-Scope-Webhook-Type:
node
X-Scope-Hmac-Sha256:
XWmrwMey6OsLMeiZKwP4FppHH3cmAiiJJAweH5Jo4bM=
X-Scope-Webhook-Id:
b54557e4-bdd9-4b37-8a5f-bf7d70bcd043
If your organization is equipped with an “on-prem” install of the Scope System, your domain value is likely to be different than the example value above.
Where:
X-Scope-Topic
(&X-Scope-Topic-Version
) specify thetopic
that triggered the webhookX-Scope-API-Version
specifies the version of the Scope GraphQL API that was used to serialize the webhook event payloadX-Scope-Domain
specifies the system that sent the webhookX-Scope-Hmac-Sha256
is used to verify the authenticity of webhooksX-Scope-Webhook-Id
is used to troubleshootX-Scope-Webhook-Type
specifies the type of payload being sent
Currently, all Scope webhook notifications include a value of node
for the X-Scope-Webhook-Type
header. In the future, we hope to introduce additional 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.
Body
{ "data": { "id": "t5V3S3X8Trt/weWu0A2YHb0cRFJfcRA2rXT3vAGTz1A=", "url": "https://cms.scopear.com/api/v3/graphql/object/t5V3S3X8Trt/weWu0A2YHb0cRFJfcRA2rXT3vAGTz1A=" } }
Where:
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.
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 a User).
How can I get more information?
By default, Scope delivers minimalistic payloads in order to encourage integrators to query the Scope GraphQL API for the specific information that they require.
GraphQL is a powerful tool that allows integrators to ask for exactly what they need.
See Using Global Node Ids and Exploring the Graph to learn how to use the Scope GraphQL API.
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
).
GraphQL Examples
Given the following webhook:
{ "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:
query { node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") { ... on ScenarioSession { scenario { id } } } }
Or, an integrator could answer the question “what ScenarioSessionEvents are associated with this ScenarioSession?” by querying the Scope GraphQL API as follows:
query { node(id: "OiSSdfK1Su4gwG4zAi2InavDB9eRE9aOnGHYRMG5dqY=") { ... on ScenarioSession { scenarioSessionEvents { nodes { id } } } } }
Customizing the payload
In lieu of querying for additional data after receiving a webhook, integrators can supply a valid GraphQL query, in advance, while subscribing to a webhook.
When a query
is supplied with a webhook subscription, it will be executed by the server each time that it prepares a notification for delivery, and the result of that execution will replace the default JSON payload.
This feature saves integrators from a round trip with the server and reduces the requirements for building integrations (i.e. integrators that supply queries in advance can more easily use platforms like Zapier).
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 can supply a query argument when creating or updating the subscription:
mutation { createWebhookSubscription( input: { webhookSubscription: { topic: { key: "scenario_session/completed" } callbackUrl: "!!!REPLACE ME WITH PRODUCTION VALUE!!!" sharedSecret: "!!!REPLACE ME WITH PRODUCTION VALUE!!!" query: """ query ($nodeId: ID!) { node(id: $nodeId) { ... on ScenarioSession { startedAt endedAt duration idleDuration numberOfStepsPossible numberOfStepsViewed percentOfStepsViewed state externalData scenarioRelease { author { id name } } } } } """ } } ) { errors { fullMessage message path type } webhookSubscription { id } } }
$nodeId (of type ID
) and $nodeType (of type String
) are supplied by the server as input parameters when executing the query that you provide.
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
).
Although updated and deleted topics are available for all Node types, not all Node types can be updated or deleted. This means that you might not ever 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 by executing the following query against the Scope GraphQL API:
query { webhookTopics { nodes { key version disabledAt } } }
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 the endpoint registered in the previous step as the callbackUrl
input value. Scope will immediately begin sending your app webhook notifications whenever events occur that pertain to the specified topic.
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 for events that pertain to your organization’s data.
Subscribing to a webhook topic
You can subscribe to a webhook topic by executing the createWebhookSubscription mutation in GraphQL.
The following example shows how to subscribe for the scenario_session/created
webhook topic (via the Scope GraphQL API).
When multiple versions are available for a given topic, each is likely to be triggered by different circumstances or behave differently in some way. If you do not supply a topic version (or ID) when executing the mutation, the latest available topic version will be used for the topic key that you supply.
GraphQL Query
mutation { createWebhookSubscription( webhookSubscription: { topic: { key: 'scenario_session/created' } callbackUrl: "https://my-custom-application.com/hooks/scopear" sharedSecret: "SOME SUPER SECRET STRING" } ) { errors { field message } webhookSubscription { id } } }
JSON response
{ "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 a valid HTTPS url 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 both a timeout period and a retry period for delivery. 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.
If there are 25 consecutive failures for a specific notification, or more than 100 errors within a 1 hour period for a specific subscription, 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.
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:
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
After having subscribed to one or more webhook topics. it is your responsibility to filter out unwanted notifications. There is no mechanism available to filter events on the server, nor does Scope intend to ever implement such a feature.
The following Ruby example demonstrates how to filters events (assuming that you are only interested in handling updated
events for ScenarioSession
resources):
#!/usr/bin/ruby def filterWebookNotification(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
mutation { updateWebhookSubscription( webhookSubscription: { id: "wMtvO+6DUTQZomQK8MP0Ppsip6hiuTe1TIihz7iadkk=" disabled: false } ) { errors { field message } webhookSubscription { id } } }
JSON response
{ "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.
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.
Add Comment