Snapshot
Project goal
Build a Goodwood-owned middleware that consumes Greenbox’s API M gateway endpoints
(Merchants, Owners, Loan Requests, Brokers) and lands data into HubSpot using canonical-ID
lookup at every write — no hs_object_id stored on the Box side, no
dangling pointers after HubSpot merges. The pipeline ships against mock endpoints
first so Greenbox-side endpoint work can proceed in parallel; the mock spec doubles as the API
contract Greenbox implements to.
Alignment meeting (05/13/2026)
Decisions confirmed at the kickoff alignment meeting with Sarah Lackey, Kris Glaittli, Brian LaMure, Kusha Kapoor, Nova Guliyev, and Steven Lentz.
AI Summary →
Decisions, action items, attendees, notable quotes. Cleanest one-page reference.
Full Transcript →
Verbatim Teams-generated transcript, 47 min, color-coded by speaker.
Recording
Hosted on Greenbox Teams — access via Sarah’s calendar event for 05/13/2026.
| Decision | Outcome |
|---|---|
| Middleware ownership | Goodwood owns the integration end-to-end — transformer, reconciler, audit, drift detection. |
| Source-of-truth read path | Greenbox exposes REST endpoints via an API M gateway for Merchants, Owners, Loan Requests, Brokers. |
| Sync cadence | Near real-time: event-driven preferred, 15-min polling as fallback. |
Round-trip hs_object_id | Retired. HubSpot lookup is by canonical Greenbox-side ID with unique constraints. |
| ID scheme | Prefixed-string IDs — MID-12345, MOID-67890, SUB-..., BID-... — per Brian’s proposal. Eliminates collision risk across object types. |
| Hosting stack | Firebase assumed for this sprint — final Firebase vs Databricks call rests with Sarah + Brian. A flip mid-sprint requires re-pricing. |
Architecture
Tech stack
API M Gateway
- REST + JSON
- Auth: API key / HMAC
- OpenAPI 3.0 spec
- Cursor-based pagination
- Prefixed-string IDs
Firebase / GCP
- Cloud Functions Gen 2
- Firestore (canonical mirror + audit)
- Cloud Scheduler
- Secret Manager
- Cloud Monitoring
- Node 20 ESM
HubSpot v4
- CRM v3 + v4 APIs
- Sandbox + Production
- Private App auth
- Canonical-ID upsert
- v4 batch associations
Scope — 13 deliverables
| # | Deliverable | Description |
|---|---|---|
| 1 | Firebase foundation | Cloud Functions Gen 2, Firestore, Secret Manager, deploy safety with explicit project-flag enforcement. |
| 2 | Mock endpoints + OpenAPI spec | Goodwood-hosted mocks for all four endpoints. The spec becomes the contract Greenbox builds to. |
| 3 | End-to-end pipeline (4 object types) | Poller → transformer → reconciler → HubSpot, covering Merchants, Owners, Loan Requests, Brokers. |
| 4 | v4 batch associations | Deal↔Merchant, Deal↔Broker, Contact↔Merchant. Idempotent. |
| 5 | Core transformations | DeclineReasons comma→semicolon, Title Case, phone normalization, invalid-email filter, picklist value-to-null fallback. |
| 6 | Audit log | Every input → transform → HubSpot response logged to box_sync_runs/{batchId}/{type}/{recordId}. Per-record lineage. |
| 7 | Drift reconciler | Daily diff job. Firestore mirror vs HubSpot canonical-ID lookup. Drift entries written to drift_alerts. |
| 8 | Webhook receiver | HTTPS Cloud Function for real-time push mode alongside polling. HMAC-signed payloads. |
| 9 | Unique-constraint migration | Backfill _unique properties on Contact & Company; enable hasUniqueValue: true post-dedup. |
| 10 | Cloud Monitoring dashboards | Per-function error rate, latency, throughput. Uptime checks on the webhook receiver. |
| 11 | HubSpot Sandbox + Private App provisioning | Goodwood-handled. Includes a separate read-only Private App for high-volume drift reads. |
| 12 | Production cutover + parallel-run | Pipeline runs alongside the legacy Box Update Service (source 51841) for validation; retire on sign-off. |
| 13 | Sprint demo + runbook | End-to-end walkthrough; operational handoff doc; cutover recommendation. |
Timeline
| Week | Focus | Milestone |
|---|---|---|
| 1 05/26 – 05/29 |
Firebase foundation. Mock endpoints stood up. OpenAPI spec published. Reconciler skeleton with canonical-ID lookup. API contract review with Brian / Kusha / Nova. | Contract accepted · Greenbox endpoint team unblocked |
| 2 06/01 – 06/05 |
Pipeline for all four objects. Transformer with core mappings + edge cases. v4 batch associations. Audit log to Firestore. Cursor management. | End-to-end mock-to-Sandbox demo |
| 3 06/08 – 06/12 |
Drift reconciler. Webhook receiver. Cloud Monitoring dashboards. Unique-constraint migration prep. If Greenbox endpoints live → swap mocks for real endpoints. | Full pipeline with observability live |
| 4 06/15 – 06/19 |
Parallel-run alongside legacy Box Update Service. Cutover plan. Runbook. Sprint demo. | Demo accepted · cutover decision |
API contract — the four endpoints
Greenbox implements the API M gateway to match this contract. Mock endpoints Goodwood hosts during Week 1 are the exact spec.
| Endpoint | Returns | Notes |
|---|---|---|
GET /api/merchants?since={iso}&limit=1000 |
Merchant rows (HubSpot Companies) | Cursor-paginated. Prefixed string IDs (MID-...). Includes status, qualification, balance, SIC, address fields. |
GET /api/merchant-owners?since={iso}&limit=1000 |
Merchant owner rows (HubSpot Contacts) | Primary + secondary owners arrive as separate rows. Resolves the multi-entity case at source. |
GET /api/loan-requests?since={iso}&limit=1000 |
Submission / loan-request rows (HubSpot Deals) | Includes DeclineReasons raw — transformer applies comma→semicolon. |
GET /api/brokers?since={iso}&limit=1000 |
Broker rows (HubSpot Broker Companies) | Prefixed string IDs (BID-...). Includes broker contact info, RAM assignment. |
POST {goodwood-webhook} OPTIONAL |
Push delta on every Box write | Real-time mode. Goodwood exposes the webhook URL. Either-or with polling endpoints. |
All endpoints: API-key or HMAC auth. Idempotent (same since + no new changes = same response). OpenAPI 3.0 spec published in Week 1.
HubSpot field mapping
The complete CREATE + UPDATE field contract. These are the fields the new endpoints must return per object type. Three tables: Company (Merchant + Broker), Contact (Owner), Deal (Loan Request). CREATE + UPDATE = field flows on both initial create and every change. CREATE only = set once on creation. Confirmed via 90-day property-history audit of the legacy Box Update Service (source 51841); slow-cadence fields marked schema-inferred pending follow-up history audit.
Company (Merchant + Broker)
| HubSpot property | Box source field | Type | Phase | Notes |
|---|---|---|---|---|
merchant_id | Merchant Id | number | CREATE + UPDATE | Canonical key. Prefixed-string form MID-.... |
merchant_status | MerchantStatus | string | CREATE + UPDATE | — |
merchant_payment_status | PaymentStatusString | string | CREATE + UPDATE | — |
last_box_update | last_box_update | datetime | CREATE + UPDATE | Integration heartbeat |
dba | DBA | string | CREATE + UPDATE | Title Case |
business_email | EmailAddress | string | CREATE + UPDATE | — |
business_fax | BusinessFax | string | CREATE + UPDATE | — |
do_they_have_a_business_bank_account | Do they have a business bank account? | enum | CREATE + UPDATE | Yes/No |
do_they_meet_minimum_revenue_requirements_ | Do they meet minimum revenue requirements? | enum | CREATE + UPDATE | Yes/No |
have_they_met_time_in_business_requirement_ | Have they met time in business requirement? | enum | CREATE + UPDATE | Yes/No |
is_the_business_open_ | Is the business open? | enum | CREATE + UPDATE | Yes/No |
company_structure | company_structure | string | CREATE + UPDATE | — |
date_established | BusinessEstablished | date | CREATE + UPDATE | ISO date |
home_based_flag | IsHomeBased | bool | CREATE + UPDATE | TRUE/FALSE |
is_business_seasonal_ | IsSeasonal | bool | CREATE + UPDATE | TRUE/FALSE |
length_of_ownership | LengthOfOwnership | number | CREATE + UPDATE | decimal years |
not_for_profit_flag | IsNotProfitOrg | bool | CREATE + UPDATE | TRUE/FALSE |
products_and_services_sold | ProductsServicesSold | string | CREATE + UPDATE | — |
sic_code | SicCode | string | CREATE + UPDATE | — |
sic_code_level_2 | SicCodeLevelTwo | string | CREATE + UPDATE | — |
sic_code_level_3 | SicCodeLevelThree | string | CREATE + UPDATE | — |
outstanding_balance | CurrentBalance | number | CREATE + UPDATE | — |
percent_balance_remaining | PerccentBalanceRemaining | number | CREATE + UPDATE | note CSV header typo |
province_from_the_box | Province | string | CREATE + UPDATE | — |
merchant_status_change_date | MerchantStatusChangeDate | date | CREATE + UPDATE | schema-confirmed |
pay_off_date | PayOffDate | date | CREATE + UPDATE | schema-confirmed |
renewal_eligible_date | FundingEligibleDate | date | CREATE + UPDATE | drives renewal cadences |
name | Name | string | CREATE only | HS-native; Title Case |
address | Street Address | string | CREATE only | HS-native; Title Case |
address2 | Apartment | string | CREATE only | HS-native; Title Case |
city | City | string | CREATE only | HS-native; Title Case |
state | State | string | CREATE only | UPPER if 2-letter |
zip | Postal Code | string | CREATE only | HS-native |
country | CountryName | string | CREATE only | HS-native |
phone | Phone Number | string | CREATE only | XXX-XXX-XXXX |
website | Website URL | string | CREATE only | HS-native |
hs_lead_status | Lead status | enum | CREATE only | HS-native |
company_type | (derived) | enum | UPDATE only | Distinguishes Merchant vs Broker |
main_lead_source / sub_lead_source | (policy) | string | UPDATE only | Source-attribution rules |
| Candidate Box-sourced fields below — schema-inferred (label / description / code reference). Verify via property-history audit during Week 1. | ||||
ram_email_address_box | (Box: RAM assignment) | string | schema-inferred | Renewal Account Manager email. Drives renewal deal-owner routing. |
broker_id | Broker Id | number | schema-inferred | Primary key on Broker Companies (prefixed BID-...) |
box_broker_id__unique | Broker Id (string) | string | schema-inferred | Already hasUniqueValue: true |
merchant_balance_remaining | (Box: Advance balance API) | number | schema-inferred | “Set by API from the Box” per schema description |
merchant_box_url | (Box: deep link) | string | schema-inferred | Deep-link URL into Box UI |
broker_of_record___greenbox | (Box flag) | enum | schema-inferred | Flag: Greenbox is broker of record |
Contact (Merchant Owner)
| HubSpot property | Box source field | Type | Phase | Notes |
|---|---|---|---|---|
merchantsownersid | MerchantOwnerId | string | CREATE + UPDATE | Canonical key. Prefixed-string form MOID-.... Top-frequency property in the legacy audit. |
associated_merchant_id | Merchant Id | string | CREATE + UPDATE | Foreign key to Company |
associated_broker_id | (derived) | string | CREATE + UPDATE | Foreign key to Broker |
last_box_update | last_box_update | datetime | CREATE + UPDATE | Integration heartbeat |
contact_type | (derived from MOID presence) | enum | UPDATE only | — |
state_region | (derived from address) | string | UPDATE only | — |
state_abbreviation | (derived from address) | string | UPDATE only | — |
province | (derived from address) | string | UPDATE only | — |
zip_code | (derived from address) | string | UPDATE only | — |
firstname | First Name | string | CREATE only | HS-native; Title Case |
lastname | Last Name | string | CREATE only | HS-native; Title Case |
phone | Phone Number | string | CREATE only | XXX-XXX-XXXX |
email | Email | string | CREATE only | ⚠️ HubSpot auto-merges on collision — endpoint must resolve multi-entity at source |
mobilephone | HomePhone | string | CREATE only | Confirm canonical property name |
main_lead_source / sub_lead_source | (policy) | string | UPDATE only | Lead-source workflow |
Deal (Loan Request / Submission)
| HubSpot property | Box source field | Type | Phase | Notes |
|---|---|---|---|---|
loan_request_id | Submission Id | number | CREATE + UPDATE | Canonical key. Prefixed-string form SUB-.... |
associated_merchantid | Merchant Id | string | CREATE + UPDATE | Foreign key to Company |
associated_brokerid | PartnerId | string | CREATE + UPDATE | Foreign key to Broker |
associated_merchantownersid | MerchantOwnerId | string | CREATE + UPDATE | Foreign key to Contact |
brokerage | Brokerage | string | CREATE + UPDATE | Partner Legal Name |
received_offer_ts | Received Date | date | CREATE + UPDATE | — |
funded_by | Funded By | string | CREATE + UPDATE | — |
greenbox_rep | GreenboxRep | enum | CREATE + UPDATE | Must match approved picklist |
merchant_payment_status | Merchant payment status | string | CREATE + UPDATE | — |
merchant_status | Merchant status | string | CREATE + UPDATE | — |
outstanding_balance | CurrentBalance | number | CREATE + UPDATE | — |
is_active_ | IsActive | bool | CREATE + UPDATE | TRUE/FALSE |
box_processing_status | LuProcessingStatusId | string | CREATE + UPDATE | Numeric pipeline status code |
decline_reason | DeclineReasons | string | CREATE + UPDATE | ⚠️ comma→semicolon transform required |
decline_date__c | DeclineDate | date | CREATE + UPDATE | — |
deactivation_date__c | DeactivationDate | date | CREATE + UPDATE | — |
inactive_reason | DeactivationReasons | string | CREATE + UPDATE | Same comma→semicolon transform |
last_box_update | last_box_update | datetime | CREATE + UPDATE | Integration heartbeat |
amount | Amount | number | CREATE only | HS-native |
dealname | Deal name | string | CREATE only | HS-native |
closedate | Close date | date | CREATE only | HS-native |
Why canonical-ID lookup (no hs_object_id round-trip)
The legacy pattern stored HubSpot’s hs_object_id back in Box on every create so future Box writes could PATCH directly. That pattern is no longer viable — when HubSpot merges two records, the merged record gets a new canonical id and the old one is retired. Any hs_object_id Box has stored becomes a dangling pointer the next time it’s used.
The modern pattern is the inverse: look up records by canonical Greenbox-side IDs every time, with HubSpot uniqueness constraints making the lookup O(1):
Per Box delta record:
→ reconciler reads canonical ID (MID-... / MOID-... / SUB-... / BID-...)
→ HubSpot v4: GET /crm/v3/objects/{type}/{canonical_id}?idProperty={canonical_prop}
→ 200 → PATCH that record | 404 → POST new record
→ no Box-side ID storage, no callback required
→ HubSpot merges are invisible to Box (canonical ID survives the merge)
Prerequisite: canonical Greenbox-side ID properties on Contact and Company set to hasUniqueValue: true in HubSpot schema. Migration is in scope (deliverable #9) and runs after the dedup pre-cleanup pass.
Greenbox responsibilities
| Item | Owner | Window |
|---|---|---|
| API contract review & sign-off on OpenAPI spec | Brian + Kusha + Nova | Week 1 |
Prefixed-string ID scheme confirmation (MID- / MOID- / SUB- / BID-) | Brian + Kusha | Week 1 |
| Tech stack decision — Firebase vs Databricks | Sarah + Brian | Pre-kickoff |
| HubSpot API daily-limit increase (request to HubSpot for build / cutover window) | Sarah | Before Week 3 |
| API M endpoints implemented & reachable from Goodwood Firebase | Brian + Kusha + Nova | Week 3 target (Week 4 latest) |
| PII classification per field exposed through the gateway | Greenbox security review | Before production cutover |
| Sprint demo attendance (1 hour) | Sarah + Kris + Brian | End of Week 4 |
Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| Greenbox endpoint slip past Week 3 | Medium | Mock-first design absorbs this. Demo runs on mocks if real endpoints not ready; cutover deferred to a short follow-on. |
| HubSpot API daily-limit pressure | Medium | Goodwood provisions a separate read-only Private App for drift reconciler reads. Greenbox to request HubSpot quota increase before Week 3 cutover load. |
| Tech-stack flip to Databricks mid-sprint | Low | SOW pricing anchored to Firebase. Transformer logic ports cleanly (pure functions); orchestration re-platform would be a change order. |
| Auth scheme negotiation | Medium | Resolved Week 1 contract review. Sprint budget covers one variant (API key, HMAC, or JWT); multi-auth = scope change. |
| Duplicate canonical IDs block unique-constraint migration | Medium | Dedup pre-cleanup pass in Week 3. If material duplicate volume remains, migration deferred to a separate engagement. |
Definition of done
- Pipeline runs end-to-end (mock or real endpoints) for all four object types into HubSpot
- Canonical-ID lookup on every write; zero
hs_object_idstored Box-side - Audit log queryable per record
- Drift reconciler running nightly
- Webhook receiver tested
- Cloud Monitoring dashboards live
- Production cutover plan accepted (parallel-run results signed off)
- Sprint demo accepted by Sarah Lackey / Kris Glaittli / Brian LaMure
Stakeholders
| Person | Role |
|---|---|
| Sarah Lackey | Greenbox — VP of Technology · project sponsor |
| Kris Glaittli | Greenbox — VP of Sales · business owner |
| Brian LaMure | Greenbox — architect, API M gateway + prefixed-string ID scheme |
| Kusha Kapoor | Greenbox — developer team lead, Box-side data owner |
| Nova Guliyev | Greenbox — endpoint co-implementer |
| Steven Lentz | Greenbox — SQL / data |
| Ryan Thibodeaux | Goodwood — lead architect, middleware owner |
public/greenbox/reports/box-hubspot-api-sync-plan-2026.html in the
greenbox-core-apps repo. Project pricing & commercial terms are in the companion
Statement of Work.