HarborCRM Task Group Skill
Environment
The shared HarborCRM API is at ${GDPEVO_ENV_BASE_URL}. All data is read-only via GET endpoints. No write operations are needed or available.
Core API Endpoints
Events, tradeshows, and import batches share a common pattern:
GET /api/events/{event_id}— event metadata (dates, follow-up windows, lead opportunity amount, campaign code)GET /api/events/{event_id}/orders— sponsor orders (account, amount, order_status, ticket_contacts, voucher_code)GET /api/events/{event_id}/badges— badge scans (badge_type, company_name, contact_name, email, phone)GET /api/events/{event_id}/sponsor_packages— synonym for /ordersGET /api/finance/invoices?event_id={event_id}— invoices (status, paid_amount, deferred_amount)GET /api/tradeshows— list of all trade showsGET /api/tradeshows/{show_id}/exhibitors— exhibitors with descriptions, CRM account linksGET /api/tradeshows/{show_id}/meeting_interest— interest scores and demo requestsGET /api/import_batches— batch metadata (campaign_code)GET /api/import_batches/{batch_id}/raw_contacts— rows to cleanGET /api/import_batches/{batch_id}/suppression— suppression list (email/phone/reason)GET /api/crm/accounts— CRM accounts (disqualified_reason, domain, status, industry)GET /api/crm/contacts— CRM contacts (opted_out, email, phone)GET /api/crm/opportunities— all opportunities across eventsGET /api/crm/campaign_members?event_id={event_id}— existing campaign member recordsGET /api/policies— policy enums and notes (sponsor_handoff status_enums, prospecting platform_enums)
Data Normalization
Email: lowercase, trim whitespace. Empty or whitespace-only → "".
Phone: strip all non-digit characters. Keep the result as-is — do NOT add a country-code prefix if the source lacks one. If the source has no phone, use "".
Sponsor Status Determination
For each sponsor order from /events/{id}/orders:
| Order Status | Invoice Status | Sponsor Status |
|---|---|---|
| confirmed | paid_deferred | paid_deferred |
| confirmed | open | open_invoice |
| proposal_sent | (no invoice) | proposal_only |
| canceled | — | exclude entirely |
- paid_deferred: package_amount = invoice amount, paid_amount = invoice paid_amount, open_balance = 0
- open_invoice: package_amount = invoice amount, paid_amount = invoice paid_amount, open_balance = amount − paid_amount
- proposal_only: package_amount = order amount, invoice_id = null, paid_amount = 0, open_balance = 0
Sponsor revenue totals:
paid_deferred= sum of package_amount for paid_deferred sponsorsopen_invoice= sum of package_amount for open_invoice sponsorsproposal_only= sum of package_amount for proposal_only sponsorsopen_invoice_balance= sum of open_balance for open_invoice sponsors (i.e., total unpaid)
Sponsor finance follow-up: all sponsors with open_invoice or proposal_only need follow-up. The due date is end_date + sponsor_followup_days_after_end. List the account names sorted ascending.
Badge Classification & Lead Qualification
For each badge from /events/{id}/badges, classify in priority order:
- sponsor_attendee — badge_type is "sponsor", OR the badge's company has an active sponsor order (confirmed or proposal_sent). Excluded from lead pipeline.
- inactive_sponsor_record — the badge's company has a canceled sponsor order. Excluded.
- non_business_badge — badge_type is "student" or "press". Excluded.
- existing_disqualified — the badge's company matches a CRM account with a non-null
disqualified_reason. Excluded. - qualified_non_sponsor_lead — everything else. Include in lead pipeline.
For each qualified lead:
opportunity_amount= event'slead_opportunity_amount(per-lead)crm_account_action=update_existingif a non-disqualified CRM account exists for the company;create_accountotherwisecrm_contact_action=create_contactunless the contact's normalized email matches an existing CRM contactcampaign_member_action=add_campaign_memberlead_pipeline_total= sum of opportunity_amount across all qualified leads
Lead follow-up due date: end_date + followup_days_after_end.
Campaign Member Actions
Reconcile badge scans against existing campaign members:
- Qualified leads with badge scans →
createcampaign member, target_statusattended - Sponsor attendees with badge scans →
createcampaign member, target_statusattended_sponsor(unless one already exists at that status →no_action) - Existing campaign members with no badge scan →
no_action(keep current status) - Excluded badge holders → campaign member action
no_import, target_statusexcluded - Sort campaign member actions by
subject_keyascending. Use"{account_name}|{contact_name}"format.
Badge-Only Contacts
For each qualified non-sponsor lead that exists only as a badge scan (no CRM contact), include in badge_only_contacts with normalized email and phone. Sort by company_name ascending.
Prospecting / Exhibitor Qualification
For trade-show prospecting tasks:
Qualification rule: An exhibitor is qualified if its description indicates it builds or OEM-manufactures AUV, ROV, or Underwater Camera platforms. Companies that only distribute, resell, provide services, or make sensors without building platforms are excluded.
Exclusion reasons (use the most specific match):
distributor_only— reseller, distributor, dealerservice_only— consulting, services, analytics without hardware manufacturingsensor_vendor_only— builds sensors/probes but not platformsresearch_only— research/lab use only, no commercial platform manufacturingnot_target_market— catch-all for non-matching
Platform classification: Derive from the exhibitor description. A platform is counted if the company builds it. Sort platforms in enum order: "AUV", "ROV", "Underwater Camera".
Priority tier assignment (unless task specifies otherwise):
A— requested demo AND interest_score ≥ 90B— requested demo AND interest_score ≥ 80C— all other qualified leads
Tier opportunity amounts: A = $120,000, B = $90,000, C = $50,000 (when amounts not specified in the task).
Ranking: demo request first (true before false), then interest_score descending, then platform count descending (broader coverage first), then company_name ascending.
CRM overlap: existing_crm_overlap_count = count of qualified leads whose crm_account_id is non-null. List matching account IDs sorted ascending.
Batch Import Cleaning
For import batch tasks:
Deduplication: Match raw contacts by normalized email. For each duplicate group, the winner is the row with the latest captured_at timestamp. When timestamps are tied, the row with the alphabetically-lower row_id wins.
Suppression: Any raw contact whose normalized email or phone matches an entry in the suppression list is suppressed (removed from import). Check both email AND phone against the suppression list.
Missing contact: A row with empty email AND empty phone after normalization is unusable — removed as missing_contact.
Clean contacts include only the importable surviving rows (winners after dedup, not suppressed, not missing contact). Each gets:
crm_action=update_existingif company has a non-disqualified CRM account,create_accountotherwiseexisting_account_id= CRM account_id if exists, else nullexisting_contact_id= CRM contact_id if matching contact exists, else null- Use the winning row's values for all data fields (company_name, captured_at, source_name, etc.)
Removal summary: removed_rows includes ALL removed rows (duplicates, suppressed, missing_contact) sorted by row_id ascending. Count duplicates separately in duplicate_summary.duplicate_removed_count. Count suppressed in suppressed_removed_count. Count missing-contact in unusable_removed_count.
Import action totals: Count only the crm_action values across clean contacts (create_account, update_existing). Do not count no_import or suppress unless those rows appear in clean_contacts.
Campaign member import count: Number of clean contacts (equals create_account + update_existing counts).
Exclusion Counts
When a task requires per-reason exclusion counts:
sponsor_attendee— badge holders from active sponsor companiesnon_business_badge— student or press badge typesexisting_disqualified— badge holders from CRM-disqualified accounts (only when no higher-priority reason applies)missing_contact— rows with no email AND no phone
Sorting Rules (by output section)
sponsor_statuses— byaccount_nameascendingqualified_lead_accounts— byaccount_nameascendingexcluded_records— bycompany_nameascending, thencontact_nameascendingbadge_decisions— bybadge_idascendingcampaign_member_actions— bysubject_keyascendingbadge_only_contacts— bycompany_nameascendingqualified_exhibitors/ranked_leads— by rank (if present) orcompany_nameascendingexcluded_near_misses/excluded_exhibitors— bycompany_nameascendingclean_contacts— byclean_contact_idascendingduplicate_keys— bykeyascendingremoved_rows— byrow_idascending- Account name/id lists — ascending order
- Platforms within a list — in enum order:
["AUV", "ROV", "Underwater Camera"]
Key Pitfalls
- Phone normalization: strip non-digits only; do not add a
1prefix for US numbers unless the source data already includes it. - Canceled sponsors: use
inactive_sponsor_recordas the exclusion reason, notexisting_disqualified, even if the CRM account is also disqualified. - Sponsor attendees with attendee badges: a badge with badge_type "attendee" from a company that has an active sponsor order is still a
sponsor_attendeeexclusion. - Press badges: treat as
non_business_badge, same as student. - Email normalization: lowercase AND trim. A field that is only whitespace normalizes to
"". - Tied dedup timestamps: when
captured_atmatches, the alphabetically-lowerrow_idwins. - Clean contacts vs removed rows: do NOT include suppressed or no_import rows in
clean_contacts. Only importable rows (create_account, update_existing) belong there. - Platform derivation: read exhibitor descriptions carefully — "builds ROVs with camera arrays" means ROVs (cameras are integrated, not standalone). "OEM underwater camera manufacturer" means Underwater Camera.
- Sponsor revenue totals:
open_invoiceis the TOTAL invoice amount, not the unpaid portion. The unpaid portion goes inopen_invoice_balance. - Date arithmetic: follow-up due dates are
end_date + followup_days_after_end. Use ISO 8601 date format (YYYY-MM-DD).