Basis fires webhooks to alert customers about changes to profile state.

Webhook Configuration

To receive webhooks, customers must implement a dedicated endpoint which can receive POST requests and act on their contents. This endpoint is typically responsible for verifying the webhook, examining its topic and body, and taking any necessary action.

Currently, webhook URLs are configured manually by the Basis team. When you are ready to receive webhooks, let us know which webhook URL you would like to use for each environment. Alternatively, you can use a single webhook URL for webhooks from all environments.

When a webhook URL is not configured for a particular environment, webhooks for that environment will be skipped.

Webhook Content

Webhooks are POST requests. Their bodies include a topic, which indicates the type of webhook, and a payload, which contains any additional relevant data.

There is currently only one webhook topic, ledger-build-complete. A ledger-build-complete webhook indicates that Basis has just built or rebuilt the ledger associated with a given profile. It does not guarantee that new data will be present, though this will often be the case.

The payload of a ledger-build-complete webhook includes the ID of the profile for which ledger build just completed. For example, the body of a ledger-build-complete webhook might look as follows:

{
  'environment': 'development',
  'topic': 'ledger-build-complete',
  'payload': {
    'profile_id': '06f6a033-8b36-4a0d-a5a8-545cbf1591b7'
  }
}

Webhook Retries

If your webhook endpoint is unreachable, responds with a status code outside the 2XX range, or takes longer than 5 seconds to respond, Basis will retry delivery of the webhook up to 3 times.

Retries are delayed using exponential backoff: the first retry occurs 30 seconds after the initial request, the second retry occurs 60 seconds after the first retry, and the third retry occurs 120 seconds after the second retry.

Basis maintains records of all webhook delivery attempts. Reach out to us if these records would be helpful in debugging your webhook implementation.

Webhook Verification

Basis webhooks include an authorization header which can be used to verify their authenticity, in the form of a bearer token. This token is a non-expiring JWT.

The token is encoded using your client secret. By decoding this JWT with your client secret, you verify that the webhook sender must have knowledge of your client secret (i.e. that the webhook sender is in fact Basis). Ensure that you use the byte string representation of your client secret when decoding the JWT.

The JWT includes the following fields:

FieldContents
kidYour client ID.
subThe subject of the webhook (varies by webhook topic; for ledger-build-complete webhooks, this will be the profile ID).
jtiThis particular webhook's unique ID. Useful for debugging webhook failures. If possible, please provide this value when making support inquiries related to particular webhooks.
iatTime when the webhook token was issued, as a UNIX timestamp.
dataThe payload of the webhook (duplicated from the webhook body).

The data field in the JWT duplicates the body of the webhook. As such, you can compare it to the body of the webhook to ensure the webhook body has not been tampered with.

Example

The following example illustrates a simple endpoint for receiving webhooks, using Python and FastAPI.

from http import HTTPStatus

from fastapi import APIRouter, Request, Response, HTTPException
import jwt


router = APIRouter()


YourClientSecretBytes = b'[REDACTED]'


@router.post('/receive')
async def receive(request: Request, response: Response):
    token = request.headers.get('Authorization').split()[1]

    try:
        decoded = jwt.decode(token, YourClientSecret, algorithms=['HS256'])
    except jwt.InvalidTokenError:
        raise HTTPException(HTTPStatus.UNAUTHORIZED)

    data = await request.json()

    if decoded['data'] != data:
        raise HTTPException(HTTPStatus.UNAUTHORIZED)

    match data['topic']:
        case 'ledger-build-complete':
            handle_ledger_build_complete(data['payload'])
        case _:
            response.status_code = HTTPStatus.ACCEPTED