Contacts, sellers and registries
Everything the platform contacts resolves to a Contact. Sellers sit above contacts as the company;
the phone and email registries sit below as the contactable endpoints.
erDiagram
Seller ||--o{ Contact : employs
Contact ||--o{ TargetPhoneNumbers : "phone endpoints"
Contact ||--o{ TargetEmails : "email endpoints"
Contact ||--o{ CampaignTarget : "dialer enrollment"
Contact ||--o{ EmailCampaignTarget : "email enrollment"
Contact ||--o{ ContactSequence : "nurture enrollment"
Contact ||--o{ Activity : "interaction log"
Contact ||--o{ FollowUpTask : "rep tasks"
Sellers
A Seller is a company selling on a marketplace. Sellers map to HubSpot Companies, not Contacts.
| Field group | Fields |
|---|---|
Identity |
|
Lifecycle |
|
Marketplace metrics |
|
Growth |
|
Categories |
|
Address |
|
Suspension |
|
HubSpot |
|
Enrichment |
|
Diagnostics |
|
payoutPainScore is the qualification signal: how much a seller is likely to suffer from
marketplace payout delays, and therefore how relevant Payability’s product is to them.
GET /api/sellers filters by search, platform, isSuspended, hubspotSyncStatus, with
pagination. The Sellers sidebar item is hidden; use /suppliers for Payability suppliers.
GET /api/suppliers lists the BigQuery-synced Supplier copy. Primary filters are
api_status, access_status, and cancelation_status (none for unset), plus search,
status, and payability_status. Detail is GET /api/suppliers/{id}.
Contacts
Contact is the single source of truth for a person, shared between the dialer, email campaigns,
sequences and HubSpot.
| Field group | Fields |
|---|---|
Identity |
|
Address |
|
Source |
|
Lifecycle |
|
Calling context |
|
HubSpot |
|
Supplier |
|
Free-form |
|
Heat status
ContactHeatStatus encodes engagement recency and is what prioritisation reads:
| Value | Meaning |
|---|---|
|
Active follow-up in progress |
|
A rep acted but the deal has not closed |
|
A task failed; an automated email track is running |
|
30 days of silence — deprioritised |
Uniqueness
Four composite unique constraints, all scoped by workspace: (subscriptionId, email),
(subscriptionId, phone), (subscriptionId, hubspotContactId), (subscriptionId, supplierKey).
Nullable-unique semantics apply — many rows may have NULL, but non-null values must be unique. This
is what makes "upsert the contact, then create the enrollment" safe for CSV imports.
Endpoints
GET /api/contacts filters by search, name, email, phone, hubspotId, sellerId,
source, lifecycleStage, leadStatus, stopCalling, hasEmail, hasSupplierKey.
The detail page is backed by GET /api/contacts/{id} plus /stats, /calls, /activities,
/emails and /phones.
POST /api/contacts/crm-queue pushes a contact onto the CRM queue on demand — the manual escape
hatch when an automatic sync did not fire.
Registries
The registries are the contactability layer. A campaign enrolls a target, but whether that target may actually be contacted is decided by its registry row.
TargetPhoneNumbers
One row per canonical phone number, globally unique on phoneNumber. Table target_phone_numbers.
| Field | Meaning |
|---|---|
|
E.164 number, globally unique |
|
Optional link to the owning contact |
|
Carrier and numbering metadata |
|
|
|
|
|
Lifecycle and block flags |
|
Sync state |
Dial eligibility (isTargetPhoneRegistryEligibleForDial): not blocked, not deleted, and
riskLevel is low or unset. medium and above are refused — a medium-risk number is more likely
to be a reassigned or flagged line, and dialing it risks both a wasted call and a compliance problem.
A CampaignTarget may point at a registry row through targetPhoneNumbersId. A target with no
registry row is still dialable: the registry enriches and restricts, it is not a prerequisite.
When a call ends in DNC, or with an uncallable disconnection reason (UNCALLABLE_DISCONNECTION_REASONS),
callEndedAnalysis sets isBlocked = true and refreshes updatedAt on the registry row. That is
what makes the number permanent across every campaign in the workspace. The cron lease
get_and_lease_due_targets does not select targets whose registry row is blocked.
GET /api/target-phone-numbers filters by search, is_active, is_blocked, risk_level,
include_deleted. The hub is at /targets/phone-numbers (/targets redirects there).
TargetEmails
One row per address, unique on (subscriptionId, email). Table target_emails.
| Field | Meaning |
|---|---|
|
Canonical address, normalised to lowercase in the application |
|
Optional link to the owning contact |
|
Lifecycle; |
|
RFC 8058 or hosted unsubscribe, or the Resend |
|
Set on |
|
Opt-out for direct one-to-one email; not part of the campaign suppression check |
|
Last marketing send, used for the cross-campaign cadence gap |
|
Minimum days between marketing emails; default |
EmailCampaignTarget.emailId is a required FK to this table, so an email recipient always has a
registry row and suppression can be joined in at lease time.
GET /api/target-emails filters by search, is_active, is_blocked, newsletter_opt_out,
include_deleted.
HubSpot synchronisation
The platform mirrors identity into HubSpot rather than treating HubSpot as the source of truth:
| Local model | HubSpot object |
|---|---|
|
Company ( |
|
Contact ( |
|
Engagement ( |
|
Task ( |
Sync direction:
-
Outbound — CRM events are published to SQS (
Crm.DialerCallEnded,Crm.NewActivity,Crm.CustomEvent,Crm.DealUpdate,Crm.TaskCreate,Contact.Update,Contact.Unsubscribe) and applied by a downstream service. Handlers only publish when a HubSpot contact id is present, so a consumer never receives an unresolvable payload. -
Inbound —
POST /api/webhooks/hubspotverifies HubSpot’s signature and republishes normalised payloads asCrm.HubSpotWebhook.
hubspotSyncStatus is synced, pending, failed or skipped.
Contact fields that gate outreach
| Field | Effect |
|---|---|
|
Excludes the contact from dialing; set on DNC |
|
Pre-validation skips the target with |
|
Warm-lead set, reserved for the |
|
A prior terminal disposition on the number blocks re-dialing |
|
|