Skip to main content

Webhooks

Each SkillSpring account can have up to five HTTPS endpoints configured that will receive certain webhook events (configured per endpoint). Currently, webhooks can only be configured via a support request to the SkillSpring team.

Supported events (as of 05/20/2022) include:

  • Coupon Creation
  • Coupon Redemption
  • Coupon Charged
  • Coupon Expiration
  • Conversation Ended
    Payload details
    The content of the Conversation_Ended payload has this shape:
    id
    string

    The ID of the conversation

    expert
    string

    The ID of the expert

    status
    string

    The status of the call

    totalParticipantSeconds
    number

    The number of seconds where participants and experts were on the call together

    Array of objects
    Array of objects

Webhook Validation​

If you have webhooks configured, events you receive will be signed. Each request will include an Authentication header that is generated according to this specification. As described by the spec, the Digest header contains a SHA256 hashed, Base64 encoded version of the body and should be validated separately.

Examples​

Node.js:

import Axios from 'axios';
import bodyParser from 'body-parser';
import crypto from 'crypto';
import express from 'express';
import http from 'http';
import httpSignature from 'http-signature';
import jwkToPem from 'jwk-to-pem';

const fetchWebhooksJwks = () =>
Axios.get<{ keys: WebhookJWK[] }>(
'https://files.us.skillspring.com/.well-known/webhook-jwks.json'
);

/**
* Gets the webhooks public PEM from our published JWKS by doing a lookup
* using the provided keyId.
*
* @param keyId a kid that we should get from a webhook request header.
* @throws when no JWK matches the provided keyId
*/
export const getWebhooksPublicPem = async (keyId: string) => {
const { data: jwks } = await fetchWebhooksJwks();
const jwk = jwks.keys.find((jwk) => jwk.kid === keyId);

if (!jwk) {
throw new Error('The provided keyId did not match any JWK');
}

return jwkToPem(jwk);
};

const app = express();

app.use(bodyParser.json());

app.post('/', async function (req, res) {
const parsed = httpSignature.parseRequest(req);
const webhooksPem = await getWebhooksPublicPem(parsed.keyId);
const base64Digest = req.body
? crypto
.createHash('sha256')
.update(JSON.stringify(req.body))
.digest('base64')
: undefined;

if (base64Digest && `SHA-256=${base64Digest}` !== req.headers.digest) {
return res.status(400).end();
}

if (!httpSignature.verifySignature(parsed, webhooksPem)) {
return res.status(401).end();
}

res.status(200).end();
});

app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

Python 3:

import flask
from hashlib import sha256
from requests_http_signature import HTTPSignatureAuth
from base64 import b64encode
from flask import request
from urllib.request import urlopen
from jwcrypto.jwk import JWKSet

app = flask.Flask(__name__)
app.config["DEBUG"] = True

# Downloads and then parses the JWKS, and exports the public key from the relevant JWK
def key_resolver(key_id, algorithm):
# https://connect-files.dev.lifeomic.com/.well-known/webhook-jwks.json - Dev JWKS
with urlopen('https://files.us.skillspring.com/.well-known/webhook-jwks.json') as f:
jwk_set = JWKSet.from_json(f.read().decode('ascii'))
pem = jwk_set.get_key(key_id).export_to_pem()
return pem

# Check that the Digest header matches the body
def digest_match(request):
return request.headers.get('Digest') == 'SHA-256=' + b64encode(sha256(request.data).digest()).decode()

@app.route('/', methods=['POST'])
def webhook_test():
if digest_match(request):
HTTPSignatureAuth.verify(request, key_resolver=key_resolver)
return 'Success'
return 'Failed'

app.run()