Webhooks

Overview

A webhook allows pushing real-time notifications to your app when an event happens in your account. We use HTTPS to send these notifications to your app as a JSON payload. A webhook endpoint is no different from creating any other WEB API.

It’s an HTTPS endpoint on your server with a URL. You can develop your endpoint on your local machine, it can be HTTP. After it’s publicly accessible, it must be HTTPS. You can use one endpoint to handle several different event types at once or set up individual endpoints for specific events. Every endpoint must be different.

Steps to create a webhook

  1. Identify and understand the event(s) to monitor
  2. Develop a webhook in your app and make sure HTTPS is supported. It must contain at least two entries, one to validate the endpoint and the main method to receive the events.
  3. Configure the webhook in the software
  4. Test the webhook in the software to ensure it works
  5. Create test data in EasyWorkforce to trigger the webhooks and ensure that the logs display the right outcome

Event Payload Object

THE EVENT PAYLOAD OBJECT (C#)

 public class EasyWorkforceEventDetails

    {

        public string Id { get; set; }

        public string EventType { get; set; }

        public string CreatedAtUtc { get; set; }

        public long CreatedAtUnixTimestamp { get; set; }

        public string Payload { get; set; }

    }

  • Id: unique event ID
  • EventType: code of the event that happened
  • CreatedAtUtc: date and time the event was executed converted to the UTC timezone.
  • CreatedAtUnixTimestamp: date and time the event was executed expressed in Unix timestamp format, see https://www.unixtimestamp.com/.
  • Payload: is a JSON with the data of the event, every event type has a different payload that the client must know to parse it.

Methods contract

Your endpoint must have two required methods, one to validate the URL and the main to receive the events.

Suppose your Url is: webh.company.com

To validate that the endpoint is online and functional we send a Get HTTP call with an URL parameter with some random string value that your method must return:

GET: webh.company.com?validationToken={validationValue}

To send the event data we send a Post HTTP call with the event payload in the body (JSON format). To return a successful result you must send any 200s code, anything else is an error for us.

POST:  webh.company.com

Body: event payload in JSON format

Header: signature, see security for more details

We strongly recommend separating the process of receiving the payload and processing it for two main reasons:

  • Response time: your endpoint should respond in less than 10 seconds, more than that could throw a time-out exception and is one of the errors that can lead to cancel the webhook subscription.
  • Internal errors: do not mix the delivery process with the message processing.

Event types

For each different event, we have a code that allows our clients to identify it, parse the payload, and process it.

Our clients must subscribe to the events when they are creating the webhook. 

Along with the description of the event we must show the code that developers should use.

Event codes must follow the format <subject>.<action> or <subject>.<section>.<action>, for example (these may not be the real ones):

  • employee.added
  • employee.compensation.updated
  • punch.added
  • punch.removed

Sending process

Our system is responsible for:

  • The events must be sent at least once.
  • We’ll try to process the event near real-time.
  • Events can be sent more than once, so consumers will need to make their endpoints idempotent to some degree or save the event IDs already processed.
  • The order of the events is not guaranteed.
  • We will retry POST failures (anything other than 200 status codes) with some delay multiple times. If the retries attempts are exhausted the subscription going to be canceled (“broken” status).

Security

Only HTTPS protocol is accepted.

Discard a message if you already received it, using the message id.

We generate a unique secret key for each webhook subscription. You can get your endpoint’s secret key on the Web App or using the public API. The secret key can have an expiration time defined at the creation time, if you set one it’s your responsibility to reset the key or your subscription can be canceled with status = Expired.

Every call includes a signature and timestamp headers. To create the hash we computed an HMAC with SHA-256 using  as entry the concatenation of:

  • The timestamp (as a string)
  • The character ‘.’
  •  the entire payload (the request body)

Headers:

  • EasyWf-Timestamp
  • EasyWf-Signature

An attacker cannot change the message and/or the timestamp without invalidating the signature. The process generates the timestamp and signature each time we send an event to your endpoint.

It is your responsibility to check that the message is authentic. Using your key, the timestamp and the payload (post body) compute an HMAC  with the SHA256 hash code and compare it with the signature header.

Key points:
  • Use HTTPS
  • Keep your key safe. 
  • Reset the key periodically
  • Verify signature
  • Save message IDs already processed
  • Avoid replay attacks by rejecting the message if the timestamp is too old.

Errors and retries

To consider a call as successful you must return any 2xx code, any other code is considered an error and the system marks the subscription as “failing”, at that time the retry process takes control of the process.

We’ll try to resend the message many times until the max number of attempts is reached, after that the subscription is set as “broken”, also we’ll send emails, if you set up them, to let you know that the webhook was disabled automatically.

To reactivate the endpoint you should do it manually using the option in the system.

After reactivation, we do not send messages that happened during the time the webhook was broken. You can obtain event messages using the public API to run a verification process and execute the ones missing.

Events available

Note: Payloads are written in C# for the purpose of displaying contracts only. Public API models are used to return complex objects.

Employee Events

NameEvent Name
Employee addedemployee.created
Employee updated employee.updated
Employee archived employee.archived
Employee recoveredemployee.recovered
Updated employee groupsemployee.nodes.updated
Updated employee compensationemployee.compensation.updated
Updated employee positionemployee.position.updated
Updated employee statusemployee.status.updated

Time & Attendance Events

NameEvent Name
Closed pay periodpayperiod.submittedtopayroll
Reopened pay periodpayperiod.reopened
Archived pay periodpayperiod.archived
Recovered pay periodpayperiod.recovered
Mutated timesheetstimesheet.mutated
New punchespunches.new
Updated punchespunches.updated
Archived punchespunches.archived
Frozen punchespunch.frozen

Advanced Scheduling Events

NameEvent Name
New Schedule publishedschedule.published
Schedule canceledschedule.canceled
New employee assigned to the scheduleschedule.employee.updated

C/C++

public class ScheduleModelExt

    {

        /// <summary>

        /// Unique Id

        /// </summary>

        public int Id { get; set; }

        /// <summary>

        /// Identifier of the plan to which it belongs

        /// </summary>

        public int SchedulePlanId { get; set; }

        /// <summary>

        /// Start date and time of the shift

        /// </summary>

        public DateTimeOffset Start { get; set; }

        /// <summary>

        /// End date and time of the shift

        /// </summary>

        public DateTimeOffset End { get; set; }

        /// <summary>

        /// Grace period expressed in minutes

        /// </summary>

        public int GracePeriodMin { get; set; }

        /// <summary>

        /// Color in hex (example: #b9f6ca)

        /// </summary>

        public string Color { get; set; }

        /// <summary>

        /// Indicate whether the shift is critical or not

        /// </summary>

        public bool Critical { get; set; }

        /// <summary>

        /// Employee Id assigned to the shift

        /// </summary>

        public int EmployeeId { get; set; }

        /// <summary>

        /// Employee full name

        /// </summary>

        public string EmployeeFullName { get; set; }

        /// <summary>

        /// Employee number

        /// </summary>

        public string EmployeeNumber { get; set; }

        /// <summary>

        /// Row version assigned by the database

        /// </summary>

        public byte[] RowVersion { get; set; }

        /// <summary>

        /// Criteria assigned to the shift according to the definition of the plan to which it belongs

        /// The criteria can be multiple and consist of nodes and custom fields

        /// </summary>

        public string Criterias { get; set; }

        /// <summary>

        /// Comments

        /// </summary>

        public string Note { get; set; }

        /// <summary>

        /// Indicate whether the shift is active or not

        /// </summary>

        public bool Active { get; set; }

        /// <summary>

        /// Break time expressed in minutes

        /// </summary>

        public int BreakTimeInMinutes { get; set; }

    }

Time Off Events

NameEvent Name
Time-off createdtimeoff.created
Time-off canceledschedule.canceled

C/C++

public class TimeOffModelExt

    {

        /// <summary>

        /// Primary key

        /// </summary>

        public int Id { get; set; }

        /// <summary>

        /// Employee unique id

        /// </summary>

        public int EmployeeId { get; set; }

        /// <summary>

        /// Employee full name

        /// </summary>

        public string EmployeeFullName { get; set; }

        /// <summary>

        /// Employee number

        /// </summary>

        public string EmployeeNumber { get; set; }

        /// <summary>

        /// Time off type reference

        /// </summary>

        public TimeOffType TimeOffType { get; set; }

        /// <summary>

        /// Request type (FullDay, HalfDay or Hours)

        /// </summary>

        public TimeOffRequestType RequestType { get; set; }

        /// <summary>

        /// Initial date and time

        /// </summary>

        public DateTimeOffset Start { get; set; }

        /// <summary>

        /// End date and time

        /// </summary>

        public DateTimeOffset End { get; set; }

        /// <summary>

        /// Number of minutes

        /// </summary>

        public double Minutes { get; set; }

        /// <summary>

        /// Comment

        /// </summary>

        public string Note { get; set; }

    }