Skip to main content
Getting started

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.

import aiohttp
import asyncio

class Signature:
    def __init__(self, current_status):
        self.current_status = current_status

async def call_api(session, id):
    async with session.get(f"https://api.webtrust.kopjra.com/v1/signatures/{id}",
                           headers={
                               "Content-Type": "application/json",
                               "Accept": "application/json",
                               "Authorization": "Basic NjhiOTkxMjEtYWIxNC00YzUwLWFlMzItNDgzZmQ4MWVkNWJkOnlBRG1YTDZ6IzNjVm4tWURPNmJsTWkybGY0MEpNMEdUcFpLVH5+Ujc1eUNEN3ZZVGU2fmJTdTJEOUZwNHFEVjl4RWhtVmRZSw=="
                           }) as response:
        if response.status != 200:
            raise Exception(f"Failed to fetch signature {id}")
        data = await response.json()
        return Signature(data['currentStatus'])

async def wait_for_final(signature_id):
    async with aiohttp.ClientSession() as session:
        async def poll_ended(signature):
            if signature.current_status in ["CONCLUDED", "REJECTED"]:
                return True
            elif signature.current_status in ["POD_FAILED", "OURL_FAILED", "CONCLUDING_FAILED_PERM"]:
                raise Exception(f"Signature {signature_id} failed with status {signature.current_status}")
            else:
                return False

        async def poll():
            signature = await call_api(session, signature_id)
            if not await poll_ended(signature):
                await asyncio.sleep(60)
                await poll()

        signature = await call_api(session, signature_id)
        if not await poll_ended(signature):
            await poll()
        return signature

async def main():
    try:
        signature = await wait_for_final(1)
        print(signature.current_status)
    except Exception as e:
        print(f"Error: {e}")

asyncio.run(main())

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.

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:

from flask import Flask, request, jsonify
import hmac
import hashlib
import base64

app = Flask(__name__)

secret = "your_secret_here"  # Read from your database

@app.route("/public/endpoint/post", methods=["POST"])
def handle_callback():
    svix_signatures_str = request.headers.get("svix-signature")
    svix_id = request.headers.get("svix-id")
    svix_timestamp = request.headers.get("svix-timestamp")

    if svix_signatures_str and svix_id and svix_timestamp:
        request_body = request.get_data(as_text=True)
        received_data = f"{svix_id}.{svix_timestamp}.{request_body}"

        secret_bytes = bytes.fromhex(secret.split("_")[1])

        signature = hmac.new(secret_bytes, received_data.encode('utf-8'), hashlib.sha256).digest()
        signature_b64 = base64.b64encode(signature).decode('utf-8')

        svix_signatures = svix_signatures_str.split(" ")
        for svix_signature in svix_signatures:
            ss_b64 = svix_signature.split(",")[1]
            if ss_b64 == signature_b64:
                # AUTHENTICATED! so do what you had to do with this callback
                break

    return "", 200

if __name__ == "__main__":
    app.run(port=3000)

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.