Introduction
Welcome to the ControlShift Labs Developer documentation. Our APIs are designed to allow software engineers to create rich integrations between ControlShift content and third-party services and websites.
We offer several APIs to customers to cover different integration use cases.
JSONP API Unauthenticated API designed to be consumed by javascript clients written by front-end engineers. Quickly display public information about petitions, efforts, and events on external sites.
Webhooks Get notified when changes happen so external systems can react.
Bulk Data Integrate your ControlShift instance with a data warehouse like Redshift for SQL access and reporting.
Authenticated REST Server Side API integration option for more advanced scenarios than are possible with the JSONP API. Uses OAuth2 to allow third-party developers to create applications and customers to grant access.
It's important to choose the right API for the project you're undertaking. For example, the Webhooks API is almost always the right choice if you're syncing ControlShift data to a CRM.
API Usage assistance is available via email at support@controlshiftlabs.com
We publish various pieces of open source code on Github which you may find useful.
JSON API Endpoints
The public JSON API is a simple way to access non-sensitive ControlShift data. Many of the JSON API endpoints are intended for use by a front-end developer to embed content on web pages outside of the platform. For example, a developer could:
- Show petitions
- Allow a user to search for near by petitions
- List petition categories
- Show petitions within an effort
The URL slugs through the API are the same as those that are used through the web to represent specific petitions or categories.
Using the JSON API from an external site
When one website includes a call to retrieve JSON data from another website, that is considered a cross-origin request. For security reasons, browsers usually block this type of request by default. There are two different ways to get around this restriction to allow calling public ControlShift JSON API endpoints from external sites: CORS and JSONP. The documentation for each endpoint indicates which options are supported.
CORS
CORS allows the ControlShift platform to specify that certain URLs are allowed to be requested cross-origin from certain domains.
For JSON requests relying on CORS to work, you'll need to configure the allowed domains from Settings > Integrations > CORS Hostnames. Once a hostname is safe-listed there, any of the endpoints that support CORS can be used on pages on that domain.
For example, to embed petition content from a ControlShift website demo.controlshiftlabs.com on your external website hq.example.org, you'd want to add hq.example.org to your CORS Hostnames.
JSONP
JSONP is an older way to allow cross-origin requests for JSON data. The way it works is by specifying a Javascript callback that will handle the JSON data. The server wraps that callback around the data it returns. This allows the call to technically be a Javascript request instead of a JSON request, and so the browser allows it.
Many front-end libraries, including jQuery, make it easy to consume JSONP endpoints. Our examples below use jQuery when demonstrating JSONP calls.
Petitions
Get a single petition
$(document).ready(function(){
var petitionSlug = 'repair-the-yellow-brick-road-1';
$.ajax({
url: 'https://demo.controlshiftlabs.com/petitions/'+petitionSlug+'.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would display your returned petition data in the console. The JSON response data would be structured like this:
{
"id": 92283,
"title": "Repair the Yellow Brick Road",
"slug": "repair-the-yellow-brick-road-1",
"who": "Oz, the Great and Terrible, Wizard",
"what": "The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!",
"rich_what": "<div>The Yellow Brick Road has been neglected; <strong>commit to repairing the damaged sections of the road in the next year!</strong></div>",
"why": "The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.",
"rich_why": "<div>The <em>Yellow Brick Road</em> is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.</div>",
"created_at": "2014-10-02T01:43:17Z",
"updated_at": "2018-05-07T15:38:39Z",
"delivery_details": "We're off to see the wizard!",
"administered_at": "2018-03-21T19:05:54Z",
"source": "homepage",
"alias": null,
"categories": [
{
"name": "Oz",
"slug": "oz"
}
],
"ended": true,
"successful": true,
"ended_story": "After we met with the wizard, he agreed to a timeline for repairing the road!",
"goal": 500,
"effort": null,
"partnership": null,
"resized_image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"creator_name": "Kristyn Arrighi",
"signature_count": 223,
"location": {
"latitude":"36.1946827",
"longitude":"-78.8087571",
"query":"Land of Oz",
"street_number":"1007",
"street":"Beech Mountain Pkwy",
"street_address":"1007 Beech Mountain Pkwy",
"locality":"Beech Mountain",
"region":"NC",
"postal_code":"28604",
"country":"US",
"country_code":"US",
"country_name":"United States",
"static_map_url":"https://d8s293fyljwh4.cloudfront.net/locations/static_maps/27575/27575-static-map.png?1532035340"
}
}
This retrieves a single petition object.
HTTP Request
GET https://demo.controlshiftlabs.com/petitions/<slug>.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Default | Description |
---|---|---|
slug | null | string - required - The petition's unique identification slug. If none is provided, you will get a 404 error. Note: submitted as a part of the endpoint path, not as a separate URL parameter |
Working Example
View and edit a working example on codepen.io:
Get petition content
fetch('https://demo.controlshiftlabs.com/api/petitions/repair-the-yellow-brick-road-1/what.json')
.then((response) => {
response.json().then((jsonData) => {
console.log(jsonData)
})
})
JSON response
{
"plain_text": "The Yellow Brick Road has been neglected. Commit to repairing the damaged sections of the road in the next year!",
"rich_text": "<div>The <strong>Yellow Brick Road</strong> has been neglected. <em>Commit to repairing the damaged sections of the road in the next year!</em></div>",
}
Retrieve the formatted "What" or "Why" text of a petition.
HTTP Request
GET https://demo.controlshiftlabs.com/api/petitions/<slug>/what.json
GET https://demo.controlshiftlabs.com/api/petitions/<slug>/why.json
- ✓ CORS supported
- × JSONP not supported
If the petition does not exist or is not visible to the public, the response will be 404 Not Found.
Get featured petitions
$(document).ready(function(){
$.ajax({
url: 'https://demo.controlshiftlabs.com/petitions/featured.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would display your returned featured petitions data in the console. The JSON would be structured like this:
{
"meta": {
"total_pages": 3,
"total_entries": 25,
"per_page": 10
},
"data": [
{
"slug": "repair-the-yellow-brick-road-1",
"url": "http://demo.controlshiftlabs.com/petitions/repair-the-yellow-brick-road-1",
"title": "Repair the Yellow Brick Road",
"who": " Oz, the Great and Terrible, Wizard",
"what": "The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!",
"rich_what": "<div>The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!</div>",
"why": "The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.",
"rich_why": "<div>The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.</div>",
"admin_status": "good",
"signature_count": 223,
"goal": 500,
"creator_name": "Kristyn Arrighi",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2014-10-02T01:43:17Z",
"updated_at": "2018-05-07T15:38:39Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"additional_image_sizes_url": [
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/form/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/horizontal/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/open_graph/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
}
],
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
{
"slug": "we-need-more-bike-lanes",
"title": "We Need More Bike Lanes",
"who": "My Mayor",
"what": "Build the bike lanes",
"rich_what": "<div>Build the bike lanes</div>",
"why": "Cyclists aren't safe.",
"rich_why": "<div>Cyclists aren't safe.</div>",
"admin_status": "good",
"signature_count": 5,
"goal": 100,
"creator_name": "Greg Dutcher",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2016-08-12T18:08:32Z",
"updated_at": "2016-09-14T12:43:45Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/167492/original/IMG_1854.JPG?1472046912",
"additional_image_sizes_url": [
]
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/167492/original/IMG_1854.JPG?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/167492/form/IMG_1854.JPG?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/167492/horizontal/IMG_1854.JPG?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/167492/open_graph/IMG_1854.JPG?1473884741"
}
],
"location": {
"query": "New York",
"latitude": "40.6974034",
"longitude": "-74.1197633",
"street": "",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2017-04-11T13:32:08Z"
}
}
],
"links": {
"self": "https://demo.controlshiftlabs.com/petitions/featured.json?page=1",
"first": "https://demo.controlshiftlabs.com/petitions/featured.json",
"prev": null,
"next": "https://demo.controlshiftlabs.com/petitions/featured.json?page=2",
"last": "https://demo.controlshiftlabs.com/petitions/featured.json?page=3"
}
}
This retrieves a JSON object compliant with the JSON API specification, containing an array of featured petitions objects on its data property.
Important Note: The previous featured petitions API endpoint at https://demo.controlshiftlabs.com/featured.json
(note the missing /petitions
path) is being deprecated and will be removed in the near future. Please use this new endpoint for retrieving featured petitions from now on.
HTTP Request
GET https://demo.controlshiftlabs.com/petitions/featured.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Default | Description |
---|---|---|
locale | null | string - optional - Locale filter for petitions |
Working Example
View and edit a working example on codepen.io:
Search petitions by keyword
JSON response example for the search using the
wizard
keyword:
{
"meta":
{
"total_pages": 1,
"total_entries": 2,
"per_page": 10,
"search_term": "wizard"
},
"data":
[
{
"slug": "let-dorothy-go-home",
"title": "Let Dorothy Go Home!",
"url": "http://demo.controlshiftlabs.com/petitions/let-dorothy-go-home",
"admin_status": "good",
"who": " Oz, the Great and Terrible, Wizard",
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"successful": false,
"ended_story": null,
"ended": false,
"ended_type": null,
"ended_reason": null,
"what": "The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!",
"rich_what": "<div>The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!</div>",
"goal": 500,
"signature_count": 223,
"creator_name": "Kristyn Arrighi",
"locale": "en",
"created_at": "2014-10-02T01:43:17Z",
"updated_at": "2018-05-07T15:38:39Z",
"why": "The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.",
"rich_why": "<div>The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.</div>",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
{
"slug": "stop-saruman-saruman-the-white",
"title": "Stop Saruman",
"url": "http://demo.controlshiftlabs.com/petitions/stop-saruman-saruman-the-white",
"admin_status": "good",
"who": "Saruman The White (Wizard)",
"target":
{
"name": "Saruman The White",
"slug": "saruman-the-white",
"context": "Wizard",
"location":
{
"latitude": "-44.7731846",
"longitude": "168.325246",
"postal_code": "9372",
"country": "NZ",
"region": "Glenorchy",
"locality": "Denver",
"query": "Isengard Lookout",
"street": "Glenorchy-Routeburn Road",
"street_number": null,
"created_at": "2021-07-19T19:55:11Z"
}
},
"successful": true,
"ended_story": "Yay, we defeated Saruman! ",
"ended": true,
"ended_type": "won",
"ended_reason": "Yay, we defeated Saruman! ",
"what": "Stop cutting down the woods",
"rich_what": "<div>Stop cutting down the woods</div>",
"goal": 100,
"signature_count": 8780,
"creator_name": "Treebeard",
"locale": "en",
"created_at": "2016-10-25T18:40:42Z",
"updated_at": "2022-03-31T18:30:36Z",
"why": "Ents want a place to live in peace",
"rich_why": "<div>Ents want a place to live in peace</div>",
"location":
{
"latitude": "-26.3168162",
"longitude": "139.4674535",
"postal_code": "3091",
"country": "AU",
"region": "Victoria",
"locality": "Denver",
"query": "Fangorn forest",
"street": "Yarrambat",
"street_number": "94C7+4R",
"created_at": "2021-07-19T19:55:11Z"
}
}
]
}
This JSON endpoint allows you to build an interface where users can search through all petitions by one or more keywords.
HTTP Request
GET https://demo.controlshiftlabs.com/petitions/search.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Default | Description |
---|---|---|
query | null | string to search for |
page | 1 | integer - optional - The page number of results for the specified search. Minimum of 1. |
Categories
Get list of categories
$(document).ready(function(){
$.ajax({
url: 'https://demo.controlshiftlabs.com/categories.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would display a list of categories in the console. The JSON would be structured like this:
[
{
"category_name": "Education",
"category_count": 18,
"slug": "education-9",
"url": "https://demo.controlshiftlabs.com/categories/education-9.json",
"signature_count": 421,
"locales": {
"es": "Educación"
}
},
{
"category_name": "Environment",
"category_count": 352,
"slug": "environment-6",
"url": "https://demo.controlshiftlabs.com/categories/environment-6.json",
"signature_count": 2606,
"locales": {
"es": "Medio Ambiente"
}
}
]
This retrieves a JSON array of category objects.
HTTP Request
GET https://demo.controlshiftlabs.com/categories.json
- ✓ CORS supported
- ✓ JSONP supported
Working Example
View and edit a working example on codepen.io:
List petitions in a category
$(document).ready(function(){
$.ajax({
url: 'https://demo.controlshiftlabs.com/categories/oz.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would return petitions data from the category with the slug
oz
. The JSON would be structured like this:
{
"current_page": 1,
"total_pages": 25,
"previous_page": null,
"next_page": 2,
"name": "Oz",
"results": [
{
"slug": "let-dorothy-go-home",
"url": "http://demo.controlshiftlabs.com/petitions/let-dorothy-go-home",
"title": "Let Dorothy Go Home!",
"who": " Oz, the Great and Terrible, Wizard",
"what": "Let poor Dorothy Gale go home to Kansas",
"rich_what": "<div>Let poor Dorothy Gale go home to Kansas</div>",
"why": "There's no place like home.",
"rich_why": "<div>There's no place like home.</div>",
"admin_status": "good",
"signature_count": 123,
"goal": 200,
"creator_name": "Tin Man",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2015-12-02T01:43:17Z",
"updated_at": "2017-05-07T15:38:39Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"additional_image_sizes_url": [
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/form/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/horizontal/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/open_graph/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
}
],
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
}
},
{
"slug": "repair-the-yellow-brick-road-1",
"url": "http://demo.controlshiftlabs.com/petitions/repair-the-yellow-brick-road-1",
"title": "Repair the Yellow Brick Road",
"who": " Oz, the Great and Terrible, Wizard",
"what": "The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!",
"rich_what": "<div>The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!</div>",
"why": "The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.",
"rich_why": "<div>The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.</div>",
"admin_status": "good",
"signature_count": 223,
"goal": 500,
"creator_name": "Kristyn Arrighi",
"locale": "en",
"created_at": "2014-10-02T01:43:17Z",
"updated_at": "2018-05-07T15:38:39Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"additional_image_sizes_url": [
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/form/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/horizontal/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/open_graph/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
}
],
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
}
]
}
This retrieves a paginated list of petitions in a category.
HTTP Request
GET https://demo.controlshiftlabs.com/categories/<category slug>.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Default | Description |
---|---|---|
category slug | null | string - required - submitted as a part of the endpoint path, not as a separate URL parameter |
page | 1 | integer - optional - The page number of results for the specified category. Minimum of 1. |
Working Example
View and edit a working example on codepen.io:
Partnerships
List petitions in a partnership
$(document).ready(function(){
$.ajax({
url: 'https://demo.controlshiftlabs.com/partnerships/our-awesome-partner/petitions.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would return petitions data from the partnership with the slug
our-awesome-partner
. The JSON would be structured like this:
{
"current_page": 1,
"total_pages": 25,
"previous_page": null,
"next_page": 2,
"name": "Oz",
"results": [
{
"slug": "let-dorothy-go-home",
"url": "http://demo.controlshiftlabs.com/petitions/let-dorothy-go-home",
"title": "Let Dorothy Go Home!",
"who": " Oz, the Great and Terrible, Wizard",
"what": "Let poor Dorothy Gale go home to Kansas",
"rich_what": "<div>Let poor Dorothy Gale go home to Kansas</div>",
"why": "There's no place like home.",
"rich_why": "<div>There's no place like home.</div>",
"admin_status": "good",
"signature_count": 123,
"goal": 200,
"creator_name": "Tin Man",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2015-12-02T01:43:17Z",
"updated_at": "2017-05-07T15:38:39Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"additional_image_sizes_url": [
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/form/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/horizontal/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/open_graph/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
}
],
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
}
},
{
"slug": "repair-the-yellow-brick-road-1",
"url": "http://demo.controlshiftlabs.com/petitions/repair-the-yellow-brick-road-1",
"title": "Repair the Yellow Brick Road",
"who": " Oz, the Great and Terrible, Wizard",
"what": "The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!",
"rich_what": "<div>The Yellow Brick Road has been neglected; commit to repairing the damaged sections of the road in the next year!</div>",
"why": "The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.",
"rich_why": "<div>The Yellow Brick Road is the main road connecting Munchkin Country to the Emerald City and in its current state it's impassable.</div>",
"admin_status": "good",
"signature_count": 223,
"goal": 500,
"creator_name": "Kristyn Arrighi",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2014-10-02T01:43:17Z",
"updated_at": "2018-05-07T15:38:39Z",
"image_url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/hero/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741",
"additional_image_sizes_url": [
{
"style": "original",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/original/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "form",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/form/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "horizontal",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/horizontal/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
},
{
"style": "open_graph",
"url": "https://d8s293fyljwh4.cloudfront.net/petitions/images/92283/open_graph/2016-06-20-1466458252-1098096-ywllowbrickroad.jpg?1473884741"
}
],
"target": {
"name": " Oz, the Great and Terrible",
"slug": "oz-the-great-and-terrible",
"context": "Wizard",
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"location": {
"query": "Land of Oz, Kensington Avenue, Buffalo, NY, United States",
"latitude": "42.9399637",
"longitude": "-78.8087571",
"street": "Kensington Ave",
"postal_code": "",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
}
]
}
This retrieves a paginated list of petitions in a partnership.
HTTP Request
GET https://demo.controlshiftlabs.com/partnerships/<partnership slug>/petitions.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Default | Description |
---|---|---|
partnership slug | null | string - required - submitted as a part of the endpoint path, not as a separate URL parameter |
page | 1 | integer - optional - The page number of results for the specified partnership. Minimum of 1. |
Working Example
View and edit a working example on codepen.io:
Efforts
Get list of efforts
JSON response example
{
"meta": {
"count": 35,
"page": 1,
"per_page": 10,
"total_pages": 4
},
"data": [
{
"slug": "ranked-choice-voting-everywhere",
"url": "https://demo.controlshiftlabs.com/efforts/ranked-choice-voting-everywhere.json",
"title": {
"en-US": "Ranked Choice Voting Everywhere!",
"es": "Votación por Orden de Preferencia en Todo el Mundo!"
},
"description": "Call on your municipality to implement ranked-choice voting.",
"image_url": "http://example.cloudfront.net/efforts/123/image/voting_maching.png",
"petitions_count": 23,
"signature_count": 9000,
"goal": 10000,
"search_method": "closest",
},
...
]
}
This JSON endpoint returns a paginated list of all Efforts and Landing Pages on the platform. The url
returned for each effort can be used to retrieve information on that effort's petitions, as specified below.
HTTP Request
GET https://demo.controlshiftlabs.com/efforts.json
- ✓ CORS supported
- × JSONP not supported
List petitions in an effort
$(document).ready(function(){
var effortSlug = 'drivers-licenses-for-all';
$.ajax({
url: 'https://demo.controlshiftlabs.com/efforts/'+effortSlug+'.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would return petitions data from the effort with the slug
drivers-licenses-for-all
. The JSON would be structured like this:
{
"title": "Drivers' Licenses for All",
"slug": "drivers-licenses-for-all",
"description": "We're asking towns and villages across New York to pass resolutions in support of the Drivers' License bill. Anyone who can pass a driving test should be able to get a license.",
"goal": 1000,
"signature_count": 511,
"petitions": [
{
"slug": "ossining-support-drivers-licenses-for-all",
"url": "http://demo.controlshiftlabs.com/petitions/ossining-support-drivers-licenses-for-all",
"title": "Ossining: Support Drivers' Licenses For All",
"who": "Ossining Village Board of Trustees",
"what": "Pass a resolution in support of the Drivers' Licenses bill",
"rich_what": "<div>Pass a resolution in support of the Drivers' Licenses bill</div>",
"why": "Undocumented immigrants need access to drivers' licenses so they can drive to work, school, and everywhere else. This will improve road safety for everyone.",
"rich_why": "<div>Undocumented immigrants need access to drivers' licenses so they can drive to work, school, and everywhere else. This will improve road safety for everyone.</div>",
"admin_status": "good",
"signature_count": 223,
"goal": 500,
"creator_name": "Jacinda Moore",
"locale": "en",
"successful": false,
"ended": false,
"created_at": "2016-10-02T01:43:17Z",
"updated_at": "2016-10-07T15:38:39Z",
"target": {
"name": "Ossining Village Board of Trustees",
"slug": "ossining-village-board-of-trustees",
"context": "",
"location": {
"query": "Ossining",
"latitude": "41.1617921",
"longitude": "-73.8874165",
"street": "",
"postal_code": "10562",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"location": {
"query": "Ossining",
"latitude": "41.1617921",
"longitude": "-73.8874165",
"street": "",
"postal_code": "10562",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
{
"slug": "yonkers-support-drivers-licenses-for-all",
"url": "http://demo.controlshiftlabs.com/petitions/yonkers-support-drivers-licenses-for-all",
"title": "Yonkers: Support Drivers' Licenses For All",
"who": "Yonkers City Council",
"what": "Pass a resolution in support of the Drivers' Licenses bill",
"rich_what": "<div>Pass a resolution in support of the Drivers' Licenses bill</div>",
"why": "Undocumented immigrants need access to drivers' licenses so they can drive to work, school, and everywhere else. This will improve road safety for everyone.",
"rich_why": "<div>Undocumented immigrants need access to drivers' licenses so they can drive to work, school, and everywhere else. This will improve road safety for everyone.</div>",
"admin_status": "good",
"signature_count": 300,
"goal": 500,
"creator_name": "Jacinda Moore",
"locale": "en",
"successful": true,
"ended_story": "We did it, we won!",
"ended": true,
"ended_type": "won",
"ended_reason": "Campaigner said they won the campaign",
"created_at": "2016-10-02T01:43:17Z",
"updated_at": "2016-10-07T15:38:39Z",
"target": {
"name": "Yonkers City Council",
"slug": "yonkers-city-council",
"context": "",
"location": {
"query": "Yonkers",
"latitude": "40.9443748",
"longitude": "-73.8993138",
"street": "",
"postal_code": "10701",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
},
"location": {
"query": "Yonkers",
"latitude": "40.9443748",
"longitude": "-73.8993138",
"street": "",
"postal_code": "10701",
"country": "US",
"region": "NY",
"street_number": "",
"created_at": "2016-06-10T19:13:19Z"
}
}
]
}
This retrieves a list of petitions in an effort.
Ended petitions will have the ended
attribute set to true
and will also include the following additional attributes:
ended_type
: with values"won"
,"lost"
and"other"
as set from petition Admin page.ended_reason
: with private reason for ending set from petition Admin page.
Additionally, successful petitions will have the successful
attribute set to true
and will include the following additional attribute:
ended_story
: with ended story from petition Settings / Admin pages.
HTTP Request
GET https://demo.controlshiftlabs.com/efforts/<effort slug>.json
- ✓ CORS supported
- ✓ JSONP supported
Parameter | Default | Description |
---|---|---|
effort slug | null | string - required - submitted as a part of the endpoint path, not as a separate URL parameter |
Working Example
View and edit a working example on codepen.io:
Search petitions in a effort
$(document).ready(function(){
var effortSlug = 'drivers-licenses-for-all';
$.ajax({
url: 'https://demo.controlshiftlabs.com/efforts/'+effortSlug+'/lookup/query.json',
dataType: 'jsonp',
data: {
'location_query': 'briarcliff manor, ny'
}
})
.done(function( data ) {
console.log(data);
});
});
The above code would return petitions, decision makers, or objectives data from the effort with the slug
drivers-licenses-for-all
. If returning decision makers, the JSON would be structured like this:
{
"closest_target": {
"slug": "ossining-village-board-of-trustees",
"name": "Ossining Village Board of Trustees",
"context": "",
"location": "Ossining, NY",
"status": "target_petition_created",
"petition": {
"slug": "ossining-support-drivers-licenses-for-all",
"title": "Ossining: Support Drivers' Licenses For All",
"url": "http://demo.controlshiftlabs.com/petitions/ossining-support-drivers-licenses-for-all",
"who": "Ossining Village Board of Trustees",
"signature_count": 223,
"goal": 500,
"created_at": "2016-10-02T01:43:17Z",
"updated_at": "2016-10-07T15:38:39Z",
}
},
"other_targets": [
{
"slug": "yorktown-town-council",
"name": "Yorktown Town Council",
"context": "",
"location": "Yorktown, NY",
"status": "target_awaiting_petition",
"create_petition_url": "https://demo.controlshiftlabs.com/efforts/drivers-licenses-for-all/petitions/creating?target_id=1234"
}
]
}
This JSON endpoint allows you to build an interface where users can search the petitions in an effort. Depending on how the effort is configured, you can search either by location, or by keyword.
HTTP Request
GET https://demo.controlshiftlabs.com/efforts/<effort slug>/lookup/query.json
- ✓ CORS supported
- ✓ JSONP supported
Query Parameters
Parameter | Required? | Description |
---|---|---|
effort slug | yes | string - submitted as a part of the endpoint path, not as a separate URL parameter |
location_query | yes, if effort is configured for location search | string to search for (will be geocoded on the server) |
query | yes, if effort is configured for keyword search | string to search for |
Working Example
View and edit a working example on codepen.io:
Calendars
Get calendar data
$(document).ready(function(){
$.ajax({
url: 'https://demo.controlshiftlabs.com/calendars/national-day-of-action-against-fracking.json',
dataType: 'jsonp',
})
.done(function(data) {
console.log(data);
});
});
The above code would display a calendar's properties the js console. The JSON would be structured like this:
{
"calendar":{
"name":"National Day of Action Against Fracking",
"description":"This is the description. It will be great.",
"slug":"national-day-of-action-against-fracking",
"image_url":"https://d8s293fyljwh4.cloudfront.net/calendars/images/26/hero/Oak-tree-in-field-007.jpg?1450370489",
"event_count":85,
"attendee_count":19,
"events_upcoming":10
}
}
This retrieves a JSON object representing a calendar.
HTTP Request
GET https://demo.controlshiftlabs.com/calendars/slug.json
Where slug is the slug of the calendar you're retrieving.
- ✓ CORS supported
- ✓ JSONP supported
Local Map
The Local Map API endpoints are primarily used to power the local map and embedded maps, but they can also be called independently, for example if you want to include the data on a different map.
These endpoints include information on events created through ControlShift, events imported into ControlShift from external systems (a.k.a. "external events"), and ControlShift groups.
Events and groups are only included if they are visible to the public. For events created in ControlShift, that means that they must be moderated as Good, not hidden, and not cancelled. External events are always visible to the public. For groups, they must have at least one member or organiser, and not be archived or unlisted.
Get organizing locations: points
Example response
[
{
"lat": 40.7032775,
"lon": -74.0170279,
"type": "Event",
"id": "picnic-in-the-park"
},
{
"lat": 8.2710078,
"lon": 148.0209055,
"type": "Group",
"id": "default-org-mariana-trench"
},
{
"lat": 41.467116,
"lon": -73.9992597,
"type": "ExternalEvent",
"id": "https://example.com/sail-on-the-hudson"
}
]
This JSON endpoint returns a complete list of latitude/longitude coordinates for publicly listed events, groups, and external events in your organisation. It's intended to be used for plotting organizing activities on a map.
HTTP Request
GET https://demo.controlshiftlabs.com/api/local/points.json
- ✓ CORS supported
- × JSONP not supported
Query Parameters
All parameters are optional and default to null (no filter).
Parameter | Description |
---|---|
filter[calendars] | List of calendar slugs. If specified, only events in those calendars will be returned. |
filter[event_types] | List of event type IDs. If specified, only events of those types will be returned. |
filter[geographies] | List of geographic shape IDs. If specified, only events/groups whose locations fall within those geographies will be returned. |
filter[geography_slugs] | Alternative way of filtering by geographic shapes. Works like filter[geographies] , but accepts a list of geography slugs instead of a list of geography IDs. |
filter[labels] | List of label IDs. If specified, only events/groups with at least one of those labels will be returned. |
filter[regions] | List of region IDs. If specified, only events/groups assigned to those regions will be returned. |
filter[start_date_range] | JSON object with a start_date and end_date , each in ISO-8601 string format, representing a range of time. Events will only be returned if their start times are within the range, but the returned groups will not be affected. |
filter[types] | List of types of results to return. Should contain at least one of event or group . |
Get organizing locations: details
Example response
{
"meta": {
"count": 23,
"page": 1,
"per_page": 3,
"total_pages": 8
},
"data": [
{
"type": "Event",
"id": "picnic-in-the-park",
"title": "Picnic in the Park",
"description": "We are having a picnic in the park and you are invited!",
"start_at": "2020-03-01T12:00:00Z"
},
{
"type": "Group",
"id": "default-org-mariana-trench",
"title": "Default Org: Mariana Trench",
"description": "This group has the deepest conversations you will ever hear."
},
{
"type": "ExternalEvent",
"id": "https://example.com/sail-on-the-hudson"
"title": "Sail on the Hudson",
"description": "Go for a sail on the Hudson river to promote environmentalism.",
"start_at": "2019-05-03T13:00:00Z"
}
]
}
This JSON endpoint returns a paginated list of publicly listed events, groups, and external events in your organisation.
It can be used alongside the /api/local/points
endpoint; the criteria for inclusion are the same.
HTTP Request
GET https://demo.controlshiftlabs.com/api/local.json
- ✓ CORS supported
- × JSONP not supported
Query Parameters
All parameters are optional.
Parameter | Default | Description |
---|---|---|
filter[boundary_box] | null | If there is a location filter, bounding box to use for geocoding the location query. |
filter[calendars] | null (no filter) | List of calendar slugs. If specified, only events in those calendars will be returned. |
filter[event_types] | null (no filter) | List of event type IDs. If specified, only events of those types will be returned. |
filter[geographies] | null (no filter) | List of geographic shape IDs. If specified, only events/groups whose locations fall within those geographies will be returned. |
filter[geography_slugs] | null (no filter) | Alternative way of filtering by geographic shapes. Works like filter[geographies] , but accepts a list of geography slugs instead of a list of geography IDs. |
filter[labels] | null (no filter) | List of label IDs. If specified, only events/groups with at least one of those labels will be returned. |
filter[location] | null | Searched location query. Should be a string such as "New York" or "90210". |
filter[regions] | null (no filter) | List of region IDs. If specified, only events/groups assigned to those regions will be returned. |
filter[start_date_range] | JSON object with a start_date and end_date , each in ISO-8601 string format, representing a range of time. Events will only be returned if their start times are within the range, but the returned groups will not be affected. |
|
filter[types] | null (no filter) | List of types of results to return. Should contain at least one of event or group . |
page | 1 | Which page of results to fetch |
per_page | 10 | How many results, maximum, should be included on each page |
search_strategy | distance_from_location | When there is a location filter, determines which results match: distance_from_location finds all points within a certain radius, vs. geography_for_location finds all geographic shapes that contain the searched location |
user_country | null | When there is a location filter, in which country should the location query be interpreted? |
Bulk Data
Bulk data schema
This retrieves information on the underlying schemas of tables exported by the Bulk Data feature. It can be used in your ETL processes to automate the setup of tables in your data warehouse.
If the Compress bulk data exports option is enabled, the extra compression_format
property will be included in the response with the format used for compressing the bulk data export CSVs.
Response format:
{
"tables": [
{
"table": {
"name": "petitions",
"columns": [
"title": {
"type": "string",
"sql_type": "character varying",
"limit": null,
"nullable": true,
"default_value": null
},
"custom_goal": {
"type": "integer",
"sql_type": "integer",
"limit": 4,
"nullable": true,
"default_value": null
},
...
]
}
},
...
],
"settings": {
"compression_format": "BZIP2"
}
}
HTTP Request
GET https://demo.controlshiftlabs.com/api/bulk_data/schema.json
- × CORS not supported
- × JSONP not supported
Bulk data columns
This retrieves information on the tables exported by the Bulk Data API. For convenience when working with third-party data warehouse tools like Amazon Redshift it can be useful to know the current columns for each table that the bulk data API exposes in CSV format.
The response is equivalent to the header row of tables exposed through our Bulk Data API. The table parameter controls which table header row is returned.
Response format:
id,petition_id,email,first_name,last_name,phone_number,postcode,created_at,join_organisation,deleted_at,unsubscribe_at,external_constituent_id,member_id,additional_fields,cached_organisation_slug,source,external_id,new_member,external_action_id,locale,bucket,country,updated_at,user_ip,confirmation_token,confirmed_at,confirmation_sent_at,last_signed_at,join_list_suppressed,old_daisy_chain_used,from_embed,user_agent,confirmed_reason,synced_to_crm_at,daisy_chain_experiment_slug,eu_data_processing_consent,from_one_click,consent_content_version_id,daisy_chain_id_used,email_opt_in_type_id,facebook_id,utm_params,postcode_id,referring_share_click_id,opt_in_sms,sms_opt_in_type_id,recaptcha_score,new_mobile_subscriber,partnership_opt_ins
HTTP Request
GET https://demo.controlshiftlabs.com/api/bulk_data/schema/columns?table=signatures
- × CORS not supported
- × JSONP not supported
Query Parameters
Parameter | Description |
---|---|
table | Which table from the bulk data schema endpoint should be returned |
Webhook Endpoints
Webhooks can be used by software engineers to integrate ControlShift with third-party systems. They allow engineers to build software that is triggered by events that take place within ControlShift. ControlShift executes HTTPS callbacks when certain events happen (e.g. a petition is launched; a category is changed).
To begin using webhooks, visit the admin for your ControlShift Platform and choose Settings > Integrations > Webhooks. To begin sending data, you'll need to add a "New Webhook Endpoint." Each Webhook Endpoint URL you specify will receive a stream of all the hooks that occur within your ControlShift account.
If you need additional information about webhooks or how to use them, please send us a support email support@controlshiftlabs.com.
What is a Webhook?
Webhooks are HTTPS callbacks sent to a URL called an endpoint. Each time something happens in ControlShift an HTTPS POST request is made to the endpoints you've defined.
Many platforms like Twilio, Github and Slack also use Webhooks.
To handle a webhook you'll need a web application that can accept the HTTPS requests. If you already have a web application set up, handling a webhook is usually as easy as adding a new URL to your application. If not you'll have to write a new web application. Most programming languages have frameworks that can make this easy. Webhooks can also be handled by serverless functions like AWS Lambda or Azure Functions.
Webhooks are just HTTP requests. Anything that can process requests from the web can process webhooks, if the application behind it is programmed properly. Webhooks are just a fancy name for using HTTP requests for event based integration between apps.
Retries
For webhook notifications successfully processed the response should have an HTTP 2XX status code. Responses returning a different status code will be marked as failed and automatically retried up to 9 times with an exponential backoff (e.g.: after 15s, 30s, 90s, etc.).
If after 9 retries the notification cannot be delivered, the webhook endpoint will be automatically disabled and will need to be re-enabled from the admin page to start receiving webhook notifications again. We do this to protect our infrastructure and ability to deliver webhooks successfully.
We recommend making sure your webhook endpoint only returns non-2XX statuses if your service is temporarily down or experiencng a transient error. Applications that consistently return an error status when they recieve particular webhooks will inevitably end up disabled once retries are exhausted.
Security
Securing access to your endpoints where webhooks are sent is important. We recommend taking a variety of steps.
- Using HTTPS with modern cypher suite.
- Restricting access by IP address
- Using basic HTTP authentication or an API token/secret in the URL.
- Verifying the signature hash of the message payload we send to authenticate ControlShift as the source of the webhook.
Webhook HMAC Authenticity Signing
We send a special HTTP header along with webhook deliveries that allows customers to verify that the body of the webhook being delivered originated from within your ControlShift account. We use HMAC and SHA 256 along with a secret generated for each endpoint
Using HMAC Authenticity signing is optional, but is a security best practice. Otherwise someone else could send requests to your webhook endpoint that impersonate ControlShift. HMAC signing guarantees that the webhooks you receive are authentic.
You can retrieve the secret or generate a new one via Settings > Integrations > Webhooks within your ControlShift platform admin.
Header | Value |
---|---|
x-controlshift-webhook-signature |
sha256:abc123hjkl6789 |
The value is the prefix sha256:
specifying the algorithm in use combined with the HMAC digest of the webhook HTTP
request body signed with the signing secret specified for your endpoint in the ControlShift admin tools.
To verify the webhooks you receive are authentic, users should calculate the same digest value and compare it to the digest
passed in the x-controlshift-webhook-signature
header. These values should always be the same.
In a Ruby on Rails application, the code for verifying the HMAC signature while processing the webhook in your code might use an implementation like:
your_hmac_secret = 'abc123' # store this securely!
secure_hash = "sha256:#{OpenSSL::HMAC.hexdigest('SHA256', your_hmac_secret, request.raw_post)}"
if secure_hash != request.headers['x-controlshift-webhook-signature']
raise "Incorrect HMAC signature header"
end
IP Addresses
Webhooks will be delivered from a stable set of the IP addresses that will not change. It is a good idea to safe-list these addresses and block webhooks originating from unknown IP addresses. The IP addresses vary depending on where your Controlshift instance is hosted. If you don't know which data center your organization is hosted in, you can check with us at support@controlshiftlabs.com.
North American data center:
- 54.144.65.46
- 54.208.56.53
European data center:
- 3.67.205.229
- 18.195.87.158
Direct AWS Access
The Bulk Data API notifies customers about new data files via webhook. In addition to allowing access via signed HTTPS request, the Bulk Data API allows direct access to these files from within the same region where your ControlShift account resides (usually us-east-1). Direct AWS Access allows for high-performance access to files on AWS S3 without leaving the AWS Cloud for customers with a point of presence in the same AWS Region. Contact support if you would like to make use of this advanced feature.
Designing a data sync
The most common use case for the Webhooks API is synchronizing ControlShift to an organization's CRM of record when an out-of the-box CRM integration is not available as part of the ControlShift product.
While designing a CRM integration sync, there are a few specific webhook events that a minimalist integration should respond to as a starting point depending on the ControlShift features that are in use.
Webhook | Description |
---|---|
signature.created |
a user signed a petition |
attendee.created |
a user signed up to attend an event |
local_chapter.member.created |
a user joined a local chapter |
unsubscribe.created |
a user unsubscribed and no longer wishes to receive email |
Responding to these events is generally sufficient to keep an external mailing list up to date so that staff do not need to shuffle CSVs back and forth.
Webhook types summary
You can configure ControlShift Labs Webhooks to return the following event types:
Type | Description |
---|---|
blast_email.created | A new blast email is ready for moderation |
category.created | A new category is created |
category.updated | A category is changed |
category.deleted | A category has been deleted and removed from the organisation |
data.full_table_exported | A full-table dump is ready for retrieval on S3. |
data.incremental_table_exported | An incremental CSV update is ready for retrieval on S3. |
email.open | Link in an email has been clicked |
email.click | Email has been opened |
event.created | A new event is published |
event.created.requires_moderation | A new event requires moderation |
event.member_email_reply_received | A member replied to an event email |
event.mentor_assigned | Mentor was assigned to the event |
event.mentor_removed | Mentor was removed from the event |
event.updated | An event is updated |
attendee.created | Member RSVP to event |
attendee.confirmed | Member confirmed RSVP and email address. This event is only emitted for organisations who have email confirmation turned on. This event is only emitted for explicit attendee confirmations, not implicit confirmations eg: for logged in users. |
attendee.updated | Member updates RSVP to event |
attendee.deleted | Attendee permanently deleted. This is currently only triggered when a member deletes its account. |
flag.created | Someone flags a problem for admin attention |
label.applied | A label is applied to a campaign, event, group, or member |
label.removed | A label is removed from a campaign, event, group, or member |
local_chapter.created | A local group has been created. |
local_chapter.last_organiser.deleted | A local chapter's last organiser leaves the group |
local_chapter.member_email_reply_received | A member replied to a group email |
local_chapter.mentor_assigned | Mentor was assigned to the local chapter |
local_chapter.mentor_removed | Mentor was removed from the local chapter |
local_chapter.organiser_request.created | A user applies to be a local chapter organiser |
local_chapter.organiser_request.approved | An admin approves a user's application to be a local chapter organiser |
local_chapter.organiser.created | An organiser is added to a local chapter |
local_chapter.member.created | A member joins a local chapter |
local_chapter.member.deleted | A member leaves or is removed from a local chapter |
member.deleted | A member is deleted |
member.deleted.resources_transferred | Resources previously owned by a deleted member have been transferred to another |
petition.ended | A petition is marked as won, lost, or ended for another reason |
petition.flagged | A petition is flagged for the first or fifth time |
petition.inappropriate.creator_message | An inappropriate petition's creator writes a message to admins |
petition.member_email_reply_received | A member replied to a petition email |
petition.mentor_assigned | Mentor was assigned to the petition |
petition.mentor_removed | Mentor was removed from the petition |
petition.launched | A new petition is launched |
petition.launched.ham | A petition passes the post-launch spam check |
petition.launched.requires_moderation | A new petition requires moderation |
petition.reactivated | A hidden or ended petition is reactivated |
petition.start.help_requested | A member has requested help while starting a petition |
petition.updated | A petition is updated |
petition.updated.requires_moderation | An updated petition requires moderation |
petition.target.response | Decision maker responded to a petition notification |
user.created | New user created. |
user.confirmed | User has confirmed its new account. This event is only emitted for organisations who have user confirmation turned on. |
user.changed | A user has changed their saved first_name, last_name, postcode, locale or email opt-in type. |
user.updated | A user is updated. |
signature.created | Member signs a petition |
signature.updated | Existing signature updated. This can happen when member unsubscribes or reasserts their opt in for receiving emails |
signature.deleted | Signature permanently deleted. Signature deletions are distinct from unsubscribes and should be treated as if the signature never happened |
signature.confirmed | |
unsubscribe.created | Member unsubscribes. This notification is emitted when member: is globally unsubscribed from the organisation by an admin or through the API, unsubscribes from partnership or unsubscribes from a petition. |
locale.created | New i18n instance created |
forum.message.requires_moderation | A new message requires moderation |
blast_email.created
Example payload for
blast_email.created
:
{
"type": "blast_email.created",
"data": {
"id": 100,
"from_name": "Jane Doe",
"from_address": "jane@example.com",
"subject": "Please help spread the word",
"recipient_count": 990,
"admin_status": "unreviewed",
"admin_reason": null,
"administered_at": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A new blast email is ready for moderation
category.created
Example payload for
category.created
:
{
"type": "category.created",
"data": {
"id": 22,
"name": "Environment",
"slug": "environment",
"external_id": null,
"created_at": "2015-08-03T12:38:46Z",
"updated_at": "2015-08-03T12:38:46Z"
},
"jid": "df67126973794c2efde76cc4"
}
A new category is created
category.updated
Example payload for
category.updated
:
{
"type": "category.updated",
"data": {
"id": 22,
"name": "Environment",
"slug": "environment",
"external_id": null,
"created_at": "2015-08-03T12:38:46Z",
"updated_at": "2015-08-03T12:38:46Z"
},
"jid": "df67126973794c2efde76cc4"
}
A category is changed
category.deleted
Example payload for
category.deleted
:
{
"type": "category.deleted",
"data": {
"id": 22,
"name": "Environment",
"slug": "environment",
"external_id": null,
"created_at": "2015-08-03T12:38:46Z",
"updated_at": "2015-08-03T12:38:46Z"
},
"jid": "df67126973794c2efde76cc4"
}
A category has been deleted and removed from the organisation
data.full_table_exported
Example payload for
data.full_table_exported
:
{
"type": "data.full_table_exported",
"data": {
"url": "https://agra-data-exports.s3.amazonaws.com/default/petitions_20150803180434.csv?AWSAccessKeyId=XXX&Expires=1438711490&Signature=ABCDE%3D",
"table": "petitions",
"s3": {
"bucket": "agra-data-exports",
"key": "/default/petitions_20150803180434.csv"
},
"compression_format": "bzip2"
},
"jid": "cf0f50d7d225e20a188be328"
}
A full-table dump is ready for retrieval on S3.
Note
URL is encoded using (7-bit) ASCII and some characters will be escaped (i.e.: '&', '<', '>', etc.). See https://www.rubydoc.info/gems/activesupport/ActiveSupport/JSON/Encoding for more details
The compression_format
field will either be bzip2
or none
depending whether data exports compression is enabled or not.
If the AWS Account ID value is set for any of the webhook endpoints, an extra s3
object attribute is included with bucket
and key
attributes for referencing the S3 object directly.
data.incremental_table_exported
Example payload for
data.incremental_table_exported
:
{
"type": "data.incremental_table_exported",
"data": {
"url": "https://agra-data-exports.s3.amazonaws.com/default/petitions_20150803180434.csv?AWSAccessKeyId=XXX&Expires=1438711490&Signature=ABCDE%3D",
"table": "petitions",
"s3": {
"bucket": "agra-data-exports",
"key": "/default/petitions_20150803180434.csv"
},
"compression_format": "bzip2"
},
"jid": "cf0f50d7d225e20a188be328"
}
An incremental CSV update is ready for retrieval on S3.
Note
URL is encoded using (7-bit) ASCII and some characters will be escaped (i.e.: '&', '<', '>', etc.). See https://www.rubydoc.info/gems/activesupport/ActiveSupport/JSON/Encoding for more details
The compression_format
field will either be bzip2
or none
depending whether data exports compression is enabled or not.
If the AWS Account ID value is set for any of the webhook endpoints, an extra s3
object attribute is included with bucket
and key
attributes for referencing the S3 object directly.
email.open
Example payload for
email.open
:
{
"type": "email.open",
"data": {
"email": "example@test.com",
"timestamp": 1513299569,
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
"event": "open",
"category": "cat facts",
"sg_event_id": "sg_event_id",
"sg_message_id": "sg_message_id",
"useragent": "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"ip": "255.255.255.255"
},
"jid": "df67126973794c2efde76cc4"
}
Link in an email has been clicked
email.click
Example payload for
email.click
:
{
"type": "email.click",
"data": {
"email": "example@test.com",
"timestamp": 1513299569,
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
"event": "click",
"category": "cat facts",
"sg_event_id": "sg_event_id",
"sg_message_id": "sg_message_id",
"useragent": "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"ip": "255.255.255.255",
"url": "http://www.controlshift.app/"
},
"jid": "df67126973794c2efde76cc4"
}
Email has been opened
event.created
Example payload for
event.created
:
{
"type": "event.created",
"data": {
"slug": "petition-hand-in",
"title": "Petition Hand In",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T22:00:00Z",
"virtual": false,
"location": {
"latitude": "40.7638667",
"longitude": "-73.8098213",
"postal_code": "11354",
"country": "US",
"region": "NY",
"locality": null,
"query": "155-06 Roosevelt Ave, Flushing, NY 11354, États-Unis",
"street": "Roosevelt Avenue",
"street_number": "155-06",
"created_at": "2021-07-23T19:09:41Z"
},
"location_venue": "Queens Public Library at McGoldrick",
"unconfirmed_location": false,
"host": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"url": "https://demo.controlshiftlabs.com/events/petition-hand-in",
"admin_status": "unreviewed",
"administered_at": null,
"admin_reason": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z",
"shifts": [
{
"id": 7,
"name": "lunch",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T20:00:00Z"
},
{
"id": 8,
"name": "debrief",
"start_at": "2021-07-29T20:00:00Z",
"end_at": "2021-07-29T22:00:00Z"
}
],
"image_url": "https://d34smfggpfnvat.cloudfront.net/events/images/2656/hero/_uploads_7db6d361-d3a4-4ba3-8d82-7ec3d432eea6_gettyimages-469869876.jpg20210705-641506-dcm2y0.jpg?1625504128",
"petition": {
"slug": "brighter-lights-for-library"
},
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A new event is published
event.created.requires_moderation
Example payload for
event.created.requires_moderation
:
{
"type": "event.created.requires_moderation",
"data": {
"slug": "petition-hand-in",
"title": "Petition Hand In",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T22:00:00Z",
"virtual": false,
"location": {
"latitude": "40.7638667",
"longitude": "-73.8098213",
"postal_code": "11354",
"country": "US",
"region": "NY",
"locality": null,
"query": "155-06 Roosevelt Ave, Flushing, NY 11354, États-Unis",
"street": "Roosevelt Avenue",
"street_number": "155-06",
"created_at": "2021-07-23T19:09:41Z"
},
"location_venue": "Queens Public Library at McGoldrick",
"unconfirmed_location": false,
"host": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"url": "https://demo.controlshiftlabs.com/events/petition-hand-in",
"admin_status": "unreviewed",
"administered_at": null,
"admin_reason": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z",
"shifts": [
{
"id": 7,
"name": "lunch",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T20:00:00Z"
},
{
"id": 8,
"name": "debrief",
"start_at": "2021-07-29T20:00:00Z",
"end_at": "2021-07-29T22:00:00Z"
}
],
"image_url": "https://d34smfggpfnvat.cloudfront.net/events/images/2656/hero/_uploads_7db6d361-d3a4-4ba3-8d82-7ec3d432eea6_gettyimages-469869876.jpg20210705-641506-dcm2y0.jpg?1625504128",
"petition": {
"slug": "brighter-lights-for-library"
},
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A new event requires moderation
event.member_email_reply_received
Example payload for
event.member_email_reply_received
:
{
"type": "event.member_email_reply_received",
"data": {
"id": 359,
"campaign_type": "Event",
"campaign_id": 8268,
"replied_email_type": "blast_email",
"status": "verified_pending_delivery",
"from": "Jane Doe <jane@example.com>",
"to": "my-organization+reply-abcde123456@cslemails.com",
"subject": "Re: We made it!",
"body_plain": "Hi Jane, I'm so glad we made it! I can't wait to see you at the event. Best, John",
"body_html": "<p>Hi Jane, I'm so glad we made it! I can't wait to see you at the event.</p><p>Best, John</p>",
"headers": {
"Message-ID": "<abcde-098765>"
},
"blast_email_id": 87,
"member_id": 1234,
"reject_reasons": null,
"created_at": "2024-09-12T15:00:00Z",
"updated_at": "2024-09-12T16:30:00Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member replied to an event email
event.mentor_assigned
Example payload for
event.mentor_assigned
:
{
"type": "event.mentor_assigned",
"data": {
"slug": "petition-hand-in",
"title": "Petition Hand In",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T22:00:00Z",
"virtual": false,
"location": {
"latitude": "40.7638667",
"longitude": "-73.8098213",
"postal_code": "11354",
"country": "US",
"region": "NY",
"locality": null,
"query": "155-06 Roosevelt Ave, Flushing, NY 11354, États-Unis",
"street": "Roosevelt Avenue",
"street_number": "155-06",
"created_at": "2021-07-23T19:09:41Z"
},
"location_venue": "Queens Public Library at McGoldrick",
"unconfirmed_location": false,
"host": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"url": "https://demo.controlshiftlabs.com/events/petition-hand-in",
"admin_status": "unreviewed",
"administered_at": null,
"admin_reason": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z",
"shifts": [
{
"id": 7,
"name": "lunch",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T20:00:00Z"
},
{
"id": 8,
"name": "debrief",
"start_at": "2021-07-29T20:00:00Z",
"end_at": "2021-07-29T22:00:00Z"
}
],
"image_url": "https://d34smfggpfnvat.cloudfront.net/events/images/2656/hero/_uploads_7db6d361-d3a4-4ba3-8d82-7ec3d432eea6_gettyimages-469869876.jpg20210705-641506-dcm2y0.jpg?1625504128",
"petition": {
"slug": "brighter-lights-for-library"
},
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Mentor was assigned to the event
event.mentor_removed
Example payload for
event.mentor_removed
:
{
"type": "event.mentor_removed",
"data": {
"slug": "petition-hand-in",
"title": "Petition Hand In",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T22:00:00Z",
"virtual": false,
"location": {
"latitude": "40.7638667",
"longitude": "-73.8098213",
"postal_code": "11354",
"country": "US",
"region": "NY",
"locality": null,
"query": "155-06 Roosevelt Ave, Flushing, NY 11354, États-Unis",
"street": "Roosevelt Avenue",
"street_number": "155-06",
"created_at": "2021-07-23T19:09:41Z"
},
"location_venue": "Queens Public Library at McGoldrick",
"unconfirmed_location": false,
"host": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"url": "https://demo.controlshiftlabs.com/events/petition-hand-in",
"admin_status": "unreviewed",
"administered_at": null,
"admin_reason": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z",
"shifts": [
{
"id": 7,
"name": "lunch",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T20:00:00Z"
},
{
"id": 8,
"name": "debrief",
"start_at": "2021-07-29T20:00:00Z",
"end_at": "2021-07-29T22:00:00Z"
}
],
"image_url": "https://d34smfggpfnvat.cloudfront.net/events/images/2656/hero/_uploads_7db6d361-d3a4-4ba3-8d82-7ec3d432eea6_gettyimages-469869876.jpg20210705-641506-dcm2y0.jpg?1625504128",
"petition": {
"slug": "brighter-lights-for-library"
},
"mentor": null
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Mentor was removed from the event
event.updated
Example payload for
event.updated
:
{
"type": "event.updated",
"data": {
"slug": "petition-hand-in",
"title": "Petition Hand In",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T22:00:00Z",
"virtual": false,
"location": {
"latitude": "40.7638667",
"longitude": "-73.8098213",
"postal_code": "11354",
"country": "US",
"region": "NY",
"locality": null,
"query": "155-06 Roosevelt Ave, Flushing, NY 11354, États-Unis",
"street": "Roosevelt Avenue",
"street_number": "155-06",
"created_at": "2021-07-23T19:09:41Z"
},
"location_venue": "Queens Public Library at McGoldrick",
"unconfirmed_location": false,
"host": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"url": "https://demo.controlshiftlabs.com/events/petition-hand-in",
"admin_status": "good",
"administered_at": "2021-07-26T17:32:15Z",
"admin_reason": null,
"created_at": "2021-07-25T13:11:02Z",
"updated_at": "2021-07-26T17:32:15Z",
"shifts": [
{
"id": 7,
"name": "lunch",
"start_at": "2021-07-29T18:00:00Z",
"end_at": "2021-07-29T20:00:00Z"
},
{
"id": 8,
"name": "debrief",
"start_at": "2021-07-29T20:00:00Z",
"end_at": "2021-07-29T22:00:00Z"
}
],
"image_url": "https://d34smfggpfnvat.cloudfront.net/events/images/2656/hero/_uploads_7db6d361-d3a4-4ba3-8d82-7ec3d432eea6_gettyimages-469869876.jpg20210705-641506-dcm2y0.jpg?1625504128",
"petition": {
"slug": "brighter-lights-for-library"
},
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
An event is updated
attendee.created
Example payload for
attendee.created
:
{
"type": "attendee.created",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"notification_level": "all_messages",
"attending_status": "attending",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"event": {
"url": "http://localhost/events/meetup-at-park",
"slug": "meetup-at-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"unsubscribe_at": null,
"confirmed_at": null,
"confirmed_reason": null,
"source": "facebook",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Member RSVP to event
attendee.confirmed
Example payload for
attendee.confirmed
:
{
"type": "attendee.confirmed",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"notification_level": "all_messages",
"attending_status": "attending",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"event": {
"url": "http://localhost/events/meetup-at-park",
"slug": "meetup-at-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"unsubscribe_at": null,
"confirmed_at": "2015-08-04T09:01:23Z",
"confirmed_reason": "email_confirmation",
"source": "facebook",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Member confirmed RSVP and email address. This event is only emitted for organisations who have email confirmation turned on. This event is only emitted for explicit attendee confirmations, not implicit confirmations eg: for logged in users.
attendee.updated
Example payload for
attendee.updated
:
{
"type": "attendee.updated",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"notification_level": "all_messages",
"attending_status": "attending",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"event": {
"url": "http://localhost/events/meetup-at-park",
"slug": "meetup-at-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"unsubscribe_at": null,
"confirmed_at": null,
"confirmed_reason": null,
"source": "facebook",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Member updates RSVP to event
attendee.deleted
Example payload for
attendee.deleted
:
{
"type": "attendee.deleted",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"notification_level": "all_messages",
"attending_status": "attending",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"event": {
"url": "http://localhost/events/meetup-at-park",
"slug": "meetup-at-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"unsubscribe_at": null,
"confirmed_at": null,
"confirmed_reason": null,
"source": "facebook",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Attendee permanently deleted. This is currently only triggered when a member deletes its account.
flag.created
Example payload for
flag.created
:
{
"type": "flag.created",
"data": {
"id": 7,
"flagged": {
"type": "ForumMessage",
"id": 224
},
"flagged_by": {
"email": "bobby@example.com",
"name": "Bobby Singer"
},
"reason": "This violates our community commitments",
"admin_status": "unreviewed",
"administered_at": null,
"created_at": "2015-08-05T13:17:46Z",
"updated_at": "2015-08-05T13:17:46Z"
},
"jid": "f848f4b38b9125f7089d59ad"
}
Someone flags a problem for admin attention
For example, a local chapter organiser flags a forum message.
label.applied
Example payload for
label.applied
:
A label is applied to a campaign, event, group, or member
label.removed
Example payload for
label.removed
:
A label is removed from a campaign, event, group, or member
local_chapter.created
Example payload for
local_chapter.created
:
{
"type": "local_chapter.created",
"data": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A local group has been created.
local_chapter.last_organiser.deleted
Example payload for
local_chapter.last_organiser.deleted
:
{
"type": "local_chapter.last_organiser.deleted",
"data": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A local chapter's last organiser leaves the group
local_chapter.member_email_reply_received
Example payload for
local_chapter.member_email_reply_received
:
{
"type": "local_chapter.member_email_reply_received",
"data": {
"id": 359,
"campaign_type": "Event",
"campaign_id": 8268,
"replied_email_type": "blast_email",
"status": "verified_pending_delivery",
"from": "Jane Doe <jane@example.com>",
"to": "my-organization+reply-abcde123456@cslemails.com",
"subject": "Re: We made it!",
"body_plain": "Hi Jane, I'm so glad we made it! I can't wait to see you at the event. Best, John",
"body_html": "<p>Hi Jane, I'm so glad we made it! I can't wait to see you at the event.</p><p>Best, John</p>",
"headers": {
"Message-ID": "<abcde-098765>"
},
"blast_email_id": 87,
"member_id": 1234,
"reject_reasons": null,
"created_at": "2024-09-12T15:00:00Z",
"updated_at": "2024-09-12T16:30:00Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member replied to a group email
local_chapter.mentor_assigned
Example payload for
local_chapter.mentor_assigned
:
{
"type": "local_chapter.mentor_assigned",
"data": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Mentor was assigned to the local chapter
local_chapter.mentor_removed
Example payload for
local_chapter.mentor_removed
:
{
"type": "local_chapter.mentor_removed",
"data": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members",
"mentor": null
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Mentor was removed from the local chapter
local_chapter.organiser_request.created
Example payload for
local_chapter.organiser_request.created
:
{
"type": "local_chapter.organiser_request.created",
"data": {
"id": 1234,
"new_organiser_question_responses": {
},
"created_at": "2015-08-03T12:38:46Z",
"admin_status": "unreviewed",
"administered_at": null,
"admin_reason": null,
"group": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members"
},
"user": {
"id": 505,
"email": "jane@example.com",
"full_name": "Jane Goodall",
"first_name": "Jane",
"last_name": "Goodall",
"phone_number": "12345-1",
"postcode": "34058-2356"
},
"reviewer": null,
"mentor": null,
"reason": "I want to be part of this amazing group"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A user applies to be a local chapter organiser
local_chapter.organiser_request.approved
Example payload for
local_chapter.organiser_request.approved
:
{
"type": "local_chapter.organiser_request.approved",
"data": {
"id": 1234,
"new_organiser_question_responses": {
},
"created_at": "2015-08-03T12:38:46Z",
"admin_status": "approved",
"administered_at": "2015-08-04T08:15:46Z",
"admin_reason": null,
"group": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members"
},
"user": {
"id": 505,
"email": "jane@example.com",
"full_name": "Jane Goodall",
"first_name": "Jane",
"last_name": "Goodall",
"phone_number": "12345-1",
"postcode": "34058-2356"
},
"reviewer": null,
"mentor": null,
"reason": "I want to be part of this amazing group"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
An admin approves a user's application to be a local chapter organiser
local_chapter.organiser.created
Example payload for
local_chapter.organiser.created
:
{
"type": "local_chapter.organiser.created",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"phone_number": null,
"postcode": "23456",
"country": "US",
"introduction": "I want to save the world!",
"notification_level": "all_messages",
"local_chapter": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members"
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
An organiser is added to a local chapter
local_chapter.member.created
Example payload for
local_chapter.member.created
:
{
"type": "local_chapter.member.created",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"phone_number": null,
"postcode": "23456",
"country": "US",
"introduction": "I want to save the world!",
"notification_level": "all_messages",
"local_chapter": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members"
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member joins a local chapter
local_chapter.member.deleted
Example payload for
local_chapter.member.deleted
:
{
"type": "local_chapter.member.deleted",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"phone_number": null,
"postcode": "23456",
"country": "US",
"introduction": "I want to save the world!",
"notification_level": "all_messages",
"local_chapter": {
"slug": "springfield-members",
"name": "Springfield Members",
"url": "https://demo.controlshiftlabs.com/local_chapters/springfield-members"
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member leaves or is removed from a local chapter
member.deleted
Example payload for
member.deleted
:
{
"type": "member.deleted",
"data": {
"member": {
"id": 100,
"email": "jane@example.com"
},
"resources": {
"new_owner": {
"id": 888,
"email": "richard@example.com",
"full_name": "Richard Chamberlain",
"first_name": "Richard",
"last_name": "Chamberlain",
"phone_number": "12345-1",
"postcode": "34058-2356"
}
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member is deleted
member.deleted.resources_transferred
Example payload for
member.deleted.resources_transferred
:
{
"type": "member.deleted.resources_transferred",
"data": {
"member": {
"id": 100,
"email": "jane@example.com"
},
"resources": {
"new_owner": {
"id": 888,
"email": "richard@example.com",
"full_name": "Richard Chamberlain",
"first_name": "Richard",
"last_name": "Chamberlain",
"phone_number": "12345-1",
"postcode": "34058-2356"
}
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
Resources previously owned by a deleted member have been transferred to another
petition.ended
Example payload for
petition.ended
:
{
"type": "petition.ended",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A petition is marked as won, lost, or ended for another reason
petition.flagged
Example payload for
petition.flagged
:
{
"type": "petition.flagged",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A petition is flagged for the first or fifth time
petition.inappropriate.creator_message
Example payload for
petition.inappropriate.creator_message
:
{
"type": "petition.inappropriate.creator_message",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
An inappropriate petition's creator writes a message to admins
petition.member_email_reply_received
Example payload for
petition.member_email_reply_received
:
{
"type": "petition.member_email_reply_received",
"data": {
"id": 359,
"campaign_type": "Event",
"campaign_id": 8268,
"replied_email_type": "blast_email",
"status": "verified_pending_delivery",
"from": "Jane Doe <jane@example.com>",
"to": "my-organization+reply-abcde123456@cslemails.com",
"subject": "Re: We made it!",
"body_plain": "Hi Jane, I'm so glad we made it! I can't wait to see you at the event. Best, John",
"body_html": "<p>Hi Jane, I'm so glad we made it! I can't wait to see you at the event.</p><p>Best, John</p>",
"headers": {
"Message-ID": "<abcde-098765>"
},
"blast_email_id": 87,
"member_id": 1234,
"reject_reasons": null,
"created_at": "2024-09-12T15:00:00Z",
"updated_at": "2024-09-12T16:30:00Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A member replied to a petition email
petition.mentor_assigned
Example payload for
petition.mentor_assigned
:
{
"type": "petition.mentor_assigned",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
Mentor was assigned to the petition
petition.mentor_removed
Example payload for
petition.mentor_removed
:
{
"type": "petition.mentor_removed",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": null
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
Mentor was removed from the petition
petition.launched
Example payload for
petition.launched
:
{
"type": "petition.launched",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A new petition is launched
petition.launched.ham
Example payload for
petition.launched.ham
:
{
"type": "petition.launched.ham",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A petition passes the post-launch spam check
petition.launched.requires_moderation
Example payload for
petition.launched.requires_moderation
:
{
"type": "petition.launched.requires_moderation",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A new petition requires moderation
petition.reactivated
Example payload for
petition.reactivated
:
{
"type": "petition.reactivated",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A hidden or ended petition is reactivated
petition.start.help_requested
Example payload for
petition.start.help_requested
:
{
"type": "petition.start.help_requested",
"data": {
"created_at": "2021-06-10T15:01:22Z",
"current_step": "who",
"email": "alice@example.com",
"help_requested_at": "2021-06-10T15:05:15Z",
"locale": "en",
"title": "Stop the Pipeline",
"token": "YAyhN3juH8Y2gZCgvgimJnQqyS1dXnkzQht5DWNXb_07cfydfwIH7w",
"updated_at": "2021-06-10T15:05:15Z",
"what": null,
"who": null,
"why": null
},
"jid": "1ebf2eb10c3d8939f42dff68"
}
A member has requested help while starting a petition
petition.updated
Example payload for
petition.updated
:
{
"type": "petition.updated",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
A petition is updated
petition.updated.requires_moderation
Example payload for
petition.updated.requires_moderation
:
{
"type": "petition.updated.requires_moderation",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
An updated petition requires moderation
petition.target.response
Example payload for
petition.target.response
:
{
"type": "petition.target.response",
"data": {
"slug": "don-t-destroy-our-park",
"title": "Don't destroy our park!",
"url": "https://demo.controlshiftlabs.com/petitions/don-t-destroy-our-park",
"admin_status": "good",
"admin_reason": null,
"administered_at": "2015-08-03T13:12:15Z",
"image_url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/hero/imag0945.jpg?1438621934",
"additional_image_sizes_url": [
{
"style": "form",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/form/imag0945.jpg?1438621934"
},
{
"style": "large",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/large/imag0945.jpg?1438621934"
},
{
"style": "open_graph",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/open_graph/imag0945.jpg?1438621934"
},
{
"style": "original",
"url": "https://d34smfggpfnvat.cloudfront.net/petitions/images/788/original/imag0945.jpg?1438621934"
}
],
"who": "Anytown City Council",
"what": "The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.",
"rich_what": "<p>The park on Main Street is an important community space. Don't approve the proposal to sell it to office building developers.</p>",
"goal": 100,
"signature_count": 0,
"creator": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"locale": "en-US",
"created_at": "2015-08-03T13:11:02Z",
"updated_at": "2015-08-03T13:12:15Z",
"why": "Studies have shown that green space is important to child development and community well-being.",
"rich_why": "<p>Studies have shown that green space is important to child development and community well-being.</p>",
"ended": true,
"ended_type": "won",
"ended_reason": "We won the campaign! The Main Street park will be protected for years to come.",
"successful": true,
"ended_story": "We won! Thanks to your support, the city council has agreed to protect the Main Street park from development.",
"mentor": {
"full_name": "Richard Rodriguez",
"email": "richard@dogooders.org"
}
},
"jid": "9fccc1957d7d7ebc93fb0f05"
}
Decision maker responded to a petition notification
user.created
Example payload for
user.created
:
{
"type": "user.created",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
New user created.
user.confirmed
Example payload for
user.confirmed
:
{
"type": "user.confirmed",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
User has confirmed its new account. This event is only emitted for organisations who have user confirmation turned on.
user.changed
Example payload for
user.changed
:
{
"type": "user.changed",
"data": {
"id": 180,
"email": "foo@bar.com",
"changes": {
"first_name": [
"Nate",
"Nathan"
],
"email_opt_in_type": [
{
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "",
"active": true
},
{
"context": "web_form",
"kind": "unchecked_checkbox",
"mailable": true,
"external_id": "",
"active": true
}
]
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A user has changed their saved first_name, last_name, postcode, locale or email opt-in type.
user.updated
Example payload for
user.updated
:
{
"type": "user.updated",
"data": {
"id": 180,
"email": "jenny@example.com",
"full_name": "Jennifer Goines",
"first_name": "Jennifer",
"last_name": "Goines",
"locale": "en",
"opt_in_sms": false,
"phone_number": "555-555-5555",
"postcode": "12345",
"country": "US",
"confirmed_at": "2015-08-03T13:30:01Z",
"join_organisation": true,
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"sms_opt_in_type": {
"active": true,
"context": "web_form",
"id": 18,
"kind": "unchecked_checkbox",
"phone_number_required": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"changes": {
"first_name": [
"Jen",
"Jennifer"
],
"email_opt_in_type": [
{
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
{
"context": "web_form",
"kind": "unchecked_checkbox",
"mailable": true,
"external_id": "",
"active": true
}
]
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
A user is updated.
signature.created
Example payload for
signature.created
:
{
"type": "signature.created",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"country_without_fallback": "",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"locale": "en",
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"source": "facebook",
"bucket": "share-campaign-v2",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"from_embed": null,
"confirmed_at": null,
"confirmed_reason": null,
"last_signed_at": "2015-08-03T13:30:01Z",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2015-08-03T15:29:09Z",
"subject": "Senator, please don't destroy our park",
"text": "I like the park and it should not be destroyed"
},
"petition": {
"url": "http://localhost/petitions/don-t-destroy-our-park",
"slug": "don-t-destroy-our-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"deleted_at": null,
"unsubscribe_at": null
},
"jid": "1ebf2eb10c3d8939f42dff68"
}
Member signs a petition
We also include optional partnership or effort slugs nested under the petition resource for signatures that take place on petitions associated with those objects.
Field | Help |
---|---|
join_organisation | If the user opts in to receive email from the organisation, this is true. If the user does not wish to receive email or join the organisation this is false. |
confirmed_at | A timestamp, set when the user confirms their email address and signature. Used for double opt in. |
confirmed_reason | The mechanism through which the user confirmed their signature |
email_opt_in_type | A representation of the mechanism that the member used while opting in or out of email. |
signature.updated
Example payload for
signature.updated
:
{
"type": "signature.updated",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"country_without_fallback": "",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"locale": "en",
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"source": "facebook",
"bucket": "share-campaign-v2",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"from_embed": null,
"confirmed_at": null,
"confirmed_reason": null,
"last_signed_at": "2015-08-03T13:30:01Z",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2015-08-03T15:29:09Z",
"subject": "Senator, please don't destroy our park",
"text": "I like the park and it should not be destroyed"
},
"petition": {
"url": "http://localhost/petitions/don-t-destroy-our-park",
"slug": "don-t-destroy-our-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"deleted_at": null,
"unsubscribe_at": null
},
"jid": "1ebf2eb10c3d8939f42dff68"
}
Existing signature updated. This can happen when member unsubscribes or reasserts their opt in for receiving emails
We also include optional partnership or effort slugs nested under the petition resource for signatures that take place on petitions associated with those objects.
Field | Help |
---|---|
join_organisation | If the user opts in to receive email from the organisation, this is true. If the user does not wish to receive email or join the organisation this is false. |
confirmed_at | A timestamp, set when the user confirms their email address and signature. Used for double opt in. |
confirmed_reason | The mechanism through which the user confirmed their signature |
email_opt_in_type | A representation of the mechanism that the member used while opting in or out of email. |
signature.deleted
Example payload for
signature.deleted
:
{
"type": "signature.deleted",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"country_without_fallback": "",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"locale": "en",
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"source": "facebook",
"bucket": "share-campaign-v2",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"from_embed": null,
"confirmed_at": null,
"confirmed_reason": null,
"last_signed_at": "2015-08-03T13:30:01Z",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2015-08-03T15:29:09Z",
"subject": "Senator, please don't destroy our park",
"text": "I like the park and it should not be destroyed"
},
"petition": {
"url": "http://localhost/petitions/don-t-destroy-our-park",
"slug": "don-t-destroy-our-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"deleted_at": null,
"unsubscribe_at": null
},
"jid": "1ebf2eb10c3d8939f42dff68"
}
Signature permanently deleted. Signature deletions are distinct from unsubscribes and should be treated as if the signature never happened
We also include optional partnership or effort slugs nested under the petition resource for signatures that take place on petitions associated with those objects.
Field | Help |
---|---|
join_organisation | If the user opts in to receive email from the organisation, this is true. If the user does not wish to receive email or join the organisation this is false. |
confirmed_at | A timestamp, set when the user confirms their email address and signature. Used for double opt in. |
confirmed_reason | The mechanism through which the user confirmed their signature |
email_opt_in_type | A representation of the mechanism that the member used while opting in or out of email. |
signature.confirmed
Example payload for
signature.confirmed
:
{
"type": "signature.confirmed",
"data": {
"id": 645,
"first_name": "Jennifer",
"last_name": "Goines",
"email": "jenny@example.com",
"phone_number": null,
"postcode": "23456",
"country": "",
"country_without_fallback": "",
"join_organisation": null,
"new_mobile_subscriber": null,
"opt_in_sms": null,
"locale": "en",
"token": "2919ec5f25d493c19690a21135d7fd902a336927",
"source": "facebook",
"bucket": "share-campaign-v2",
"utm_source": "facebook",
"utm_campaign": "share-campaign-v2",
"utm_medium": "social",
"utm_content": "share-variant-3",
"utm_term": "parks",
"user_ip": "127.0.0.1",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/43.0.2357.130 Chrome/43.0.2357.130 Safari/537.36",
"from_embed": null,
"confirmed_at": "2015-08-04T09:01:23Z",
"confirmed_reason": "double_opt_in",
"last_signed_at": "2015-08-03T13:30:01Z",
"additional_fields": {
},
"partnership_opt_ins": [
{
"partnership": {
"slug": "friends-of-the-new-york-library"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "pre_checked_checkbox",
"mailable": true,
"external_id": "abcd12345",
"active": true
},
"eu_data_processing_consent": true,
"consent_content_version": {
"id": 123,
"external_id": "abc345",
"consent_type": "explicit"
},
"member": {
"id": 747,
"created_at": "2015-08-03T13:30:01Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2015-08-03T15:29:09Z",
"subject": "Senator, please don't destroy our park",
"text": "I like the park and it should not be destroyed"
},
"petition": {
"url": "http://localhost/petitions/don-t-destroy-our-park",
"slug": "don-t-destroy-our-park"
},
"created_at": "2015-08-03T13:30:01Z",
"updated_at": "2015-08-03T13:30:01Z",
"deleted_at": null,
"unsubscribe_at": null
},
"jid": "1ebf2eb10c3d8939f42dff68"
}
We also include optional partnership or effort slugs nested under the petition resource for signatures that take place on petitions associated with those objects.
Field | Help |
---|---|
join_organisation | If the user opts in to receive email from the organisation, this is true. If the user does not wish to receive email or join the organisation this is false. |
confirmed_at | A timestamp, set when the user confirms their email address and signature. Used for double opt in. |
confirmed_reason | The mechanism through which the user confirmed their signature |
email_opt_in_type | A representation of the mechanism that the member used while opting in or out of email. |
unsubscribe.created
Example payload for
unsubscribe.created
:
[
{
"type": "unsubscribe.created",
"data": {
"id": 3,
"email": "river@example.com",
"unsubscribe_organisation": true,
"unsubscribable_type": null,
"created_at": "2015-08-03T13:38:49Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
},
{
"type": "unsubscribe.created",
"data": {
"id": 3,
"email": "river@example.com",
"unsubscribe_organisation": false,
"unsubscribable_type": "PartnershipSubscription",
"created_at": "2015-08-03T13:38:49Z"
},
"jid": "030ee43a4fa9788e084d9cfc"
},
{
"type": "unsubscribe.created",
"data": {
"id": 3,
"email": "river@example.com",
"unsubscribe_organisation": true,
"unsubscribable_type": "Signature",
"created_at": "2015-08-03T13:38:49Z",
"blast_email": {
"id": 199,
"subject": "Please help spread the word"
}
},
"jid": "030ee43a4fa9788e084d9cfc"
}
]
Member unsubscribes. This notification is emitted when member: is globally unsubscribed from the organisation by an admin or through the API, unsubscribes from partnership or unsubscribes from a petition.
First example: Admin globally unsubscribes a member from the member page
Second example: Member unsubscribes from partnership
Note:
unsubscribe_organisation
can have true or false values depending on member's input at the unsubscribe page (only if the
Allow global unsubscribes
option is enabled for the organisation)
Third example: Member unsubscribes from petition
Notes:
unsubscribe_organisation
can have true or false values depending on member's input at the unsubscribe page (only if the
Allow global unsubscribes
option is enabled for the organisation)
The
blast_email
property will only be included if member unsubscribed by clicking on the link included on the petition blast email's footer
locale.created
Example payload for
locale.created
:
{
"type": "locale.created",
"data": {
"id": 22,
"name": "es",
"action_kit_external_id": null,
"created_at": "2015-08-03T12:38:46Z",
"updated_at": "2015-08-03T12:38:46Z"
},
"jid": "df67126973794c2efde76cc4"
}
New i18n instance created
forum.message.requires_moderation
Example payload for
forum.message.requires_moderation
:
{
"type": "forum.message.requires_moderation",
"data": {
"id": 10,
"thread_title": "Hello everyone",
"content": "I just wanted to say hi",
"admin_status": "approved_automatically",
"administered_at": null,
"sent_at": "2015-08-05T13:17:46Z",
"created_at": "2015-08-05T13:17:46Z",
"updated_at": "2015-08-05T13:17:46Z",
"member": {
"id": 10829,
"created_at": "2015-08-05T13:17:46Z"
},
"discussable": {
"discussable_type": "LocalChapter",
"discussable_id": 56
}
},
"jid": "f848f4b38b9125f7089d59ad"
}
A new message requires moderation
- unreviewed
- unreviewed_contains_obscenity
- flagged
- approved_automatically
- approved_manually
- flagged_deleted
- inappropriate
- inappropriate_contains_obscenity
- LocalChapter
- Event
Bulk Data
To perform advanced reporting or analytics on your ControlShift data, you can mirror all your data into an external database. This can be a helpful tool for answering high-level questions about member engagement or integrating activity with data from other tools. Once your data is in an external data warehouse replica analysts can use SQL to answer questions about activity or join it with data from other sources.
We provide a set of automated bulk exports and webhooks, along with examples (linked below) on how to use them.
It's possible to consume the Bulk Data API in its underlying format as CSV files in an S3 bucket or as a higher level HTTPS Webhook API that is not specific to AWS or S3. Many data warehouse integration technologies like BigQuery S3 Transfers, Airbyte or AWS Data Glue are able to natively process files in S3 buckets. However, if you are using a different technology or want to implement a custom integration you can use our webhook events to get the same data in a cloud platform agnostic way.
We provide a ControlShift to Redshift Pipeline as an example of sample code that demonstrates how to use the high-level webhooks to mirror your ControlShift data into Redshift. Similar strategies can be used to mirror your data into other data warehouses. We've designed the underlying APIs to work flexibly regardless of your technical architecture. Since we expose the file events as standard HTTPS webhooks they should be compatible with any programming language.
Export schedule and webhooks
Every night, we'll export the most up-to-date version of all of your data into a set of CSV files, one for each internal ControlShift table. The data.full_table_exported indicates such an export. These full CSV files should replace the existing data in your mirror database.
Additionally, once a minute, we'll produce CSV files with any new rows that have been added to ControlShift's internal tables. The data.incremental_table_exported webhooks indicates a set of these added-rows exports. Note that the incremental exports do not include any updates or deletions of existing rows; you'll have to wait for the nightly export to receive fresh data with updates and deletions included.
Bulk Data Data Schemas
The bulk data webhooks include exports of the following tables:
- action_text_rich_texts
- admin_notes
- attendee_shifts
- attendees
- blast_emails
- calendars
- campaign_admins
- campaigns_partnerships
- categories
- categorized_petitions
- comments
- consent_content_versions
- contents
- crm_sync_logs
- custom_links
- daisy_chain_rules
- daisy_chain_steps
- daisy_chains
- data_exports
- data_processing_consents
- drip_email_preferences
- efforts
- email_opt_in_types
- email_opt_in_uploads
- event_imports
- event_types
- events
- export_download_logs
- external_events
- import_batches
- labelings
- labels
- local_chapter_members
- local_chapters
- locales
- locations
- members
- messages_to_target
- model_contents
- objective_collections
- objectives
- partnership_admins
- partnership_subscriptions
- partnerships
- petition_flags
- petition_starts
- petitions
- regions
- share_clicks
- shifts
- signatures
- sms_opt_in_types
- stories
- target_collections
- target_notifications
- targets
- targets_for_petitions
- targets_in_collections
- teams
- timeline_posts
- unsubscribes
- user_invitations
- users
For full information on the schema of each table, use the /api/bulk_data/schema.json
API endpoint.
Bulk Data Files
Each table exposed by the bulk data API is made available as a CSV file with the URL to download each file sent via webhook.
We expire access to data 6 hours after it has been generated. This means that if you are building an automated system to ingest data from this API it must process webhook notifications within 6 hours.
Once webhook data has expired, the download URLs will return a 403 error response.
The files on S3 can be accessed directly before they are expired by using the following URL format:
Incremental table export:
https://<data exports bucket>.s3.amazonaws.com/<your organization slug>/incremental/<table name>/<date in YYYYMMDD format>/<table name>_<timestamp>.csv
Full table export:
https://<data exports bucket>.s3.amazonaws.com/<your organization slug>/full/<table name>/<date in YYYYMMDD format>/<table name>_<timestamp>.csv
Where <data exports bucket>
depends on which data center your organization is hosted on:
- For organizations hosted in the US the bucket is:
agra-data-exports-production
- For organizations hosted in the EU the bucket is:
agra-data-exports-production-eu-central-1
The value for <your organization slug>
is the identifier for your organization within the ControlShift platform. You can find this value using the Organization Retrieve API endpoint or by contacting our team at support@controlshiftlabs.com.
Finally, when the compression for data exports is enabled the filename includes an additional .bz2
suffix.
Files compression
When the Compress bulk data exports option is enabled (available at the Webhooks integration page), incremental and nightly bulk data export files will be compressed in bzip2
format. This will improve the performance for fetching the files from S3 since they will be considerably smaller.
Interpreting the share_clicks table
The share_clicks
table is designed to help you understand in detail how social media sharing influences member actions.
Every time a member clicks on a social sharing link on a petition or event page, it's recorded in the share_clicks
table,
and we add a unique sharing token to the campaign link they share. When another member follows the shared link and
signs a petition, we record the referring_share_click_id
for the new signature. Thus it is possible to trace the origin
of a signature to the member who shared a campaign on social media.
Auto-generated share clicks
For button clicks on campaign pages, we can create share click records on the fly, using Javascript. But when members click on social sharing links in emails, or forward campaign emails to their friends, it's impossible to track that activity directly.
To support the referring-member functionality for emails, we pre-create share_clicks
records representing the idea that a user might click on the social sharing links in the Thanks email, or
forward the email to their friends. Then we add the unique sharing tokens to the links in the email, so that any
signatures resulting from those email-based shares can get a referring_share_click_id
. We do this for both the
Thanks For Signing email, and the Thanks For Creating Petition email.
The pre-created share_clicks
records have true
values in the auto_generated
column, to allow distinguishing them
from records that represent a member definitely clicking on something.
Column by column
Column | Explanation |
---|---|
id | Unique identifier for this share click. Not guaranteed to be sequential. |
page_type | Campaign type (Petition or Event) |
page_id | Campaign ID; corresponds to id in the petitions or events table |
medium | What kind of share (e.g. "facebook" or "email") |
member_id | If available, which member did the share click. Corresponds to id in the members table. Might be null if e.g. a member arrived on a petition page and immediately shared without signing. |
created_at | When the member clicked on the sharing button. Or for auto-generated share clicks, when the email was sent. |
updated_at | Usually the same as created_at , but would be updated if the record was changed after creation |
token | Random unique identifier used to refer to this share click in URLs |
auto_generated | Whether this is an auto-generated share click. Boolean. |
Interpreting the unsubscribes table
The unsubscribes
table is used for keeping a record of all member unsubscribes.
There are several kinds of unsubscribes:
- Petition unsubscribe: member is unsubscribing just from a petition they have signed.
- Event unsubscribe: member is unsubscribing just from an event they have RSVPed to.
- Partnership unsubscribe: member is unsubscribing from all actions they have taken in petitions and events from a partner organisation.
- Organisation unsubscribe: member is unsubscribing from all actions they have taken with the organisation.
Depending the kind of unsubscribe the member is taking, the database record will have different values on its attributes:
Unsubscribe Kind | Attribute values |
---|---|
Petition unsubscribe | unsubscribable_type = "Signature" .unsubscribable_id corresponds to id in the signatures table.unsubscribe_object and unsubscribe_organisation may be true or false depending the option the member chooses when unsubscribing. |
Event unsubscribe | unsubscribable_type = "Attendee" .unsubscribable_id corresponds to id in the attendees table.unsubscribe_object and unsubscribe_organisation may be true or false depending the option the member chooses when unsubscribing. |
Partnership unsubscribe | unsubscribable_type = "PartnershipSubscription" .unsubscribable_id corresponds to id in the partnership_subscriptions table.unsubscribe_object and unsubscribe_organisation may be true or false depending the option the member chooses when unsubscribing. |
Organisation unsubscribe | unsubscribable_type = "Organisation" .unsubscribe_object = false .unsubscribe_organisation = true . |
Additionally the following columns apply for all kinds of unsubscribe:
Column | Explanation |
---|---|
blast_email_id | Blast email ID; corresponds to id in the blast_emails table. May be null if unsubscribe was not triggered by the member clicking on a blast email's unsubscribe link (e.g.: when unsubscribing member from the API or from org admin page). |
Member's email address. | |
external_id | If unsubscribe was synced to CRM this column will store the ID on the external system. |
externally_unsubscribed_at | When the unsubscribe was synced to CRM. |
created_at | When the unsubscribe was recorded in the platform. |
updated_at | Same as created_at as unsubscribe records cannot be updated after creation. |
ControlShift to Redshift Pipeline
Setting up an Amazon Redshift integration is a great way to learn more about the actions your members are taking or perform sophisticated analytics, but it is an advanced topic that requires knowledge of Amazon Web Services, SQL, and Terraform.
Following these instructions we'll use a custom AWS Lambda to receive data from ControlShift's bulk data API webhooks and two different mechanisms to load our data into Redshift:
- Amazon's Lambda Redshift Loader, for processing incremental exports and full table exports for small tables.
- AWS Glue, for processing full table exports for large tables (i.e.:
signatures
).
The final result will be a replica of the tables that back your instance of ControlShift that are refreshed in full each night and appended with inserts on a per-minute basis.
ControlShift has authored a Terraform module that makes the entire setup process straightforward and allows for updates to the infrastructure over time.
Example Flow
ControlShift Redshift Flow For Small Data Exports
ControlShift Redshift Flow For Large Data Exports
Redshift Pipeline Setup
If you are not yet using Redshift or Terraform, we've written an example Terraform plan that should create a working integration, a new Redshift instance and appropriate permissions from an otherwise empty AWS Account.
If you are already using Terraform or Redshift it is probably best to either fork this example or use the module we provide directly in your own TF plan
Applying the Terraform plan in your AWS account will create all of the resources necessary and output the Webhook URL that you'll need to configure within your ControlShift platform settings.
Prepare Redshift Schema
We need to prep tables in your Redshift Data Warehouse to receive our ControlShift Data. We'll use psql
for this.
- Install
psql
if you don't have it already. - Download the current SQL DDL for ControlShift's tables tables.sql or generate an up to date one using this script. that uses our Schema API.
- Finally, you'll need to load that set of tables into your Redshift instance with the following command:
psql -h <cluster_endpoint> -U <database_user> -d <database_name> -p <cluster_port> -f petitions_schema.sql
Configure ControlShift's Webhook
Finally, you'll need to log into your admin panel. Settings > Integrations > Webhooks. Then add the bulk data webhook pointing at your Lambda endpoint output by the Terraform plan.
Working with compressed bulk data files
As described in Bulk data files compression, you can enable compression for bulk data export files. Doing so has the additional benefit of allowing to horizontally scale up AWS Glue's processing of large files as detailed in Best practices to scale Apache Spark jobs and partition data with AWS Glue, improving greatly the performance of the process for importing large data exports.
Small data exports, processed with the controlshift-redshift-loader
Lambda function will also take advantage of the compression on source files by performing the correct COPY
command on Redshift to process the compressed files directly without needing to decompress.
More information
More information is available in the READMEs of the various projects that this integration uses. For help with troubleshooting your Redshift sync, please see our help center. We can also provide assistance for customers configuring these tools via support@controlshiftlabs.com.
Authenticated REST API
The Authenticated REST API allows customers to build applications that interact with their own data or securely grant access to third-party app developers without exposing administrative credentials. It is designed to be consumed server-side, in contrast to the JSONP API which is designed for unauthenticated javascript integrations.
If you're synchronizing data from ControlShift into another system then the Webhooks API is likely a better choice for that use case.
Getting Started
We support both API keys and OAuth2 for authenticating with the API. API keys are the recommended approach, because they are simpler to set up and use. OAuth2 is supported for now, but may be deprecated in the future, so we do not recommend using it for new applications.
HTTP Requests Format
The Authenticated REST API expects payloads in JSON format when submitting POST
and PUT
requests. Additionally, all responses will also be returned in this format. To ensure compatibility, the requests made to these endpoints should include the Accept
and Content-Type
headers with the value application/json
.
Using API Keys
We support Bearer Authentication, using API keys that can be configured through the platform.
- Add a new API key Log in as an Organisation Owner and navigate to Settings > Integrations > API Keys. Add a new API key and give it a name. Copy down the full key.
- Include your key in an Authorization HTTP header on your API request, like this:
curl https://demo.controlshiftlabs.com/api/v1/organisation -H "Authorization: Bearer cs_aBcDeFgHiJ0123456789kLmNoPqRsT"
Using OAuth2
We use OAuth2 to allow organizations to securely grant access to API Applications. OAuth2 is a standardized protocol that allows user credentials to be exchanged for a secret token which can then be used to access an API. There are OAuth2 implementations available for most common programming languages. You'll probably want to read some general documentation on how OAuth2 works before attempting to use OAuth2 to access the API.
An API Application must be configured within your ControlShift instance before using the API. Access is then granted to this application, and exchanged for a token which is used to authenticate your access to API endpoints.
Set up a new API Application Log in as an Organisation Owner and navigate to Settings > Integrations > Legacy OAuth Apps. Add a New Application. Use
urn:ietf:wg:oauth:2.0:oob
as the callback URL unless you plan to allow self-service application authorization.Clone the oauth-api-example github repo.
Follow the instructions in the README.md file to get that example working with your credentials.
Do something similar in your actual code.
Note: Access tokens have a 2 hours expiration, after that period you can use the old token to generate a new one.
Rate Limits
We apply the following per-domain rate limits on HTTP requests made to the API:
- OAuth Token exchange (
/oauth/token
): 10 requests per minute. - Authenticated REST API (
/api/v1/*
): 1000 requests per minute.
Once the limit is reached, we will return an HTTP response with status code 429 - Too Many Requests
and the following headers:
RateLimit-Limit
: returns the number of requests per minute allowed on the endpoint, values will match the ones listed above depending if you are invoking the OAuth Token Exchange or one of the Authenticated REST API endpoints.RateLimit-Remaining
: this header will always have0
as value, since this response is only returned when the rate limit has been reached.RateLimit-Reset
: returns the number of seconds until the current rate limit is reset.
Organisation
Retrieve
Get up to date statistics and basic information about the current organisation.
GET /api/v1/organisation
{
"organisation": {
"slug": "foo",
"name": "An Organisation Name",
"petitions_count": 3,
"signatures_count": 4,
"blast_emails_count": 0,
"members_count": 5
}
}
Email Opt In Types
List
Get a list of the email opt in types that have been configured for this organisation.
Email opt in types represent the way that members have opted in to your email list.
Context
The context this opt in type was displayed to users in.
Value | Description |
---|---|
web_form | an action on the web |
in a confirmation email | |
offline | collected offline via a paper form |
external | represents opt ins gathered in other systems |
Kind
The sort of opt in that was gathered
Value | Description |
---|---|
email_confirmation | an action on the web |
implicit | a message displayed while taking action |
offline | collected offline via a paper form |
unchecked_checkbox | a checkbox that began unchecked |
physical_signature | a wet ink signature |
pre_checked_checkbox | a checkbox that began checked |
radio | a radio button with yes/no options |
Active
This parameter is true if the opt in type is currently being used in it's specified context.
Mailable
Whether this opt-in type should be considered valid for email.
external_id
External IDs are set by customers through the admin interface while configuring opt in types. You can use them to match up email opt in types with your external systems, or to refer to email opt in types consistently through this API.
GET /api/v1/organisation/email_opt_in_types
{
"email_opt_in_types": [{
"context": "web_form",
"kind": "implicit",
"mailable": true,
"active": true,
"external_id": "your-external-id"
}]
}
Consent Content Versions
List
Get a list of the Data Processing Consent Content Versions that have been configured for this organisation.
These content versions track changes to the Terms of Service, Privacy Policy and Data Processing Consent disclaimers.
GET /api/v1/organisation/consent_content_versions
Response body
{
"consent_content_versions": [
{
"id": 123,
"external_id": "consent-version-may-2018",
"created_at": "2018-05-30T17:15:59Z",
"privacy_policy": "Privacy Policy - ...",
"tos": "Terms of Use Agreement - ...",
"data_processing_consent_label": "I consent to my data being processed as described in the <a href=\"/tos\" target=\"_blank\">Terms of Service</a> and the <a href=\"/privacy_policy\" target=\"_blank\">Privacy Policy.</a>",
"current": false
},
{
"id": 711,
"external_id": "consent-version-sept-2021",
"created_at": "2021-09-30T11:10:44Z",
"privacy_policy": "Privacy Policy v2 - ...",
"tos": "Terms of Use Agreement - ...",
"data_processing_consent_label": "I consent to my data being processed as described in the <a href=\"/tos\" target=\"_blank\">Terms of Service</a> and the <a href=\"/privacy_policy\" target=\"_blank\">Privacy Policy.</a>",
"current": false
},
...
]
}
Members
The platform transparently creates a member record for any email address that creates a user account, signs a petition, or attends an event.
Members are unique by email address, and all platform activity is tracked by member id.
When someone signs a petition or attends an event, we lookup the email address to see if they have taken action before and either reuse the existing member record or create a new one if your organisation has not interacted with that person before.
Lookup
GET response body for Lookup
{
"member": {
"id": 123,
"email": "foo@bar.com",
"created_at": "2015-06-01T15:37:47Z",
"updated_at": "2017-07-01T11:23:45Z",
"external_id": "abc123",
"data_processing_consent": {
"consented_to_latest_version": false,
"most_recent_consent_from_member": {
"consented_at": "2018-01-01T04:00Z",
"consent_content_version": {
"id": "25",
"external_id": "def456",
"created_at": "2017-12-31T23:59Z"
}
}
}
}
}
Find a member by email address. Once you have obtained a member id you can use this identifier in other API calls.
GET /api/v1/members/lookup?email=foo@bar.com
Show
GET response body for Show
{
"member": {
"id": 123,
"email": "foo@bar.com",
"created_at": "2015-06-01T15:37:47Z",
"updated_at": "2017-07-01T11:23:45Z",
"external_id": "abc123",
"data_processing_consent": {
"consented_to_latest_version": false,
"most_recent_consent_from_member": {
"consented_at": "2018-01-01T04:00Z",
"consent_content_version": {
"id": "25",
"external_id": "def456",
"created_at": "2017-12-31T23:59Z"
}
}
}
}
}
Find a member by id.
The data_processing_consent
block is only present when the data processing consent feature is enabled.
GET /api/v1/members/123
Activity
GET response body for Activity
{
"first_name": "Jane",
"last_name": "Doe",
"phone_number": "555-555-5555",
"created_at": "2015-06-01T15:37:47Z",
"signatures": [
{
"email": "jane.doe@example.com",
"petition": {
"slug": "save-the-whales"
}
...
}
...
]
...
}
Get information about a member's activity on the platform. This can be useful if you're building a member dashboard that includes information from ControlShift and other toolsets.
The member activity report includes basic biographical information as well as petitions created or signed, events hosted or attended, group memberships, forum posts, partnership subscriptions, and unsubscribe history.
GET /api/v1/members/123/activity
Destroy
DELETE response body for Destroy
{
"member": {
"deleted": true,
"id": 123,
"email": "foo@bar.com"
}
}
Permanently deletes a member with specified id and all associated records including petitions, signatures, events. The deletion feature is designed to honor requests from members around the right to be forgotten.
DELETE /api/v1/members/123
Deletes are synchronous and may take several seconds depending on how many resources are owned by the member.
Ownership of Petitions and Events created by the deleted member will be re-assigned to the user account specified in the organisation's settings.
Note that if the member has a user account that is associated with one or more Legacy OAuth Apps, the member cannot be deleted unless those apps are first deleted through the web UI. If the member cannot be deleted for any reason, the JSON response will contain error messages explaining the problem.
Anonymize
POST response body for Anonymize
{
"member": {
"deleted": true,
"id": 123,
"email": "foo@bar.com"
}
}
Anonymizes the member with specified id. This permanently deletes the member and all associated signatures. However, petition signatures from the anonymized member are preserved, anonymized, on petition data exports, signature counts, and petition letters. The method of anonymization is configured in organisation name privacy settings.
POST /api/v1/members/123/anonymize
Anonymization is an asynchronous process which can take some time to complete depending on how many resources are owned by the member. If the anonymization process is correctly triggered by the API request, the HTTP response will have a 202 Accepted
status code and will not include any payload. If the member instead, cannot be anonymized for any reason, the response will have a 422 Unprocessable Entity
status and the JSON body will contain error messages explaining the problem.
Ownership of Petitions and Events created by the deleted member will be re-assigned to the user account specified in the organisation's settings.
Note that if the member has a user account that is associated with one or more Legacy OAuth Apps, the member cannot be anonymized unless those apps are first deleted through the web UI.
Unsubscribe
POST response body for Unsubscribe
{
"member": {
"unsubscribed": true,
"id": 123,
"email": "foo@bar.com"
}
}
Unsubscribes a member with specified id from emails from event hosts and petiton creators. Member will still receive transactional emails, and will be resubscribed if they opt in while signing a new petition or attending a new event, etc.
POST /api/v1/members/123/unsubscribe
Update Email Opt In Type
POST body
external_id=foo
Changes the email opt in type for the member with the specified id to the external id in the POST body. This will update the opt in type for all associated signatures, attendee records, user accounts and other data held by ControlShift to the opt in type specified by external_id.
This feature may be useful for upgrading the recorded consent when new consent for email communications has been collected in external systems.
Most likely you will be changing historical consents that may no longer be mailable for legal reasons to a new mailable consent type.
No history is stored recording the original consent type, it is replaced by the new one specified.
POST /api/v1/members/123/update_email_opt_in_type
Petitions
Petition are pieces of content that can be signed by members.
Show
GET response Body
{
"petition": {
"admin_events_status": "auto",
"admin_status": "good",
"alias": "teatime",
"campaigner_contactable": true,
"can_download_signers": true,
"created_at": "1776-12-16T21:32Z",
"custom_goal": 500,
"delivery_details": "We will throw the petition into the harbor.",
"external_facebook_page": "https://facebook.com/example",
"external_site": "https://example.com/do-not-tax-our-tea",
"goal": 500,
"hide_petition_creator": false,
"hide_recent_signers": false,
"hide_signature_form": false,
"id": 123,
"launched": true,
"locale": "en-US",
"petition_creator_name_override": "Anonymous Tea Enthusiast",
"redirect_to": null,
"show_progress_bar": true,
"signature_count_add_amount": 100,
"slug": "no-taxes-on-tea",
"source": "homepage",
"title": "No Taxes on Tea",
"updated_at": "1776-12-16T21:32Z",
"what": "Stop taxing our tea",
"rich_what": "<div>Stop taxing our tea</div>",
"who": "King George",
"why": "No taxation without representation!",
"rich_why": "<div><strong>No taxation without representation!</strong></div>",
"title_locked": false,
"what_locked": true,
"who_locked": false,
"why_locked": false,
"delivery_details_locked": false,
"external_facebook_page_locked": false,
"external_site_locked": false,
"categories_locked": false,
"url": "https://demo.controlshiftlabs.com/petitions/no-taxes-on-tea",
"public_who": "George III, King of Great Britain",
"ended": true,
"successful": true,
"ended_type": "won",
"ended_reason": "We overthrew the government and they don't tax our tea anymore.",
"ended_story": "This was an overwhelming success!",
"image": {
"urls": {
"open_graph": "http://cdn.example.com/petitions/images/25/open_graph/a-little-teapot.png?1516647704",
"horizontal": "http://cdn.example.com/petitions/images/25/horizontal/a-little-teapot.png?1516647704",
"hero": "http://cdn.example.com/petitions/images/25/hero/a-little-teapot.png?1516647704",
"original": "http://cdn.example.com/petitions/images/25/original/a-little-teapot.png?1516647704",
"form": "http://cdn.example.com/petitions/images/25/form/a-little-teapot.png?1516647704"
},
"description": "Picture of a small teapot with flowers on it and money tucked under the lid"
},
"public_signature_count": 234,
"admin_notes": [
{
"source": "legacy",
"body": "An older note about this campaign",
"created_at": "2018-01-25T23:45:12Z",
"user": null
},
{
"source": "user",
"body": "We should have a call with this campaigner but they have no telephone.",
"created_at": "2019-10-09T12:34:45Z",
"user": {
"email": "organiser_2@example.com",
"full_name": "Sarah Organiser"
}
}
],
"creator": {
"full_name": "Patrick Henry",
"first_name": "Patrick",
"last_name": "Henry",
"email": "patrick.henry@example.com",
"phone_number": "555-555-5555"
},
"mentor": {
"member_id": 123,
"full_name": "Alexander Hamilton",
"email": "alex.hamilton@example.com",
"phone_number": null,
"postcode": "12345"
},
"reviewer": {
"member_id": 234,
"full_name": "Thomas Jefferson",
"email": "thomas.jefferson@example.com",
"phone_number": "555-444-3333",
"postcode": null
},
"location": {
"query": "boston harbor",
"latitude": "42.3376368",
"longitude": "-70.99304",
"street_number": "",
"street": "",
"region": "MA",
"postal_code": "",
"country": "US",
"created_at": "2017-11-16T22:38:29Z"
},
"decision_makers": [
{
"slug": "king-george",
"name": "George III",
"context": "King of Great Britain",
"phone_number": "",
"email": "",
"location": {
"query": "buckingham palace",
"latitude": "51.501364",
"longitude": "-0.1440787",
"street_number": "",
"street": "",
"region": "London",
"postal_code": "SW1A 1AA",
"country": "UK",
"created_at": "2017-11-16T22:38:29Z"
}
}
],
"effort": {
"slug": "no-taxation-without-representation"
},
"partnership": {
"slug": "the-american-revolution",
"title": "The American Revolution!"
},
"labels": [
"Tea related",
"taxes"
],
"categories": [
{
"slug": "avoidingtaxes",
"name": "Avoiding Taxes"
},
{
"slug": "boats",
"name": "Boats"
}
]
}
}
Find information about a petition by URL slug.
GET /api/v1/petitions/no-taxes-on-tea
The slug
used to identify a petition for API purposes is the same as the slug
that appears in the member-facing petition page URL.
For example, for a petition whose petition page is at https://demo.controlshiftlabs.com/petitions/save-our-library
,
the petition slug
is save-our-library
.
List
GET response body
{
"petitions": [
{
"id": 123,
"slug": "no-taxes-on-tea",
"title": "No Taxes on Tea",
...
},
{
"id": 124,
"slug": "stop-burning-coal-1",
"title": "Stop Burning Coal",
...
},
...
],
"meta": {
"current_page": 1,
"total_pages": 12,
"previous_page": null,
"next_page": 2
}
}
Get a paginated list of all petitions, including ones that are unlaunched or otherwise not visible to the public. Includes all the same data as the single-petition endpoint for each petition.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
GET /api/v1/petitions?page=1
Create
POST /api/v1/petitions
{
"petition": {
"title": "No Taxes on Tea",
"what": "Stop taxing our tea",
"why": "<strong>No taxation without representation!</strong>",
"categories": ["Tax Policy", "End Colonialism"],
"creator": {
"first_name": "Patrick",
"last_name": "Henry",
"email": "patrick.henry@example.com",
"locale": "en-US"
},
"targets": [
{
"name": "George III",
"context": "King of Great Britain"
}
],
"partnership": {
"slug": "american-whigs"
},
"region": {
"slug": "new-york-city"
}
}
}
Response body
{
"status": "success",
"petition": {
"admin_events_status": "auto",
"admin_status": "good",
"alias": null,
"campaigner_contactable": true,
"can_download_signers": true,
"created_at": "1776-12-16T21:32Z",
"custom_goal": null,
"delivery_details": null,
"external_facebook_page": null,
"external_site": null,
"goal": 100,
"hide_petition_creator": false,
"hide_recent_signers": false,
"hide_signature_form": false,
"id": 123,
"launched": true,
"locale": "en",
"petition_creator_name_override": null,
"redirect_to": null,
"show_progress_bar": true,
"signature_count_add_amount": null,
"slug": "no-taxes-on-tea",
"source": "API",
"title": "No Taxes on Tea",
"updated_at": "1776-12-16T21:32Z",
"what": "Stop taxing our tea",
"rich_what": "<div>Stop taxing our tea</div>",
"who": null,
"why": "No taxation without representation!",
"rich_why": "<div><strong>No taxation without representation!</strong></div>",
"title_locked": null,
"what_locked": null,
"who_locked": null,
"why_locked": null,
"delivery_details_locked": null,
"external_facebook_page_locked": null,
"external_site_locked": null,
"categories_locked": null,
"url": "https://demo.controlshiftlabs.com/petitions/no-taxes-on-tea",
"public_who": "George III, King of Great Britain",
"ended": false,
"successful": false,
"image": null,
"public_signature_count": 0,
"admin_notes": [],
"creator": {
"full_name": "Patrick Henry",
"first_name": "Patrick",
"last_name": "Henry",
"email": "patrick.henry@example.com",
"phone_number": null
},
"mentor": null,
"reviewer": null,
"location": null,
"decision_makers": [
{
"name": "George III",
"slug": "george-iii",
"context": "King of Great Britain",
"phone_number": null,
"email": null
}
],
"effort": null,
"partnership": {
"slug": "american-whigs",
"title": "Whigs in the American Colonies"
},
"labels": [],
"categories": [
{
"slug": "tax-policy",
"name": "Tax Policy"
},
{
"slug": "end-colonialism-now",
"title": "End Colonialism"
}
]
}
}
Create a new petition.
POST /api/v1/petitions
If a creator
is specified, that user will become the petition's leader, and will receive an email asking them to confirm that they are leading the petition.
Once the leader confirms, the petition will work the same way as a petition created via the web UI.
The URL in the response is the public petition page where supporters can sign the petition.
To update a petition after creation, use the returned slug
with the Petition Update endpoint.
The request body should be a JSON block containing one "petition"
object, which can have the following properties:
Field | Type | Description | Required? |
---|---|---|---|
campaigner_contactable | Boolean | Whether members of the public can contact the petition creator via the public petition page | no; default is true |
categories | List of Strings | Names of categories that should be applied to the new petition. Must be categories that already exist. If category names are translated, the original/main language names should be used here. | no |
creator | User block (see below) | User who will be the petition leader. If a user account does not already exist with this email, we will create one. | no; default is no leader |
delivery_details | String | How the petition will be delivered | no |
external_facebook_page | String | URL of a facebook page for the campaign | no |
external_site | String | URL of a website about the campaign | no |
group[slug] | String | Unique identifier for an existing group this petition should be associated with | no |
hide_petition_creator | Boolean | Whether to suppress the display of the petition creator’s name on the petition page | no; default is false |
hide_recent_signers | Boolean | Whether to suppress the "recently signed" and "recently shared" display | no; default is false |
labels | List of Strings | Labels that should be applied to the new petition. Any labels that do not already exist will be created | no |
locale | String | The ISO 639-1 two-letter code for the language the petition content is in. Must be one of the languages supported by ControlShift. | no; defaults to organisation's default language |
location | Location block (see below) | Information about the location associated with the petition. If latitude and longitude are included, we’ll use them. Otherwise, we’ll geocode the fields provided. | no |
partnership[slug] | String | Unique identifier for an existing partnership this petition should be associated with | no |
petition_creator_name_override | String | Name to display on the petition page instead of the creator's account name | no |
region[slug] | String | Unique identifier for an existing region this petition should be associated with | no |
source | String | Optional, open-ended string representing where this petition came from | no; default is "API" |
targets | List of 0-1 Target blocks (see below) | Decision makers for this petition. If a block includes a slug , we’ll use it to look up an existing decision maker. Otherwise, we’ll try to create a new one. Any decision maker created this way will be set to "published". Specifying more than one decision maker is not yet supported. |
no; default is no decision maker |
title | String | Title of the petition | yes |
what | String | Text of the petition ask in HTML format | yes |
who | String | Who the petition is addressed to | yes, unless targets includes a decision maker |
why | String | Text explaining why the petition is important in HTML format | yes |
A Location block is a JSON object with some or all of the following properties:
Field | Type | Description |
---|---|---|
country | String | ISO 3166-1 two-letter code for the country |
latitude | Floating-point number | Latitude |
locality | String | City |
longitude | Floating-point number | Longitude |
postal_code | String | Postcode / ZIP code |
region | String | State/province/etc. |
street | String | Street name |
street_number | String | Street number (e.g. "123" in "123 Main Street") |
If the block does not include latitude and longitude, it must include country and at least one of locality, region, or postal code, so that we can geocode the location.
A Target block is a JSON object that has either the unique slug of an existing decision maker, like this:
{"slug": "president-joe-biden"}
or a set of the following properties for creating a new decision maker:
Field | Type | Description | Required? |
---|---|---|---|
context | String | Job title or additional context about the decision maker | no |
String | Email address for contacting the decision maker | no | |
name | String | Decision maker's name | yes |
phone_number | String | Phone number for contacting the decision maker | no |
A User block is a JSON object with some or all of the following properties:
Field | Type | Description | Required? |
---|---|---|---|
String | Email address | yes | |
first_name | String | First/given name | no |
last_name | String | Last/family name | no |
locale | String | The ISO 639-1 two-letter code for the user's preferred language. Must be one of the languages supported by ControlShift. | no; defaults to organisation's default language |
phone_number | String | Phone number | no |
postcode | String | Postcode / ZIP code | no |
Update
PUT request body
{
"petition": {
"signature_count_add_amount": 100
}
}
Updates the content and/or settings of the petition whose slug is in the URL.
PUT /api/v1/petitions/no-taxes-on-tea
The request body should contain only the fields you would like to update, in JSON format. All other fields will be left unchanged.
The fields that can be changed this way are:
Field | Type | Description |
---|---|---|
admin_events_status | String | Whether the petition creator can create events associated with the petition. Must be one of "on" (events can be created), "off" (events cannot be created), or "auto" (use the default setting based on moderation status and number of signatures). |
alias | String | Used for the petition's short URL. Setting alias to "foo" will result in a short URL of /p/foo |
campaigner_contactable | Boolean | Whether members of the public can contact the petition creator via the public petition page |
can_download_signers | Boolean | Whether the petition creator can download information about petition signers |
categories_locked | Boolean | Petition creator cannot change which categories the petition is in |
custom_goal | Integer | Signature goal displayed on the petition page. Set to null to automatically calculate the goal from the current signature total. |
delivery_details | String | "How do you plan to deliver the petition?" text |
delivery_details_locked | Boolean | Petition creator cannot change the delivery details |
external_facebook_page | String | URL of a Facebook page associated with the petition |
external_facebook_page_locked | Boolean | Petition creator cannot change the Facebook page URL |
external_site | String | URL of a non-ControlShift page associated with the petition |
external_site_locked | Boolean | Petition creator cannot change the external site URL |
hide_petition_creator | Boolean | Do not display the petition creator's name or profile picture on the petition page |
hide_recent_signers | Boolean | Do not display the list of recent signers on the petition page |
hide_signature_form | Boolean | Turn off the form that allows people to sign the petition via the petition page |
partnerships | Array | List of partnerships this petition should be associated with, formatted like [{"slug": "foo"}, {"slug": "bar"}] . If the petition was associated with any partnerships that are not included in the list, it will be removed from those partnerships. |
petition_creator_name_override | String | Name to display for the petition creator on the petition page. Set to null to use the name from the petition creator's user account. |
redirect_to | String | If set, visitors to the petition page will be redirected to this URL |
show_progress_bar | Boolean | Whether to show the signature progress bar on the petition page |
signature_count_add_amount | Integer | If set, the displayed signature total on the petition page will be this much higher than the actual number of signatures recorded by the platform. This is usually used to reflect signatures that were gathered elsewhere and cannot be added to the platform, or signers whose data has been anonymized. A negative number can be specified to subtract from the total. |
title | String | Petition title |
title_locked | Boolean | Petition creator cannot change the title |
what | String | Petition text in HTML format |
what_locked | Boolean | Petition creator cannot change the "what" field |
who | String | Name of the person this petition is targeting. Petitions associated with a decision maker will not necessarily use this setting. |
who_locked | Boolean | Petition creator cannot change the "who" field |
why | String | "Why is this important?" text in HTML format |
why_locked | Boolean | Petition creator cannot change the "why" field |
Update images
PUT request headers
...
Content-Type: "multipart/form-data; boundary=-----------314550593530941214802297487787"
...
PUT request body
-----------------------------314550593530941214802297487787
Content-Disposition: form-data; name="file"; filename="save_the_whales.jpg"
Content-Type: image/jpeg
Content-Length: 249206
<ENCODED BINARY DATA>
-----------------------------314550593530941214802297487787--
The image shown on the petition page can be updated by making a multipart PUT request with the image file attached on the file
field.
PUT /api/v1/petitions/no-taxes-on-tea/images/image
To remove the image without replacing it, perform a DELETE request to the same URL.
DELETE /api/v1/petitions/no-taxes-on-tea/images/image
It's possible to specify a different image that will be used when the petition page is shared on social media via a request to
PUT /api/v1/petitions/no-taxes-on-tea/images/social_share_image
Destroy
DELETE response body
{
"status": "success",
"message": "Petition has been scheduled for deletion.",
"petition": {
"slug": "no-taxes-on-tea"
}
}
Permanently deletes the petition and all associated information including signature data via an HTTP DELETE request.
This is an asynchronous task, so while API calls will return with a success message immediately it may take up to several minutes for the petition and all of its data to be completely destroyed.
DELETE /api/v1/petitions/no-taxes-on-tea
Apply Label
POST request body
{
"name": "We Love Tea!"
}
Applies an existing Label to a specific petition.
Note: Currently it is not possible to create Labels via the API. The Label needs to already exist to allow tagging the petition with it.
The request body must include a name
attribute with the name of the label to be applied to the petition.
POST /api/v1/petitions/no-taxes-on-tea/labelings
Response on success will not have a body, and status code will be 201 - Created
This example would apply the "We Love Tea!" label to the petition.
Remove Label
Removes a Label from the petition.
The name of the Label must be included in the URL and be correctly percent encoded as per RFC 3986.
Response on success will not have a body, and status code will be 200 - Ok
DELETE /api/v1/petitions/no-taxes-on-tea/labelings/We%20Love%20Tea%21
This example would remove the "We Love Tea!" label from the petition.
Assign Mentor
PUT request body
{
"mentor": {
"email": "suzie@example.com"
}
}
PUT request response
{
"status": "success",
"data": {
"mentor": {
"email": "suzie@example.com",
"member_id": 123,
"full_name": "Suzie Greenberg",
"phone_number": "555-555-5555",
"postcode": "12345"
}
}
}
Assigns an admin user as the mentor for the petition.
The email address specified will be used to look up an admin user who will be assigned as the petition's mentor.
PUT /api/v1/petitions/no-taxes-on-tea/mentor
PUT request body
{
"mentor": null
}
To remove the mentor from the petition, the request body should include null
instead of the email block.
Assign Reviewer
To allow delegation of moderation tasks to specific people or teams, the platform allows specifying a reviewer for each petition.
Staff are able to filter the moderation queue to just the tasks that have been assigned to them through the ControlShift platform web interface.
PUT request body
{
"reviewer": {
"type": "User",
"email": "suzie@example.com"
}
}
PUT request response
{
"status": "success",
"data": {
"reviewer": {
"type": "User",
"email": "suzie@example.com",
"full_name": "Suzie Greenberg",
"member_id": 123,
"phone_number": "555-555-5555",
"postcode": "12345"
}
}
}
PUT request body
{
"reviewer": {
"type": "Team",
"slug": "moderation-helpers"
}
}
PUT request response
{
"status": "success",
"data": {
"reviewer": {
"type": "Team",
"name": "Moderation Helpers",
"slug": "moderation-helpers"
}
}
}
Assigns an admin user or team as the reviewer for the petition that is pending moderation.
The email address or team slug will be used to look up an admin user or team that will be assigned to review the petition.
PUT /api/v1/petitions/no-taxes-on-tea/reviewer
PUT request body
{
"reviewer": null
}
To remove the reviewer from the petition, the request body should include null
instead of the email or slug block.
Signatures
Signatures are actions that people have taken on Petitions. All signature actions are namespaced by URL underneath the petitions that the signature was recorded against.
Lookup
GET response body for Lookup
{
"signature": {
"bucket": "",
"country_without_fallback": "US",
"from_embed": null,
"last_signed_at": "2020-02-01T10:00:00Z",
"user_agent": null,
"deleted_at": null,
"additional_fields": {},
"id": 123,
"country": "US",
"created_at": "2020-02-01T10:00:00Z",
"updated_at": "2020-02-01T10:00:00Z",
"unsubscribe_at": null,
"email": "foo@bar.com",
"confirmed_at": null,
"confirmed_reason": null,
"first_name": "Foo",
"join_organisation": true,
"last_name": "Bar",
"phone_number": "555-555-5555",
"postcode": "10010",
"source": "",
"token": "abcDEFghi123456789jklMNO",
"user_ip": "123.123.123.123",
"utm_campaign": null,
"utm_content": null,
"utm_medium": null,
"utm_source": null,
"utm_term": null,
"eu_data_processing_consent": false,
"consent_content_version": null,
"data_processing_consent_type": "explicit",
"partnership_opt_ins": [
{
"partnership": {
"slug": "tea-drinkers-for-freedom"
},
"opted_in": false
},
{
"partnership": {
"slug": "american-revolution"
},
"opted_in": true
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "radio_sure",
"mailable": true,
"external_id": null,
"active": true
},
"member": {
"id": 111222333,
"created_at": "2020-02-01T10:00:00Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2020-02-01T10:00:19Z",
"subject": "King George, Don't Tax our Tea!",
"text": "We don't want taxation without representation."
},
"petition": {
"url": "https://your-organisation.controlshiftlabs.com/petitions/no-taxes-on-tea",
"slug": "no-taxes-on-tea"
}
}
}
Find a specific signature by email address. Once you have obtained a signature id you can use this identifier in other API calls.
GET /api/v1/petitions/no-taxes-on-tea/signatures/lookup?email=foo@bar.com
The JSON response has a single "signature" object, which may include the following fields:
Field | Explanation |
---|---|
additional_fields | A json object with the signer's responses to any custom fields |
bucket | Legacy tracking field; bucket parameter that was in the petition page URL when the member signed |
confirmed_at | When the member confirmed their signature. A null here indicates that the signature has not been confirmed. |
confirmed_reason | For a confirmed signature, how it was confirmed. The most common values are authenticated (meaning a logged-in user with a confirmed account signed), double_opt_in (meaning the signer clicked through a confirmation email), and facebook (meaning the member signed with Facebook, and Facebook indicated that the email address was confirmed). |
consent_content_version | For signers that consented to data processing, this identifies the exact terms they agreed to. Only present for organisations that use data processing consent. |
country | Signer's country, if it was explicitly included on the signature form. Otherwise, the organisation's country, if one is set. |
country_without_fallback | Signer's country, if it was explicitly included on the signature form. Otherwise, null . |
created_at | When the member signed the petition |
deleted_at | If the signer clicked the "This wasn't me" link to delete their signature, this records the time when that happened. A null indicates the signature was not deleted. |
Signer's email address | |
email_opt_in_type | Details on the way in which the signer was asked to opt in to email updates from the petition and organisation. May be omitted only if join_organisation is null . |
eu_data_processing_consent | Whether the signer consented to having their data processed as part of this signature. A false or null likely indicates that the member had already consented under the current terms and did not need to give additional consent for data processing. Only present for organisations that use data processing consent. |
first_name | Signer's first or given name |
from_embed | Boolean, true if the member signed via an embedded form |
id | Unique internal ID for this signature. This ID can be used in certain other API calls. |
join_organisation | Whether the signer opted in to email updates from the petition and organisation |
last_name | Signer's last or family name |
last_signed_at | When the member signed the petition |
member | Unique ID and creation time of the member record for the signer's email address |
message_to_target | If the petition was configured to ask members to write messages to a decision maker, this includes the subject line, text, and moderation information about the signer's message. May be omitted if the petition is not configured for messages to a decision maker. |
new_mobile_subscriber | If this is true, it means the signature resulted in the member being subscribed to SMS messages, and they were not previously subscribed. |
opt_in_sms | Whether the signer opted in to SMS updates from the organisation |
partnership_opt_ins | Array of objects describing whether the signer opted in to email updates from partnerships associated with the petition. Each object in the array identifies a partnership by unique slug , and indicates whether the signer opted_in . |
petition | Basic identifying information about the petition this signature belongs to, including its slug and the petition page URL. May also include identifying information on any effort, landing page, or partnerships the petition is associated with. |
phone_number | Signer's phone number |
postcode | Signer's postal or zip code |
sms_opt_in_type | Details on the way in which the signer was asked to opt in to SMS updates. May be omitted only if opt_in_sms is null . |
source | Legacy tracking field; source parameter that was in the petition page URL when the member signed |
token | Unique token identifier for this signature, used in some URLs |
unsubscribe_at | If the signer has unsubscribed from petition updates, this records the time when that happened. A null indicates that the signer has not unsubscribed. |
updated_at | When the signature record was last updated. If it has never been updated, this will be the same as created_at . |
user_agent | User Agent string indicating what kind of web browser the member signed from |
user_ip | IP address the member signed from |
utm_campaign | Tracking field; utm_campaign parameter that was in the petition page URL when the member signed |
utm_content | Tracking field; utm_content parameter that was in the petition page URL when the member signed |
utm_medium | Tracking field; utm_medium parameter that was in the petition page URL when the member signed |
utm_source | Tracking field; utm_source parameter that was in the petition page URL when the member signed |
utm_term | Tracking field; utm_term parameter that was in the petition page URL when the member signed |
Create
POST /api/v1/petitions/no-taxes-on-tea/signatures/
{
"signature": {
"first_name": "Foo",
"last_name": "Bar",
"email": "foo@bar.com",
"postcode": "10010",
"phone_number": "555-555-5555",
"country": "US",
"email_opt_in_type_external_id": "abcd12345",
"join_organisation": true
}
}
Response body
{
"signature": {
"bucket": "",
"country_without_fallback": "US",
"from_embed": null,
"last_signed_at": "2020-02-01T10:00:00Z",
"user_agent": null,
"deleted_at": null,
"additional_fields": {},
"id": 123,
"country": "US",
"created_at": "2020-02-01T10:00:00Z",
"updated_at": "2020-02-01T10:00:00Z",
"unsubscribe_at": null,
"email": "foo@bar.com",
"confirmed_at": null,
"confirmed_reason": null,
"first_name": "Foo",
"join_organisation": true,
"last_name": "Bar",
"phone_number": "555-555-5555",
"postcode": "10010",
"source": "",
"token": "abcDEFghi123456789jklMNO",
"user_ip": "123.123.123.123",
"utm_campaign": null,
"utm_content": null,
"utm_medium": null,
"utm_source": null,
"utm_term": null,
"eu_data_processing_consent": false,
"consent_content_version": null,
"data_processing_consent_type": "explicit",
"partnership_opt_ins": [],
"email_opt_in_type": {
"context": "external",
"kind": "external",
"mailable": true,
"external_id": "abcd12345",
"active": false
},
"member": {
"id": 111222333,
"created_at": "2020-02-01T10:00:00Z"
},
"petition": {
"url": "https://your-organisation.controlshiftlabs.com/petitions/no-taxes-on-tea",
"slug": "no-taxes-on-tea"
}
}
}
Creates a new signature for the specified petition. This API endpoint can be used to record externally collected signatures for a particular petition within your instance of the ControlShift platform.
POST /api/v1/petitions/no-taxes-on-tea/signatures/
All of the same post-signature actions that the platform usually supports including webhooks, CRM syncs, and other integrations will still be triggered, in the same way they are for normal signatures.
For organisations that require email confirmation, a confirmation email will be sent to the action taker.
The request body should be a JSON block containing one "signature"
object, which can have the following properties:
Field | Description | Required? |
---|---|---|
first_name |
The first name of the member who took action | yes |
last_name | The last name of the member who took action | yes |
The email address of the member who took action | yes | |
postcode | The postal or zip code of the member. Depending on your configuration, this may be validated for your country. | Depends on platform settings |
phone_number | The phone number of the member who took action | Depends on platform settings |
locale | The language and localization setting of the member who has taken action. This is specified as an ISO_639-1 two-character language code combined with an optional ISO 3166 two character country code. For eg en-US or en . |
no; default is en |
country | The country specified as an ISO 3166 two character country code. | Depends on platform settings |
email_opt_in_type_external_id | The External ID for the ControlShift email opt in type to use for this action. The email opt in type must have "external" context. You can list available email opt in types with this endpoint. | yes |
join_organisation | Whether or not the member has opted in to email communications from this campaign and organisation. | yes, unless "defer opt-in to confirmation email" is enabled |
partnership_opt_ins | For partnership campaigns, whether or not this member has opted into communication from each partnership. This should be a JSON object like {"123": true, "456": false} , mapping partnership IDs to the opt-in status. |
no |
eu_data_processing_consent | Whether or not member has given consent for GDPR data processing. This field must be true if the organisation requires data processing consent for user actions with use_eu_data_processing_consent = true . |
yes, if data processing consent is enabled |
consent_content_version_external_id | The External ID for the data processing consent content version the member has consented to on this action. The consent content version tracks what the privacy policy, terms of service, and checkbox label were at the time of this action. You can list available consent content versions via this endpoint. | yes, if data processing consent is enabled |
utm_source | UTM tracking field | no |
utm_campaign | UTM tracking field | no |
utm_content | UTM tracking field | no |
utm_medium | UTM tracking field | no |
utm_term | UTM tracking field | no |
Update email opt in type
Updates the email opt in type associated with a single signature. This can be used to change the email opt in type
if a more legally compliant form of consent was gathered elsewhere. The email opt in type for the signature is updated
to the opt in type specified by external_id
.
No history is stored recording the original consent type, it is replaced by the new one specified in the API call.
POST /api/v1/petitions/no-taxes-on-tea/signatures/123
POST body
external_id=foo
Destroy
DELETE response body for Destroy
{
"signature": {
"deleted": true,
"id": 123,
"email": "foo@bar.com"
}
}
Permanently deletes the signature with the specified ID. Note that this will decrement the signature count on the petition.
DELETE /api/v1/petitions/no-taxes-on-tea/signatures/123
List
GET response body for List
{
"petition": {
"title": "No Taxes on Tea!",
"slug": "no-taxes-on-tea"
},
"signatures": [
{
"bucket": "",
"country_without_fallback": "US",
"from_embed": null,
"last_signed_at": "2020-02-01T10:00:00Z",
"user_agent": null,
"deleted_at": null,
"additional_fields": {},
"id": 123,
"country": "US",
"created_at": "2020-02-01T10:00:00Z",
"updated_at": "2020-02-01T10:00:00Z",
"unsubscribe_at": null,
"email": "foo@bar.com",
"confirmed_at": null,
"confirmed_reason": null,
"first_name": "Foo",
"join_organisation": true,
"last_name": "Bar",
"phone_number": "555-555-5555",
"postcode": "10010",
"source": "",
"token": "abcDEFghi123456789jklMNO",
"user_ip": "123.123.123.123",
"utm_campaign": null,
"utm_content": null,
"utm_medium": null,
"utm_source": null,
"utm_term": null,
"eu_data_processing_consent": false,
"consent_content_version": null,
"data_processing_consent_type": "explicit",
"partnership_opt_ins": [],
"email_opt_in_type": {
"context": "web_form",
"kind": "radio_sure",
"mailable": true,
"external_id": null,
"active": true
},
"member": {
"id": 111222333,
"created_at": "2020-02-01T10:00:00Z"
},
"message_to_target": {
"admin_reason": null,
"admin_status": "approved",
"administered_at": "2020-02-01T10:00:19Z",
"subject": "King George, Don't Tax our Tea!",
"text": "We don't want taxation without representation."
},
"petition": {
"url": "https://your-organisation.controlshiftlabs.com/petitions/no-taxes-on-tea",
"slug": "no-taxes-on-tea"
}
}
],
"meta": {
"current_page": 2,
"total_pages": 2,
"previous_page": 1,
"next_page": null
}
}
List the signatures that are part of a Petition.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
Hash | Description |
---|---|
meta | pagination information |
petition | basic information about the petition |
signatures | an array of signature results |
GET /api/v1/petitions/no-taxes-on-tea/signatures?page=2
Unsubscribe
POST response body for Unsubscribe
{
"signature": {
"unsubscribed": true,
"notice": "You have successfully unsubscribed from the petition.",
"id": 123,
"email": "foo@bar.com"
}
}
Unsubscribes a member who has signed the petition from any emails sent by petition creator. They will still receive transactional emails.
POST /api/v1/petitions/no-taxes-on-tea/signatures/123/unsubscribe
Efforts and Landing Pages
Efforts and Landing Pages are both collections of related petitions. In efforts, each petition must be associated with a specific decision maker or objective, while landing pages allow for simply grouping petitions together without the structure of decision makers or objectives.
Within the API, both Efforts and Landing Pages use the efforts
URLs.
Create Petition
POST /api/v1/efforts/hardware-stores-stop-selling-bee-poison/petitions
{
"petition": {
"title": "Acme Hardware, Stop Selling Bee Poison!",
"targets": [
{
"name": "Joe Hammer",
"context": "Manager at Acme Hardware"
}
],
"what": "Stop selling pesticides containing neonicotinoids, which the EPA has found harmful to bees.",
"why": "<strong>Pollinators are important.</strong>",
"categories": "Environment",
"location": {
"street_number": 123,
"street": "Main St",
"locality": "Chicago",
"region": "IL",
}
}
}
Response body
{
"status": "success",
"petition": {
"admin_events_status": "auto",
"admin_status": "good",
"alias": null,
"campaigner_contactable": true,
"can_download_signers": true,
"created_at": "2019-06-01T21:32Z",
"custom_goal": null,
"delivery_details": null,
"external_facebook_page": null,
"external_site": null,
"goal": 100,
"hide_petition_creator": false,
"hide_recent_signers": false,
"hide_signature_form": false,
"id": 123,
"launched": true,
"locale": "en",
"petition_creator_name_override": null,
"redirect_to": null,
"show_progress_bar": true,
"signature_count_add_amount": null,
"slug": "acme-hardware-stop-selling-bee-poison-23",
"source": "API",
"title": "Acme Hardware, Stop Selling Bee Poison!",
"updated_at": "2019-06-01T21:32Z",
"what": "Stop selling pesticides containing neonicotinoids, which the EPA has found harmful to bees.",
"rich_what": "<div>Stop selling pesticides containing neonicotinoids, which the EPA has found harmful to bees.</div>",
"who": null,
"why": "Pollinators are important.",
"rich_why": "<div><strong>Pollinators are important.</strong></div>",
"title_locked": false,
"what_locked": true,
"who_locked": false,
"why_locked": false,
"delivery_details_locked": null,
"external_facebook_page_locked": null,
"external_site_locked": null,
"categories_locked": null,
"url": "https://demo.controlshiftlabs.com/petitions/acme-hardware-stop-selling-bee-poison-23",
"public_who": "Joe Hammer, Manager at Acme Hardware",
"ended": false,
"successful": false,
"image": null,
"public_signature_count": 0,
"creator": null,
"mentor": null,
"reviewer": null,
"location": {
"latitude": 12.345,
"longitude": -34.567,
"postal_code": "12345",
"country": "US",
"region": "IL",
"locality": "Chicago",
"query": "Acme Hardware, 123 Main St, Chicago, IL",
"street": "Main St",
"street_number": 123,
"created_at": "2019-06-01T21:32Z"
},
"decision_makers": [
{
"name": "Joe Hammer",
"slug": "joe-hammer",
"context": "Manager at Acme Hardware",
}
],
"effort": {
"slug": "hardware-stores-stop-selling-bee-poison"
},
"partnership": null,
"labels": [],
"categories": [
{
"slug": "environment",
"name": "Environment"
}
]
}
}
Create a new petition in the effort or landing page.
POST /api/v1/efforts/hardware-stores-stop-selling-bee-poison/petitions
If a creator
is specified, that user will become the petition's leader, and will receive an email asking them to confirm that they are leading the petition.
Once the leader confirms, the petition will work the same way as a petition created via the web UI.
The URL in the response is the public petition page where supporters can sign the petition.
To update a petition after creation, use the returned slug
with the Petition Update endpoint.
Depending on how the effort or landing page is configured, several attributes may be set automatically on the created petition:
- Image
- Whether to show the progress bar
- Whether any of the title, who, what, or why fields are locked
The request body should be a JSON block containing one "petition"
object, which can have the following properties:
Field | Type | Description | Required? |
---|---|---|---|
campaigner_contactable | Boolean | Whether members of the public can contact the petition creator via the public petition page | no; default is true |
categories | List of Strings | Names of categories that should be applied to the new petition, in addition to any default categories configured for the effort/landing page. Must be categories that already exist. If category names are translated, the original/main language names should be used here. | no; default is the categories configured on the effort/landing page |
creator | User block (see above) | User who will be the petition leader. If a user account does not already exist with this email, we will create one. | no; default is no leader |
delivery_details | String | How the petition will be delivered | no |
external_facebook_page | String | URL of a facebook page for the campaign | no |
external_site | String | URL of a website about the campaign | no |
group[slug] | String | Unique identifier for an existing group this petition should be associated with | no |
hide_petition_creator | Boolean | Whether to suppress the display of the petition creator’s name on the petition page | no; default is false |
hide_recent_signers | Boolean | Whether to suppress the "recently signed" and "recently shared" display | no; default is false |
labels | List of Strings | Labels that should be applied to the new petition. Any labels that do not already exist will be created | no |
locale | String | The ISO 639-1 two-letter code for the language the petition content is in. Must be one of the languages supported by ControlShift. | no; defaults to organisation's default language |
location | Location block (see above) | Information about the location associated with the petition. If latitude and longitude are included, we’ll use them. Otherwise, we’ll geocode the fields provided. | no; default is the location of the decision maker or objective in an effort, or no location in a landing page |
objective[slug] | String | Unique identifier for an existing objective this petition should be associated with. Only available within an "objectives" effort, and must be one of the objectives configured for that effort. | only if the effort is an objectives effort |
partnership[slug] | String | Unique identifier for an existing partnership this petition should be associated with | no |
petition_creator_name_override | String | Name to display on the petition page instead of the creator's account name | no |
region[slug] | String | Unique identifier for an existing region this petition should be associated with | no |
source | String | Optional, open-ended string representing where this petition came from | no; default is "API" |
targets | List of 0-1 Target blocks (see above) | Decision makers for this petition. If a block includes a slug , we’ll use it to look up an existing decision maker. Otherwise, we’ll try to create a new one. In a decision-makers-based effort, the specified decision maker will be added to the effort if it is not already associated with it. Any decision maker created this way will be set to "published". Specifying more than one decision maker is not yet supported. |
no; default is no decision maker |
title | String | Title of the petition. If a title is specified, it will be used, even if there is a different default. | yes, unless a default title has been specified for the effort/landing page |
what | String | Text of the petition ask in HTML format. If a value is specified, it will be used, even if there is a different default. | yes, unless a default "what" has been specified for the effort/landing page |
who | String | Who the petition is addressed to. If a value is specified, it will be used, even if there is a different default. | yes, unless a default "who" has been specified for the effort/landing page or targets includes a decision maker |
why | String | Text explaining why the petition is important in HTML format. If a value is specified, it will be used, even if there is a different default. | yes, unless a default "why" has been specified for the effort/landing page |
Partnerships
Partnerships are other organisations you collaborate with using the platform.
List
GET response body
{
"partnerships": [
{
"id": 99,
"slug": "fight-fascism",
"title": "Fight Fascism",
"description": "Fighting fascism one day at a time. With petitions.",
"external_id": "no-fascism-123",
"created_at": "2015-12-02T01:43:17Z",
"updated_at": "2015-12-02T01:43:17Z",
"url": "https://demo.controlshiftlabs.com/partnerships/fight-fascism",
"image": {
"urls": {
"open_graph": "http://cdn.example.com/partnerships/images/25/open_graph/a-little-teapot.png?1516647704",
"horizontal": "http://cdn.example.com/partnerships/images/25/horizontal/a-little-teapot.png?1516647704",
"hero": "http://cdn.example.com/partnerships/images/25/hero/a-little-teapot.png?1516647704",
"original": "http://cdn.example.com/partnerships/images/25/original/a-little-teapot.png?1516647704",
"form": "http://cdn.example.com/partnerships/images/25/form/a-little-teapot.png?1516647704"
}
}
},
{
"id": 107,
"slug": "save-whales",
"title": "Save the Whales",
"description": "We're devoted to all policies that are good for whales.",
"external_id": null,
"created_at": "2018-01-02T12:32:45Z",
"updated_at": "2018-12-22T01:43:17Z",
"url": "https://demo.controlshiftlabs.com/partnerships/save-whales",
"image": null
}
],
"meta": {
"current_page": 1,
"total_pages": 1,
"previous_page": null,
"next_page": null
}
}
Get a paginated list of all partnerships.
GET /api/v1/partnerships?page=1
Show
GET response Body
{
"partnership": {
"id": 99,
"slug": "fight-fascism",
"title": "Fight Fascism",
"description": "Fighting fascism one day at a time. With petitions.",
"external_id": "no-fascism-123",
"created_at": "2015-12-02T01:43:17Z",
"updated_at": "2015-12-02T01:43:17Z",
"url": "https://demo.controlshiftlabs.com/partnerships/fight-fascism",
"image": {
"urls": {
"open_graph": "http://cdn.example.com/partnerships/images/25/open_graph/a-little-teapot.png?1516647704",
"horizontal": "http://cdn.example.com/partnerships/images/25/horizontal/a-little-teapot.png?1516647704",
"hero": "http://cdn.example.com/partnerships/images/25/hero/a-little-teapot.png?1516647704",
"original": "http://cdn.example.com/partnerships/images/25/original/a-little-teapot.png?1516647704",
"form": "http://cdn.example.com/partnerships/images/25/form/a-little-teapot.png?1516647704"
}
}
}
}
Find information about a partnership by URL slug.
GET /api/v1/partnerships/fight-fascism
The slug
used to identify a partnership for API purposes is the same as the slug
that appears in member-facing URLs.
For example, for a partnership whose member-facing hub is at https://demo.controlshiftlabs.com/partnerships/new-york-for-clean-water
,
the partnership slug
is new-york-for-clean-water
.
List Petitions
GET response Body
{
"partnership": {
"title": "Fight Fascism",
"slug": "fight-fascism"
},
"petitions": [
{
"id": 123,
"slug": "no-taxes-on-tea",
"title": "No Taxes on Tea",
...
},
{
"id": 124,
"slug": "stop-burning-coal-1",
"title": "Stop Burning Coal",
...
},
...
],
"meta": {
"current_page": 1,
"total_pages": 12,
"previous_page": null,
"next_page": 2
}
}
Retrieves a paginated list of a partnership's petitions.
GET /api/v1/partnerships/fight-fascism/petitions
List Events
GET response Body
{
"partnership": {
"title": "Fight Fascism",
"slug": "fight-fascism"
},
"events": [
{
"id": 123,
"slug": "house-party",
"title": "House Party for Justice",
...
},
{
"id": 124,
"slug": "big-rally",
"title": "A big rally",
...
},
...
],
"meta": {
"current_page": 1,
"total_pages": 12,
"previous_page": null,
"next_page": 2
}
}
Retrieves a paginated list of a partnership's events.
GET /api/v1/partnerships/fight-fascism/events
Events
Events represent real-world or virtual meetings that members can RSVP to. RSVP records are called "attendees".
The authenticated REST API endpoints for events only deal with events created in ControlShift, whether through the web or the API. Events imported from another system (such as Mobilize America or EveryAction/VAN) are not included.
List
GET response body
{
"events": [
{
"slug": "deliver-the-petition-to-the-wizard",
"title": "Deliver the Petition to the Wizard",
"url": "https://demo.controlshiftlabs.com/events/deliver-the-petition-to-the-wizard",
"description": "Join us as we travel to the Emerald City to deliver our petition to the Wizard.",
"start_at": "2018-03-01T12:00:00Z",
"end_at": "2018-03-01T13:30:00Z",
"start_in_zone": "2018-03-01T12:00:00-05:00",
"end_in_zone": "2018-03-01T13:30:00-05:00",
"time_zone": "America/New_York",
"virtual": false,
"launched_at": "2018-02-03T12:34:56Z",
"locale": "en-US",
"host_address": null,
"max_attendees_count": null,
"external_id": "control-shift-deliver-the-petition-to-the-wizard",
"host": {
"member_id": 123,
"full_name": "Dorothy of Kansas",
"email": "dorothy@example.com",
"phone_number": "555-555-5555",
"postcode": "66012"
},
"location": {
"query": "wizard room",
"latitude": "42.3376368",
"longitude": "-70.99304",
"street_number": "",
"street": "",
"region": "MA",
"postal_code": "",
"country": "US",
"created_at": "2017-11-16T22:38:29Z"
},
"location_venue": "The Wizard's Throne Room",
"shifts": [
{
"id": "103",
"name": "Lunch",
"start_at": "2018-03-01T12:00:00Z",
"end_at": "2018-03-01T13:00:00Z"
},
{
"id": "105",
"name": "Demonstration",
"start_at": "2018-03-01T13:00:00Z",
"end_at": "2018-03-01T13:30:00Z"
}
],
"petition": {
"slug": "repair-the-yellow-brick-road-1",
"url": "https://demo.controlshiftlabs.com/petitions/repair-the-yellow-brick-road-1"
},
"labels": ["oz-organizer-help"]
},
...
],
"meta": {
"current_page": 1,
"total_pages": 1,
"previous_page": null,
"next_page": null
}
}
Returns a paginated list of all events, including ones that are unlaunched or otherwise not visible to the public. External events are not included.
The response includes all the same data as the single-event endpoint for each event.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
GET /api/v1/events?page=1
Show
GET response Body
{
"event": {
"slug": "deliver-the-petition-to-the-wizard",
"title": "Deliver the Petition to the Wizard",
"url": "https://demo.controlshiftlabs.com/events/deliver-the-petition-to-the-wizard",
"description": "Join us as we travel to the Emerald City to deliver our petition to the Wizard.",
"start_at": "2018-03-01T12:00:00Z",
"end_at": "2018-03-01T13:30:00Z",
"start_in_zone": "2018-03-01T12:00:00-05:00",
"end_in_zone": "2018-03-01T13:30:00-05:00",
"time_zone": "America/New_York",
"virtual": false,
"launched_at": "2018-02-03T12:34:56Z",
"locale": "en-US",
"host_address": null,
"max_attendees_count": null,
"external_id": "control-shift-deliver-the-petition-to-the-wizard",
"host": {
"member_id": 123,
"full_name": "Dorothy of Kansas",
"email": "dorothy@example.com",
"phone_number": "555-555-5555",
"postcode": "66012"
},
"location": {
"query": "wizard room",
"latitude": "42.3376368",
"longitude": "-70.99304",
"street_number": "",
"street": "",
"region": "MA",
"postal_code": "",
"country": "US",
"created_at": "2017-11-16T22:38:29Z"
},
"location_venue": "The Wizard's Throne Room",
"mentor": {
"member_id": 456,
"full_name": "Glinda the Good",
"email": "glinda@example.com",
"phone_number": "555-555-5555",
"postcode": "12345"
},
"petition": {
"slug": "repair-the-yellow-brick-road-1",
"url": "https://demo.controlshiftlabs.com/petitions/repair-the-yellow-brick-road-1"
},
"labels": ["oz-organizer-help"]
}
}
Find information about an event by URL slug.
GET /api/v1/events/deliver-the-petition-to-the-wizard
The slug
used to identify a event for API purposes is the same as the slug
that appears in the member-facing event page URL.
For example, for a event whose event page is at https://demo.controlshiftlabs.com/events/chapter-meeting-1
,
the event slug
is chapter-meeting-1
.
Create
POST /api/v1/events
{
"event": {
"title": "March Against Gun Violence",
"description": "We're going to meet at the park and march to City Hall. Bring a sign!",
"start_at": "2025-06-01T15:00-05:00",
"end_at": "2025-06-01T17:30-05:00",
"time_zone": "America/Chicago",
"host": {
"first_name": "Gabrielle",
"last_name": "Giffords",
"email": "gabrielle.giffords@example.com"
},
"location": {
"street_number": "123",
"street": "Main Street",
"locality": "Kansas City",
"region": "KS",
"postal_code": "12345",
"country": "US"
},
"location_venue": "City Park",
"event_type": {
"id": 12
},
"group": {
"slug": "coalition-for-gun-reform"
},
"labels": [
"gun-control topic",
"congress"
],
"region": {
"slug": "new-york-city"
}
}
}
Response body
{
"status": "success",
"event": {
"slug": "march-against-gun-violence-1",
"title": "March Against Gun Violence",
"url": "https://demo.controlshiftlabs.com/events/march-against-gun-violence-1",
"description": "We're going to meet at the park and march to City Hall. Bring a sign!",
"start_at": "2025-06-01T20:00Z",
"end_at": "2025-06-01T22:30Z",
"start_in_zone": "2025-06-01T15:00-05:00",
"end_in_zone": "2025-06-01T17:30-05:00",
"time_zone": "America/Chicago",
"virtual": false,
"launched_at": "2025-05-23T21:32Z",
"locale": "en-US",
"host_address": null,
"max_attendees_count": null,
"host": {
"member_id": 234,
"full_name": "Gabrielle Giffords",
"email": "gabrielle.giffords@example.com",
"phone_number": null,
"postcode": null
},
"location": {
"latitude": 39.168100,
"longitude": -94.776063,
"postal_code": "12345",
"country": "US",
"region": "KS",
"locality": "Kansas City",
"query": "City Park, 123 Main Street, Kansas City, KS 12345, US",
"street": "Main Street",
"street_number": "123",
"created_at": "2025-05-23T21:32Z",
"extra_location_info": null
},
"location_venue": "City Park",
"event_type": {
"name": "Protest"
},
"shifts": [],
"local_chapter": {
"name": "Coalition for Gun Reform",
"slug": "coalition-for-gun-reform",
"url": "https://demo.controlshiftlabs.com/groups/coalition-for-gun-reform"
},
"labels": [
"gun-control topic",
"congress"
],
"campaigner_contactable": true,
"event_host_name_override": null,
"forum_enabled": true,
"hidden_address": false,
"hide_recent_attendees": false,
"redirect_to": null,
"sharing_disabled": false,
"web_conference_url": null,
"mentor": null,
"reviewer": null
}
}
Create a new event.
POST /api/v1/events
If the event is created successfully, the host will receive an email asking them to confirm that they are hosting.
Once the host confirms, the event will work the same way as an event created via the web UI.
The URL in the response is the public event page where members can RSVP.
To update an event after creation, use the returned slug
with the Event Update endpoint.
The request body should be a JSON block containing one "event"
object, which can have the following properties:
Field | Type | Description | Required? |
---|---|---|---|
campaigner_contactable | Boolean | Whether members of the public can contact the event host via the public event page | no; default is true |
description | String | Description of the event | yes |
end_at | String | ISO 8601 date/time that the event ends | no; default is no specific end time |
event_host_name_override | String | Name to display on the event page instead of the host’s account name | no |
event_type[id] | Integer | Unique ID of an existing Event Type that should be set on the event | no |
extra_location_info | String | Information about the event’s location | no |
forum_enabled | Boolean | Whether the forum on the event page should be enabled | no; defaults to organisation setting |
group[slug] | String | Unique identifier for an existing group this event should be associated with | no |
hidden_address | Boolean | Whether the event’s full address should be obscured before RSVP | no; default is false |
hide_recent_attendees | Boolean | Whether to suppress the display of attendee’s names in the "recently shared" display. | no; default is false |
host | User block (see above) | User who will host the event. If a user account does not already exist with this email, we will create one. | yes |
labels | List of Strings | Labels that should be applied to the new event. Any labels that do not already exist will be created | no |
locale | String | The ISO 639-1 two-letter code for the language the event content is in. Must be one of the languages supported by ControlShift. | no; defaults to organisation's default language |
location | Location block (see above) | Information about either the exact location of an in-person event, or the location a virtual event is relevant to. May be omitted entirely for a virtual event that doesn't have a specific audience location. If latitude and longitude are included, we’ll use them. Otherwise, we’ll geocode the fields provided. | only for in-person events |
location_venue | String | Venue name. Only relevant for in-person events | no |
max_attendees_count | Integer | Maximum number of attendees allowed | no |
partnership[slug] | String | Unique identifier for an existing partnership this event should be associated with | no |
region[slug] | String | Unique identifier for an existing region this event should be associated with | no |
start_at | String | ISO 8601 date/time that the event starts | yes |
time_zone | String | tz database code for the event’s time zone (e.g. "America/New_York") | yes |
title | String | Title of the event | yes |
virtual | Boolean | Whether this is a virtual event | no; defaults to the organisation default for new events |
web_conference_url | String | URL for a Zoom or other virtual meeting associated with this event | no |
Update
PUT request body
{
"event": {
"description": "We're going to meet at the park and march to City Hall. Bring a sign!",
"max_attendees_count": null
}
}
Updates the content and/or settings of the event whose slug is in the URL.
PUT /api/v1/events/march-against-gun-violence
The request body should contain only the fields you would like to update, in JSON format. All other fields will be left unchanged.
The fields that can be changed this way are:
Field | Type | Description |
---|---|---|
campaigner_contactable | Boolean | Whether members of the public can contact the event host via the public event page |
description | String | Description of the event |
event_host_name_override | String | Name to display for the event host on the event page. Set to null to use the name from the event host's user account. |
extra_location_info | String | Additional non-address information about the event's location, e.g. "7th floor" or "Park in the back parking lot" |
forum_enabled | Boolean | Enables or disables the discussion forum feature for the event's attendees |
hidden_address | Boolean | Indicates that the event's address is private. The city/locality will be shown on the web, but the full address will only be provided to members who RSVP to the event. |
hide_recent_attendees | Boolean | Do not display the names of people who recently RSVPed in the post-RSVP sharing prompt. |
host_address | String | The mailing address of the event host. May be used to mail event materials. |
location_venue | String | Venue name. Only relevant for in-person events. |
max_attendees_count | Integer | Limit on how many people, besides the host, may sign up for this event. Set to null for no limit. |
redirect_to | String | If set, visitors to the event page will be redirected to this URL |
sharing_disabled | Boolean | Turns off the social sharing prompts for the event |
title | String | Event title |
web_conference_url | String | Video call or web conference tool URL for attendees to access the event |
Update image
PUT request headers
...
Content-Type: "multipart/form-data; boundary=-----------314550593530941214802297487787"
...
PUT request body
-----------------------------314550593530941214802297487787
Content-Disposition: form-data; name="file"; filename="save_the_whales.jpg"
Content-Type: image/jpeg
Content-Length: 249206
<ENCODED BINARY DATA>
-----------------------------314550593530941214802297487787--
The image shown on the event page can be updated by making a multipart PUT request with the image file attached on the file
field.
PUT /api/v1/events/march-against-gun-violence/images/image
To remove the image without replacing it, perform a DELETE request to the same URL.
DELETE /api/v1/events/march-against-gun-violence/images/image
Apply Label
POST request body
{
"name": "We Love Tea!"
}
Applies an existing Label to a specific event.
Note: Currently it is not possible to create Labels via the API. The Label needs to already exist to allow tagging the event with it.
The request body must include a name
attribute with the name of the label to be applied to the event.
POST /api/v1/events/deliver-the-petition-to-the-wizard/labelings
Response on success will not have a body, and status code will be 201 - Created
This example would apply the "We Love Tea!" label to the event.
Remove Label
Removes a Label from the event.
The name of the Label must be included in the URL and be correctly percent encoded as per RFC 3986.
Response on success will not have a body, and status code will be 200 - Ok
DELETE /api/v1/events/deliver-the-petition-to-the-wizard/labelings/We%20Love%20Tea%21
This example would remove the "We Love Tea!" label from the event.
Assign Mentor
PUT request body
{
"mentor": {
"email": "suzie@example.com"
}
}
PUT request response
{
"status": "success",
"data": {
"mentor": {
"email": "suzie@example.com",
"member_id": 123,
"full_name": "Suzie Greenberg",
"phone_number": "555-555-5555",
"postcode": "12345"
}
}
}
Assigns an admin user as the mentor for the event.
The email address specified will be used to look up an admin user who will be assigned as the event's mentor.
PUT /api/v1/events/deliver-the-petition-to-the-wizard/mentor
PUT request body
{
"mentor": null
}
To remove the mentor from the event, the request body should include null
instead of the email block.
Assign Reviewer
To allow delegation of moderation tasks to specific people or teams, the platform allows specifying a reviewer for each event.
Staff are able to filter the moderation queue to just the tasks that have been assigned to them through the ControlShift platform web interface.
PUT request body
{
"reviewer": {
"type": "User",
"email": "suzie@example.com"
}
}
PUT request response
{
"status": "success",
"data": {
"reviewer": {
"type": "User",
"email": "suzie@example.com",
"full_name": "Suzie Greenberg",
"member_id": 123,
"phone_number": "555-555-5555",
"postcode": "12345"
}
}
}
PUT request body
{
"reviewer": {
"type": "Team",
"slug": "moderation-helpers"
}
}
PUT request response
{
"status": "success",
"data": {
"reviewer": {
"type": "Team",
"name": "Moderation Helpers",
"slug": "moderation-helpers"
}
}
}
Assigns an admin user or team as the reviewer for the event that is pending moderation.
The email address or team slug will be used to look up an admin user or team that will be assigned to review the event.
PUT /api/v1/events/deliver-the-petition-to-the-wizard/reviewer
PUT request body
{
"reviewer": null
}
To remove the reviewer from the event, the request body should include null
instead of the email or slug block.
Attendees
Attendees are member RSVPs to an Event. All attendee actions are namespaced by URL underneath the events that the attendee was recorded against.
Lookup
GET response body for Lookup
{
"attendee": {
"notification_level": "all_messages",
"attending_status": "attending",
"additional_fields": {},
"id": 123,
"country": "US",
"created_at": "2020-02-01T10:00:00Z",
"updated_at": "2020-02-01T10:00:00Z",
"unsubscribe_at": null,
"email": "foo@bar.com",
"confirmed_at": null,
"confirmed_reason": null,
"first_name": "Foo",
"join_organisation": true,
"last_name": "Bar",
"phone_number": "123-456",
"postcode": "10010",
"source": "",
"token": "abcDEFghi123456789jklMNO",
"user_ip": "123.123.123.123",
"utm_campaign": "",
"utm_content": "",
"utm_medium": "",
"utm_source": "",
"utm_term": "",
"eu_data_processing_consent": false,
"consent_content_version": null,
"partnership_opt_ins": [
{
"partnership": {
"slug": "the-lollipop-guild"
},
"opted_in": true
},
{
"partnership": {
"slug": "scarecrows-for-infrastructure-improvements"
},
"opted_in": false
}
],
"email_opt_in_type": {
"context": "web_form",
"kind": "radio_sure",
"mailable": true,
"external_id": null,
"active": true
},
"member": {
"id": 111222333,
"created_at": "2020-02-01T10:00:00Z"
},
"event": {
"url": "https://your-organisation.controlshiftlabs.com/events/deliver-the-petition-to-the-wizard",
"slug": "deliver-the-petition-to-the-wizard"
}
}
}
Find an attendee by email address. Once you have obtained the attendee's id you can use this identifier in other API calls.
GET /api/v1/events/deliver-the-petition-to-the-wizard/attendees/lookup?email=foo@bar.com
Create
POST /api/v1/events/deliver-the-petition-to-the-wizard/attendees
{
"attendee": {
"attending_status": "attending",
"notification_level": "all_messages",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@lovelace.com",
"postcode": "12345",
"phone_number": "555-555-555",
"locale": "en-GB",
"country": "GB",
"email_opt_in_type_external_id": "canvass-signature-2022",
"join_organisation": true,
"partnership_opt_ins": {
"123": true,
"456": false
},
"eu_data_processing_consent": true,
"consent_content_version_external_id": "revised-terms-2022-february",
"utm_source": "facebook",
"utm_medium": "button",
"utm_campaign": "infrastructure-petition-delivery-2022",
"utm_term": null,
"utm_content": null
}
}
Response body
{
"attendee": {
"notification_level": "none",
"attending_status": "attending",
"additional_fields": {},
"id": 880,
"country": "US",
"created_at": "2020-08-24T14:08:53Z",
"updated_at": "2020-09-17T20:36:36Z",
"unsubscribe_at": null,
"email": "ada@lovelace.com",
"confirmed_at": null,
"confirmed_reason": null,
"first_name": "Ada",
"join_organisation": true,
"last_name": "Lovelace",
"new_mobile_subscriber": null,
"phone_number": null,
"postcode": "12345",
"source": "",
"token": "efxqQSMHj1UidVLppu3eT17H",
"user_ip": null,
"utm_campaign": null,
"utm_content": null,
"utm_medium": null,
"utm_source": null,
"utm_term": null,
"join_group": null,
"eu_data_processing_consent": true,
"partnership_opt_ins": [],
"consent_content_version": {
"id": 55,
"external_id": null,
"consent_type": "explicit"
},
"data_processing_consent_type": "explicit",
"email_opt_in_type": {
"context": "external",
"kind": "external",
"mailable": true,
"external_id": "canvass-signature-2022",
"active": false
},
"member": {
"id": 1124,
"created_at": "2019-08-28T20:50:38Z"
},
"event": {
"url": "https://your-organisation.controlshiftlabs.com/events/deliver-the-petition-to-the-wizard",
"slug": "deliver-the-petition-to-the-wizard"
}
}
}
Creates a new attendee for the specified event. This API endpoint can be used to record externally collected RSVPs for a particular event within your instance of the ControlShift platform.
POST /api/v1/events/deliver-the-petition-to-the-wizard/attendees/
All of the same post-RSVP actions that the platform usually supports including webhooks, CRM syncs, and other integrations will still be triggered, in the same way they are for normal RSVPs.
For organisations that require email confirmation, a confirmation email will be sent to the action taker.
The request body should be a JSON block containing one "attendee"
object, which can have the following properties:
Field | Description | Required? |
---|---|---|
attending_status | Whether the member is attending the event. One of attending , not_attending , attended , confirmed , or no_show . |
yes |
notification_level | Email notification preference for the event forum. One of all_messages (an email for every post), none (no emails), or pending (preference will be determined during email confirmation). |
yes |
first_name |
The first name of the member who took action | yes |
last_name | The last name of the member who took action | yes |
The email address of the member who took action | yes | |
postcode | The postal or zip code of the member. Depending on your configuration, this may be validated for your country. | Depends on platform settings |
phone_number | The phone number of the member who took action | Depends on platform settings |
locale | The language and localization setting of the member who has taken action. This is specified as an ISO_639-1 two-character language code combined with an optional ISO 3166 two character country code. For eg en-US or en . |
no; default is en |
country | The country specified as an ISO 3166 two character country code. | Depends on platform settings |
email_opt_in_type_external_id | The External ID for the ControlShift email opt in type to use for this action. The email opt in type must have "external" context. You can list available email opt in types with this endpoint. | yes |
join_organisation | Whether or not the member has opted in to email communications from this campaign and organisation. | yes, unless "defer opt-in to confirmation email" is enabled |
partnership_opt_ins | For partnership campaigns, whether or not this member has opted into communication from each partnership. This should be a JSON object like {"123": true, "456": false} , mapping partnership IDs to the opt-in status. |
no |
eu_data_processing_consent | Whether or not member has given consent for GDPR data processing. This field must be true if the organisation requires data processing consent for user actions with use_eu_data_processing_consent = true . |
yes, if data processing consent is enabled |
consent_content_version_external_id | The External ID for the data processing consent content version the member has consented to on this action. The consent content version tracks what the privacy policy, terms of service, and checkbox label were at the time of this action. You can list available consent content versions via this endpoint. | yes, if data processing consent is enabled |
utm_source | UTM tracking field | no |
utm_campaign | UTM tracking field | no |
utm_content | UTM tracking field | no |
utm_medium | UTM tracking field | no |
utm_term | UTM tracking field | no |
Update email opt in type
Updates the email opt in type associated with a single attendee. This can be used to change the email opt in type
if a more legally compliant form of consent was gathered elsewhere. The email opt in type for the attendee is updated
to the opt in type specified by external_id
.
No history is stored recording the original consent type, it is replaced by the new one specified in the API call.
POST /api/v1/events/deliver-the-petition-to-the-wizard/attendees/123
POST body
external_id=foo
Destroy
DELETE response body for Destroy
{
"attendee": {
"deleted": true,
"id": 123,
"email": "foo@bar.com"
}
}
Permanently deletes the attendee with the specified ID.
DELETE /api/v1/events/deliver-the-petition-to-the-wizard/attendees/123
List
List the attendees that have RSVPed to an Event.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
Hash | Description |
---|---|
meta | pagination information |
event | basic information about the event |
attendees | an array of attendee results |
GET /api/v1/events/deliver-the-petition-to-the-wizard/attendees?page=2
Unsubscribe
POST response body for Unsubscribe
{
"attendee": {
"unsubscribed": true,
"notice": "You have successfully unsubscribed from the event.",
"id": 123,
"email": "foo@bar.com"
}
}
Unsubscribes a member who has RSVPed to the event from any emails sent by event host. They will still receive transactional emails.
POST /api/v1/events/deliver-the-petition-to-the-wizard/attendees/123/unsubscribe
Calendars
Calendars are collections of related events.
Show
GET response Body
{
"calendar": {
"name": "Movie Screenings For Healthcare Access",
"description": "We're watching this amazing and powerful movie about the fight for health care for all.",
"slug": "movie-screenings-for-healthcare-access",
"url": "https://demo.controlshiftlabs.com/calendars/movie-screenings-for-healthcare-access",
"partnership": {
"slug": "space-captains-for-health-care",
"url": "https://demo.controlshiftlabs.com/partnerships/space-captains-for-health-care"
}
}
}
Find information about a calendar by URL slug.
GET /api/v1/calendars/movie-screenings-for-healthcare-access
The slug
used to identify a calendar for API purposes is the same as the slug
that appears in member-facing URLs.
For example, for a calendar whose member-facing hub is at https://demo.controlshiftlabs.com/calendars/day-of-action-for-justice
,
the calendar slug
is day-of-action-for-justice
.
List Events
List the events that are part of a Calendar.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
Hash | Description |
---|---|
meta | pagination information |
calendar | basic information about the calendar |
events | an array of event results |
GET /api/v1/calendars/movie-screenings-for-healthcare-access/events?page=2
Create Event
POST /api/v1/calendars/movie-screenings-for-healthcare-access/events
{
"event": {
"title": "Movie Screening at the Community Center",
"description": "We're going to watch this amazing movie",
"start_at": "2025-06-01T15:00-05:00",
"end_at": "2025-06-01T17:30-05:00",
"time_zone": "America/Chicago",
"host": {
"first_name": "Cori",
"last_name": "Bush",
"email": "cori.bush@example.com"
},
"host_address": "Congressional Office, 6724-A Page Ave., St. Louis, MO 63133",
"location": {
"street_number": "123",
"street": "Main Street",
"locality": "Kansas City",
"region": "KS",
"postal_code": "12345",
"country": "US"
},
"location_venue": "Community Center",
"extra_location_info": "We'll be in the multi-purpose room on the first floor.",
"event_type": {
"id": 12
},
"labels": [
"health care"
]
}
}
Response body
{
"status": "success",
"event": {
"slug": "movie-screening-at-the-community-center-2",
"title": "Movie Screening at the Community Center",
"url": "https://demo.controlshiftlabs.com/events/movie-screening-at-the-community-center-2",
"description": "We're going to watch this amazing movie",
"start_at": "2025-06-01T20:00Z",
"end_at": "2025-06-01T22:30Z",
"start_in_zone": "2025-06-01T15:00-05:00",
"end_in_zone": "2025-06-01T17:30-05:00",
"time_zone": "America/Chicago",
"virtual": false,
"launched_at": "2025-05-23T21:32Z",
"locale": "en-US",
"host_address": "Congressional Office, 6724-A Page Ave., St. Louis, MO 63133",
"max_attendees_count": null,
"location": {
"latitude": 39.168100,
"longitude": -94.776063,
"postal_code": "12345",
"country": "US",
"region": "KS",
"locality": "Kansas City",
"query": "Community Center, 123 Main Street, Kansas City, KS 12345, US",
"street": "Main Street",
"street_number": "123",
"created_at": "2025-05-23T21:32Z",
"extra_location_info": "We'll be in the multi-purpose room on the first floor."
},
"location_venue": "Community Center",
"event_type": {
"name": "Movie Screening"
},
"shifts": [],
"calendar": {
"name": "Movie Screenings For Healthcare Access",
"slug": "movie-screenings-for-healthcare-access",
"url": "https://demo.controlshiftlabs.com/calendars/movie-screenings-for-healthcare-access"
},
"labels": [
"health care"
]
}
}
Create a new event in the calendar.
POST /api/v1/calendars/movie-screenings-for-healthcare-access/events
If the event is created successfully, the host will receive an email asking them to confirm that they are hosting.
Once the host confirms, the event will work the same way as an event created via the web UI.
The URL in the response is the public event page where members can RSVP.
To update an event after creation, use the returned slug
with the Event Update endpoint.
The request body should be a JSON block containing one "event"
object, which can have the following properties:
Field | Type | Description | Required? |
---|---|---|---|
campaigner_contactable | Boolean | Whether members of the public can contact the event host via the public event page | no; default is true |
description | String | Description of the event | yes |
end_at | String | ISO 8601 date/time that the event ends | no; default is no specific end time |
event_host_name_override | String | Name to display on the event page instead of the host’s account name | no |
event_type[id] | Integer | Unique ID of an existing Event Type that should be set on the event | no |
extra_location_info | String | Information about the event’s location | no |
forum_enabled | Boolean | Whether the forum on the event page should be enabled | no; defaults to calendar setting or organisation setting |
group[slug] | String | Unique identifier for an existing group this event should be associated with | no |
hidden_address | Boolean | Whether the event’s full address should be obscured before RSVP | no; default is false |
hide_recent_attendees | Boolean | Whether to suppress the display of attendee’s names in the "recently shared" display. | no; default is false |
host | User block (see above) | User who will host the event. If a user account does not already exist with this email, we will create one. | yes |
host_address | String | Host's mailing address | only if the calendar requires it |
labels | List of Strings | Labels that should be applied to the new event. Any labels that do not already exist will be created | no |
locale | String | The ISO 639-1 two-letter code for the language the event content is in. Must be one of the languages supported by ControlShift. | no; defaults to organisation's default language |
location | Location block (see above) | Information about either the exact location of an in-person event, or the location a virtual event is relevant to. May be omitted entirely for a virtual event that doesn't have a specific audience location. If latitude and longitude are included, we’ll use them. Otherwise, we’ll geocode the fields provided. | only for in-person events |
location_venue | String | Venue name. Only relevant for in-person events | no |
max_attendees_count | Integer | Maximum number of attendees allowed | no |
partnership[slug] | String | Unique identifier for an existing partnership this event should be associated with | no |
region[slug] | String | Unique identifier for an existing region this event should be associated with | no |
start_at | String | ISO 8601 date/time that the event starts | yes |
target[slug] | String | Unique identifier for an existing decision maker this event should be associated with. Only relevant for constituency-bound calendars, where it must belong to the decision maker collection associated with the calendar. | only in constituency-bound calendars |
time_zone | String | tz database code for the event’s time zone (e.g. "America/New_York") | yes |
title | String | Title of the event | yes |
virtual | Boolean | Whether this is a virtual event | no; defaults to the organisation default for new events |
web_conference_url | String | URL for a Zoom or other virtual meeting associated with this event | no |
Event Types
List
GET /api/v1/event_types
{
"event_types": [
{
"id": 123,
"name": "Training",
"localized_names": {
"es": "Capacitación",
"fr": "Entraînement"
},
"created_at": "2020-02-01T10:00:00Z",
"updated_at": "2020-02-01T10:00:00Z",
"external_id": "abc-123",
},
{
"id": 125,
"name": "Canvassing",
"localized_names": {
"es": "Campaña puerta a puerta",
"fr": "Démarchage"
},
"created_at": "2020-03-15T12:30:00Z",
"updated_at": "2021-05-01T11:28:13Z",
"external_id": "fde-953",
},
...
],
"meta": {
"current_page": 1,
"total_pages": 3,
"previous_page": null,
"next_page": 2
}
}
Get a list of the event types that have been configured for this organisation. Event types are used for categorizing events (e.g.: House Parties, Fundraising, Training, etc.), and for CRMs supporting event type synchronization, we include the the event's type while syncing.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
GET /api/v1/event_types/?page=1
Hash | Description |
---|---|
meta | pagination information |
event_types | an array of information about the event types |
The external_id
field will only be included if an integration with a CRM supporting event types synchronization is in place.
If the integration with EveryAction/VAN is set up for the organization, the following fields will also be included in the response:
Field | Description |
---|---|
attendee_attended_status_id | ID of the status for attendees marked as Attended |
attendee_attending_status_id | ID of the status for attendees marked as Attending |
attendee_confirmed_status_id | ID of the status for attendees marked as Confirmed |
attendee_default_role_id | ID of the default role for event attendees |
attendee_not_attending_status_id | ID of the status for attendees marked as Not Coming |
attendee_no_show_status_id | ID of the status for attendees marked as No Show |
attendee_waiting_list_status_id | ID of the status for attendees marked as On Waiting List |
host_default_role_id | ID of the default role for event hosts |
Groups
Groups are organized groups of supporters that host events and create petitions. They may be associated with a specific location or geographic region, or they may be "virtual" groups that organize around a topic.
In API responses and other data sets, Groups are sometimes referred to as "local chapters". This is merely an artifact of how data is stored within the platform; the two terms can be used interchangeably.
List
Get a paginated list of all groups and information about each of them.
This is a paginated response. You can advance to the next page using the page parameter, which accepts a page number integer. By default the first page is returned.
GET /api/v1/local_chapters/?page=1
Hash | Description |
---|---|
meta | pagination information |
local_chapters | an array of information about the local chapters |
Show
Find information about a group by URL slug.
GET /api/v1/local_chapters/save-gotham
The slug
used to identify a group for API purposes is the same as the slug
that appears in the member-facing group page URL.
For example, for a group whose group page is at https://demo.controlshiftlabs.com/groups/brooklyn-for-bike-lanes
,
the group slug
is brooklyn-for-bike-lanes
.
Group Members
Retrieve a paginated list of all of the people who are members of a particular group.
GET /api/v1/local_chapters/save-gotham/members
Hash | Description |
---|---|
meta | pagination information |
local_chapter | Basic information about the group |
members | Array of information about each supporter in this group |
Apply Label
POST request body
{
"name": "We Love Tea!"
}
Applies an existing Label to a specific group.
Note: Currently it is not possible to create Labels via the API. The Label needs to already exist to allow tagging the group with it.
The request body must include a name
attribute with the name of the label to be applied to the group.
POST /api/v1/local-chapters/tea-drinking-society/labelings
Response on success will not have a body, and status code will be 201 - Created
This example would apply the "We Love Tea!" label to the group.
Remove Label
Removes a Label from the group.
The name of the Label must be included in the URL and be correctly percent encoded as per RFC 3986.
Response on success will not have a body, and status code will be 200 - Ok
DELETE /api/v1/local-chapters/tea-drinking-society/labelings/We%20Love%20Tea%21
This example would remove the "We Love Tea!" label from the group.
Assign Mentor
PUT request body
{
"mentor": {
"email": "suzie@example.com"
}
}
PUT request response
{
"status": "success",
"data": {
"mentor": {
"email": "suzie@example.com",
"member_id": 123,
"full_name": "Suzie Greenberg",
"phone_number": "555-555-5555",
"postcode": "12345"
}
}
}
Assigns an admin user as the mentor for the group.
The email address specified will be used to look up an admin user who will be assigned as the group's mentor.
PUT /api/v1/local-chapters/tea-drinking-society/mentor
PUT request body
{
"mentor": null
}
To remove the mentor from the group, the request body should include null
instead of the email block.
Webhook Endpoints
Webhooks can be used by software engineers to integrate ControlShift with third-party systems. They allow engineers to build software that is triggered by events that take place within ControlShift. This authenticated endpoint provides information about webhook endpoints and allows for the creation of new ones.
This is useful for customers who would like to build automated reporting to detect when webhooks have been disabled or are setup incorrectly.
List Webhook Endpoints
Show information about all of the webhook endpoints configured for organisation.
GET /api/v1/webhook_endpoints
GET response body for Index (List)
[
{
"id":1,
"organisation_id":7,
"url":"https://example.com/test1",
"created_at":"2019-06-19T15:02:47Z",
"updated_at":"2019-06-19T15:02:47Z",
"disabled_at":null,
"last_failure":null
},
{
"id":2,
"organisation_id":7,
"url":"https://example.com/test2",
"created_at":"2019-06-19T15:10:47Z",
"updated_at":"2019-06-19T15:45:47Z",
"disabled_at":"2019-06-19T15:45:47Z",
"last_failure":"2019-06-19T15:45:01Z"
}
]
Create Webhook Endpoint
POST /api/v1/webhook_endpoints
{
"endpoint": {
"url": "https://example.com/test1",
"basic_auth_username": "my_secret_user",
"basic_auth_password": "passw0rd",
"aws_account_id": "355687156842"
}
}
Response body
{
"status": "success",
"endpoint": {
"url": "https://example.com/test1",
"signing_secret": "abc122"
}
}
Creates a new webhook endpoint that will receive webhooks from the platform.
POST /api/v1/webhook_endpoints
The request body should be a JSON block containing one "endpoint"
object, which can have the following properties:
Field | Type | Description | Required? |
---|---|---|---|
url | string | URL webhook will be POSTed to. | yes |
basic_auth_username | string | Optional. HTTP Basic Auth credential to use with your endpoint | no |
basic_auth_password | string | no | |
aws_account_id | string | Optional. Advanced feature. See Bulk Data API docs | no |