Skip to main content

Webhooks

Each video service 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 LifeOmic 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:
    Loading ....

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()