Callbacks - Webhooks

Prerequisites

Signature processing in Web Trust happens in an asynchronous way. Meaning that the API call you make to create a signature will immediately return a signature in status SUBSCRIBED.

So how do you know when the signature transits to another state, for instance CONCLUDED?

You have two ways to make this verification: polling or callbacks.

Polling

This is the most basic technique to check for anything API-related. You just check the signature every X seconds to look for the state you are interesting in.

Let’s code an example that tries to check for the CONCLUDED state using short-polling; unfortunately, you can not use long-polling to poll Web Trust.

Example
async function callAPI(id: number): Promise<Signature> { const res = await fecth( `https://api.webtrust.kopjra.com/v1/signatures/${id}`, { "headers": { "Content-Type": "application/json", "Accept": "application/json", "Authorization": "Basic NjhiOTkxMjEtYWIxNC00YzUwLWFlMzItNDgzZmQ4MWVkNWJkOnlBRG1YTDZ6IzNjVm4tWURPNmJsTWkybGY0MEpNMEdUcFpLVH5+Ujc1eUNEN3ZZVGU2fmJTdTJEOUZwNHFEVjl4RWhtVmRZSw==", }, "method": "GET" } ); // Avoiding Error management for simplicity return await res.json(); } async function waitForFinal(signatureId: number): Promise<Signature> { return new Promise<Signature>(async (resolve, reject) => { function pollEnded(signature: Signature): boolean { if ( signature.currentStatus === "CONCLUDED" || signature.currentStatus === "REJECTED" ) { resolve(signature); return true; } else if ( signature.currentStatus === "POD_FAILED" || signature.currentStatus === "OURL_FAILED" || signature.currentStatus === "CONCLUDING_FAILED_PERM" ) { reject(new Error(`Signature ${signatureId} failed with status ${signature.currentStatus}`)); return true; } else { return false; } } function poll() { setTimeout(async () => { if (!pollEnded(await callAPI(signatureId))) { poll(); } }, 60*1000); // Minimum checking interval 1 minute } if (!pollEnded(await callAPI(signatureId))) { poll(); } }); }

The waitForFinal function will return the signature when it is CONCLUDED or REJECTED, will throw an error for the other final states, and if the state is not final, it will retry to get the signature after a minute has passed.

However, this mechanism has certain pitfalls.

  • Signatures may take long to complete; consider that a signature has a life of a month. So you would be making several useless API calls.

  • Increasing the interval may make you wait more than needed.

  • Having several signatures active may lead to too many concurrent API calls, and you may surpass the call rate.

Callbacks (alias webhooks)

A better approach to being aware of state changes is to receive push notifications exactly when these changes happen. Web Trust implements a callback mechanism to tackle exactly this.

First of all, you need to register a callback.

Example
const res = await fetch("https://api.webtrust.kopjra.com/v1/callbacks", { "headers": { "Content-Type": "application/json", "Accept": "application/json", "Authorization": "Basic NjhiOTkxMjEtYWIxNC00YzUwLWFlMzItNDgzZmQ4MWVkNWJkOnlBRG1YTDZ6IzNjVm4tWURPNmJsTWkybGY0MEpNMEdUcFpLVH5+Ujc1eUNEN3ZZVGU2fmJTdTJEOUZwNHFEVjl4RWhtVmRZSw==", }, "method": "POST", "body": JSON.stringify({ "resourceType": "Signature", "endpoint": "https://my.company.com/public/endpoint/post", "onlyFinalStatuses": false, "events": ["USER_CONCLUDED"], }); console.log(await res.json()); });

Let’s dive into the payload for a second.

  • resourceType indicates for which type of resources the endpoint will be called. Interaction is used for the Webpage and Data Forensic API, and Signature is used for all the Document APIs.

  • endpoint is defined by you and will be called for each event. It should be public and listening to a POST. In the body of the POST Web Trust will send the Signature/Interaction that changed state.

  • onlyFinalStatuses, when set to true, will reduce the callbacks to those corresponding to changes to final states. If set to false instead, it will call the endpoint for all state changes.

  • events is used to define a finer-grained callback that allows you to receive events not only when there are state changes but also when certain conditions are verified. Currently the only supported event is USER_CONCLUDED. This event will be fired when the signer completes her actions in the signing environment, thus before the signature transits to CONCLUDING. This may be useful if you want to give some immediate feedback to the signer and don’t want to wait for the CONCLUDING state.

Beware that in order to specify the events property, onlyFinalStatuses must be false; otherwise, a 400 error will be thrown.

The response to the previous API call should be something like,

{
  "id": 123,
  "resourceType": "Interaction",
  "endpoint": "https://my.company.com/public/endpoint/post",
  "onlyFinalStatuses": false,
  "events": [
    "USER_CONCLUDED"
  ],
  "secret": "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH"
}

endpoint should respond as quick as possible with a 200 to avoid automatic retries. So avoid blocking actions.

Save the secret in your database; you will need it afterwards to authenticate the calls.

Callback payload

Each callback will send the entire updated interaction or signature (depending on the resourceType indicated) as the payload. Using this payload you can get all the data you need and even make more GET requests to get other entities. An example of this payload can be found here.

Securing the endpoint

Since the endpoint must be public, it would be nice to have a way of securing it to avoid attacks.

Web Trust authenticates the callbacks using HMAC with SHA-256⁣; this allows for you to verify the authenticity of the payload. This is optional but highly recommended.

Each callback is registered in SVIX to manage errors, retries, and security. Each POST to endpoint will include the following headers:

  • svix-id: the unique message identifier for the callback message. This identifier is unique across all messages but will be the same when the same callback is being resent (e.g., due to a previous failure).

  • svix-timestamp: timestamp in seconds since epoch.

  • svix-signature: the Base64-encoded list of signatures (space-delimited). For instance:⁣ v1,base64 v1,base64 v2,base64 .

To verify the authenticity of the callback, one could implement the following in the endpoint:

Example
import express from "express"; import {join} from "path"; import {createHmac} from "crypto"; const app = express(); const port = 3000; const secret; // Read from your database. You got it when you created the callback. ... app.post("/public/endpoint/post", (req, res) => { // To this endpoint we will receive the callbacks const headers = req.headers; const svix_signatures_str = headers["svix-signature"]; const svix_id = headers["svix-id"]; const svix_timestamp = headers["svix-timestamp"]; if (svix_signatures_str && svix_id && svix_timestamp) { // create a payload to be signed const receivedData = `${svix_id}.${svix_timestamp}.${JSON.stringify(rep.body)}`; // get the secret bytes const secretBytes = new Buffer(secret.split('_')[1]; // signed the received payload with the secret you already have const signature = createHmac( "sha256", secretBytes ).update(receivedData).digest('base64'); const svix_signatures = svix_signatures_str.split(" "); if (svix_signatures.length > 0) { for (const svix_signature of svix_signatures) { const ss_b64 = svix_signature.split(",")[1]; // At least one of the svix_signatures must match if (ss_b64 === signature) { // AUTHENTICATED! so do what you had to do with this callback break; } } } } res.status(200).end(); // Always respond 2xx even in case of error }); ... app.listen(port, () => { console.log(`Example app listening on port ${port}`) });

Management and retries

Web Trust callbacks are managed using SVIX to manage errors, retries, and security.

If the registered endpoint goes into a timeout or does not answer with an HTTP code different from 2xx, then the system will automatically retry the delivery according to the following schema:

  • after 5 seconds;

  • after 5 minutes;

  • after 30 minutes;

  • after 2 hours;

  • after 5 hours;

  • after 10 hours;

  • after 10 hours.

Besides the API call, you can create a callback using the Web Trust dashboard.

Use the CREATE button.

After at least one callback is created, you can check your SVIX dashboard by clicking INSPECT CALLBACKS.

By clicking on the different endpoint, you can see the details of the callback delivery over time.