Skip to main content

Create an Integration With Webhooks

Webhooks allow your dispatch service to listen for JSON messages from Voltus. You can then take an action when they are received. It is generally more reliable and has lower latency than polling, but requires you to have a server set up that can receive HTTP requests from the Internet.

The general approach with this option is:

  • Provision a server that is capable of handling webhook requests from Voltus
  • Voltus will send webhook requests containing resource URLs in the payloads
  • Your server should make a subsequent request to the resource URL to obtain dispatch information

While we provide source code examples in-line for illustrative purposes, we recommend checking out the source code from GitHub in the voltus-api-examples repository if you want to follow along interactively. Run the initial setup guidelines outlined in the repository’s README.md. Also, some parts of this tutorial assume you have curl installed.

How to use dispatch webhooks

This section shows you what your server will receive from Voltus if you use webhooks to dispatch.

Requirements

  • A server URL (SERVER_URL): Get the URL to your server that can receive dispatch signals and return a 2xx response. For testing purposes, you can go to https://webhook.site/ and copy the URL from the place labeled "Your unique URL" (please copy it from here, not from the address bar!). Later, from the same webpage, you will see the requests your server will need to handle.
  • A Voltus Sandbox API key (SANDBOX_API_KEY): If you do not have one, reach out to your Voltus point of contact or api-support@voltus.co.

Getting a webhook setup

After setting the above two environment variables SANDBOX_API_KEY and SERVER_URL (you can also modify the example text inline), send this request to register the URL to the Voltus API:

curl --request POST \
--url https://sandbox.voltus.co/2022-04-15/webhooks \
--header 'Content-Type: application/json' \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}" \
--data "{
\"events\": [
{
\"name\": \"dispatch.create\"
},
{
\"name\": \"dispatch.update\"
}
],
\"url\": \"${SERVER_URL}/dispatch-listener\"
}"

Your server should receive a POST request with the following content and respond with a 200. If the URL does not respond with a 200, the above curl request will fail, and you will need to retry.

{
"event": {
"name": ""
},
"resource": ""
}

Sending a basic dispatch

Send the following curl request to create a basic dispatch which will send out a dispatch webhook request from Voltus at notification_time. Before running, you'll need to change the notification_time, start_time, and end_time to now or some time in the near future.

curl --request POST \
--url https://sandbox.voltus.co/2022-04-15/scenarios \
--header 'Content-Type: application/json' \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}" \
--data '{
"scenario_type": "basic",
"notification_time": "2023-12-01T16:40:00Z",
"start_time": "2023-12-01T16:50:00Z",
"end_time": "2023-12-01T17:00:00Z",
"sites": [
{
"name": "My Site",
"customer_location_id": "democustomerlocationid",
"commitment": 5
},
{
"name": "Another Site",
"customer_location_id": "democustomerlocationid2",
"commitment": 50
}
],
"program": {
"name": "A Program",
"timezone": "PST",
"market": "CAISO",
"program_type": "capacity"
}
}'

At notification_time (set in the above request), your server will receive a POST request with the following content, where dkmq is the unique ID for the dispatch.

{
"event": {
"name": "dispatch.create"
},
"resource": "/2022-04-15/dispatches/dkmq"
}

Voltus’s API uses thin webhooks, which sends just the URL to the resource that has new information. To see the full dispatch info, such as start and end time, send the following request to the Voltus API:

curl --request GET \
--url https://sandbox.voltus.co/2022-04-15/dispatches/dkmq \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}"

Which will return something like the following:

{
"dispatches": [
{
"id": "dkmq",
"authorized": true,
"test": false,
"start_time": "2023-12-01T16:50:00Z",
"end_time": "2023-12-01T17:00:00Z",
"program": {
"name": "A Program",
"timezone": "PST",
"market": "CAISO",
"program_type": "ancillary_services"
},
"sites": [
{
"name": "My Site",
"id": "d4z9",
"customer_location_id": "democustomerlocationid",
"drop_by": 5,
"commitment": 5
},
{
"name": "Another Site",
"id": "9jeq",
"customer_location_id": "democustomerlocationid2",
"drop_by": 50,
"commitment": 50
}
]
}
],
"page": 0,
"perPage": 0
}

Updating a dispatch

If Voltus updates or cancels a dispatch, your server will receive a POST request with this body:

{
"event": {
"name": "dispatch.update"
},
"resource": "/2022-04-15/dispatches/dkmq"
}

Deleting the test webhook

To clean up this test, you will need to de-register the webhook server. To do this, you'll need the webhook ID. Get it by running this:

curl --request GET \
--url https://sandbox.voltus.co/2022-04-15/webhooks \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}"

Which will return something like:

{
"webhooks": [
{
"url": "webhook.test.url/webhook",
"events": [
{
"name": "dispatch.create"
},
{
"name": "dispatch.update"
}
],
"id": "wVcgZeEOXCc"
}
],
"page": 0,
"perPage": 0
}

Note the id field. Then delete the webhook by calling something like this, replacing wVcgZeEOXCc with the actual ID you got in the response above:

curl --request DEL \
--url https://sandbox.voltus.co/2022-04-15/webhooks/wVcgZeEOXCc
--header 'Content-Type: application/json' \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}"

Write a service that receives webhooks

In this part of the tutorial, we will write a minimal flask server that can receive POST requests with the following possible bodies, get additional dispatch info from the Voltus API, and then call functions you will fill in with your organization-specific logic to curtail the sites.

Conceptual overview

We’re going to create a webhook server that can handle the three different messages we get sent: registration, new dispatch, and updated dispatch.

Registration

This webhook gets called when a webhook is first registered with an empty payload:

Example payload:

{
"event": {
"name": ""
},
"resource": ""
}

New dispatch

This webhook gets called when a dispatch is first created.

Example payload:

{
"event": {
"name": "dispatch.create"
},
"resource": "/2022-04-15/dispatches/{DISPATCH_ID}"
}

Dispatch update

This webhook gets called when a dispatch is updated.

Example payload:

{
"event": {
"name": "dispatch.update"
},
"resource": "/2022-04-15/dispatches/{DISPATCH_ID}"
}

An example server

Let’s put what we learned together and write a simple service that can receive the above payload types and take the appropriate action:

  • Acknowledge webhook registration for empty payloads.
  • Create the dispatch with the given start_time, end_time, and applicable sites for the dispatch.create type.
  • Cancel the dispatch if the end_time has been updated to be in the past or authorized marks the dispatch as canceled for the dispatch.update messages.
  • Update the dispatch start_time and/or end_time for other dispatch.update messages.

This code can be found in voltus-api-examples/blob/main/webhooks/webhook-server.py. You will need to fill in your company-specific logic.

import json
import os
import urllib
import datetime
from typing import Dict

import requests
from dateutil import parser as date_parser
from flask import Flask, request

VOLTUS_API_URL = os.getenv("VOLTUS_API_URL", "https://sandbox.voltus.co")
VOLTUS_API_KEY = os.getenv("VOLTUS_API_KEY", "secret")


app = Flask(__name__)

requests_session = requests.Session()
requests_session.headers.update(
{"X-Voltus-API-Key": VOLTUS_API_KEY, "Accept": "application/json"}
)


def get_dispatch_info(url: str):
url = urllib.parse.urljoin(VOLTUS_API_URL, url)
r = requests_session.get(url)
r.raise_for_status() # raise an exception if request fails
return r.json()


def dispatch_program(command: str, dispatch_info: Dict):
dispatch_info["start_time"] = date_parser.parse(dispatch_info["start_time"])
if "end_time" in dispatch_info and dispatch_info["end_time"] is not None:
dispatch_info["end_time"] = date_parser.parse(dispatch_info["end_time"])
else:
dispatch_info["end_time"] = None

if command == "dispatch.create":
return create_dispatch(dispatch_info)
else:
now = datetime.datetime.now(datetime.timezone.utc)
if dispatch_info["end_time"] < now or not dispatch_info["authorized"]:
return cancel_dispatch(dispatch_info)
else:
return update_dispatch(dispatch_info)


def create_dispatch(dispatch_info):
# TODO: Fill in with your company-specific logic
# Remember that this could get called twice if the first time you return a non-200 response
# Remember that end_time could be None
# sites could have one sites, many sites, or all of your sites
return (
"Dispatch with ID {} starting at {} and ending at {} for sites {} created".format(
dispatch_info["id"],
dispatch_info["start_time"],
dispatch_info["end_time"],
dispatch_info["sites"],
),
200,
)


def update_dispatch(dispatch_info):
# TODO: Fill in with your company-specific logic
# start_time and end_time can change to be earlier or later
# Remember that this could get called twice if the first time you return a non-200 response, so it's possible that the dispatch is already up-to-date
return (
"Dispatch with ID {} starting at {} and ending at {} for sites {} updated".format(
dispatch_info["id"],
dispatch_info["start_time"],
dispatch_info["end_time"],
dispatch_info["sites"],
),
200,
)


def cancel_dispatch(dispatch_info):
# TODO: Fill in with your company-specific logic to cancel this dispatch, likely by ID
# Remember that this could get called twice if the first time you return a non-200 response, so it's possible that the dispatch is already cancelled
return "Dispatch with ID {} cancelled".format(dispatch_info["id"]), 200


@app.post("/dispatch-listener")
def dispatch_listener():
webhook_info = json.loads(request.data)
if webhook_info["event"]["name"] == "" and webhook_info["resource"] == "":
return "Hello webhook!", 200
dispatch_info = get_dispatch_info(webhook_info["resource"])
return dispatch_program(webhook_info["event"]["name"], dispatch_info)


if __name__ == "__main__":
app.run()

If you checked out the voltus-api-examples repository as suggested above, you should be able to run the above as follows (assuming you are in the root of the repository and have activated the virtual environment):

cd webhooks
python webhook-server.py

This code uses the secret public credentials, so GET dispatches will always return one dispatch with ID asdf in the near future. Let's tell your service about this dispatch by emulating the request Voltus would send your service when this dispatch gets created. In a new terminal, send a mocked create request to the service:

curl --request POST \
--url http://localhost:5000/dispatch-listener \
--header 'Content-Type: application/json' \
--data '{
"event": {
"name": "dispatch.create"
},
"resource": "/2022-04-15/dispatches/asdf"
}'

The server should return a 200 response with the following message:

Dispatch with ID asdf starting at 2023-07-14 16:37:33+00:00 and ending at 2023-07-14 17:37:33+00:00 for sites [{'name': 'Site 1', 'id': 'asdf', 'customer_location_id': '1', 'commitment': 12}, {'name': 'Site 2', 'id': 'lkjh', 'customer_location_id': '2', 'commitment': 40}] created

Next, send a test update request to the server:

curl --request POST \
--url http://localhost:5000/dispatch-listener \
--header 'Content-Type: application/json' \
--data '{
"event": {
"name": "dispatch.update"
},
"resource": "/2022-04-15/dispatches/asdf"
}'

The server should return a 200 response with the following message, showing the dispatch now starting and ending slightly later:

Dispatch with ID asdf starting at 2023-07-14 16:40:45+00:00 and ending at 2023-07-14 17:40:45+00:00 for sites [{'name': 'Site 1', 'id': 'asdf', 'customer_location_id': '1', 'commitment': 12}, {'name': 'Site 2', 'id': 'lkjh', 'customer_location_id': '2', 'commitment': 40}] updated

Note that this service is retrieving the dispatch information using the public sandbox credentials secret, which always returns the same responses with updated times.

Next, let's use your personal sandbox credentials to run simulated dispatches.

Publishing and testing the service in sandbox

For this step, you will need a Sandbox API key (SANDBOX_API_KEY). If you do not have one, reach out to your Voltus point of contact or api-support@voltus.co.

Publishing the service

Set the environment variables for sandbox and start the service from the voltus-api-examples/webhooks directory:

On Linux or MacOS:

export VOLTUS_API_URL = "https://sandbox.voltus.co"
export VOLTUS_API_KEY = "<SANDBOX_API_KEY>"
python webhook-server.py

Or on Windows Powershell:

$Env:VOLTUS_API_URL = "https://sandbox.voltus.co"
$Env:VOLTUS_API_KEY = "<SANDBOX_API_KEY>"
python webhook-server.py

Once the service is deployed and accessible over the internet (the details of this are beyond the scope of this tutorial), get the URL of the server.

Send the following request to register your webhook server with the Voltus Sandbox API. This should return a 200 OK.

export SANDBOX_API_KEY=<SANDBOX_API_KEY>
export YOUR_SERVER_URL=<your server url e.g. https://yourserver.com>
curl --request POST \
--url https://sandbox.voltus.co/2022-04-15/webhooks
--header "Content-Type: application/json" \
--header "X-Voltus-API-Key: ${SANDBOX_API_KEY}" \
--data "{
\"events\": [
{
"name": \"dispatch.create\"
},
{
\"name\": \"dispatch.update\"
}
],
\"url\": \"${YOUR_SERVER_URL}/dispatch-listener\"
}"

For local testing, you can instead go to https://webhook.site/ and copy the URL from the place labeled "Your unique URL" and send the above request with the copied URL. Later, from the same webpage, you will see the requests your server will need to handle, and can manually send them as a request to your local server with a curl command like:

curl --request POST \
--url http://localhost:5000/dispatch-listener \
--header 'Content-Type: application/json' \
--data '{
"event": {
"name": "{NAME_SENT_TO_WEBHOOK_SITE}"
},
"resource": "{RESOURCE_SENT_TO_WEBHOOK_SITE}"
}'

Creating a dispatch simulation

We will repeat the steps from Sending a basic dispatch.

Run the dispatch scenario by running the curl command found at Sandbox Dispatch Simulations - Basic Dispatch. Before running, you'll need to change the notification_time, start_time, and end_time to now or some time in the near future. At the notification_time you sent, your server will receive a request:

{
"event": {
"name": "dispatch.create"
},
"resource": "/2022-04-15/dispatches/dkmq"
}

And should respond with a 200 and something like the following:

Dispatch with ID r4y5 starting at 2023-12-01T16:50:00+00:00 and ending at 2023-12-01T17:00:00+00:00 for sites [{'name': 'My Site', 'id': 'd4z9', 'customer_location_id': 'democustomerlocationid', 'drop_by': 5},{'name': 'Another Site', 'id': '9jeq', 'customer_location_id': 'democustomerlocationid2', 'drop_by': 50},] created

Next, run the other two dispatch simulations outlined in Sandbox Dispatch Simulations (Ancillary services and Back to back) and confirm your software works as expected for each scenario, which includes more complicated dispatches, including updates, null end times, and back-to-back dispatches.

Clean up webhook registration in sandbox

Once you have passed all dispatch simulations, if you will be using the same server for production, delete your webhook registration from sandbox as outlined in Deleting the Test Webhook so future sandbox dispatch simulations won't accidentally curtail your production sites.

Confirm connection to production

For this step, you will need a Production API key (VOLTUS_API_KEY). If you do not have one, reach out to your Voltus point of contact or api-support@voltus.co. Your server needs to be publicly available on the Internet. Set the environment variables for production and start the service from the voltus-api-examples/webhooks directory:

On Linux or MacOS:

export VOLTUS_API_URL = "https://api.voltus.co"
export VOLTUS_API_KEY = "<VOLTUS_API_KEY>"
python webhook-server.py

Or on Windows Powershell:

$Env:VOLTUS_API_URL = "https://api.voltus.co"
$Env:VOLTUS_API_KEY = "<VOLTUS_API_KEY>"
python webhook-server.py

Next, in a new terminal, register your server with a production POST webhook request. This should return a 200 OK.

export VOLTUS_API_KEY=<VOLTUS_API_KEY>
export YOUR_SERVER_URL=<your server url e.g. https://yourserver.com>
curl --request POST \
--url https://api.voltus.co/2022-04-15/webhooks \
--header "Content-Type: application/json" \
--header "X-Voltus-API-Key: ${VOLTUS_API_KEY}" \
--data "{
\"events\": [
{
"name": \"dispatch.create\"
},
{
\"name\": \"dispatch.update\"
}
],
\"url\": \"${YOUR_SERVER_URL}/dispatch-listener\"
}"

Now, create a production test dispatch by running the following via curl (set the start and end times to sometime in the future before doing so, otherwise the request will fail):

curl --request POST \
--url https://api.voltus.co/2022-04-15/dispatches \
--header 'Content-Type: application/json' \
--header "X-Voltus-API-Key: ${VOLTUS_API_KEY}" \
--data '{
"start_time": "2023-07-06T19:30:00-07:00",
"end_time": "2023-07-06T19:40:00-07:00"
}'

Our server should respond something like the following as output (depending on your exact configuration with Voltus):

Dispatch with ID r4y5 starting at 2023-07-31 16:38:59+00:00 and ending at 2023-07-31 17:38:59+00:00 for sites [{'name': 'Test Site 1', 'id': 'd4z9', 'customer_location_id': '1', 'drop_by': 5}] created

If a webhook request fails

During a production dispatch, if your server responds to the webhook request with anything other than a status code of 200, 201, 202, or 204, Voltus will resend the request with an exponential backoff up to 5 times. If after 5 times Voltus fails to receive a successful response, Voltus will send your dispatch contacts an email alerting them that the webhook automation has failed and ask you to manually curtail.

Next steps

Congratulations! You have successfully set up and tested a basic dispatch service.

After passing all the simulated dispatches in sandbox and confirming your connection to production with a self-scheduled test dispatch, your integration is ready to accept new sites. Contact your Sales Director and api-support@voltus.co for next steps. Each new site must still pass a dispatch verification test. Dispatch verifications are site-specific end-to-end tests in the production environment.