HubSpot

components/hubspot/client.py mirrors a supplier’s Entrust verification lifecycle onto their HubSpot contact, so Growth & Retention can see in the CRM whether a supplier has started, passed, been denied, or is under review for ID verification.

Contract

Auth

HubSpot Private App token — settings.HUBSPOT_PRIVATE_APP_TOKEN (Secrets Manager).

Matching property

supplierkey on the contact — searched with a CRM EQ filter, limit 1.

Written property

entrust_verification_status.

class EntrustVerificationStatus:
    IN_PROGRESS = 'in_progress'
    VERIFIED = 'verified'
    DENIED = 'denied'
    IN_REVIEW = 'in_review'
Value Set when

in_progress

POST /entrust/sessions created (or reused nothing and minted) a session.

verified

The workflow run resolved to approved.

in_review

The run is held for manual review.

denied

Any other terminal status — declined, abandoned, error.

update_entrust_verification_status(supplier_key, status, logger=None)

Finds the contact whose supplierkey equals supplier_key and PATCHes its entrust_verification_status. Returns True when a contact was found and updated, False otherwise.

Best effort by design. HubSpot is a downstream, non-authoritative sync target, so:

  • a missing contact logs a warning and returns False;

  • any API error is logged, reported to Sentry via sentry_sdk.capture_exception(), and returns False;

  • the method never raises, so it can never fail session creation or the SWF aftermath task.

Multiple matches are unexpected (the key is meant to be unique) — the first is taken.

An optional correlation-aware logger can be passed so the lines join the rest of the request or task log; otherwise a module logger is used.

Why a local client instead of pyb-hubspot

The shared pyb-hubspot library requires Python >= 3.12, while this service is pinned to Python 3.9 (its pyb-sdk dependency supports only 3.9). Rather than import a 3.12-only package, this client builds directly on hubspot-api-client (Python >= 3.7, no conflicting dependencies) and reproduces just the two calls needed: a CRM contact search by property, and a partial property PATCH.

Callers

  • interface/web_api/endpoint_handling/create_entrust_session.py — in_progress on create.

  • task/entrust/handle_identity_document_validation_response/implementation.py — verified / in_review / denied on completion, via __sync_external_status.

Both receive the client through construction (swf_app.py injects it into TaskFactory; the endpoint handler builds its own). Both call sites emit a matching supplier event through SupplierEventLogger at the same moment, with the same status value — in the aftermath task the two are driven together by __sync_external_status.

Tests

development/unit_tests/components_/test_hubspot_client.py and development/unit_tests/interface_/web_api/endpoint_handling/test_create_entrust_session_hubspot.py.

Operational notes

  • The two custom contact properties (supplierkey, entrust_verification_status) must exist in the HubSpot portal; a typo in either name silently degrades to "no contact found" or a PATCH error, both of which only appear in logs and Sentry.

  • Because failures are swallowed, HubSpot drift is not visible in the underwriting data. If the CRM looks stale, check Sentry for HubSpotClient errors rather than the task status.