Webhooks
With webhooks you can create REST endpoints which create Engine executions when called.
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.
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.
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.
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.
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.
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"}}
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.
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.
Field | Description |
---|---|
Flow | The flow which is started when the webhook is called |
Enabled | If unset, the HTTP endpoint is not available |
Is productive | If set, the executions are started in productive mode. See Development and Productive Mode. |
Require login token cookie | If 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. |
Key | An 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. |
URL | A preview of the base URL of the webhook (Readonly) |
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.
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.
When a new webhook is created, a random API key will be generated for you.
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.
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.
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.
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
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
The value of the API key will not be passed to the execution.
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.
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
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.
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:
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
:
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>
'''
),
)
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
:
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),
)
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}]
Get the food with name "apple"
curl -X GET https://jollyroger.cloudomation.com/api/latest/webhook/food/call/apple
{"name": "apple", "price": 1}
Delete a food item.
curl -X DELETE https://jollyroger.cloudomation.com/api/latest/webhook/food/call/apple
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}
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}