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 theConversation_Ended
payload has this shape:id stringThe ID of the conversation
expert stringThe ID of the expert
status stringThe status of the call
totalParticipantSeconds numberThe number of seconds where participants and experts were on the call together
Array of objectsArray 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()