SuperOffice Webhooks API

Ratings
*** Applies to Web Client and NetServer Services Only (onsite & online) ***

Webhooks are a means to broadcast events that happen inside SuperOffice as they occur. A webhook payload includes information that describes what has changed, and is broadcast to all applications that have subscribed to a corresponding event.

The webhook workflow begins with applications subscribing to events that are interesting. The way an application does this is by adding one or more webhook definitions in SuperOffice.

For example: the Partner Application registers an interest in "contact.created" events with SuperOffice. From then on, whenever a contact record is created, SuperOffice will notify the Partner Application that the event has occurred. The Partner Application can then update its own state, get more information, or just log the information.

Lets start by looking at a webhook definition - what the Partner Application uses to register its interest in events with SuperOffice.

Webhook Definition

A webhook subscription contains the follow properties:

Property Name Description
Name A name to distinguish events from one another.
Events An array of entity.event names.
TargetURL Defines a URL where webhook payloads are sent. Must be HTTPS and must respond to POST
Secret Optional shared secret. Used for creating a SHA256 HMAC if set.
State Unknown = 0, Active = 1, Stopped = 2, TooManyErrors = 3
Type Name of webhook plugin that handles dispatching this webhook: “webhook”, “crmscript” etc.
Headers Hook-specific custom headers to be added to the webhook payload.
Properties Hook-specific data properties to be added to the webhook payload.

While a webhook name need not be unique, it should be unique enough to distinquish it from others and describe its purpose.

The Events property is an array of one or more event names the subscriber is subscribing to. The format of a single event name is <entity>.<event>, where entity is one of the available entities (see below), and the event is one of created, changed or deleted.

TargetURL defines where to send the POST request containing the webhook payload. The webhook payload contains all relevant information needed by subscribers to take action. The Target URL must be HTTPS, and it must respond to a POST request with a 200 OK response. The HTTPS certificate must be valid. If the certificate is not valid (i.e. self-signed, expired, not valid for host-name, revoked, etc), then the webhook will be rejected.

When security is important, which should be always, a webhook definition specifies a shared secret that both the sender and receiver use to validate a webhook payload. When an event notification is sent, the webhook payload is hashed and base64 encoded and the resulting value is added to a header in the POST request. The header key is X-SuperOffice-Signature. It’s up to the subscriber to validate the header value. This is thoroughly explained in a later section.

The state of a webhook is by default Active but can be set to Stopped. If the sender does not receive a successful 200 response from the server after seven attempts, SuperOffice will set to state of the webhook to TooManyErrors.

A webhook Type must match the plugin responsible for dispatching notifications. As of this writing, the only supported value is “webhook”.

Webhook Headers are any additional header values SuperOffice appends to a request sent with each notification. Headers are a simple “string”:”string” value.

Webhook Properties are any additional values SuperOffice should append to each request sent with each notification. Properties are a “string”: {object} value.

Event Details

Event name descriptors are a combination the webhook entity and event type, i.e. “contact.created” is one such descriptor.

Available event entities are: activity, associate, contact, person, project, projectmember, sale and salestakeholder. Each entity raises an event when created, changed or deleted. Therefore, all possible event names are:

Create Events Changed Events Deleted Events Other Events
activity.created activity.changed activity.deleted  
associate.created associate.changed associate.deleted  
contact.created contact.changed contact.deleted  
document.created document.changed document.deleted document.edited*
person.created person.changed person.deleted person.consented*
person.unconsented*
project.created project.changed project.deleted  
projectmember.created projectmember.changed projectmember.deleted  
sale.created sale.changed sale.deleted sale.completed*
sale.lost*
sale.sold*
salestakeholder.created salestakeholder.changed salestakeholder.deleted  
ticket.created** ticket.changed**    

* Added in v8.3 R04
** Added in v8.4

Webhook Subscription

Webhook subscriptions are created using SuperOffice NetServer core and NetServer web services; both SOAP and REST API’s.

Below I will demonstrate how to create webhook using both NetServer core and web services.

NetServer Core

There are two distinct ways to create webhooks using NetServer core, and there is a difference in behavior between them.

There is the traditional Row classes SuperOffice.CRM.Rows.WebhookRow, and then there is the SuperOffice.CRM.Webhooks.WebhookManager.

The key difference is when using WebhookManager, the TargetURL is pre-checked prior to saving, by sending out a test webhook payload, and is expected to send a 200 OK response. If the TargetURL fails to respond, the save operation will fail.

Creating webhooks via the SOAP or REST API’s, NetServer uses the WebhookManager, therefore the TargetURL must already exist and respond accordingly.

// SuperOffice.CRM.Rows: no verification check for TargetUrl

var name      = "Tonys Contact Handler";
var events    = "contact.created,contact.changed,contact.deleted";
var targetUrl = "https://www.myserver.com/superoffice/webhookhandler";
var secret    = "Something Super Secret";
var type      = "webhook";
var state     = SuperOffice.Data.WebhookState.Active;

var webHookRow = WebhookRow.CreateNew();

webHookRow.SetDefaults();
webHookRow.Name      = name;
webHookRow.Events    = events;
webHookRow.TargetUrl = targetUrl;
webHookRow.Secret    = secret;
webHookRow.State     = state;
webHookRow.Type      = type;

webHookRow.Save();


// SuperOffice.CRM.Webhooks.WebhookManager: verifies the existence of the TargetUrl

var webhookManager = WebhookManager.GetCurrent();
var webhook        = new Webhook(0, name, targetUrl, events, secret);
webhookManager.SaveWebhook(webhook);

NetServer SOAP Web Services

SuperOffice.CRM.Services.WebhookAgent is used to manage webhooks. Using WebhookAgent.CreateDefaultWebhook will automatically set the Type and State to webhook and Active, respectively. The webhook will also contain two default Events, contact.created and contact.deleted. These are easy to replace, but nice to have for testing purposes.

Use the WebhookAgent.SaveWebhook method to save or update a webhook.

using (var wa = new WebhookAgent())
{
    var wh = wa.CreateDefaultWebhook(); // defines two default events
                                        // contact.created & contact.deleted

    wh.Name      = "Tonys Contact Handler";
    wh.Events    = new [] { "contact.created", "contact.changed", "contact.deleted" };
    wh.TargetUrl = "https://www.myserver.com/superoffice/webhookhandler";
    wh.Secret    = "Something Super Secret";

    wh = wa.SaveWebhook(wh);
}

The WebhookAgent has methods to get an existing webhook by id GetWebhook(int id), and a delete method to permanently remove a webhook, DeleteWebhook(int id). The table below lists all available WebhookAgent Methods.

Method Name Description
CreateDefaultWebhook() Returns new Webhook with default values.
DeleteWebhook(id) Deletes the webhook.
GetLastError(id) Return the most recent error message received when calling this webhook.
GetAllWebhooks(string, string webhookState) Returns all webhooks, according to filter criteria.
GetWebhook(id) Gets a Webhook by Id.
SignalWebhook(string, int, StringObjectDictionary) Signal webhooks that an event has occurred. All webhooks listening for the event will be notified.
TestWebhook(webhook) Pings a webhook with a ‘test’ event, returns SUCCESS(true) or FAILURE(false) + the response from the webhook target.

NetServer REST Web Services

NetServer REST API’s are an abstraction over the NetServer SOAP API, and therefore has the same behavior.

To create a new webhook, you can either build the JSON structure yourself, or issue a GET request the api/v1/Webhook/default url and get the structure prepopulated with the defaults.

{
"WebhookId": 0,
"Name": null,
"Events": [
  "contact.created",
  "contact.deleted"
],
"TargetUrl": null,
"Secret": null,
"State": "Active",
"Type": "webhook",
"Headers": null,
"Properties": null,
"Registered": "0001-01-01T00:00:00",
"RegisteredAssociate": null,
"Updated": "0001-01-01T00:00:00",
"UpdatedAssociate": null
}

Save: POST api/v1/Webhook

{
  "Name": "Tonys Contact Handler",
  "Events": [
        "contact.created",
        "contact.changed",
        "contact.deleted"
    ],
  "TargetUrl": "https://www.myserver.com/superoffice/webhookhandler",
  "Secret": "Something Super Secret",
  "State": "Active",
  "Type": "webhook"
}

Available REST URL’s

 

Verb URL
GET api/v1/Webhook/default
GET api/v1/Webhook/{id}
GET api/v1/Webhook/{webhookId}/LastError
GET api/v1/Webhook?nameFilter={nameFilter}&eventFilter={eventFilter}&statusFilter={statusFilter}
POST api/v1/Webhook
POST api/v1/Webhook/Test
POST api/v1/Webhook/{eventName}/{primaryKey}
PUT api/v1/Webhook/{id}
DELETE api/v1/Webhook/{id}

 

POST api/v1/Webhook will register a new webhook definition.

POST api/v1/Webhook/Test will check that the webhook definition is OK, and verify that the target URL responds to a test POST with 200 OK.

POST api/v1/Webhook/foo.bar/123 will signal that the event 'foo.bar' has happened to id 123. Any webhooks registered for the event 'foo.bar' will be notified. You can add additional details (like field changes) in the body of the POST.


Webhook Notification

Now that webhooks have been created and saved in SuperOffice, notifications can be sent out when an event occurs. A webhook notification is referred to as a WebhookPayload, and contains the following properties:

Property Name Description
EventId A GUID that uniquely identifies this event.
Timestamp The datetime when the event occured.
Changes An array of fields that are connected to the change.
Event The name of the event.
PrimaryKey The entity identity that was affected
Entity The type of entity that was affected, i.e. activity, associate, contact, person etc.
ContextIdentifier Customer id for Online users: "Cust1234". Not used for On-site installations.
ChangedByAssociateId Associate id of the user that triggered the event.
WebhookName The given name of the webhook.

A webhook payload for the event contact.changed is send as the following JSON message:

UserAgent: NetServer-Webhook/8.2.123.456
X-SuperOffice-Signature: abcXyz123==
X-SuperOffice-Event: contact.changed
X-SuperOffice-EventId: 88f91933-edce-4c1a-8ded-ade8e2f72434

{ "EventId":"88f91933-edce-4c1a-8ded-ade8e2f72434", "Timestamp":"2018-04-05T08:28:01.5732501Z", "Changes":["contact_id","updated_associate_id","soundEx","updated","name"], "Event":"contact.changed", "PrimaryKey":18, "Entity":"contact", "ContextIdentifier":"Cust54321", "ChangedByAssociateId":5, "WebhookName":"Tonys Contact Handler" }

Notifications are sent out in a fire-and-forget fashion and do not expect a response to these POST requests. There is no way to prevent changes or interrupt the normal workflow of SuperOffice.

Note that the event name, event id and signature are sent as HTTP headers, to help the recipient route and filter the notification without having to parse the body.

Changes Field Names

Fields names that appear in a notification Changes property are the names of the fields as the appear in the database. 

Entity Name Field Source
Activity Appointment Table
Associate Associate Table
Contact Contact Table
Person Person Table
Project Project Table
ProjectMember ProjectMember Table
Sale Sale Table
SaleStakeholder SaleStakeholder Table

WebhookPayload Headers

Header Name Description
X-SuperOffice-Event The event name, i.e. contact.created, project.changed.
X-SuperOffice-EventId A GUID that uniquely identifies this event.
X-SuperOffice-Retry The number of retries this webhook has been tried to be sent.
X-SuperOffice-Signature The hash/base64 encoded secret.

Webhook Secrets

How do you know that the notification was sent from SuperOffice, and not from some random hacker? A webhook secret is used as an additional layer of security in order to verify that the webhook sent to the receiver has not been tampered with.

Only when a webhook definition contains a secret value will SuperOffice append an X-SuperOffice-Signature header to each event notification. It is then up to the receiver to verifying signature of the payload prior to processing the message.

So how does a receiver validate the X-SuperOffice-Signature header value? Let’s first review how the signature is generated.

SuperOffice uses the shared secret as a key in the HMAC SHA256 algorithm, which in turn is used to hash the body of the webhook JSON value. The result of the hash is then base64 encoded and used to populate the X-SuperOffice-Signature header value.

The responsiblity of the receiver is to use the shared secret in the same manner, and essensially do exactly the same thing. The receiver takes the body of the request; essentially the webhook payload, hash and base64 encode it, and then compare the results with the value from the X-SuperOffice-Signature HTTP header.

If the values match then you can be confident the webhook is a valid message that has not been tampered with. If not, the webhook has likely been tampered with mid-stream and should be ignored.

Here is a validation example using C#.

/// <summary>
/// Validates the X-SuperOffice-Signature webhook header value.
/// </summary>
/// <param name="storedSecret">The shared secret stored on the application side.</param>
/// <param name="headerValue">The value from X-SuperOffice-Signature header</param>
/// <param name="body">JSON representation of the webhook</param>
/// <returns></returns>
private bool IsValidWebHook(
    string storedSecret,
    string headerValue,
    System.IO.Stream body)
{
    var validationResult = false;

    // ensure it is the correct encoding
    var secret = System.Text.Encoding.UTF8.GetBytes(storedSecret);

    // hash and base64 encode the stored shared secret
    using (var hasher = new System.Security.Cryptography.HMACSHA256(secret))
    {
        var sha256 = hasher.ComputeHash(body);
        var base64 = Convert.ToBase64String(sha256);

        // verify the values match!

        if (base64 == headerValue)
        {
            validationResult = true;
        }
    }

    return validationResult;
}

Here is a validation routine using Node/Javascript:

You need to be careful to compute the hash based on the request string, and not a parsed and converted representation, since whitespace and line delimiters are significant. See this article  for an in-depth example using Express and Node.

/**
@signature is X-SuperOffice-Signature header value
@secret is the stored shared secret
@ req is the HTTP request
@ buf is the body of the HTTP request
**/
function isValidWebHook(signature, secret, req, buf) {

    // generate the signature locally
    const computedSignature = crypto
      .createHmac("sha256", secret)
      .update(buf.toString())
      .digest("base64");

    // compare generated vs. header value
    if (computedSignature === signature) {
      return true;
    } else {
      return false;
    }

}

Conclusion

No longer must integrations poll SuperOffice and ask for the latest changes. SuperOffice Webhooks actively send messages to subscribers when events occur in a SuperOffice, and provide an opportunity for applications to react accordingly.

Post Comment To the top