Event webhooks

What are Setyl event webhooks?

Event hooks in Setyl are like notifications that are sent out when specific events happen in your organization. These notifications are in the form of messages sent over the internet to a web address that you provide. These messages contain information about the events in a specific format called JSON.

Think of these Setyl event hooks as triggers that can start processes in your own software systems. To make use of these event hooks, you’ll need to create a special online service that can receive these messages. You’re responsible for creating this service and making sure it’s available on the internet, separate from Setyl.

Setyl event hooks are based on the idea of webhooks, which is a common practice in the industry.

You’re allowed to set up a maximum of 5 active and confirmed event hooks in your organization at once. Each of these event hooks can be set up to notify you about multiple types of events.

Which events can you choose?

When you’re setting up an event hook for the first time, you get to pick which types of events it will send notifications for. The options you can choose from are a selection of the different kinds of events that Setyl keeps track of.

These events can involve changes related to people, assets, licenses, and other modifications within Setyl. For instance, you could set up an event hook to let you know whenever a new person is added. This way, you can use these hooks to kick off actions you need to perform internally whenever a new person becomes part of your system. This might include tasks like creating a support ticket or sending out an email message.

Full list of currently supported events (the list will be updated):

  1. People
    • person_added: New person added
    • person_added_via_integration: New person added through integration with an external system
    • person_updated: Person updated
    • person_updated_via_integration: Person updated via integration
    • person_archived: Person archived
    • person_reinstated: Person reinstated
    • person_merged: Person merged
  2. Assets
    • asset_added: New asset added
    • asset_updated: Asset updated
    • asset_archived: Asset archived
    • asset_reinstated: Asset reinstated
    • asset_linked: Asset linked
    • asset_unlinked: Asset unlinked
  3. Licenses
    • license_added: New license added
    • license_updated: License updated
    • license_archive: License archived
    • license_reinstated: License reinstated

Requests sent by Setyl

Once events that fit the event types you’ve selected happen in your organization, the event hook gets activated on its own and sends a request to your outside service. This request comes with a payload in JSON format that contains details about the event. The structure of this JSON payload is similar to what you’d get when you ask for data through the Setyl API. A sample JSON payload is provided in “Sample event delivery payload”.

These requests that Setyl sends to your outside service can either be HTTPS or HTTP requests. The ongoing delivery of events is handled through POST requests, while a one-time GET request is used to confirm your endpoint.

One-time verification request

Once you’ve set up and activated your webhook endpoint, Setyl will send a single GET request to your endpoint as a validation step. This request includes a validation value that your service must send back. This verification is a way to ensure that you have control over the endpoint.

It’s important to note that this one-time validation request is the only time Setyl will use a GET request for your external service. The ongoing notifications about events will be sent through HTTPS or HTTP POST requests. To handle this special validation request differently, your web service can distinguish between GET and POST.

Here’s how your service should handle the validation:

  1. The request from Setyl will have an HTTP header called X-Setyl-Verification-Challenge.
  2. Your service needs to read the value from this header and return it within the response body as a JSON object named “verification”: { "verification": "value_from_header" }.
  3. The value_from_header is obtained from the request’s HTTP header, but you need to send it back enclosed in a JSON object.

Here’s an example of Node.js code that demonstrates this one-time verification process:

// Event hook initial verification
// Extract header 'x-setyl-verification-challenge' from Setyl request
// Return value as JSON object verification

app.get("/setylCallback", (request, response) => {
  var returnValue = {
    "verification": request.headers['x-setyl-verification-challenge'],
  };
  console.log("Event hook verification request received.")
  response.json(returnValue);
});

Ongoing event delivery

Once the validation is done, for ongoing event notifications, Setyl will use HTTPS or HTTP POST requests to send information to your service. The JSON content in these requests contains specific details about the events that took place.

This content is structured as an object corresponding to the event type. It’s the same type of object that the Setyl API outlines for this particular entity. You can refer to the Setyl API documentation to learn more about this object.

Setyl does its best to deliver events reliably. The events are guaranteed to be delivered at least once. However, delivery might be slowed down by network conditions. Sometimes, multiple requests could arrive together after a delay, or events might not arrive in the order they occurred. To ensure proper ordering, you can use the time stamp found in the X-Setyl-Event-Fired-At HTTP header of each event. To identify duplicate deliveries, you can compare the X-Setyl-Event-UUID value of incoming events with the values of previously received events.

It’s important to note that there’s no specific promise regarding the maximum time between when an event happens and when it’s delivered.

Timeout and Retry

When Setyl contacts your external service, it maintains a standard timeout of 3 seconds. In case of failures, Setyl will attempt retries using an exponential backoff strategy. It will make 10 retry attempts over a period of roughly 4 hours and 30 minutes. However, responses with a status code in the 4xx range won’t be retried.

Note: It’s crucial that your external service can provide responses to Setyl’s requests within the 3-second timeout restriction.

Note: If the maximum number of retries is reached or if your server returns a 4xx status code, the webhook endpoint will be deactivated, and no further events will be dispatched to it.

For more details on the HTTP responses you need to send, refer to “Responses to Ongoing Event Delivery Requests”.

Security

For security purposes, it’s advisable to restrict requests to only those originating from Setyl. One of the simplest ways to achieve this is by establishing a secret token that can be used to verify the authenticity of the information.

The secret token is created automatically when you register your webhook endpoint in Setyl. You can find it in the form when you’re creating the endpoint, as well as in the form when you’re editing the endpoint later on.

Next, you should configure an environment variable on your server to store this token securely. This token will be used later on to verify the signature of the webhook payload, ensuring its authenticity.

Once your secret token is configured, Setyl employs it to generate a hash signature for every payload. This hash signature is then included in the headers of each request under the label X-Setyl-Signature.

You should compute a hash using your SECRET_TOKEN and make sure that the resulting hash matches the hash provided by Setyl. Setyl employs an HMAC hex digest method to compute this hash.

While your language and server implementations might not exactly match the examples provided, there are several crucial points to highlight:

  • Regardless of your implementation, the hash signature always commences with sha256=. This employs your secret token’s key and the payload body.

  • Utilizing a simple == operator is not recommended. Instead, opt for a method like secure_compare or crypto.timingSafeEqual. These methods facilitate “constant time” string comparisons, which assist in mitigating specific timing attacks that can exploit regular equality operators or loops in Just-In-Time optimized languages.

Testing Values

No matter which programming language you use for implementing HMAC verification in your code, you can utilize the following secret and payload values to validate the correctness of your implementation:

  • Secret: “It’s a Secret to Everybody”

  • Payload: “Hello, World!”

If your implementation is accurate and employs the SHA-256 algorithm, the signatures you generate should align with the following signature values:

  • Generated Signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

  • Expected X-Setyl-Signature: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

Ruby Example

Here’s an example of how you could define a verify_signature function in Ruby:

def verify_signature(payload_body)
  signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['X-Setyl-Signature'])
end

You can then call this function when you receive a webhook payload:

post '/payload' do
  request.body.rewind
  payload_body = request.body.read
  verify_signature(payload_body)
  push = JSON.parse(payload_body)
  "I received some JSON: #{push.inspect}"
end

Python Example

Here’s an example of how you can define the verify_signature function in Python and use it to verify signatures when receiving a webhook payload:

import hashlib
import hmac

def verify_signature(payload_body, secret_token, signature_header):
    parts = signature_header.split("=")
    if len(parts) != 2 or parts[0] != "sha256":
        raise Exception("Invalid signature header format")

    received_signature = parts[1]
    expected_signature = hmac.new(secret_token.encode("utf-8"), payload_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected_signature, received_signature):
        raise Exception("Signatures didn't match")

# Usage example:
payload_body = "your_payload_here"
secret_token = "your_secret_token"
signature_header = "sha256=your_received_signature"

try:
    verify_signature(payload_body, secret_token, signature_header)
    print("Signature verified successfully")
except Exception as e:
    print("Signature verification failed:", e)

Node.js Example

Here’s an example of how you can define the verifySignature function in Node.js and use it to verify signatures when receiving a webhook payload:

async function verifySignature(secret, header, payload) {
    const [algorithm, sigHex] = header.split("=");

    const keyBytes = new TextEncoder().encode(secret);
    const key = await crypto.subtle.importKey(
        "raw",
        keyBytes,
        { name: "HMAC", hash: { name: "SHA-256" } },
        false,
        ["verify"]
    );

    const sigBytes = hexToBytes(sigHex);
    const dataBytes = new TextEncoder().encode(payload);

    return await crypto.subtle.verify(
        algorithm,
        key,
        sigBytes,
        dataBytes
    );
}

function hexToBytes(hex) {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < bytes.length; i++) {
        bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
    }
    return bytes;
}

// Usage example:
const secret = "your_secret_here";
const header = "sha256=your_received_signature";
const payload = "your_payload_here";

verifySignature(secret, header, payload)
    .then(result => {
        if (result) {
            console.log("Signature verified successfully");
        } else {
            console.log("Signature verification failed");
        }
    })
    .catch(error => {
        console.error("Error during signature verification:", error);
    });

Responses to Ongoing Event Delivery Requests

Your external service should respond to ongoing POST requests with an empty body and an HTTP status code of 200 (OK) or 204 (No Content).

To ensure efficiency, it’s recommended that you promptly return the HTTP response without waiting for any of your internal processes that might be triggered by the event to finish.

Setting Up Webhooks

The fundamental steps for registering and confirming a new webhook endpoint, along with configuring events for it, involve the following actions:

  1. Develop your external web service to accept incoming event webhook calls from Setyl.
  2. Register the endpoint of your external service with Setyl and specify event hook settings.
  3. Activate the endpoint (as part of this process, Setyl will send a GET request to your external service to confirm your control over the endpoint).

To register an endpoint and setting up webhooks, please visit the Company Settings page.

Sample event delivery payload

The following is an example of a JSON payload of a request from Setyl to your external service:

[
  {
    "url": "https://setyl.test/api/v1/people/f012b69d-8aba-4445-ae8f-4e995b2582ad",
    "view_url": "https://setyl.test/users/f012b69d-8aba-4445-ae8f-4e995b2582ad",
    "uuid": "95c34649-9ba0-47af-9485-8274e4cd9992",
    "first_name": "Harvey",
    "last_name": "Specter",
    "email": "harvey@specter.com",
    "phone": "+381631111111",
    "job_title": "Lawyer",
    "avatar_url": null,
    "notes": null,
    "address": "1050 Pittsford Victor Rd, Pittsford, New York(NY), 14534",
    "role_name": "employee",
    "human_role_name": "Employee",
    "personal_identifier": 12345,
    "login_username": null,
    "join_date": "2021-08-11 12:04:51 UTC",
    "leave_date": null,
    "last_sign_in_at": null,
    "updated_at": "2023-08-11 11:54:51 UTC",
    "created_at": "2023-08-11 11:54:51 UTC",
    "state_name": "active",
    "human_state_name": "Active",
    "state_events": [
      "mark_as_onboarding",
      "mark_as_active",
      "mark_as_offboarding"
    ],
    "locations_string": "10 Downing Street, London, GB",
    "locations": [
      {
        "name": "10 Downing Street, London, GB,",
        "uuid": "a293dad6-d680-40ba-b5c8-f8ee0766dede",
        "source_type": "microsoft-dynamics-hr",
        "human_source_type": "Microsoft Dynamics 365 Human Resources"
      }
    ],
    "departments_string": "Department for Audit",
    "departments": [
      {
        "name": "Department for Audit",
        "uuid": "0b1ffabb-051c-4540-a031-fc78cd4c92a8",
        "source_type": null,
        "human_source_type": null
      }
    ],
    "legal_entities_string": "LE for Audit",
    "legal_entities": [
      {
        "name": "LE for Audit",
        "uuid": "4baf9ea5-e2d3-4986-a718-3a97f472bdb1",
        "source_type": null,
        "human_source_type": null
      }
    ]
  }
]