Webhooks are a method of subscribing to a web site or service to receive data via HTTP to a site or service you control. This allows true external integration and intercommunication between systems while maintaining platform independence. It extends on the idea of web apis to provide two-way communication between systems.
Any subsequent interaction with Telligent Community after receiving Webhook data would be done via REST, so a look at the REST API: Making Requests topic might be useful.
[toc]
Why Should I Use Webhooks?
Webhooks complement the Telligent Community REST API by allowing an external application to respond to changes that occur within Telligent Community when they happen. Whereas the REST API provides functionality that enables proactive code (based on some external action), webhooks provide the means to execute reactive code (respond to some internal event).
You might use webhooks to:
- Update your own database with custom audit fields when content is created.
- Track the popularity of certain content (through ratings and likes) and display on an external dashboard.
- Perform operations on Telligent Community data without needing to provide a compiled dll to the server as is necessary in In-Process API implementations
- Operate in another language besides C# (all Webhook and REST integration is done via HTTP and other universal standards)
Creating an Endpoint for Receiving Webhook Data
When certain changes occur within Telligent Community, if a webhook is subscribed to it, a HTTP POST payload will be sent to the webhook's configured URL. In order to receive webhook payloads from Telligent Community, you will need to set up a webhook endpoint on your server that accepts HTTP POSTs. Since the data is JSON over HTTP, any language can be used to receive the data and act on it.
For this sample, we used an ASP.net MVC project as the framework, but we could have used an HttpHandler or anything that handles HTTP Requests. This also can be done using a technology other than .NET(Java, PHP, etc). For alternate technologies you simply need to be able to build or use a component that responds to HTTP Posts from another web application.
Validating Message Origin
Because webhooks operate via HTTP requests, your external service needs a way to validate the information being posted as truly originating from your Telligent Community instance. To accomplish this, each webhook is assigned pre-generated secret key when created. This secret key is used to create a hash signature of the post data that makes up each payload. You can validate that the sender of the HTTP request is the Telligent Community site by hashing the post data using your secret key, and checking that the signatures match.
HTTP requests made to your webhook's configured URL endpoint will contain several special headers:
Header | Description |
---|---|
X-Telligent-Webhook-Sender | Url of the site sending this event information. |
X-Telligent-Webhook-Signature | HMAC base64 hash of the payload, using a site-generated secret key. |
Validation requires three things: the raw data from the HTTP post, your stored secret key, and the hash sent along with the post. The first two are sent to the validation method as "data" and "secret", respectively.
First we set up the hash method using UTF-8 encoding and our secret key.
private string CalculateSignature(string data, string secret) { // Use UTF-8 as the encoding var encoding = new System.Text.UTF8Encoding(); byte[] secretByte = encoding.GetBytes(secret); HMACSHA256 hmacsha256 = new HMACSHA256(secretByte);
Then we use the hash function to generate the hash of the raw data.
byte[] dataBytes = encoding.GetBytes(data); byte[] dataHash = hmacsha256.ComputeHash(dataBytes);
Next the hash needs to be converted to base64 to check against the hash provided with the post.
// Convert byte data to readable Base64 string return Convert.ToBase64String(dataHash); }
Finally, we can incorporate the use of this method into your handler:
string senderUrl = HttpContext.Request.Headers["X-Telligent-Webhook-Sender"]; string hashSignature = HttpContext.Request.Headers["X-Telligent-Webhook-Signature"]; string rawPostData; using (var reader = new System.IO.StreamReader(HttpContext.Request.InputStream)) { rawPostData = reader.ReadToEnd(); } // Validate the posted data's authenticity string calculatedSignature = CalculateSignature(rawPostData, SECRET_KEY); if (!hashSignature.Equals(calculatedSignature)) // The signatures do mot match only if the wrong secret is used, // or the data is not from the community site. return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
Handling Data Sent to Your Webhook
All webhook payloads include an identifier for the type of event (TypeId
) as well as the date when the event occurred (DateOccurred
). In addition, each contains an subcontainer (EventData) that includes the data relevant to that specific event. Full sample data for each available event can be found when editing a webhook in Administration > Integration > Webhooks. Please note: only the minimum data is included in the actual HTTP post (ids, date). In order to retrieve more information or act on that information, you will need to make additional REST requests.
Sample payload data:
{ "events": [ { "TypeId": "407ad3bc-8269-493e-ac56-9127656527df", "DateOccurred": "2015-12-04T16:31:55.5383926Z", "EventData": { "ActorUserId": 2100, "ContentId": "4c792b81-6f09-4a45-be8c-476198ba47be" } }, { "TypeId": "3b75c5b9-4705-4a97-93f5-a4941dc69bc9", "DateOccurred": "2015-12-04T16:48:03.7343926Z", "EventData": { "ActorUserId": 2100, "ContentId": "4c792b81-6f09-4a45-be8c-476198ba47be" } } ] }
In the handler, we first need to fetch the Http headers and validate the message origin as described above:
// Secret key will be a random length string of random letters and numbers private const string SECRET_KEY = "arsuwuxsy2lesiq7a835gdttu39"; [HttpPost] [AllowAnonymous] [ActionName("Index")] public ActionResult Callback() { try { string senderUrl = HttpContext.Request.Headers["X-Telligent-Webhook-Sender"]; string hashSignature = HttpContext.Request.Headers["X-Telligent-Webhook-Signature"]; string rawPostData; using (var reader = new System.IO.StreamReader(HttpContext.Request.InputStream)) { rawPostData = reader.ReadToEnd(); } // Validate the posted data's authenticity string calculatedSignature = CalculateSignature(rawPostData, SECRET_KEY); if (!hashSignature.Equals(calculatedSignature)) // The signatures do mot match only if the wrong secret is used, // or the data is not from the community site. return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
Then we can move to parsing the data we've received and performing actions with it:
dynamic json = new JavaScriptSerializer().DeserializeObject(rawPostData); foreach (var webhookEvent in json["events"]) { var typeId = webhookEvent["TypeId"].ToString(); // Ensure that the event type is the one you want to handle if (typeId == "98b84792-bbcc-4c27-8df3-f594322b5087") // Blog Post Created { var userId = Int32.Parse(webhookEvent["EventData"]["ActorUserId"].ToString()); var contentId = Guid.Parse(webhookEvent["EventData"]["ContentId"].ToString()); var blogPostId = Int32.Parse(webhookEvent["EventData"]["BlogPostId"].ToString()); // do something with the data // for instance, make a REST request using the senderUrl } } return null; } catch (Exception ex) { return new HttpStatusCodeResult(HttpStatusCode.Conflict, ex.ToString()); } }
For information on making REST requests, please refer to the REST API documentation.
Registering a Webhook in Telligent Community
Once you have implemented your webhook handler you need identify it to Telligent Community by registering it. Navigate to Administration > Integration > Webhooks to register your webhook url.
Each webhook event type has a specific payload format with the relevant event information, which can be viewed when editing or creating a webhook. When configuring your webhook, select only events you would like to receive data for. This is useful for optimizing site performance, as the site only sends data you choose as important, limiting the number of HTTP requests to your external service.
In addition to the url and list of subscribed events, webhook configuration also includes a secret key generated by the Telligent Community site when your webhook is registered. Information on the use of this secret key can be found in the Validation section above.
Finally, there is also a setting to disable the webhook. You can use this setting to temporarily disable the webhook without deleting the information completely from the site.
You should now receive any new events that correspond to your subscriptions.
Optimization and Failure Recovery
As mentioned above, Telligent Community will only send event data for events you subscribed to, in order to limit HTTP traffic. Another method the site uses to limit HTTP traffic is to collect event data into a group as events happen, and send all collected data at specified (short) intervals. Even though the events will not be sent in "real-time", you will still always receive events listed in the order they occurred, so you can process data in the correct order.
If Telligent Community cannot reach or webhook handler or encounters an error, it will assume the messages were not received and will flag them to be resent later. Successive retries will be spaced farther and farther apart, until after a certain number the url is placed in a suspended state. Events will continue to be logged for your url, but you will need to go back in and edit your webhook to re-enable sending. At that point, events will begin to propagate to your url. These events will be in order and batched, though it may take several batches to catch up because there is a limit to messages per post. This way, even if your endpoint is unavailable for a certain time period, you will still eventually receive all events that happened during that period, in chronological order.
Full Sample
using System; using System.Net; using System.Security.Cryptography; using System.Web; using System.Web.Mvc; using System.Web.Script.Serialization; namespace SampleWebhookHandler.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { return View(); } private const string SECRET_KEY = "arsuwuxsy2lesiq7a835gdttu39"; [HttpPost] [AllowAnonymous] [ActionName("Index")] public ActionResult Callback() { try { string senderUrl = HttpContext.Request.Headers["X-Telligent-Webhook-Sender"]; string hashSignature = HttpContext.Request.Headers["X-Telligent-Webhook-Signature"]; string rawPostData; using (var reader = new System.IO.StreamReader(HttpContext.Request.InputStream)) { rawPostData = reader.ReadToEnd(); } // Validate the posted data's authenticity string calculatedSignature = CalculateSignature(rawPostData, SECRET_KEY); if (!hashSignature.Equals(calculatedSignature)) // The signatures do mot match only if the wrong secret is used, // or the data is not from the community site. return new HttpStatusCodeResult(HttpStatusCode.BadRequest); dynamic json = new JavaScriptSerializer().DeserializeObject(rawPostData); foreach (var webhookEvent in json["events"]) { var typeId = webhookEvent["TypeId"].ToString(); // Ensure that the event type is the one you want to handle if (typeId == "98b84792-bbcc-4c27-8df3-f594322b5087") // Blog Post Created { var userId = Int32.Parse(webhookEvent["EventData"]["ActorUserId"].ToString()); var contentId = Guid.Parse(webhookEvent["EventData"]["ContentId"].ToString()); var blogPostId = Int32.Parse(webhookEvent["EventData"]["BlogPostId"].ToString()); // do something with the data } } return new HttpStatusCodeResult(200); } catch (Exception ex) { return new HttpStatusCodeResult(HttpStatusCode.Conflict, ex.ToString()); } } private string CalculateSignature(string data, string secret) { var encoding = new System.Text.UTF8Encoding(); byte[] secretByte = encoding.GetBytes(secret); HMACSHA256 hmacsha256 = new HMACSHA256(secretByte); byte[] dataBytes = encoding.GetBytes(data); byte[] dataHash = hmacsha256.ComputeHash(dataBytes); return Convert.ToBase64String(dataHash); } } }