Transaction Fulfillment

Once a partner applications back-end receives a transaction initiation webhook notification and retrieves the transaction request information, it can begin the process of fulfilling the request and updating the transaction response.

ZipRight handles the following webhook notifications from the EPC platform:

  • New transaction initiated by a lender from the ZipRight user-interface
  • Transaction response delivered by ZipRight's back-end successfully processed by EPC

Transaction request processing


When a lender user on Web Version of Encompass initiates a new transaction via the ZipRight user-interface, the EPC platform prepares the request and pushes a webhook notification to the ZipRight back-end. This webhook has the resource type urn:elli:epc:transaction - and event type created.


Step 1: Verify the webhook

First of all, the ZipRight Backend verifies the incoming webhook's signature. Verification requires extracting the Elli-Signature request header and comparing it against the SHA256 HMAC signature generated from the webhook body and shared secret webhook signingkey specified in the product configuration.

The following snippet demonstrates the signature validation in the ZipRight backend:

ellie_signature = '{}'.format(connexion.request.headers.get('Elli-Signature'))

req_body_str = json.dumps(body)

message = bytes(req_body_str, 'ascii')
secret = bytes(signingkey, 'ascii')

hmac_digest = hmac.new(secret, message, digestmod=hashlib.sha256).digest()
mesg_signature_str = str(base64.b64encode(hmac_digest), 'ascii')

if mesg_signature_str == ellie_signature:
	# Accept and process the request
  
  ...
  
  return 'Accepted', 202
else
	return 'Unauthorized', 401

Step 2: Identify the transaction event type

ZipRight uses the webhook eventType and resourceType to determine whether the webhook notification is communicating new transaction request creation, or a transaction response fulfillment event.

if event_type == 'created' and resource_type == 'urn:elli:epc:transaction':
  # Initiate request processing thread
  
 ...
elif event_type == 'created' and resource_type == 'urn:elli:epc:transaction:event'
 # Initiate response fulfillment reconciliation thread
  
 ...

Step 3: Process the request

If it is a new request, ZipRight offloads the request processing to a new thread and returns the HTTP 202 (Accepted) status to EPC. The following snippet demonstrates the transaction type identification and processing:

if event_type == 'created' and resource_type == 'urn:elli:epc:transaction':
  # Update dummy request logs with initial entry for the request
  request_tracker_obj = dict()
  request_tracker_obj['status'] = 'VALIDATION_REQUESTED'
  request_tracker_obj['resource_id'] = None
  app.request_tracker[transaction_id] = request_tracker_obj
  
  # Kick off request processing thread
  access_token = app.token_manager.get_token()
  request_processor = threading.Thread(target=verify_with_usps, args=(transaction_id))
  request_processor.start()

First, retrieve the transaction request:

ZipRight's business logic involves calling the USPS public API to look up and verify the city and state specified for the subject property as received in the request loan data. To retrieve the subject property data required to perform this validation, ZipRight's back-end uses the resourceRef received in the webhook to call back into the transactions REST API and retrieve the transaction request. The following snippet demonstrates the request retrieval operation:

def get_property_data(transaction_id) -> dict:
	"""Retrieve loan subject property data from transaction request"""
    
    # Prepare API request
    access_token = app.token_manager.get_token()
    url = EPC_BASE_URL + EPC_TRANSACTIONS_URL + transaction_id
    headers = {
        "Authorization": "Bearer" + " " + access_token
    }

    # Retrieve transaction request
    response = get(url=url, headers=headers)
    
    # Raise exception for API failures
    if response.status_code != codes.ok:
      response.raise_for_status()
    
    # Extract and return subject property information
    response_obj = response.json()
    epc_request_object = response_obj.get("request", None)
    loan_object = epc_request_object.get("loan", None)
    property_object = loan_object.get("property", None)

    return property_object

Second, verify the ZIP code and update transaction response:

Once ZipRight has the subject property ZIP code, it calls the USPS Web Service and gets the associated city and state. Upon a successful USPS service response, ZipRight delivers its completed response for the transaction - with the USPS verified subject property city and state information - by invoking the REST API's PATCH /partner/v2/transactions/response endpoint. This is a multi-step process for ZipRight:

def update_response(status, property_information, transaction_id) -> dict:

    # Prepare response update
  	access_token = app.token_manager.get_token()
    url = EPC_BASE_URL + EPC_TRANSACTIONS_URL + transaction_id + "/response"
    headers = {
        "Authorization": "Bearer" + " " + access_token
    }

    content_type = MIMETYPES.get("JSON")
    payload = {
        "status": status,
        "partnerStatus": "ZIP Code Verification Completed",
        "referenceNumber": str(uuid.uuid4()),
        "respondingParty": {
            "name": "ZipRight Corp.",
            "address": "P.O. BOX 509124, SUITE 300",
            "city": "San Diego",
            "state": "CA",
            "postalCode": "92150",
            "pointOfContact": {
                "name": "John Doe",
                "role": "Manager",
                "phone": "8009864343",
                "email": "[email protected]"
            }
        },
        "loanFormat": LOAN_FORMAT,
        "loan": {
            "property": propery_information
        }
    }

    # Deliver response update
    response = patch(url=url, content_type=content_type, headers=headers, body=payload)

    # Raise exception for API failures
    if response.status_code != codes.ok:
      response.raise_for_status()

   return response

ZipRight uses the received city and state to generate a PDF document and upload it to EPC as an attachment. Uploading the attachment involves three steps:

  1. ZipRight sends EPC a request to upload an attachment with its type. In response, EPC sends ZipRight a URL, authorization token, and resource id.
  2. ZipRight streams the PDF file to the URL it received in the previous step
  3. After a successful upload, ZipRight delivers its completed response for the transaction, providing the resource id of the attachment along with the verified property data

The code snippets below demonstrate the three steps of the upload process:

def _prepare_upload_file(file_name, transaction_id):
	"""Utility function for staging transaction response attachment"""
    
    # Prepare response resource request
  	access_token = app.token_manager.get_token()

    url = EPC_BASE_URL + EPC_TRANSACTIONS_URL + transaction_id + "/response/resources"
    headers = {
        "Authorization": "Bearer" + " " + access_token,
    }

    content_type = MIMETYPES.get("JSON")
    payload = [
        {
            "name": file_name,
            "mimeType": "application/pdf"
        }
    ]

    # Create response resource
    response = post(url=url, content_type=content_type, headers=headers, body=payload)

    # Raise exception for API failures
    if response.status_code != codes.ok:
      response.raise_for_status()

    return response
...
if prepare_upload_rsp.status_code == 200:
  prepare_upload_rsp_obj = prepare_upload_rsp.json()
  resource_id = prepare_upload_rsp_obj[0].get("id", None)
  partner_resource_location = prepare_upload_rsp_obj[0].get("location", None)
  partner_resource_authorization_header = prepare_upload_rsp_obj[0].get("authorization", None)
  partner_resource_mimeType = prepare_upload_rsp_obj[0].get("mimeType", None)
  partner_file_name = prepare_upload_rsp_obj[0].get("name", None)

  # Prepare upload request
  url = partner_resource_location
  headers = {
    "Authorization":  partner_resource_authorization_header,
    "Content-Type": partner_resource_mimeType
   }
  
  # Upload file
  response = put_file(url, file_name, headers=headers)
  
  # Raise exception for API failures
  if response.status_code != codes.ok:
    response.raise_for_status()
...
...
  # Prepare transaction update request
  access_token = app.token_manager.get_token()
  
  url = EPC_BASE_URL + EPC_TRANSACTIONS_URL + transaction_id + "/response"
  headers = {
    "Authorization": "Bearer" + " " + access_token
  }

  content_type = MIMETYPES.get("JSON")
  payload = {
    # Response status and loan data
    "status": status,
    "partnerStatus": "Inspection Scheduled",
    "referenceNumber": "CXV90345",
    "respondingParty": {
      "name": "AddressVerify Corp.",
      "address": "P.O. BOX 509124, SUITE 300",
      "city": "SAN DIEGO",
      "state": "CA",
      "postalCode": "92150",
      "pointOfContact": {
        "name": "John Doe",
        "role": "Manager",
        "phone": "8009864343",
        "email": "[email protected]"
      }
    },
    "loanFormat": LOAN_FORMAT,
    "loan": {
      "property": propery_information
    },
    # Reference uploaded resource (file attachment)
    "resources": [
      {
        "id":resource_id,
        "name": file_name,
        "mimeType":mimeType
      }
    ]
  }

  # Update transaction response
  response = patch(url=url, content_type=content_type, headers=headers, body=payload)
  
  # Raise exception for API failures
  if response.status_code != codes.ok:
    response.raise_for_status()
...

Response fulfillment callback


EPC notifies integrations of transaction response processing success or errors via callback transaction events, enabling Partners to update and synchronize their internal management of orders against transaction processing success/errors in the EPC platform, and drive the appropriate workflows in their application. Once ZipRight successfully delivers its completed response to the EPC platform - it waits for a callback webhook notification for a new urn:elli:epc:transaction:event resource being created:

{
  "eventTime" : "2020-05-02T11:08:52Z",
  "eventType" : "created",
  "meta" : {
    "resourceType" : "urn:elli:epc:transaction:event",
    "resourceId" : "{{TRANSACTION_EVENT_ID}}",
    "instanceId" : "{{PRODUCT_NAME}}",
    "resourceRef" : "https://api.elliemae.com/partner/v2/transactions/{{TRANSACTION_ID}}/events/{{TRANSACTION_EVENT_ID}}"
  }
}

Once ZipRight receives this webhook - it retrieves the specific transaction event (resourceRef) and verifies that the transaction event is of type urn:elli:epc:transaction:response:processed - indicating response fulfillment. Once this is confirmed, ZipRight logs completion of the transaction in its dummy request tracker - and the transaction fulfillment process is completed 🎉

elif event_type == 'created' and resource_type == 'urn:elli:epc:transaction:event':
  # Call back into transaction event - and verify it is of type urn:elli:epc:transaction:response:processed
  
  ...

  # Update dummy request tracker with completed status
	req_tracker = app.request_tracker.get(transaction_id, None)
  if req_tracker is not None:
  	req_tracker['status'] = "REQUEST_COMPLETED"

You can read more about the response processing callback events events emitted by the EPC platform in the Deeper Dive section.