Webhooks
Webhooks enable you to programmatically receive create, update, and delete event notifications. When these events are triggered in Fulcrum, we immediately push a notification to your server in the form of an HTTP POST request with a JSON payload containing the event’s data.
To leverage webhooks, you set up one or more servers that act as endpoints and register their URLs with Fulcrum. Your endpoint code should be able to receive and parse the JSON request and include your business logic, which could include firing off an email or SMS message, integrating with another service, tasking work, or whatever else you may need to do with your data.
Authorization, Permissions, and Roles
In order to use webhooks, your plan must have the Developer Pack enabled. Additionally, the user working with webhooks must be a member with a Role that has the Change Organization Profile permission. As the name implies, this permission gives the user the ability to do more than just manage webhooks. Since webhooks work at the organization level, it makes sense to have this permission tied to managing webhooks.
By default, the only role to have the above permission is the Owner System Role. This means the owner is initially the only person who can do anything with webhooks. Any Custom Role can explicitly be granted the Change Organization Profile permission to allow those users to work with webhooks.
Managing Fulcrum Webhooks
Webhooks can be managed via the web from the Settings page of your Fulcrum account. They can also be managed programmatically via the Webhooks REST API.
The total number of webhooks that a plan can have active at one time is ten. Typically, a single webhook can be used to send notifications to a web server and then that web server is what determines what to do with the data.
Choose an Endpoint
Since webhooks are HTTP-based callbacks, our Fulcrum servers make HTTP POST requests to the web server URLs you provide. This means you need an endpoint that can receive these POST requests and conform to the responsibilities outlined below.
You can write your own endpoint, adapt existing code, or use an integration tool, such as Zapier.
Write A Custom Endpoint
It’s likely you will want to write your own endpoint to implement functionality custom to your organization. Below are some very simple examples of starter endpoints.
<?php
$input = file_get_contents('php://input'); # POST data from webhook
$payload = json_decode($input, true);
# do something with the webhook payload...
print $payload['type']; # record.create
print $payload['data']['form_values']['9272']; # 2018-01-19
?>
The details you’ll need for implementing your own endpoint are discussed below, but it can be helpful to use a service such as Webhook.site for inspecting the actual webhook payloads sent by Fulcrum while developing your endpoints.
Webhook Safety
Be cautious when sending webhook payloads to 3rd party sites as you are providing them with your data!
Details
- You may register multiple webhooks for the same organization. This allows for multiple points of integration with your Fulcrum data.
- Each webhook event will initiate a separate HTTP POST request to each webhook URL registered within the organization.
- Your webhook endpoint is free to do anything with the response, as long as it complies with the Responsibilities section below. Update a database record, send an email, kick off some custom analysis, or anything else you can imagine.
Events
The following table describes all the events that can be raised and the data that will be sent to your webhook.
Event Type | Event Data | Examples |
---|---|---|
form.create | Data of the form just created. | Example payload |
form.update | Data of the form just updated. | Example payload |
form.delete | Data of the form just deleted. | Example payload |
record.create | Data of the record just created. | Example payload |
record.update | Data of the record just updated. | Example payload |
record.delete | Data of the record just deleted. | Example payload |
choice_list.create | Data of the choice list just created. | Example payload |
choice_list.update | Data of the choice list just updated. | Example payload |
choice_list.delete | Data of the choice list just deleted. | Example payload |
classification_set.create | Data of the classification set just created. | Example payload |
classification_set.update | Data of the classification set just updated. | Example payload |
classification_set.delete | Data of the classification set just deleted. | Example payload |
Notice that each resource type is singular. The event type itself is present tense.
Data Format
The relevant HTTP headers in the request are:
Content-Type: application/json
The event data is delivered as a JSON-encoded string in the POST request. This is indicated by the Content-Type header. And after parsing the payload as JSON, it will look something like:
{
"id": "<event id>",
"type": "<event type>",
"owner_id": "<id of the owning organization>",
"data": "<JSON representation of the resource>"
}
See the above example payloads for details of what each event will look like.
Webhook States
- A webhook can be either active or inactive. By default, a webhook is active.
- An active webhook will receive requests for events as they occur within your Fulcrum organization.
- A webhook marked as inactive will stop receiving event requests. If an event's request is processing while the webhook is made inactive, this request will finish. Any other existing, unprocessed event requests (including retries) will be canceled and not sent to your endpoint.
- If an inactive webhook is made active, it will begin receiving event requests once more, as soon as new event requests are created for that organization.
Responsibilities
Fulcrum's webhooks are responsible for delivering your data to your endpoint. Your endpoint's primary responsibility is to receive this data. We expect an HTTP 200
- 207
response code to indicate successful receipt of the request. Even if you're not interested in this event or data, for whatever reason, we expect a successful response, since you have, indeed, received the data.
Any response outside the 200
- 207
range will be treated as a failure, and the request retried later.
To keep the webhooks as fast as possible, we require a successful response code within 20 seconds of initiating the HTTP POST request to your server. After 20 seconds, the request will time out and be retried again later.
After receiving the data, you can perform further actions on it. Some of these actions may be fast (e.g. inserting the data into a database) while others may be slow (e.g. building a PDF report, or sending an email). Perform all your heavy lifting either on another thread or after you've successfully responded. This will avoid your processing running long enough to cause the request to time out.
Failures and Retries
It's understood that networks and servers go down. To accommodate that, we've made the webhook event requests fault tolerant.
A request made to a webhook URL that cannot connect, times out, returns a status code outside the 200
- 207
range, or experiences some other error will fail and retried again later.
The frequency of the retries is subject to exponential backoff. Meaning that the more times a request fails, the longer we'll wait until we retry again. In the worst case, we'll retry the request a maximum of 25 times over roughly 20 days. After this point it will be assumed that the server will never respond and the request will not be retried again.
Security
TLS Requirements
Fulcrum will only connect to endpoints that support TLS 1.2.
There are no features built into Fulcrum's webhooks to verify the content or validate the authenticity of the event payload being delivered.
For example, it could be possible, for some 3rd party to know of your webhook endpoint's URL and forge a payload. Or there may be other considerations that make you wary of trusting data given to you.
If this level of security is needed, it seems practical that one use the webhook as an alert. You can then initiate a request to the Fulcrum API to retrieve the relevant data. Once you've securely initiated the request on your own, with your credentials, to the endpoint you know is the Fulcrum service, you can be confident the data is valid and authentic.
Notes
The organization which owns the resource in the payload is designated by the owner_id
attribute in the event data. This is useful in the case of a service that provides multiple clients a service built on top of Fulcrum. This single webhook can be used to receive event payloads for multiple organizations and still determine to which organization the event belongs.
Examples
Forms
{
"id": "f4ef6656-172c-4c09-9808-f07e03357519",
"type": "form.create",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Intersections",
"description": "Intersection in Colorado Springs",
"bounding_box": null,
"record_title_key": "94f8",
"status_field": { },
"id": "295eda4a-7795-4881-9f62-085a930b356e",
"record_count": 0,
"created_at": "2013-09-21T19:18:55Z",
"updated_at": "2013-09-21T19:18:55Z",
"elements": [
{
"type": "TextField",
"key": "94f8",
"label": "Name",
"description": null,
"required": false,
"disabled": false,
"hidden": false,
"data_name": "name",
"default_value": null,
"visible_conditions_type": null,
"visible_conditions": null,
"required_conditions_type": null,
"required_conditions": null,
"numeric": false
}
]
}
}
{
"id": "dfb667fb-f192-4d6d-8794-e89ece5db6ed",
"type": "form.update",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Intersections",
"description": "Intersection in the city of Colorado Springs.",
"bounding_box": null,
"record_title_key": "94f8",
"status_field": { },
"id": "295eda4a-7795-4881-9f62-085a930b356e",
"record_count": 0,
"created_at": "2013-09-21T19:18:55Z",
"updated_at": "2013-09-21T19:19:41Z",
"elements": [
{
"type": "TextField",
"key": "94f8",
"label": "Name",
"description": null,
"required": false,
"disabled": false,
"hidden": false,
"data_name": "name",
"default_value": null,
"visible_conditions_type": null,
"visible_conditions": null,
"required_conditions_type": null,
"required_conditions": null,
"numeric": false
}
]
}
}
{
"id": "42edfb16-cb13-41e1-857c-bbba57e7b072",
"type": "form.delete",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Intersections",
"description": "Intersection in the city of Colorado Springs.",
"bounding_box": null,
"record_title_key": "94f8",
"status_field": { },
"id": "295eda4a-7795-4881-9f62-085a930b356e",
"record_count": 0,
"created_at": "2013-09-21T19:18:55Z",
"updated_at": "2013-09-21T19:19:41Z",
"elements": [
{
"type": "TextField",
"key": "94f8",
"label": "Name",
"description": null,
"required": false,
"disabled": false,
"hidden": false,
"data_name": "name",
"default_value": null,
"visible_conditions_type": null,
"visible_conditions": null,
"required_conditions_type": null,
"required_conditions": null,
"numeric": false
}
]
}
}
Records
{
"id": "8faf0917-1987-4ac6-bcc7-4fbf71d191f3",
"type": "record.create",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"status": null,
"version": 1,
"id": "7553fd44-78bb-41eb-a453-8b301ae5e52e",
"form_id": "295eda4a-7795-4881-9f62-085a930b356e",
"project_id": null,
"created_at": "2013-09-21T19:20:16Z",
"updated_at": "2013-09-21T19:20:16Z",
"client_created_at": "2013-09-21T19:20:16Z",
"client_updated_at": "2013-09-21T19:20:16Z",
"created_by": "dev Test",
"created_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"updated_by": "dev Test",
"updated_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"assigned_to": null,
"assigned_to_id": null,
"form_values": {
"94f8": "I25 and Garden of the Gods"
},
"latitude": 38.8968321491252,
"longitude": -104.831140637398,
"altitude": null,
"speed": null,
"course": null,
"horizontal_accuracy": null,
"vertical_accuracy": null
}
}
{
"id": "1908484a-fc1b-4c0b-ba79-ddeb83790637",
"type": "record.update",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"status": null,
"version": 2,
"id": "7553fd44-78bb-41eb-a453-8b301ae5e52e",
"form_id": "295eda4a-7795-4881-9f62-085a930b356e",
"project_id": null,
"created_at": "2013-09-21T19:20:16Z",
"updated_at": "2013-09-21T19:20:27Z",
"client_created_at": "2013-09-21T19:20:16Z",
"client_updated_at": "2013-09-21T19:20:27Z",
"created_by": "dev Test",
"created_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"updated_by": "dev Test",
"updated_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"assigned_to": null,
"assigned_to_id": null,
"form_values": {
"94f8": "I25 and Garden of the Gods"
},
"latitude": 38.8968237991091,
"longitude": -104.830228686333,
"altitude": null,
"speed": null,
"course": null,
"horizontal_accuracy": null,
"vertical_accuracy": null
}
}
{
"id": "1371c81d-367b-45d3-9f7c-91da5de9518e",
"type": "record.delete",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"status": null,
"version": 3,
"id": "7553fd44-78bb-41eb-a453-8b301ae5e52e",
"form_id": "295eda4a-7795-4881-9f62-085a930b356e",
"project_id": null,
"created_at": "2013-09-21T19:20:16Z",
"updated_at": "2013-09-21T19:20:27Z",
"client_created_at": "2013-09-21T19:20:16Z",
"client_updated_at": "2013-09-21T19:20:27Z",
"created_by": "dev Test",
"created_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"updated_by": "dev Test",
"updated_by_id": "960247b1-aa51-4d80-bfa1-a1d0baf8d87e",
"assigned_to": null,
"assigned_to_id": null,
"form_values": {
"94f8": "I25 and Garden of the Gods"
},
"latitude": 38.8968237991091,
"longitude": -104.830228686333,
"altitude": null,
"speed": null,
"course": null,
"horizontal_accuracy": null,
"vertical_accuracy": null
}
}
Choice Lists
{
"id": "138b54d0-4681-41b9-8efa-5b7cabd0eaa2",
"type": "choice_list.create",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Tree Size",
"description": "Categories for Trees Based on SIze.",
"id": "c50d3a63-f444-4677-8bbd-4685f7800e49",
"created_at": "2013-09-21T19:30:32Z",
"updated_at": "2013-09-21T19:30:32Z",
"choices": [
{
"label": "Under 4 ft",
"value": "small"
},
{
"label": "Between 4 and 10 ft",
"value": "medium"
},
{
"label": "Between 10 and 50 ft",
"value": "large"
},
{
"label": "Over 50 ft",
"value": "x-large"
}
]
}
}
{
"id": "02a17aac-f875-49a4-931a-0e1f8558852d",
"type": "choice_list.update",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Tree Size",
"description": "Categories for Trees Based on SIze.",
"id": "c50d3a63-f444-4677-8bbd-4685f7800e49",
"created_at": "2013-09-21T19:30:32Z",
"updated_at": "2013-09-21T19:31:03Z",
"choices": [
{
"label": "Under 1 ft",
"value": "mini"
},
{
"label": "Between 1 and 4 ft",
"value": "small"
},
{
"label": "Between 4 and 10 ft",
"value": "medium"
},
{
"label": "Between 10 and 50 ft",
"value": "large"
},
{
"label": "Over 50 ft",
"value": "x-large"
}
]
}
}
{
"id": "ca7baf27-e88b-47a8-988a-5b8f9eb4c889",
"type": "choice_list.delete",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Tree Size",
"description": "Categories for Trees Based on SIze.",
"id": "c50d3a63-f444-4677-8bbd-4685f7800e49",
"created_at": "2013-09-21T19:30:32Z",
"updated_at": "2013-09-21T19:31:03Z",
"choices": [
{
"label": "Under 1 ft",
"value": "mini"
},
{
"label": "Between 1 and 4 ft",
"value": "small"
},
{
"label": "Between 4 and 10 ft",
"value": "medium"
},
{
"label": "Between 10 and 50 ft",
"value": "large"
},
{
"label": "Over 50 ft",
"value": "x-large"
}
]
}
}
Classification Sets
{
"id": "0e7d69c7-94e9-40e2-b3b7-802c88c5fb47",
"type": "classification_set.create",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Building Type",
"description": "Categories for building types.",
"id": "ff22c928-bb41-4360-acc1-aff44eeaa702",
"created_at": "2013-09-21T19:27:06Z",
"updated_at": "2013-09-21T19:27:06Z",
"items": [
{
"label": "School",
"value": "school",
"child_classifications": [
{
"label": "Elementary",
"value": "elem"
},
{
"label": "Middle",
"value": "mid"
},
{
"label": "High",
"value": "hi"
},
{
"label": "College/University",
"value": "univ"
}
]
},
{
"label": "Bank",
"value": "bank",
"child_classifications": [
{
"label": "ATM",
"value": "atm"
},
{
"label": "Branch",
"value": "branch"
}
]
}
]
}
}
{
"id": "86bd5c0b-da59-4fe8-bc82-664eb35fc456",
"type": "classification_set.update",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Building Type",
"description": "Categories for building types.",
"id": "ff22c928-bb41-4360-acc1-aff44eeaa702",
"created_at": "2013-09-21T19:27:06Z",
"updated_at": "2013-09-21T19:27:25Z",
"items": [
{
"label": "School",
"value": "school",
"child_classifications": [
{
"label": "Elementary",
"value": "elem"
},
{
"label": "Middle",
"value": "mid"
},
{
"label": "High",
"value": "hi"
},
{
"label": "College/University",
"value": "univ"
}
]
},
{
"label": "Bank",
"value": "bank",
"child_classifications": [
{
"label": "ATM",
"value": "atm"
},
{
"label": "Branch",
"value": "branch"
},
{
"label": "Headquarters",
"value": "hq"
}
]
}
]
}
}
{
"id": "22b3ed8e-17dc-4557-b0d1-92030f3b2230",
"type": "classification_set.delete",
"owner_id": "00053caf-4b6e-4c86-88b6-64695895dffe",
"data": {
"name": "Building Type",
"description": "Categories for building types.",
"id": "ff22c928-bb41-4360-acc1-aff44eeaa702",
"created_at": "2013-09-21T19:27:06Z",
"updated_at": "2013-09-21T19:27:25Z",
"items": [
{
"label": "School",
"value": "school",
"child_classifications": [
{
"label": "Elementary",
"value": "elem"
},
{
"label": "Middle",
"value": "mid"
},
{
"label": "High",
"value": "hi"
},
{
"label": "College/University",
"value": "univ"
}
]
},
{
"label": "Bank",
"value": "bank",
"child_classifications": [
{
"label": "ATM",
"value": "atm"
},
{
"label": "Branch",
"value": "branch"
},
{
"label": "Headquarters",
"value": "hq"
}
]
}
]
}
}
Updated about 1 year ago