Aller au contenu

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 to application/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 the Webhook in order to authenticate the Webhook 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 the File the event was triggered from.
  • event: the name of the event.
  • data: additional data specific to that event.
  • owner: the owner of the Webhook.
  • 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 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.

smartshape-cli webhook create \
    "https://my.domain.com/path/to/webhook/endpoint" \
    "attribute_updated" \
    "my-secret-token"
curl -X PUT http://smartshape.io.test/webhook/ \
    --header "Authorization: Bearer ${AUTH_TOKEN}" \
    --data '{
        "event": "attribute_updated",
        "url": "https://my.domain.com/path/to/webhook/endpoint",
        "secretToken": "my-secret-token",
        "[SECRET_TOKEN]"
    }'

Listing Webhooks

Existing Webhooks can be listed using either:

smartshape-cli webhook list
curl -X GET http://smartshape.io.test/webhook/ \
    --header "Authorization: Bearer ${AUTH_TOKEN}"

Deleting a Webhook

Existing Webhooks can be deleted using either:

The following example will delete the Webhook with its ID equal to 5c6aeed079bf3f432d3c929e:

smartshape-cli webhook delete 5c6aeed079bf3f432d3c929e
curl -X DELETE http://smartshape.io.test/webhook/5c6aeed079bf3f432d3c929e \
    --header "Authorization: Bearer ${AUTH_TOKEN}"

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 the attribute_updated event and stream all the received JSON event payloads on stdout.
  • 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 run smartshape-cli attribute upsert for each line, using the different space-separated values as $0, $1 and $2.

November 29, 2023 March 27, 2019