Overview
The goal of Engine’s webhooks is to give your application greater control over and knowledge of what’s going on inside Engine. By configuring a subscription, Engine will send notifications about key events and allow your application to respond with instructions if necessary. This way, you can be better prepared to respond as needed and give Engine an added layer of flexibility.
Subscriptions
In order to receive updates from Engine, you must configure a subscription to the event topic you want to receive updates about. To do this, send a POST request to /appManagement/subscriptions
and have the body be a SubscriptionDefinition. A basic SubscriptionDefinition is as follows:
{
"topic": "RegistrationChanged",
"enabled": true,
"url": "https://example.com/regUpdate",
"authId": "e2c2c6e2-7b39-4a16-9d0c-48d5b61f3172"
}
The above sample subscription tells Engine to send a notification to "https://example.com/regUpdate" anytime the RegistrationChanged topic is triggered. Since no subtopics are specified, it includes all subtopics under the RegistrationChanged topic. It uses a SubscriptionAuthDefintion with ID e2c2c6e2-7b39-4a16-9d0c-48d5b61f3172
, retrieved from the response of an earlier POST request to /appManagement/subscriptions/authConfigurations
, which was used to create a subscription authentication configuration.
A more verbose SubscriptionDefinition can be seen below:
{
"topic": "RegistrationChanged",
"subtopics": ["ScoreChanged", "CompletionChanged"],
"enabled": true,
"url": "https://example.com/regUpdate",
"authId": "e2c2c6e2-7b39-4a16-9d0c-48d5b61f3172",
"timeoutMS": 5000,
"asyncMode": "Thread",
"expiresMS": 600000,
"filters": [
{
"target": "CourseId",
"matches": ["myCourse", "/^test_/"]
}
],
"strictOrdering": false,
"retryAttempts": 4,
"retryDelaySeconds": 60,
"ignoreBeforeDate": "2022-11-02T22:33:16Z",
"bodyFormat": "Full"
}
This sample subscription definition includes more properties than the previous one, such as the subtopics
property, which limits the subscription to only sending notifications when a registration's score or completion is changed, and the filters
property, which specifies that the subscription should only apply to courses with a course ID of "myCourse" or those that start with "test_". Further, the timeoutMs
and retryAttempts
properties specify that the notification should time out if the request to the endpoint url takes more than 5 seconds, and that the notification should be retried 4 more times after that, before giving up.
In a normal subscription, you might not use all of the available properties, but they are included in the above sample to give you an idea of what their values might look like. Here are more details on each of these allowed properties:
topic
- [required] The topic to subscribe to. Each topic has its own message body and list of applicable subtopics and filters. See the full list of topics here.subtopics
- Subtopics are attached to each event, and can be used to narrow down the set of events the subscription is interested in. Not specifying subtopics assumes the subscription is interested in all subtopics.enabled
- [required] If set tofalse
, this subscription will not trigger.url
- [required] The endpoint this subscription will send messages to.authId
- A reference guid to a SubscriptionAuthDefinition, which defines the Authorization behavior we'll use when making requests to yoururl
.timeoutMS
- The time until a message attempt will timeout. Defaults to5000
.asyncMode
- Can be either"Thread"
or"Sync"
, to determine whether the immediate message attempt will be asynchronous (in its own thread) or synchronous respectively. Synchronous delivery should be used sparingly as it may severely degrade performance. Defaults to"Thread"
.expiresMS
- If this expects a response body, this is how long Engine will cache said response. Defaults to the value determined by theLocalCacheExpiryMilliSeconds
configuration value.filters
- Allows a subscription to futher control when it triggers based on targets such as CourseId, RegistrationId, or Tenant. Matches is an array of string literals or RegEx to match against the target property. If multiple are defined, the filter applies if any match.strictOrdering
- If set totrue
, Engine will drop old messages off the queue if a newer one has already been sent within the same context (i.e. forRegistrationChanged
, this is the registration). This comes at a cost to performance and should be used sparingly. Defaults tofalse
.retryAttempts
- If a Notification fails, how many times will Engine retry before dropping the message. Takes the lesser value of this andSimpleQueueMaxRetries
.retryDelaySeconds
- The initial length of time Engine will wait before retrying after a failure. This time will be doubled after each retry. Takes the greater value of this andSimpleQueueRetryInitialDelaySeconds
.ignoreBeforeDate
- If set, the subscription will drop all queued messages of events which were triggered before the specified date.bodyFormat
- A place to specify the format of the message body, if multiple exist. Topics which utilize this will elaborate in their documentation.
Subscription Url Authorization
In many cases, the endpoint you create to receive these notifications may require authentication. To have Engine add an authorization header to messages, you will need to create a subscription auth configuration. This is done by sending a POST request to appManagement/subscriptions/authConfigurations
with a SubscriptionAuthDefinition.
Once created, an auth configuration can be used by any number of subscriptions. A sample SubscriptionAuthDefinition is as follows:
{
"subscriptionAuthType": "Oauth2ClientCredentials",
"key": "myClientId",
"secret": "mySecret",
"scope": "read",
"tokenUrl": "https://example.com/authorization"
}
This sample sets Engine up to use an OAuth2 client credentials workflow, where Engine will request a token from "https://example.com/authorization" with the provided key/secret and scope. This token will then be used in the Authorization header on our notification requests.
subscriptionAuthType
- This determines the method of authorization and the interpretation of the other property values. Options include:"Basic"
: Useskey
andsecret
to create a Basic Auth header"HMACSHA256"
: Creates an HMAC-SHA256 hash of the request body using thesecret
, and will include it in ax-signature
header"HMACSHA512"
: Creates an HMAC-SHA512 hash of the request body using thesecret
, and will include it in ax-signature
header"Oauth2ClientCredentials"
: Retrieves a token fromtokenUrl
using thekey
,secret
andscope
, caches it, and then uses it to create a Bearer token auth header.
key
- The key/username/clientID depending on the auth mode.secret
- The password or secret depending on the auth mode. For HMAC, this is the base64 of the secret.scope
- The scope for OAuth2.tokenUrl
- The token retrieval endpoint for OAuth2.
An optional header for both POST requests is engineTenantName
. If configured, it will apply a subscription to that tenant only. Otherwise, the subscription will be saved at the system level and apply to all tenants. Subscriptions on the tenant level can only use auth configurations on the same tenant or on the system level.
Using a Client Certifcate on Webhook Requests
Alongside the other authentication mechanisms above, you can also configure Engine to attach a client certificate to the requests that we send. There are a few steps to configure this:
- Have a PKCS12 key store file saved somewhere accessible to Engine on the server (e.g.,
/example/path/exampleCert.p12
). - Configure the
SubscriptionClientCertificate
setting with the path to this file. - Configure the
SubscriptionClientCertificatePassword
setting with the password for accessing the client certificate.
As an alternative to uploading the file to Engine's server, one can also configure the SubscriptionClientCertificate
setting to the base64 encoding of the PKCS12 file itself. This method must be done through the configuration file, not the api.
Note that these are configured only at the system level, and will apply to all notification/exchange requests sent from this instance of Engine.
Notifications versus Exchanges
Some topics are defined as Notifications and others as Exchanges. Notifications are messages sent to your endpoint where Engine does not expect a response other than a 200-level status indicating it was received successfully.
Notifications can be sent either synchronously or asynchronously, and will be queued to retry automatically if they fail. One event in Engine can trigger multiple notifications if there are multiple subscriptions interested in the event.
On the other hand, Exchanges are requests sent where Engine is expecting a specific response from your application, which will differ depending on the topic of the Exchange.
Exchanges are only sent synchronously, and assume a default behavior on failure rather than retrying the request. Exchanges can only use one response, and will select a single subscription to act on if multiple subscriptions are defined for the same exchange. For this reason, it’s highly recommended that subscriptions for Exchanges not overlap in any way (i.e. it could be ambiguous which subscription Engine should use).
Webhook Request Payload
Each message Engine sends will be wrapped in a standard payload envelope. The body
property for each envelope will be unique to the topic of the subscription, as shown in the Subscriptions Reference page.
The standard notification request envelope is as follows:
{
"payloadId": "3ccefe0f-a236-4d20-b081-868ebc1d1a3a",
"subscriptionId": "fe9eafe9-d5ea-4d08-8ce0-4ab1ae9c5b27",
"topic": "RegistrationChanged"
"subtopics": ["ScoreChanged"]
"tenantName": "myTenant",
"timestamp": "22-10-10T21:51:45Z",
"bodyVersion": "1.0",
"messageVersion": "1.0",
"body": {}
}
payloadId
- A unique id for each individual message sent.subscriptionId
- The id of the subscription which generated this message.topic
- The topic this message is about, which in turn determines the structure of the body.subtopics
- The subtopics which are relevant to the event. Note that if any of the subtopics specified in the subscription are present, the message will be sent.tenantName
- The tenant where the event occurred.timestamp
- When the event occurred.bodyVersion
- The version of the body object sent. Any updates or changes to a topic's body schema will result in a change to the version.messageVersion
- The version of the standard envelope itself. Any updates or changes to the envelope's schema will result in a change to the version.body
- The body of the message, which will have a differenct schema based on the topic.
Error Handling and Monitoring
In the event a notification fails, Engine will queue the message to be retried based on the subscription’s “retryAttempts” and “retryDelaySeconds” values. On each failed attempt, Engine logs the message it’s trying to send and the error that occurred on the INFO level. On the initial and final retry attempts, Engine will also trigger the “NotificationFailed” topic, which if configured will notify a system that a message is failing, the error that occurred, and the payload that message is trying to send.
In the event that an exchange fails, Engine will assume some default behavior and try again the next time the event is triggered. For example, if the XapiGetAuthority exchange fails, Engine will default to rejecting the request in question. The failed attempt is logged on the INFO level in the same format as if a notification failed.
At any time, you can retrieve the lastExceptionReference
and lastExceptionDate
for a given subscription through the API via GET appManagement/subscriptions
and GET appManagement/subscriptions/{subscriptionId}
. There are also performance metrics you can monitor related to how long requests are taking and how many requests are queued under the sub_postback
metric or webhooks_queue
label.
Potential Queue Implementations
As outlined above, failed messages are sent into a queue. Out of the box, Engine has an SQL-based queue which serves as the default implementation. However, one can switch out this queue for another using the QueuePlugin. To do so, just implement the plugin and a class which inherits from our MessageQueue class and Engine will use your queue implementation. To configure certain things about your implementation, you can use the SystemQueuePluginParameters
or the TenantQueuePluginParameters
settings to pass information (such as stringified json) in. You may find that you'd rather process the queue entries yourself rather than having Engine do that. If so, you should turn off the EventMessageProcessor
for all instances using the BackgroundProcessors
setting.
Alternatively, the Java version of Engine comes with a built-in implementation of the QueuePlugin called "AmazonSqs". This is set up to interface with SQS directly. To use it, refer to it in the "Plugins" setting and set the configuration in either the "SystemQueuePluginParameters" or "TenantQueuePluginParameters". An IndividualQueueConfiguration object is as follows:
{
"queueUrl": "http://mySqsServer.com",
"accessKey": "myKey",
"secretKey": "mySecret",
"region": "us-east-1"
}
queueUrl
- [required] The url of the SQS instance.accessKey
- The access key. Credentials are gotten from the DefaultCredentialsProviderChain if either accessKey or secretKey is omitted.secretKey
- The secret key.region
- The region of the SQS server. The Region is determined by the DefaultRegionProviderChain if omitted.
The configuration itself is made up of potentially two IndividualQueueConfiguration objects. The first is called mainQueue
, which must be configured. The second is called deadLetterQueue
, which is where Engine will send messages which have exceeded their retry count for further examination or processing. If deadLetterQueue
is not configured, Engine will simply log and drop the message. So, the top level of the configuration should look like so:
{
"mainQueue": { }, // IndividualQueueConfiguration
"deadLetterQueue": { } // IndividualQueueConfiguration
}
SystemQueuePluginParameters
and TenantQueuePluginParameters
can be used in conjunction to configure different queues for different tenants. The System and each Tenant need some kind of valid queue configured, so at minimum SystemQueuePluginParameters
must be set. If you decide to configure multiple queues, know that Engine's "EventMessageProcessor" will only process the "System" level queue; you will have to process any additional configured queues yourself. Recall that if you'd rather Engine not process any queues, you can turn the processor off using the "BackgroundProcessors" setting.
Implementing your Endpoint
There are a few things that you should keep in mind when implementing your endpoint to receive these webhook requests from Engine:
Important: Notifications may come 'out of order', especially if we've failed in our first attempt and are retrying the request. It is critical that your endpoint keeps track of the
timestamp
from the last request you successfully processed, and you ignore any incoming request for the same subscription if it has an earlier timestamp and is updating the same resource (eg, the same registration).The alternative to this is to use the
strictOrdering
flag on your subscription, but that can impact the performance on the Engine side, as we will have to do more locking while processing the notifications.For notifications, your endpoint should return a 200-level status in all cases, unless it's a problem that retrying would potentially fix. Otherwise, we will be queuing and retrying the request with no possibility of success.
Be aware that Engine will also NOT follow 302/307 responses, so make sure the endpoint is directly accessible without any redirects.
- You must ensure that your endpoint returns as quickly as possible, especially for
RegistrationChanged
subscriptions or any type of exchange subscription. Often the time required waiting for a response from your endpoint will impact response times for the learner.
Note for Upgrading Customers
Engine’s previous postback systems (ApiRollupRegistrationPostbackUrl
and ApiImportResultsPostbackUrl
) are still in place and can be configured as they were before. Their behavior will not change in this version of Engine, and you are free to use them instead of the new Webhooks Subscriptions if you do not need any of the improved funtionality of those. Please note, though, that we will remove these older postback mechanisms in a future version of Engine.