Skip to main content
Version: 9 - Germknödel

Webhooks

With webhooks you can create REST endpoints which create Engine executions when called.

note

Webhooks enable inbound communication to Engine. If you want to connect to a third party system from Engine you can do so with a connector.

Use Cases

Use webhooks to

  • start Engine executions from third party systems
  • receive notifications from third party systems
  • receive callbacks from asynchronous processes running in third party systems
  • create custom status HTML pages which display information gathered from Engine
  • expose information or functionality to consumers which do not have an Engine user

Concept

As long as an enabled webhook exists in Engine the REST endpoint is available to receive calls.

note

Each call to a productive webhook will consume one connection from your connection allowance.

Roles and Permissions

When a webhook starts an execution, the execution inherits all role permission of the webhook (given that the role of the webhook has the propagate flag set True). For more on this refer to RBAC.

HTTP method

The endpoint accepts the HTTP methods GET, DELETE, HEAD, POST, PUT, and PATCH. The method which was used is passed to the execution. If the HTTP method allows for a body payload and a body payload was provided, it is also passed to the execution.

warning

A HTTP GET request should only be used to retrieve a resource, while a HTTP POST request should be used to create a resource. Calling an Engine webhook creates an execution. Thus the recommended way to call a webhook is by using a POST request. For compatibility reasons Engine also allows calling webhooks using a GET request.

example

Consider a webhook named "captain" in the workspace "jollyroger".

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
# the execution will receive in the input_value:
# {"method": "GET"}

curl -X PUT https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
# the execution will receive in the input_value:
# {"method": "PUT"}

curl -X DELETE https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
# the execution will receive in the input_value:
# {"method": "DELETE"}

Path

Each webhook creates a base URL where it can be called. All calls below that base URL are also accepted. The path which was used is also passed to the execution.

example

Consider a webhook named "captain" in the workspace "jollyroger".

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
# the execution will receive in the input_value:
# {"path": None}

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call/
# the execution will receive in the input_value:
# {"path": ""}

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call/some/path
# the execution will receive in the input_value:
# {"path": "some/path"}

Headers

All HTTP headers which are sent from the client are passed to the execution.

example

Consider a webhook named "captain" in the workspace "jollyroger".

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call -H "my-header: value"
# the execution will receive in the input_value:
# {"headers": {"my-header": "value"}}
note

Curl also sets other headers which are passed to the execution. For brevity they were omitted from the example above.

Query parameters

All query parameters which are used are passed to the execution.

example

Consider a webhook named "captain" in the workspace "jollyroger".

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
# the execution will receive in the input_value:
# {"query": {}}

curl https://jollyroger.cloudomation.com/api/latest/webhook/captain/call?mode=search&q=foo
# the execution will receive in the input_value:
# {"query": {"mode": "search", "q": "foo"}}

Configuration

Please see the table below for the different webhook fields and their meanings.

Keep in mind that when exporting and importing webhooks, not all fields will be part of the export-import process. You will need to reconfigure some fields upon importing to another workspace. See the Import/Export property for webhooks here.

FieldDescription
FlowThe flow which is started when the webhook is called
EnabledIf unset, the HTTP endpoint is not available
Is productiveIf set, the executions are started in productive mode. See Development and Productive Mode.
Require login token cookieIf set, the client has to provide a valid Engine login token for the call to succeed. See Authentication. The execution will additionaly receive auth in the input_value containing information about the Engine user.
KeyAn API key. If set, the client has to provide the API key for the call to succeed. The API key can be specified as a query parameter or as a key in a JSON payload.
URLA preview of the base URL of the webhook (Readonly)
warning

Make sure to safely store your webhook keys. Once a webhook key is set, it cannot be read via the UI. Hence, you cannot recover a lost webhook key, you can only set a new one.

important

Editing the fields is_enabled, require_login and key is only possible via a dedicated webhook auth endpoint. This means that when changing these fields in the UI, you need to enter your password to re-authenticate yourself.

note

When a new webhook is created, a random API key will be generated for you.

note

A webhook in development-mode will immediately return with HTTP 402 when no user was active in the Engine UI within the last 10 minutes.

Supported content-types

Engine webhooks support the content-types application/json or application/yaml. If no request body is used, the value of the "Content-Type" header is ignored.

Public webhooks

A webhook can be considered public when it requires neither a login token nor an API key. For cloud workspaces, a public webhook can be called by anybody on the internet. For on-premise installations, public webhooks can be called by anybody who has network access to your Engine workspace.

warning

Be considerate and careful when exposing a public webhook. Always validate untrusted user input!

Private webhooks

A webhook can be considered private when it requires a login token and/or an API key.

note

If your webhook requires a login token and you call it without a valid session, you will be redirected to the login screen. After successful authentication you will again be redirected to the webhook call.

If the original request was not a GET request and it contained a payload, the payload will be lost.

example

Consider a webhook named "captain" in the workspace "jollyroger" which requires a login token.

# call without a token
$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 401 Unauthorized
...

"401 Unauthorized"

# call with a token
$ curl -i -H "x-cloudomation-token: ey..." https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 200 OK
example

Consider a webhook named "captain" in the workspace "jollyroger" which requires an API key.

# call without API key
$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 401 Unauthorized

# call with API key in the headers
$ curl -i -H "Authorization: Bearer my-secret-key" https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 200 OK

# call with API key in query parameter
$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call\?key=my-secret-key
HTTP/1.1 200 OK

# when using a method which allows for a body payload
# the API key can also be specified in the JSON body
$ curl -i -H "Content-Type: application/json" -d '{"key": "my-secret-key"}' -X POST https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 200 OK
note

The value of the API key will not be passed to the execution.

note

If the API key is specified in multiple places, they will be applied in the order: body payload -> query parameter -> authentication header

Synchronous and asynchronous webhooks

Per default webhooks are called synchronously. The HTTP call will block until the execution reaches the ENDED_SUCCESS status. In synchronous mode the output_value of the execution will be returned to the client in the response payload as JSON.

It is possible to call a webhook asynchronously by adding async to the query parameters. In asynchronous mode the HTTP response code will be 201 (Created) and the ID of the created execution will be returned in the response body.

example

Consider a webhook named "captain" in the workspace "jollyroger".

# synchronous mode, the output_value is returned as JSON
$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
...

{"output": "value", "returned": "here"}

# asynchronous mode, the execution ID is returned as plaintext
$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call\?async
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
...

aeae71f4-b062-4655-a333-d4d58014cf64
tip

Do not use synchronous webhooks for long-running executions. In most cases a HTTP timeout will interrupt your query. The execution will continue running though.

Error handling

When the execution does not end with ENDED_SUCCESS Engine will return the HTTP status 500 (Internal Server Error). The status and the status message of the execution will be returned in the response body.

example

Consider a webhook named "captain" in the workspace "jollyroger".

$ curl -i https://jollyroger.cloudomation.com/api/latest/webhook/captain/call
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
...

ENDED_ERROR: status-message which was set by the execution

Custom responses

You can return custom responses from webhooks. There are three helper methods available:

example

Use of a custom response to perform a redirect

The helper webhook_response allows for customization of status, headers, and body:

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
return this.webhook_response(
status=302, # the HTTP status code to return
headers={ # Additional HTTP headers to return
'Location': 'https://cloudomation.com',
},
body='response-body', # The body payload to return
)

The helper webhook_html_response accepts a HTML string and will set the status to HTTP 200 and content-type to text/html:

example

Use of a custom HTML response

import random
import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
return this.webhook_html_response(
body=(
f'''
<link rel="stylesheet" href="/default-styling.css">
<h1>Welcome to my homepage</h1>
<p>Rolling the dice: {random.randint(1,6)}</p>
<a href="call">roll again</a>
'''
),
)
tip

To apply sane default styling to your html responses, you can use our default stylesheet:

<link rel="stylesheet" href="/default-styling.css">

The helper webhook_json_response accepts any object which can be JSON serialized and will set the status to HTTP 200 and content-type to application/json:

example

Use of a custom JSON response

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
return this.webhook_json_response(
body={
"some": [
"nested",
"object",
],
},
)

CRUD Example

The following is a simple flow script which implements a REST API for a food store. It shows how to read the path of the endpoint and return a custom body and HTTP status.

import json
import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
input = this.get('input_value')
storage_setting = system.setting("food store")
try:
storage_setting.get("value")
except flow_api.exceptions.ResourceNotFoundError:
storage_setting.save(
value=[
{
"name": "apple",
"price": 1,
},
{
"name": "banana",
"price": 2,
},
]
)

storage_setting.acquire()
storage = storage_setting.get("value")

status = None
body = None
if input['method'] == 'GET':
if not input['path']:
status = 200
body = storage
else:
for item in storage:
if item["name"] == input["path"]:
status = 200
body = item
break
elif input["method"] == 'POST':
item = {
"name": input["json"]["name"],
"price": input["json"]["price"],
}
storage.extend([item])
storage_setting.save(value=storage)
body = item
status = 201
elif input["method"] == 'DELETE':
storage = [x for x in storage if x["name"] != input["path"]]
storage_setting.save(value=storage)
body = ""
status = 200
elif input["method"] == 'PATCH':
for item in storage:
if item["name"] == input["path"]:
item["name"] = input["json"].get("name", item["name"])
item["price"] = input["json"].get("price", item["price"])
storage_setting.save(value=storage)
body = item
status = 200
break

return this.webhook_response(
status=status,
body=json.dumps(body),
)
example

List all items in the food store

curl -X GET https://jollyroger.cloudomation.com/api/latest/webhook/food/call
[{"name": "banana", "price": 2}, {"name": "apple", "price": 1}]
example

Get the food with name "apple"

curl -X GET https://jollyroger.cloudomation.com/api/latest/webhook/food/call/apple
{"name": "apple", "price": 1}
example

Delete a food item.

curl -X DELETE https://jollyroger.cloudomation.com/api/latest/webhook/food/call/apple
example

Create a new food item

curl -X POST https://jollyroger.cloudomation.com/api/latest/webhook/food/call -d '{"name": "apple", "price": 2}' -H 'Content-Type: application/json'
{"name": "apple", "price": 2}
example

Update an existing item

curl -X PATCH https://jollyroger.cloudomation.com/api/latest/webhook/food/call/apple -d '{"price": 1}' -H 'Content-Type: application/json'
{"name": "apple", "price": 1}