# APAC PQL ↔ HubSpot Writeback

Notes on the two data flows between the APAC PQL model and HubSpot: the model reading the target account list for P0 override, and the model writing score results back to HubSpot for Ops to use. For syncing with Engineering and Ops.

## Background

- HubSpot list `19544` is a Company target-account segment. A unique match by
  portal ID, account ID, non-personal email domain, or official website domain
  is escalated to P0 regardless of sales owner assignment or other rules.
- After scoring an Active tenant, the model writes P0/P1/P2 back to a HubSpot contact property, which Ops uses to trigger emails via HubSpot workflows / dynamic lists. Sales-owned tenants do not write back.

## Service Keys

| Key name | Purpose | Scopes | Used by |
|---|---|---|---|
| `APAC PQL Screening` | Read target account list (19544) membership for P0 override | `crm.lists.read`, `crm.objects.contacts.read`, `crm.objects.companies.read` | PQL model sync job |
| `APAC PQL Writeback` | Write P0/P1/P2 score results; write email/track status after Ops sends | `crm.objects.contacts.write`, `crm.objects.contacts.read` | PQL bot and Selva's agent |

## HubSpot Property

- **Object**: Contact properties
- **Property label**: APAC PQL Priority Tier
- **Internal name**: `apac_pql_priority_tier`
- **Field type**: Dropdown select
- **Options**: `P0` / `P1` / `P2` / `Not Scored`
- **Data Agent prompt**: unchecked

## Write Contract for Engineering

```python
import requests
from urllib.parse import quote

HUBSPOT_TOKEN = "..."
BASE_URL = "https://api.hubapi.com/crm/objects/2026-03/contacts"

def write_pql_tier(email: str, tier: str):
    resp = requests.patch(
        f"{BASE_URL}/{quote(email, safe='')}?idProperty=email",
        headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
        json={"properties": {"apac_pql_priority_tier": tier}},
    )
    if resp.status_code == 404:
        return "contact_not_found"
    resp.raise_for_status()
    return "updated"
```

Scoring processes one SQS message per invocation. After committing an Active
tenant's local score and snapshot, it updates the existing Contact whose primary email
matches the normalized tenant email. It does not use upsert and never creates
a missing Contact. List read should target list ID 19544 only.

## Sync Note for Ops (draft)

> Hi team, PQL priority now syncs into HubSpot via a Contact property:
> - `APAC PQL Priority Tier` (internal name: `apac_pql_priority_tier`), values P0 / P1 / P2 / Not Scored
>
> Email workflows / dynamic lists can filter on `apac_pql_priority_tier`. Data is written after each eligible Active tenant scoring event; Sales-owned tenants are excluded. Flag early if the field name or values need to change.

## Open Items

- Deployment of the writeback token to the Scoring Lambda
- Failure alerting for the sync job
