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-Type
must 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-token
header against the actual value set when creating theWebhook
in order to authenticate theWebhook
event 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 theFile
the 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 create
CLI 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 list
CLI command.
Deleting a Webhook
¶
Existing Webhooks
can be deleted using either:
- The
DELETE /webhook/
Web API endpoint. - The
webhook delete
CLI 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
Webhook
for each event. - Start a web server that will print each received event payload.
- Delete all the
Webhooks
it 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_updated
will listen to theattribute_updated
event and stream all the received JSON event payloads onstdout
.jq
will output a new line for each event, with the file ID, the current (ISO 8601 formatted) date and the object ID separated by spaces.xargs
will runsmartshape-cli attribute upsert
for each line, using the different space-separated values as$0
,$1
and$2
.