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

platform (amazon, walmart, shopify), datasource (smartscout, manual), sellerId (globally unique), name, businessName, email, phone

Lifecycle

lifeCycleStage — new, enriched, qualified, opportunity, closed_won, closed_lost

Marketplace metrics

estimateSales, avgPrice, percentFba, numberReviewsLifetime, numberReviews30Days, numberAsins, numberTopAsins, numberWinningBrands, numBrands, singleBrand, brandNameMatch

Growth

moMGrowth, threeMonthGrowth, sixMonthGrowth, yearGrowth, moMGrowthCount, sixMonthGrowthCount

Categories

primaryCategory, primarySubCategory

Address

street, city, state, country, zipCode

Suspension

isSuspended, lastSuspendedDate, startedSellingDate

HubSpot

hubspotCompanyId (unique), hubspotOwnerId, hubspotSyncStatus, hubspotLastSyncedAt, hubspotEngagementId

Enrichment

website, description, linkedinUrl, industry, annualRevenue, numberOfEmployees, payoutPainScore, enrichedAt, scrapedAt

Diagnostics

note, errors (JSON array)

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

firstName, lastName, email, phone, mobilePhone, companyName, jobTitle, website

Address

street, city, state, country, zipCode

Source

source (campaign, hubspot, import, manual; stored as a string), sellerId

Lifecycle

lifecycleStage (mirrors HubSpot lifecyclestage), leadStatus (mirrors hs_lead_status), heatStatus, leadScore, totalRevenue

Calling context

disposition, nextAction, stopCalling, validConversation, lastCallRecordingUrl

HubSpot

hubspotContactId, hubspotOwnerId, hubspotSyncStatus, hubspotLastSyncedAt, hubspotEngagementId, hubspotSyncPriority, hubspotDealId, hubspotDealStatus, hubspotDealStage

Supplier

supplierKey, supplierStatus, supplierCancellationStatus, supplierMarketplace

Free-form

notes, customFields (JSON)

Heat status

ContactHeatStatus encodes engagement recency and is what prioritisation reads:

Value Meaning

hot

Active follow-up in progress

warm

A rep acted but the deal has not closed

warming

A task failed; an automated email track is running

idle

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

phoneNumber

E.164 number, globally unique

contactId

Optional link to the owning contact

carrierName, countryCode, mobileCountryCode, mobileNetworkCode

Carrier and numbering metadata

lineType

LineTypes — mobile, landline, fixedVoip, nonFixedVoip, tollFree, sharedCost, premium, voicemail, pager, voip, uan, unknown, other

riskLevel

RiskLevel — low, medium, high, critical

isActive, isDeleted, isBlocked

Lifecycle and block flags

syncTwilio, syncHubspot, hubspotSyncedAt

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

email

Canonical address, normalised to lowercase in the application

contactId

Optional link to the owning contact

isActive, isDeleted, isBlocked

Lifecycle; isBlocked is set on spam complaints

newsletterOptOut / newsletterOptOutAt

RFC 8058 or hosted unsubscribe, or the Resend email.unsubscribed event

marketingOptOut / marketingOptOutAt

Set on email.complained

oneToOneOptOut

Opt-out for direct one-to-one email; not part of the campaign suppression check

newsletterLastSentAt

Last marketing send, used for the cross-campaign cadence gap

newsletterGapDays

Minimum days between marketing emails; default 7

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

Seller

Company (hubspotCompanyId)

Contact

Contact (hubspotContactId)

Call

Engagement (hubspotEngagementId)

FollowUpTask

Task (hubspotTaskId)

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/hubspot verifies HubSpot’s signature and republishes normalised payloads as Crm.HubSpotWebhook.

hubspotSyncStatus is synced, pending, failed or skipped.

Contact fields that gate outreach

Field Effect

stopCalling

Excludes the contact from dialing; set on DNC

lifecycleStage in SKIP_CALL_LIFECYCLE_STAGES (customer, marketingqualifiedlead)

Pre-validation skips the target with lifecycle_stage

lifecycleStage in HUBSPOT_WARM_LEAD_LIFECYCLE_STAGES (salesqualifiedlead, opportunity, closedwon, closedlost)

Warm-lead set, reserved for the warm_lead skip reason

disposition

A prior terminal disposition on the number blocks re-dialing

HUBSPOT_WARM_LEAD_LIFECYCLE_STAGES and CallSkipReason.WARM_LEAD are defined but not currently referenced by the dial pre-validation path. The active lifecycle guard uses SKIP_CALL_LIFECYCLE_STAGES.