Subscriptions API

Snapdocs Connect's eClose and Quality Control products broadcast events through one shared Subscriptions API. Rather than polling for changes against HttpGet endpoints, you register an endpoint you host; Snapdocs then creates HttpPost calls when an event takes place that your subscription signed up to know about. Your endpoint will send a 200 upon receipt of a valid shaped event, and manage any necessary handling, processing, and follow-up API calls that make sense for the workflow of that event.

Setup is two steps:

  1. Stand up an HTTPS endpoint in your system that can receive HttpPOST requests, which can trigger the appropriate work.
  2. Create a subscription for the events you want, using your newly created endpoint's URL.

Your OAuth scopes determine which events you can subscribe to. Each product documents its own event catalog: eClose Events Catalog, Quality Control Events Catalog. Notary Connect and eVault deliver webhooks through their own systems; see How webhooks work.

Manage subscriptions

The webhook event

Events arrive as a POST with Content-Type: application/json. The envelope carries a unique event ID, the identifier of the record the event is about, the event name, a Unix timestamp, and a payload object with event-specific detail. An eClose example below for when a preview package is available for a given borrower:

{
	"event_id": "572f592a-fbec-49d9-a28a-88d8e38175be",
	"closing_uuid": "d679e2ad-278d-e547-9756-84639ba3865b",
	"event_name": "borrower.preview_available",
	"created_at": 1618936005,
	"payload": {
		"external_identifiers": [{
			"external_system": "other_los",
			"external_type": "file_number",
			"value": "1234"
		}]
	}
}
📘
external_identifiers may have 0 or more records, echoing the identifiers your system supplied when it created the record. Snapdocs includes the events you pass in, expecting your system can better match to its underlying record via system ID.

Events never contain PII. Names, addresses, and document contents require an authenticated follow-up call; Webhook security covers this and the rest of the webhook security model.

Handle the requests

  • Subscribe narrowly. Configure your endpoint to receive only the event types your integration acts on. Listening for everything puts undue strain on your server.
  • Handle duplicates. Endpoints occasionally receive the same event more than once. Make your event processing idempotent.
  • Verify the sender. Validate the HMAC signature on every delivery, ensuring the source that hits your endpoint is the Snapdocs webhook subscription.
  • Don't assume ordering. Events are not guaranteed to arrive in the order they occurred. Use created_at to sequence them.

Respond, then process

Respond 200 as soon as the webhook arrives, before processing the event and regardless of whether processing later succeeds. In production, most listeners push the message onto a queue and return immediately.

HTTP/1.1 200 OK
Content-Type: application/json

{
	"status": "OK",
	"code": 200,
	"message": "webhook received successfully"
}

If Snapdocs doesn't receive a 200, it re-sends the notification up to 13 times over 24 hours from the first attempt in an exponential backoff pattern, with increasing gaps between attempts.

Example listener

A minimal AWS Lambda listener, receiving events and routing one of them to a handler:

import json
import os
import logging

logger = logging.getLogger()
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'WARNING').upper()
logger.setLevel(level=LOG_LEVEL)

success_code = 200
success_body = {
        "status": "OK",
        "code": success_code,
        "message": "webhook received successfully"
    }


def download_scanback_document(document_uuid):
    logger.info("downloading scanback document %s", document_uuid)
    pass


def process_snapdocs_event(event_body):
    try:
        # check event body
        logger.debug("%s %s %s", event_body.get('event_id'), event_body.get('closing_uuid'),
                    event_body.get('event_name'))
        logger.debug("lender system attributes %s", event_body.get("payload"))
        # typically we put the message into a message broker to be processed downstream
        if event_body.get('event_name') == 'document.created':
            if event_body.get('document_type') == 'scanback_documents':
                download_scanback_document(event_body.get('document_uuid'))
    except Exception as e:
        logger.error(e)
        return 500, {
            "status": "Failed to process event",
            "code": 500,
            "message": f"webhook received but could not process {str(e)}"
        }
    return success_code, success_body


def handle(event, context):
    code = success_code
    response_body = success_body
    try:
        if event['body']:
            body = json.loads(event['body'])
            code, response_body = process_snapdocs_event(body)
        else:
            raise "no data found in the post body"
    except Exception as e:
        code = 400
        response_body = {
            "status": "Bad Request",
            "code": code,
            "message": f"failed to parse the webhook, {str(e)}"
        }
    finally:
        return {"statusCode": code, "body": json.dumps(response_body)}

For a local listener to test against, see Test your integration.