Contact and registry models
Seller
Table sellers. A marketplace seller company; maps to a HubSpot Company.
| Field group | Fields | Notes |
|---|---|---|
Keys |
|
|
Classification |
|
|
Identity |
|
Email and phone are globally unique, unlike |
Brand |
|
|
Scale |
|
|
Growth |
|
|
Address |
|
|
Status |
|
|
HubSpot |
|
|
Enrichment |
|
|
Diagnostics |
|
Enrichment failures |
Indexes: subscriptionId, platform, sellerId, hubspotCompanyId, country, isSuspended,
primaryCategory.
Supplier
Table suppliers. A Payability supplier-summary copy from BigQuery
PayabilitySheets.v_supplier_summary_extended. Distinct from Seller (marketplace company /
SmartScout) and not linked to Contact.
| Field group | Fields | Notes |
|---|---|---|
Keys |
|
Identity is Payability |
Names |
|
|
Source times |
|
BQ |
Sync |
|
Our last write. Set on every successful upsert, including stale key re-pulls |
Status |
|
Overwritten from BQ on each match |
Primary contact |
|
|
Address |
|
|
Flags |
|
|
Scale |
|
|
Ops |
|
Calendar date |
Extra |
|
Remaining BQ columns (tax id, UTMs, purchase/mp/chargeback windows, UW fields) |
Unique: (subscriptionId, supplierKey).
Indexes: subscriptionId, sourceUpdatedAt, lastSyncedAt, status.
Inbound Payability import
suppliers/payability-import (5x daily ET, also suppliers/payability-import.requested) queries
PayabilitySheets.v_supplier_summary_extended for up to 1000 rows after Configs
supplier_sync_cursor ({ updated, supplier_key }), then upserts them into the workspace in
HUBSPOT_CONTACT_SYNC_SUBSCRIPTION_ID.
-
Skip when
supplier_keyis missing. -
Create when the key is new to the workspace.
-
Overwrite mapped columns on match so status changes apply (unlike HubSpot contact fill-nulls).
-
Stamp
lastSyncedAton every successful upsert — this is the last-updated time we trust, not BQupdated. -
Advance the cursor to the last row’s
(updated, supplier_key)keyset so same-day rows are not skipped. -
A missing cursor starts at
1970-01-01/ empty key. -
After the incremental batch, re-fetch up to 1000 existing suppliers with the oldest
lastSyncedAt(excluding keys just imported). The view’supdatedcolumn does not move when other constituent tables change, so a key re-pull is how status still lands andlastSyncedAtadvances.
The workspace list is /suppliers (GET /api/suppliers). Operators filter on apiStatus,
accessStatus, and cancelationStatus first. The Sellers sidebar item is hidden; /sellers
remains reachable by URL.
Contact
Table contacts. The single source of truth for a person.
| Field group | Fields | Notes |
|---|---|---|
Keys |
|
|
Identity |
|
|
Address |
|
|
Source |
|
|
Calling |
|
|
HubSpot |
|
|
Supplier |
|
|
Free-form |
|
Unique: (subscriptionId, email), (subscriptionId, phone),
(subscriptionId, hubspotContactId), (subscriptionId, supplierKey). Nullable-unique semantics —
many `NULL`s are allowed, non-null values must be unique.
Indexes: subscriptionId, sellerId, email, phone, disposition, hubspotContactId,
supplierKey, source, lifecycleStage, leadStatus, stopCalling.
Relations: subscription, seller, emailCampaignTargets, activities, targetPhoneNumbers,
targetEmails, emails, campaignTargets, followUpTasks, contactSequences.
Inbound HubSpot import
contacts/hubspot-import (5x daily ET, also contacts/hubspot-import.requested) queries
hubspot4.v_contacts_latest for up to 1000 contacts that are not in outreach_app.contacts, then
writes them into the workspace in HUBSPOT_CONTACT_SYNC_SUBSCRIPTION_ID.
-
Skip when
hubspotContactIdalready exists. -
Else match email, then phone, on a local contact with a null HubSpot id and fill empty fields only.
-
Else create a
Contactwithsource = hubspot. -
Create or link
TargetEmails/TargetPhoneNumberswhen email or phone is present. -
Does not enroll campaign targets.
TargetPhoneNumbers
Table target_phone_numbers. Canonical registry of dial-to numbers.
| Field | Type | Notes |
|---|---|---|
|
|
Globally unique across workspaces |
|
|
|
|
|
|
|
|
Carrier lookup data |
|
|
|
|
|
Only |
|
|
|
|
|
Sync intent flags |
|
|
Relations: subscription, contact, campaignTargets, plus calls through relation
CallRegistryTargetPhone.
Set isBlocked to suppress a number across every campaign; the DNC path does this automatically.
TargetEmails
Table target_emails. Canonical registry of email addresses and their opt-out state.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
Normalised to lowercase by the application |
|
|
|
|
|
|
|
|
RFC 8058, hosted unsubscribe, or |
|
|
Set on |
|
|
Direct email only; not part of campaign suppression |
|
|
Last marketing send, for the cadence gap |
|
|
Minimum days between marketing emails; default |
Unique: (subscriptionId, email).
EmailCampaignTarget.emailId is a required FK here, which is what allows suppression and cadence
to be joined into the target-leasing query.
Activity
Table Activity. The unified interaction log.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
|
|
|
Nullable for backward compatibility with pre- |
|
|
|
|
|
|
|
|
|
|
|
default |
|
|
default |
|
|
SMS chat id |
|
|
|
|
|
Provider id, used for idempotent upserts |
|
|
Unique: (subscriptionId, externalId).
Indexes: subscriptionId, type, hubspotEngagementId, createdAt, contactPhone,
contactId, taskId.
Relations: subscription, contact, task, contactSequenceSteps.
Calls are not duplicated into Activity; GET /api/activity merges the Call table in at query
time, because Call carries far more structure than an activity row could hold.