Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save billforward-alex/9482ea52d99b058db35c07b2e3c29522 to your computer and use it in GitHub Desktop.
Save billforward-alex/9482ea52d99b058db35c07b2e3c29522 to your computer and use it in GitHub Desktop.
Subscribing your webhook to additional audit events
#!/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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment