This page documents the Webhooks functionality available in Rally.
Webhooks are an advanced tool that can be used to integrate your Rally subscription with other, external services. A webhook makes an HTTP POST request to a URL of your choosing whenever certain types of changes occur in your Rally subscription.
Webhooks are "user-defined HTTP callbacks". They are usually triggered by some event,
such as pushing code to a repository or a comment being posted to a blog. When that
event occurs, the source site makes an HTTP request to the URI configured for the webhook.
Users can configure them to cause events on one site to invoke behaviour on another.
The target audience for this document is developers who want to efficiently extract information about changes happening in their Rally data.
You should be familiar with HTTP, JSON, and Rally's Web Services API.
Every change in Rally results in a Change Message
, which can potentially be matched by a webhook. Examples of changes include:
Essentially any change at all results in a message that can be matched to a webhook.
A webhook includes a set of conditions that determine which change messages it will match. Conditions are very flexible and allow you to decide very precisely which changes you want to be notified about.
When a webhook matches a change message, a JSON payload containing information about precisely what changed will be posted to the webhook's Target URL.
Webhooks are created and managed through an HTTP API.
The base URL for this API is: https://rally1.rallydev.com/apps/pigeon/api/v2
Briefly, the following actions are available. Each will be described in more detail below.
HTTP Verb | Path | Action |
---|---|---|
POST | /webhook |
Create a new webhook |
GET | /webhook |
Return a paged result set listing webhooks in your subscription |
GET | /webhook/{id} |
Return a specific webhook by its id |
PATCH | /webhook/{id} |
Update a webhook |
DELETE | /webhook/{id} |
Delete a webhook |
All requests to the webhooks API must be authenticated, proving that the request comes from a user with a valid and active Rally account. There are several ways you can authenticate.
First, you need a valid key, which can be:
Then, when making requests to the webhooks API, pass the key as the ZSESSIONID cookie. The method for doing this will depend on the programming language and HTTP library that you use. Here is an example of a raw HTTP request, showing how to authenticate.
GET /apps/pigeon/api/v2/webhook HTTP/1.1
Host: rally1.rallydev.com
Cookie: ZSESSIONID={my-key}
Note that the webhooks API does not currently support HTTP Basic authentication, so you cannot simply pass a username and password directly.
If you do not authenticate properly, a response status of 401 (Unauthorized)
will be return from all requests.
You create a webhook by making a POST request to /apps/pigeon/api/v2/webhook
with a JSON body.
Here is an example:
{
"AppName": "My Cool App",
"AppUrl": "www.example.com",
"Name": "My first webhook",
"TargetUrl": "http://my.service.com/ready/to/receive/post",
"ObjectTypes": ["HierarchicalRequirement", "Defect"],
"Expressions": [
{
"AttributeName": "PlanEstimate",
"Operator": ">",
"Value": 10
},
{
"AttributeName": "Workspace",
"Operator": "=",
"Value": "31685839-67fe-4f9d-9470-da56c222998b"
}
]
}
The response should have an HTTP status of 200 and a body that looks something like this:
{
"AppName": "My Cool App",
"AppUrl": "www.example.com",
"Name": "My first webhook",
"TargetUrl": "http://my.service.com/ready/to/receive/post",
"ObjectTypes": ["HierarchicalRequirement", "Defect"],
"Expressions": [
{
"AttributeID": null,
"AttributeName": "PlanEstimate",
"Operator": ">",
"Value": 10
},
{
"AttributeID": null
"AttributeName": "Workspace",
"Operator": "=",
"Value": "31685839-67fe-4f9d-9470-da56c222998b"
}
],
"CreatedBy": null,
"CreationDate": "2016-06-23T20:54:26.978Z",
"Disabled": false,
"Security": null,
"LastUpdateDate": "2016-06-23T20:54:26.978Z",
"ObjectUUID": "51c8d135-a8ce-4bd2-9865-e3ec045cb47e",
"OwnerID": "4481dd2f-2810-4fb9-bd85-bb81daf2510c",
"SubscriptionID": 100,
"_objectVersion": 1,
"_ref": "https://rally1.rallydev.com/apps/pigeon/api/v2/webhook/51c8d135-a8ce-4bd2-9865-e3ec045cb47e",
"_type": "webhook",
}
At its core, a webhook consists of two things:
TargetUrl
, where the webhook's payload will be POSTed.Expressions
, which determine the conditions under which the webhook will fire. (Expressions will be explained in detail below.)In addition to these, there are a few more attributes that are required.
The following are all the attributes that you can have in a webhook:
Name | type | required? | Description |
---|---|---|---|
AppName |
String | yes | Informational. Describes the external app or integration. |
AppUrl |
String | yes | Informational. A URL where more information about the app can be found. |
Name |
String | yes | Informational. To help you identify different webhooks that you have created. |
TargetUrl |
String (URL) | yes | The URL of a web service that can receive an HTTP POST request containing the payload. |
Expressions |
Array[Expression] | yes | Array of one or more Expression maps. See below for details. |
ObjectTypes |
Array[String] | no | Optional array of object types that the webhook can match. |
Security |
String (40 chars max) | no | If provided (via create or update), the value of this field will be present in the Header of the request posted to the TargetUrl (can be used to validate that the request sender is authorized). WARNING: This value should not contain any double quote (") characters. |
Disabled |
Boolean | no | If set to true, the webhook will not fire until re-enabled. |
OwnerID |
String (UUID) | no | UUID. The ObjectUUID of a Rally User who will own the webhook. |
CreatedBy |
String | no | Informational. Intended to identify the tool (if any) used to create the webhook. |
The following are additional read-only status fields that are updated by the system upon webhook firing:
Name | type | Description |
---|---|---|
FireCount |
Number | Total number of times this webhook has been fired (includes both successful and unsuccessful attempts |
ErrorCount |
Number | Number of consecutive webhook firing failures, is reset to 0 upon successful firing |
LastStatus |
Number | HTTP status code returned by last firing of the webhook |
LastWebhookResponseTime |
Integer | Elapsed time of webhook fire and receiving the response, success code is 200 |
LastSuccess |
Timestamp | Updated when firing the webhook results in a 200 level status code |
LastFailure |
Timestamp | Updated when firing the webhook fails, times-out or results in a non 200 level status code |
Note that the above fields will only have non-null values if the webhook has never been fired.
A webhook contains an array of Expressions.
Generally, an Expression
consists of an Attribute
(specified by either ID or Name), an Operator
, and a Value
. When any object is changed, the value of the attribute on the changed object is compared, using the given operator, to the Value
field of the Expression
.
For example, consider the following Expression
:
{
"AttributeName": "ScheduleState",
"Operator": "=",
"Value": "In-Progress"
}
This will match whenever there is a change to any object that has a ScheduleState
attribute with a value of "In-Progress". Note that the ScheduleState
attribute itself need not be changed - any change to an object in that state will match.
Currently, expressions are always combined with logical and
; that is, all Expressions
in a webhook must match the change message in order for the webhook as a whole to match.
You can also specify Attributes by ID, instead of Name. For example, the following Expression is (almost) equivalent to the one show above:
{
"AttributeID": "aad205e0-2fbe-11e4-8c21-0800200c9a66",
"Operator": "=",
"Value": "In-Progress"
}
Because "aad205e0-2fbe-11e4-8c21-0800200c9a66" is the ObjectUUID
of the AttributeDefinition
for ScheduleState
, this Expression will also match on changes to any object with a ScheduleState
of "In-Progress".
There are a couple of differences when using an Attribute's ID instead of its Name. The ID is guaranteed to match using only the intended Attribute, whereas the Name is less precise. If, for example, a new Attribute named "ScheduleState" were added to another object in the model, your webhook could begin to match changes that you did not intend.
Also, using AttributeID
guarantees that your expressions will continue to work correctly even if an attribute get renamed, as can happen for custom attributes.
You can find Attribute IDs by examining TypeDefinitions
and AttributeDefinitions
defined in the WSAPI Object Model.
For example, the AttributeID for ScheduleState, used above, was discovered by looking at https://rally1.rallydev.com/slm/webservice/v2.0/TypeDefinition/-51084/Attributes and copying the ObjectUUID
property of the "ScheduleState" attribute. (Note: make sure you are logged in to Rally).
The ObjectTypes
field in a webhook is an optional way to limit the types of objects that can match. You can think of it as a shorthand for an Expression.
For example, this:
{ ...
"ObjectTypes": ["Defect", "TestCase"]
}
is essentially equivalent to this:
{ ...
"Expressions": [
{
"AttributeName": "ObjectType",
"Operator": "~", // "equals-one-of" operator
"Value": ["Defect", "TestCase"]
}
}
except that there is not actually an attribute named "ObjectType".
If ObjectTypes
is omitted or empty, it means "any type" - that is, the webhook can match changes to objects of any type, as long as its Expressions match.
Note: WSAPI typically refers to Portfolio Items like so:
PortfolioItem/<Name>
(e.g.PortfolioItem/Feature
,PortfolioItem/Initiative
orPortfolioItem/MyCustomPortfolioItemName
). To scope webhooks to Portfolio Items, omit thePortfolioItem/
and use the name only (e.g.Feature
,Initiative
orMyCustomPortfolioItemName
) in theObjectTypes
array.
Example webhook to get changes to Feature
PortfolioItems for a specific project:
{
"Expressions": [
{
"AttributeName" : "Project",
"Operator": "=",
"Value": "2b286e96-5ef8-443c-b630-dc2cdbf6fcd8"
}
],
"ObjectTypes": ["Feature"],
"TargetUrl": "http://somecool.url/webhook",
"Name": "Feature changes in Project X",
"AppName": "KG",
"AppUrl": "www.thebomb.com"
}
The required fields in an Expression
depend on the Operator
.
The following operators require both a Value
and exactly one of AttributeID
or AttributeName
.
Operator | Description |
---|---|
= |
Equal |
!= |
Not equal |
< |
Less than |
<= |
Less than or equal |
> |
Greater than |
>= |
Greater than or equal |
changed-to |
Value changed to |
changed-from |
Value changed from |
The following operators require an AttributeID
or AttributeName
, and a Value
that is an Array of individual values.
Operator | Description |
---|---|
~ |
"Equals one of". Matches when the object's value for the attribute is equal to one of the values given in the Expression |
!~ |
"Equals none of". Matches when the object's value for the attribute is not equal to any of the values given in the Expression |
The following operators require only an AttributeID
or AttributeName
(no Value
)
Operator | Description |
---|---|
has |
The object has some (non-null) value for the attribute |
!has |
The object does not have the attribute, or its value is null |
changed |
The value of the attribute was changed on the object |
Create a webhook by POST
ing to the /api/v2/webhook
endpoint, as detailed above, in the section "Example: Creating a Webhook".
Note that the response will contain a _ref
field containing the direct URL of the created webhook, which can be used to get, update, or delete it.
Get an individual webhook by making an HTTP GET
request to its _ref
URL.
Delete a webhook by making an HTTP DELETE
request to its _ref
URL.
Update a webhook by making an HTTP PATCH
request to its _ref
URL.
Only fields sent in the body of the PATCH
request will be updated. Omitted fields will retain their old value. This means that you can, for example, send an update containg only this:
{"Disabled": true}
to disable a webhook.
Note that Array-valued fields, including ObjectTypes
and Expressions
, cannot be partially updated - they can only be replaced. In other words, if you want, for example, to add a single Expression
to an existing webhook, you must send in an Expressions
array containing all existing expressions, in addition to the the new expression. Any expressions not sent will be deleted.
Query for webhooks in your subscription by making an HTTP GET
request to /api/v2/webhook
, optionally with query parameters.
Parameter | Description | Default | Example(s) |
---|---|---|---|
order | The field name to sort by and an optional direction, separated by a space. | ascending (asc) | order=AppName desc , or order=Disabled desc,Name |
pagesize | The number of results to return per page | 20, max 200 | pagesize=100 |
start | The start index for the query | 1 | start=101 |
Example query:
To get 200 results, second page of results, ordered by Name descending:
GET
/api/v2/webhook?pagesize=200&start=201&order=Name desc
Response:
{
"Results": [ /// 200 items
{
"LastUpdateDate": "2015-09-10T14:31:52.460Z",
"Expressions": [/*...*/],
"SubscriptionID": 123,
"_ref": "https://rally1.....",
"_type": "webhook",
"OwnerID": "cc1fcb3f-7f12-4abe-b1fc-123cd2123123",
"TargetUrl": "https://foo.bar/blah",
"_objectVersion": 1,
"Disabled": false,
"Name": "Zze Best Webhook", //sort by name descending
"AppName": "Best App!",
"ObjectUUID": "123ba678-12e4-1ae4-b234-12f4bbb8901c"
"CreationDate": "2015-09-10T14:31:52.460Z",
"AppUrl": "https://best.app"
},
{/*...*/},
/*...and so on*/
],
"TotalResultCount": 467,
"PageSize": 200,
"StartIndex": 201 // second page of results
}
100 results, first page of results, ordered by Name ascending
Upon firing, a webhook will POST a JSON payload to its TargetUrl
. The content of this payload is as follows:
{
"rule": { ... Webhook that matched ... },
"message": { ... Change Message (see below) ... }
}
The "rule" is the Webhook that fired, in the same format as is returned by the webhooks HTTP API.
The "message" is the Change Message that matched. It describes an object in the Rally object model that changed, what changes were made, when, and who changed it.
Here is an example of a Change Message:
{
"message_id": "6750d9fe-c289-478f-b188-39d483c9ea39",
"message_version": 2,
"subscription_id": 209,
"action": "Updated",
"object_id": "6ab3d609-8011-47f8-2372-4d20ec313c20",
"object_type": "HierarchicalRequirement",
"ref": "https://rally1.rallydev.com/slm/webservice/v2.x/hierarchicalrequirement/6ab3d609-8011-47f8-2372-4d20ec313c20",
"detail_link": "https://rally1.rallydev.com/slm/#/detail/userstory/52790947441",
"project": {
"uuid": "749a8fd7-dfa9-897b-bec7-d86e01aa8b3a",
"name": "My Project"
},
"transaction": {
"timestamp": 1466627088236,
"trace_id": "c38ee7f5-543d-4214-bf02-55a180227826",
"user": {
"uuid": "1d1c15d7-9de6-4df8-aaf1-b9adb4edfb56",
"username": "some.user@example.com",
"email": "some.user@example.com"
}
},
"changes": {... (see below) ...},
"state": {... (see below) ...},
}
It contains these fields:
Name | Type | Description |
---|---|---|
message_id |
UUID | An id unique to this change message |
message_version |
Integer | Currently always 2 |
subscription_id |
Integer | SubscriptionID of your Rally subscription |
action |
String | One of "Created", "Updated", "Recycled" |
object_id |
UUID | ObjectUUID of the changed object |
object_type |
String | Type of the changed object. eg: "Defect", "HierarchicalRequirement", "Project", etc. |
ref |
URL | Link to the changed object in WSAPI |
detail_link |
URL | If applicable to the type, a link to the HTML page where the object can be viewed or edited |
project |
Object | If applicable to the type, contains the uuid and name of the containing project. |
transaction |
Object | Contains the user who made the change, and the timestamp when it happened. |
state |
Object | See below |
changes |
Object | See below |
The state
map contains the entire state of the object after the change. It takes the form of a map where the keys are AttributeUUIDs, and the values are maps containing the attribute's "value", "type", "name", "display_name", and "ref".
Here is an example of the state
map from a change message:
"state": {
"500a0d67-9c48-4145-920c-821033e4a832": {
"value": "My Cool Story",
"type": "String",
"name": "Name",
"display_name": "Name",
"ref": null
},
"ae8ecc9f-b9a0-42a4-a6e3-c83d7f8a7070": {
"value": {
"name": "0",
"ref": "https://rally1.rallydev.com/slm/webservice/v2.x/project/749a8fd7-dfa9-421b-bec7-d86e01aa8b3a",
"detail_link": "https://rally1.rallydev.com/slm/#/detail/project/18951302164",
"id": "749a8fd7-dfa9-421b-bec7-d86e01aa8b3a",
"object_type": "Project"
},
"type": "Project",
"name": "Project",
"display_name": "Project",
"ref": null
},
... etc ...
}
The changes
map is similar in shape to the state
map, but it contains only attributes that were changed. The map representing each attribute contains, in addition to the value
as it appears in the state
section, fields called old_value
, added
, and removed
.
Example:
"changes": {
"f5b1fb22-6c15-44b5-a592-19189fafe5f2": {
"value": 8,
"old_value": 7,
"added": null,
"removed": null,
"type": "Integer",
"name": "VersionId",
"display_name": "VersionId",
"ref": null
},
"aad205e0-2fbe-11e4-8c21-0800200c9a66": {
"value": {
"name": "Defined",
"ref": "https://rally1.rallydev.com/slm/webservice/v2.x//c7ce12e8-bb4f-457d-af13-accd7868edb1",
"detail_link": null,
"id": "c7ce12e8-bb4f-457d-af13-accd7868edb1",
"order_index": 1,
"object_type": "State"
},
"old_value": {
"name": "In-review",
"ref": "https://rally1.rallydev.com/slm/webservice/v2.x//0955ce77-41a8-44cc-ba87-9de63749098e",
"detail_link": null,
"id": "0955ce77-41a8-44cc-ba87-9de63749098e",
"order_index": 0,
"object_type": "State"
},
"added": null,
"removed": null,
"type": "State",
"name": "ScheduleState",
"display_name": "Schedule State",
"ref": null
}
}
For all attribute types other than Collection
, the old_value
field will be populated, and the added
and removed
fields will be null.
For attributes of type Collection
, the old_value
field will be null, the added
field will contain an array of child objects that were added to the collection, and removed
will contain an array of child objects that were removed from the collection.
If a webhook fires and receives an HTTP response status of 410
(Gone) from the target, the currently-firing webhook will be immediately deleted, and will not fire again.
You can use this feature as a mechanism to clean up unwanted webhooks, by making your service return 410
(rather than, say, 404
) when it is no longer needed.
There are currently no specific latency guarantees.
Typically, webhooks will fire about 1 to 2 seconds after a change is made in Rally.
There is currently no retry mechanism. If your webhook endpoint is not able to accept the POST request (eg: your server is temporarily down, there is a network outage, etc.), no further attempts will be made to POST that payload.
You can find an Attribute UUID by querying the type definition endpoint in Rally.
_refObjectName
. (Note that the User Story type is called "HierarchicalRequirement")ElementName
or ObjectUUID
field of the attribute, depending on whether you are creating an Expression by AttributeName
or AttributeID
.When a payload is POSTed to your server, how can you be sure that the request came from Rally, and not from an attacker?
There are some simple practices that you can use to make your webhooks as secure as possible.
targetUrl
field of your webhooks to an https://...
endpoint, rather than http://...
, and we will call you over a secure, TLS-encrypted connection. Make sure that your server supports incoming HTTPS requests.targetUrl
's path. For example, something like https://your.domain.com/webhooks/ca95aaab-836f-42b9-84fc-265f250755f4
.By following these steps, you can minimize the probability that an outside attacker will be able to successfully spoof a request to your webhook server. The unique identifier in your URL can be neither guessed nor intercepted, and cannot be discovered without access to your Rally subscription's webhooks data.
Rally reserves the right to suspend or disable webhooks based on non-usage or misuse.