#!/bin/bash
# https://sipb.mit.edu/doc/safe-shell/
set -euf -o pipefail
command -v jq >/dev/null 2>&1 || { echo >&2 "I require 'jq' but it's not installed. Try 'brew install jq'. Aborting."; exit 1; }
trap "echo 'Command syntax: BF_add_webhook_sub.sh private_token bf_subdomain webhook_subscriptions [specific_webhook_id]
Example invocation: ./BF_add_webhook_sub.sh MY_PRIVATE_TOKEN api-sandbox \"Account.Created,Subscription.Paid\"'" EXIT SIGHUP SIGINT SIGTERM
if [ -z ${1+x} ]; then
echo >&2 "Please provide your BillForward private token as the first arg to this script."; exit 1;
fi
if [ -z ${2+x} ]; then
echo >&2 "Please provide the BillForward subdomain as the second arg to this script. We accept 'api' or 'api-sandbox' as valid inputs."; exit 1;
fi
if [ -z ${3+x} ]; then
echo >&2 "Please provide a comma-delimited list of the events you wish to subscribe to. For example: \"Subscription.Cancelled,Subscription.Paid\""; exit 1;
fi
trap "echo 'A failure occurred; we did not reach the end of the script.'" EXIT SIGHUP SIGINT SIGTERM
PRIVATE_TOKEN="$1"
SCHEME='https'
SUBDOMAIN="$2"
DOMAIN="$SUBDOMAIN.billforward.net"
CONTEXT='v1'
API_URL="$SCHEME://$DOMAIN/$CONTEXT"
if [ -z ${4+x} ]; then
echo "Looking up first webhook on your organization..."
EXISTING_WEBHOOK="$(curl -s --fail "$API_URL/webhooks?records=1" \
-H "Authorization: Bearer $PRIVATE_TOKEN" | jq '.results[0]')"
EXISTING_WEBHOOK_ID="$(echo "$EXISTING_WEBHOOK" | jq '.id' -r)"
echo "Found webhook '$EXISTING_WEBHOOK_ID'."
else
EXISTING_WEBHOOK_ID="$4"
echo "Using the specified webhook ID, '$EXISTING_WEBHOOK_ID'..."
EXISTING_WEBHOOK="$(curl -s --fail "$API_URL/webhooks/$EXISTING_WEBHOOK_ID" \
-H "Authorization: Bearer $PRIVATE_TOKEN" | jq '.results[0]')"
fi
echo "Your webhook looks like this (before we make any changes):
$EXISTING_WEBHOOK"
PROPOSED_WEBHOOK_SUBSCRIPTIONS="$(echo "\"$3\"" | jq '. | split(",") | map(split(".") | {domain: .[0], action: .[1], deleted:false})')"
PUTPAYLOAD=$(echo $EXISTING_WEBHOOK \
| jq ".webhookSubscriptions |= (.+ ($PROPOSED_WEBHOOK_SUBSCRIPTIONS - (. | map({domain, action, deleted}))))")
echo "We will submit the following payload to PUT /webhooks:
$PUTPAYLOAD"
PUTRESPONSE="$(curl -s --fail "$API_URL/webhooks" \
-H "Authorization: Bearer $PRIVATE_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-X PUT \
-d "$PUTPAYLOAD")"
UPDATEDWEBHOOK="$(echo "$PUTRESPONSE" | jq '.results[0]')"
echo "Your webhook now looks like this:
$UPDATEDWEBHOOK"
trap - EXIT SIGHUP SIGINT SIGTERM
Save this script somewhere.
Give it execute permissions:
chmod +x add_webhook_subscription.sh
Install dependencies:
# jq parses JSON in the command-line
brew install jq
Command syntax:
BF_add_webhook_sub.sh private_token bf_subdomain webhook_subscriptions [specific_webhook_id]
private_token - your BillForward User's private token (looks like a uuid)
bf_subdomain - accepted values: ['api', 'api-sandbox']
webhook_subscriptions - string containing a comma-delimited list of the domain/action audit events you wish to subscribe to. For example: "Account.Created,Subscription.Paid"
specific_webhook_id - (optional) specify a particular Webhook by ID, instead of grabbing the first one that we see.
Example invocation:
./BF_add_webhook_sub.sh MY_PRIVATE_TOKEN api-sandbox "Account.Created,Subscription.Paid"
This will:
- Grab the first Webhook entity that we find in your BillForward instance
- Add explicit subscriptions to the audit events that you specify (for example: Account.Created, Subscription.Paid)
The script cannot be used to unsubscribe.
The script is idempotent (so does not create additional subscriptions to the same event, if you already have one).
======
By default (i.e. when first created via the UI), your webhook would look like this:
{
"@type": "webhook",
"created": "2016-10-20T16:58:06Z",
"changedBy": "78FB9BDC-D972-4AE4-8984-255B3151826B",
"updated": "2017-07-21T17:55:39Z",
"id": "BDE4F864-8015-4272-ACBD-6DA26DBE2834",
"URL": "",
"consecutiveFailures": 0,
"deleted": false,
"webhookSubscriptions": []
}
Note "webhookSubscriptions": []
— this webhook has not explicit subscribed to any audit events, so we apply the default behaviour, which is: "subscribe to all audit events".
the bash script I've provided, sends a PUT, which adds a WebhookSubscription to that list:
"webhookSubscriptions": [{
"domain": "Account",
"action": "Created"
}, {
"domain": "Subscription",
"action": "Paid"
}]
The PUT /webhooks
endpoint has "replace" semantics, and updates your Webhook entity in-place. The webhook's webhookSubscriptions
list is a OneToMany entity relationship (i.e. one Webhook has many WebhookSubscriptions).
In your PUT request: adding a WebhookSubscription to the webhookSubscriptions
list expresses "I would like to create an extra WebhookSubscription entity". No id
is provided for your WebhookSubscription, so the API understands "this is a new WebhookSubscription entity, that needs to be created".
If you want to update existing WebhookSubscriptions (i.e. to delete them): you would need to mutate their existing entry in the WebhookSubscription list, and change "deleted"
to false
. The API sees that an id
already exists for that WebhookSubscription, so it understands "use this payload to replace the existing WebhookSubscription entity by that id".
Deleting existing WebhookSubscriptions is not something that this bash script does, but it is easy to do manually (by producing a PUT payload in a similar way to how this script does). A "delete" script could be written to automate the process.