Webhooks
Webhooks allow you to trigger a URL if for example an Attribute is edited
or a new Annotation is created. You can configure Webhooks to listen for
specific events like Annotation modifications or File uploads. SmartShape
will send a POST request with data to the webhook URL.
You’ll need to set up your own Webhook receiver to receive information from
SmartShape, and eventually send it to another app, according to your needs.
Overview¶
Webhooks are "user-defined HTTP
callbacks". They are usually triggered by some event, such as editing an
Annotation or uploading a File. When that event occurs, SmartShape makes
an HTTP request to the URI configured for the Webhook. The action taken may be
anything. Common uses are to update a 3rd party database or run some
computations.
Webhooks can be used to update an external app/database, trigger some
computations, send notifications or building an activity log. They are
available globally, owned by the user who registers them and registered only
for a specific event.
Webhook endpoint implementation¶
Requirements¶
If you are writing your own endpoint (web server) that will receive SmartShape
Webhooks keep in mind the following things:
- The endpoint must return a valid HTTP response complying with the following requirements:
- The status code must be
200(aka "OK"). - The body must contain
{"success": true}. - The header
Content-Typemust be set toapplication/json. - If the endpoint does not return a (valid) response, SmartShape will consider the hook failed and retry it.
- The endpoint must check the value of the
x-smartshape-tokenheader against the actual value set when creating theWebhookin order to authenticate theWebhookevent source. - The endpoint should send its HTTP response as fast as possible. If it waits too long, SmartShape may decide the hook failed and retry it.
Examples¶
Here is some boilerplate code to implement a valid Webhook endpoint:
import falcon
import bcrypt
from gunicorn.six import iteritems
SECRET = "actual-webhook-secret-token"
def webhook_endpoint(req, res):
body = json.loads(req.stream.read())
print(body)
if 'event' not in body or not bcrypt.checkpw(SECRET, req.get_header('x-smartshape-token')):
res.status_code = 400
else:
res.status = falcon.HTTP_200
res.content_type = 'application/json'
res.body = json.dumps({'success': True})
class StandaloneApplication(gunicorn.app.base.BaseApplication):
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super(StandaloneApplication, self).__init__()
def load_config(self):
config = dict([(key, value) for key, value in iteritems(self.options)
if key in self.cfg.settings and value is not None])
for key, value in iteritems(config):
self.cfg.set(key.lower(), value)
def load(self):
return self.application
app = falcon.API()
app.add_sink(webhook_endpoint, '/callback')
options = {
'bind': '%s:%s' % ('0.0.0.0', '8042'),
'accesslog': '-',
}
StandaloneApplication(app, options).run()
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
const secret = "actual-webhook-secret-token";
app.use(bodyParser.json({limit: '50mb'}));
app.post(config.ServerApiEndpoint + '/callback', function (req, res) {
console.log(req.body);
if (!req.body.hasOwnProperty('event')
|| !bcrypt.compareSync(secret, req.headers['x-smartshape-token'])) {
res.status(400);
} else {
res.status(200).json({ success: true });
}
});
const server = app.listen(8042);
Content of a Webhook event¶
Each Webhook event is a JSON payload that always contain the same mandatory
fields:
file: the ID of theFilethe event was triggered from.event: the name of the event.data: additional data specific to that event.owner: the owner of theWebhook.runTimestamp: the Unix timestamp for the time and date at which the event was triggered.uuid: a universal unique ID for that specific event.
Example:
{
"file": "5c6aeed079bf3f432d3c929e",
"event": "attribute_updated",
"data": {
"user": {
"email": "patrick.star@smartshape.io"
},
"nodeIdToAttributes": {
"5b337c601e24e904da827738": { "my attribute": "myValue 42" }
}
},
"owner": "test@smartshape.io",
"runTimestamp": 1550824906016,
"uuid": "4ba57abb-4ab6-435c-bdc4-d0a5e3bdeb97",
}
Available Webhook events¶
The attribute_updated event¶
This event is triggered by the POST /scene/attributes
API endpoint.
{
"file": "5c6aeed079bf3f432d3c929e",
"event": "attribute_updated",
"data":
{
"user": {
"email": "test@smartshape.io"
},
"nodeIdToAttributes": {
"5b337c601e24e904da827738": { "new attr 1": "myValue 42" }
}
},
"owner": "test@smartshape.io",
"runTimestamp": 1550824906016,
"uuid": "43a57abb-4ab6-435c-bdc4-d0a5e3bdeb96"
}
The file_uploaded event¶
This event is triggered by the POST /file/upload/
API endpoint.
{
"file": "5c6fb52c194afb281976f5e4",
"event": "file_uploaded",
"data": {
"parent": "/",
"key": "/test_model.scene",
"owner": "test@smartshape.io",
"roles": [],
"user": "test@smartshape.io",
"isLink": false,
"shareToken": "",
"size": 6581919,
"created": "2019-01-20T08:39:08.529Z",
"updated": "2019-01-20T08:39:08.529Z",
"attributes": {},
"allowLivePublicMeetings": false,
"state": "queued",
"error": "",
"sceneTreeNames": [],
"sceneTreeRoots": [],
"favoritedBy": [],
"_id": "5c6fb52c194afb281976f5e4",
"revisionDate": "2019-01-20T08:39:08.529Z",
"type": "file",
"bucket": "0ee0bd3740ea8e21",
"name": "test_model.scene",
"__v": 0
},
"owner": "test@smartshape.io",
"runTimestamp": 1550824748604,
"uuid": "a36b623a-c1ed-49b5-9585-3532fe8e9881"
}
The file_optimized event¶
This even is triggered when an uploaded file has been optimized and is ready for visualization.
{
"file": "5c6fb52c194afb281976f5e4",
"event": "file_optimized",
"data": {
"parent": "/",
"key": "/test_model.scene",
"owner": "test@smartshape.io",
"roles": [],
"user": "test@smartshape.io",
"isLink": false,
"shareToken": "",
"size": 6581919,
"created": "2019-01-20T08:39:08.529Z",
"updated": "2019-01-20T08:39:08.723Z",
"attributes": {},
"allowLivePublicMeetings": false,
"state": "converting",
"error": "",
"sceneTreeNames": [ "default" ],
"sceneTreeRoots": [ "5da2149a-5623-75dd-65ba-37dc6c4275aa" ],
"favoritedBy": [],
"__v": 0,
"revisionDate": "2019-01-20T08:39:08.529Z",
"type": "file",
"bucket": "0ee0bd3740ea8e21",
"name": "test_model.scene",
"_id": "5c6fb52c194afb281976f5e4"
},
"owner": "test@smartshape.io",
"runTimestamp": 1550824758998,
"uuid": "081dfc81-a818-4b24-ae7f-c25953e1da09"
}
The annotation_created event¶
This event is triggered by the POST /annotation/create/
API endpoint.
{
"file": "5c6aeed079bf3f432d3c929e",
"event": "annotation_created",
"data": {
"targets": [],
"tags": [],
"readPermissions": [],
"writePermissions": [],
"permissions": {
"_id": "5c6fbb654fb090280ecc926a",
"writeTargets": [],
"readTargets": [],
"delete": [],
"writeTitle": [],
"readTitle": [],
"writeDiscussion": [],
"readDiscussion": [],
"writeDescription": [],
"readDescription": []
},
"_id": "5c6fbb654fb090280ecc926b",
"author": "test@smartshape.io",
"file": "5c6aeed079bf3f432d3c929e",
"lastUpdate": 1550826341717,
"date": 1550826341717,
"type": 0,
"content": "DESCRIPTION",
"color": "#fff",
"cameraZ": 3.394551,
"cameraY": 1.988481,
"cameraX": 3.394551,
"name": "test1",
"__v": 0
},
"owner": "test@smartshape.io",
"runTimestamp": 1550826342065,
"uuid": "46a6c861-de21-4a90-ba3c-fc33942701d5"
}
The annotation_updated event¶
This event is triggered by the POST /annotation/update/
API endpoint.
{
"file": "5c6aeed079bf3f432d3c929e",
"event": "annotation_updated",
"data": {
"old": {
"targets": [],
"tags": [],
"readPermissions": [],
"writePermissions": [],
"permissions": [],
"__v": 0,
"author": "test@smartshape.io",
"file": "5c6aeed079bf3f432d3c929e",
"lastUpdate": 1550826341717,
"date": 1550826341717,
"type": 0,
"content": "DESCRIPTION",
"color": "#fff",
"cameraZ": 3.394551,
"cameraY": 1.988481,
"cameraX": 3.394551,
"name": "test1",
"_id": "5c6fbb654fb090280ecc926b"
},
"new": {
"targets": [],
"tags": [],
"readPermissions": [],
"writePermissions": [],
"permissions": [],
"__v": 0,
"author": "test@smartshape.io",
"file": "5c6aeed079bf3f432d3c929e",
"lastUpdate": 1550826341717,
"date": 1550826341717,
"type": 0,
"content": "DESCRIPTION",
"color": "#fff",
"cameraZ": 3.394551,
"cameraY": 1.988481,
"cameraX": 3.394551,
"name": "TITLE3",
"_id": "5c6fbb654fb090280ecc926b"
}
},
"owner": "test@smartshape.io",
"runTimestamp": 1550826412941,
"uuid": "a90c85c3-fa3e-4a88-be6f-8c1270c4c27b"
}
The annotation_removed event¶
This event is triggered by the DELETE /annotation/delete/
API endpoint.
{
"file": "5c6aeed079bf3f432d3c929e",
"event": "annotation_removed",
"data": {
"targets": [],
"tags": [],
"readPermissions": [],
"writePermissions": [],
"permissions": {
"_id": "5c6fbb654fb090280ecc926a",
"writeTargets": [],
"readTargets": [],
"delete": [],
"writeTitle": [],
"readTitle": [],
"writeDiscussion": [],
"readDiscussion": [],
"writeDescription": [],
"readDescription": []
},
"__v": 0,
"author": "test@smartshape.io",
"file": "5c6aeed079bf3f432d3c929e",
"lastUpdate": 1550826341717,
"date": 1550826341717,
"type": 0,
"content": "DESCRIPTION",
"color": "#fff",
"cameraZ": 3.394551,
"cameraY": 1.988481,
"cameraX": 3.394551,
"name": "TITLE3",
"_id": "5c6fbb654fb090280ecc926b"
},
"owner": "test@smartshape.io",
"runTimestamp": 1550826499086,
"uuid": "154bee55-edf1-42ad-bf31-18231acee13b"
}
The configuration_created event¶
This event is triggered by the POST /file/configuration/
API endpoint.
{
"file":"5f56415276fca42bf953eaef",
"event":"configuration_created",
"data":{
"name":"New configuration by test",
"author":"test@smartshape.io",
"lastModificationAuthor":null,
"lastActivationDate":"1970-01-01T00:00:00.000Z",
"lockedBy":null,
"disableCamera":true,
"cameraPosition":[],
"cameraType":null,
"environmentFile":null,
"environmentBrightness":1,
"environmentOrientation":0,
"layersEnabled":true,
"enabledLayers":[],
"behaviorsEnabled":true,
"enabledBehaviors":[
"5f56417d76fca42bf953eb15",
"5f56417d76fca42bf953eb17"
],
"enabledEffects":[],
"transparencyEffectParameters":{
"alphaCoefficient":0,
"_id":"5f5b3ba35d45744bc74b16e0"
},
"explodedViewDistance":null,
"explodedViewEnabled":false,
"environmentEnabled":true,
"skyboxEnabled":true,
"iblEnabled":true,
"upVector":"Y+",
"zNear":0.1,
"cameraFoVIndex":0,
"activeOnStart":false,
"modifiersEnabled":false,
"modifiers":[],
"sceneEnabled":false,
"rememberSelection":false,
"sceneTree":0,
"selectedNodes":[],
"autosaveCameraPosition":false,
"collaborationEnabled":true,
"collaborativeAvatarsEnabled":true,
"collaborativeSelectionEnabled":true,
"enabledClippingPlanes":[],
"clippingPlanesEnabled":false,
"enabledQuotations":[],
"quotationsEnabled":false,
"readPermissions":[],
"writePermissions":[],
"_id":"5f5b3ba35d45744bc74b16e1",
"date":1599814563390,
"file":"5f56415276fca42bf953eaef",
"__v":0
},
"owner":"test@smartshape.io",
"runTimestamp":1599814563406,
"uuid":"37e82939-93fe-4712-b8b2-4c3d20995220"
}
The configuration_updated event¶
This event is triggered by the POST /file/configuration/
API endpoint.
{
"file":"5f56415276fca42bf953eaef",
"event":"configuration_updated",
"data":{
"old":{
"name":"New configuration by test",
"author":"test@smartshape.io",
"lastModificationAuthor":"test@smartshape.io",
"lastActivationDate":"2020-09-11T08:56:04.406Z",
"lockedBy":null,
"disableCamera":true,
"cameraPosition":[],
"cameraType":null,
"environmentFile":null,
"environmentBrightness":1,
"environmentOrientation":0,
"layersEnabled":true,
"enabledLayers":[],
"behaviorsEnabled":true,
"enabledBehaviors":[
"5f56417d76fca42bf953eb15",
"5f56417d76fca42bf953eb17"
],
"enabledEffects":[],
"transparencyEffectParameters":{
"alphaCoefficient":0,
"_id":"5f5b3ba35d45744bc74b16e0"
},
"explodedViewDistance":0,
"explodedViewEnabled":false,
"environmentEnabled":true,
"skyboxEnabled":true,
"iblEnabled":true,
"upVector":"Y+",
"zNear":0.1,
"cameraFoVIndex":0,
"activeOnStart":false,
"modifiersEnabled":false,
"modifiers":[],
"sceneEnabled":false,
"rememberSelection":false,
"sceneTree":0,
"selectedNodes":[],
"autosaveCameraPosition":false,
"collaborationEnabled":true,
"collaborativeAvatarsEnabled":true,
"collaborativeSelectionEnabled":true,
"enabledClippingPlanes":[],
"clippingPlanesEnabled":false,
"enabledQuotations":[],
"quotationsEnabled":false,
"readPermissions":[],
"writePermissions":[],
"lastModificationDate":1599814564114,
"__v":0,
"date":1599814563390,
"file":"5f56415276fca42bf953eaef",
"_id":"5f5b3ba35d45744bc74b16e1"
},
"new":{
"name":"New configuration by test",
"author":"test@smartshape.io",
"lastModificationAuthor":"test@smartshape.io",
"lastActivationDate":"2020-09-11T08:56:04.406Z",
"lockedBy":null,
"disableCamera":true,
"cameraPosition":[],
"cameraType":null,
"environmentFile":null,
"environmentBrightness":1,
"environmentOrientation":0,
"layersEnabled":false,
"enabledLayers":[],
"behaviorsEnabled":true,
"enabledBehaviors":[
"5f56417d76fca42bf953eb15",
"5f56417d76fca42bf953eb17"
],
"enabledEffects":[],
"transparencyEffectParameters":{
"alphaCoefficient":0,
"_id":"5f5b3ba35d45744bc74b16e0"
},
"explodedViewDistance":0,
"explodedViewEnabled":false,
"environmentEnabled":true,
"skyboxEnabled":true,
"iblEnabled":true,
"upVector":"Y+",
"zNear":0.1,
"cameraFoVIndex":0,
"activeOnStart":false,
"modifiersEnabled":false,
"modifiers":[],
"sceneEnabled":false,
"rememberSelection":false,
"sceneTree":0,
"selectedNodes":[],
"autosaveCameraPosition":false,
"collaborationEnabled":true,
"collaborativeAvatarsEnabled":true,
"collaborativeSelectionEnabled":true,
"enabledClippingPlanes":[],
"clippingPlanesEnabled":false,
"enabledQuotations":[],
"quotationsEnabled":false,
"readPermissions":[],
"writePermissions":[],
"lastModificationDate":1599815472275,
"__v":0,
"date":1599814563390,
"file":"5f56415276fca42bf953eaef",
"_id":"5f5b3ba35d45744bc74b16e1"
}
},
"owner":"test@smartshape.io",
"runTimestamp":1599815472307,
"uuid":"03cf14d0-f624-4aaa-811b-c85598fa5f3a"
}
The configuration_removed event¶
This event is triggered by the DELETE /file/configuration/
API endpoint.
{
"file":"5f56415276fca42bf953eaef",
"event":"configuration_removed",
"data":{
"name":"New configuration by test",
"author":"test@smartshape.io",
"lastModificationAuthor":"test@smartshape.io",
"lastActivationDate":"2020-09-11T09:23:36.585Z",
"lockedBy":null,
"disableCamera":true,
"cameraPosition":[],
"cameraType":null,
"environmentFile":null,
"environmentBrightness":1,
"environmentOrientation":0,
"layersEnabled":true,
"enabledLayers":[],
"behaviorsEnabled":true,
"enabledBehaviors":[
"5f56417d76fca42bf953eb15",
"5f56417d76fca42bf953eb17"
],
"enabledEffects":[],
"transparencyEffectParameters":{
"alphaCoefficient":0,
"_id":"5f5b42160a6fa16c5af0e7c7"
},
"explodedViewDistance":0,
"explodedViewEnabled":false,
"environmentEnabled":true,
"skyboxEnabled":true,
"iblEnabled":true,
"upVector":"Y+",
"zNear":0.1,
"cameraFoVIndex":0,
"activeOnStart":false,
"modifiersEnabled":false,
"modifiers":[],
"sceneEnabled":false,
"rememberSelection":false,
"sceneTree":0,
"selectedNodes":[],
"autosaveCameraPosition":false,
"collaborationEnabled":true,
"collaborativeAvatarsEnabled":true,
"collaborativeSelectionEnabled":true,
"enabledClippingPlanes":[],
"clippingPlanesEnabled":false,
"enabledQuotations":[],
"quotationsEnabled":false,
"readPermissions":[],
"writePermissions":[],
"renderMode":"phong",
"lastModificationDate":1599816215895,
"__v":0,
"date":1599816214772,
"file":"5f56415276fca42bf953eaef",
"_id":"5f5b42160a6fa16c5af0e7c8"
},
"owner":"test@smartshape.io",
"runTimestamp":1599816222062,
"uuid":"afc4179f-1258-4f28-a674-01bb821c60dc"
}
Creating a new Webhook¶
A new Webhook can be created using either:
- The
PUT /webhook/Web API endpoint. - The
webhook createCLI command.
The following example creates a new Webhook for the attribute_updated event
with a callback set to https://my.domain.com/path/to/webhook/endpoint:
Note: the Webhook secret is how incoming events can be authenticated by
the callback web server. It must be set to a non-trivial and must be secured
like any other sensitive credentials.
Listing Webhooks¶
Existing Webhooks can be listed using either:
- The
GET /webhook/Web API endpoint. - The
webhook listCLI command.
Deleting a Webhook¶
Existing Webhooks can be deleted using either:
- The
DELETE /webhook/Web API endpoint. - The
webhook deleteCLI command.
The following example will delete the Webhook with its ID equal to
5c6aeed079bf3f432d3c929e:
Scripting Webhooks using smartshape-cli¶
Spinning a web server, setting up routes and checking the Webhook secret can
be a bit tedious and unpractical. Especially when doing some prototyping.
To make this kind of use cases easier, smartshape-cli provides a very
convenient command: webhook listen. This command will automagically:
- Create a
Webhookfor each event. - Start a web server that will print each received event payload.
- Delete all the
Webhooksit created when exiting.
Using pipes and Bash/PowerShell utilities, one can easily script Webhooks
with nothing but a shell.
The following example uses the webhook listen and attribute upsert commands
combined with jq and xargs to insert/update the mtime ("modification
time") Attribute of each object when any other Attribute is updated:
smartshape-cli webhook listen --event attribute_updated \
| jq --unbuffered -rc '
[
[.file],
[.runTimestamp / 1000 | todateiso8601],
[.data.nodeIdToAttributes | to_entries[] | select(.value | has("mtime") | not) | .key]
]
| combinations
| join(" ")
' \
| xargs -L1 sh -c '
smartshape-cli attribute upsert \
$0 \
"{\"mtime\": \"$1\"}" \
--query "id:$2" \
> /dev/null
'
smartshape-cli webhook listen --event attribute_updatedwill listen to theattribute_updatedevent and stream all the received JSON event payloads onstdout.jqwill output a new line for each event, with the file ID, the current (ISO 8601 formatted) date and the object ID separated by spaces.xargswill runsmartshape-cli attribute upsertfor each line, using the different space-separated values as$0,$1and$2.