All Docs
FeaturesNurtureHubUpdated March 20, 2026

Introducing the Contacts tRPC Router

Introducing the Contacts tRPC Router

Version: 1.0.6

This release ships the full tRPC router for contacts — the core API layer that underpins the contacts UI and every CRM sync operation in NurtureHub. If you are building on the NurtureHub API or integrating a CRM, this is the foundation you will work against.


Overview

The contacts router exposes a set of type-safe tRPC procedures for managing contacts across their full lifecycle: creation, categorisation, sequencing, engagement tracking, and bulk import. Every procedure is org-scoped, so all data is automatically isolated to the calling organisation — no additional filtering is required by consumers.


Procedures Reference

contacts.create

Create a new contact within the organisation.

Type: Mutation
Scope: Org-scoped

const contact = await trpc.contacts.create.mutate({
  firstName: 'Sarah',
  lastName: 'Okafor',
  email: 'sarah@example.com',
  phone: '+44 7700 900000',
});

contacts.update

Update one or more fields on an existing contact.

Type: Mutation
Scope: Org-scoped

await trpc.contacts.update.mutate({
  id: 'contact_abc123',
  email: 'sarah.okafor@newemail.com',
});

contacts.archive

Soft-archive a contact. Archived contacts are removed from active lists and sequences but are not permanently deleted.

Type: Mutation
Scope: Org-scoped

await trpc.contacts.archive.mutate({ id: 'contact_abc123' });

contacts.assignCategory

Assign one of the twelve predefined categories to a contact. This is the trigger point for AI-powered nurture sequence generation — assigning a category immediately initiates the three-email sequence pipeline in the agency's brand voice.

Type: Mutation
Scope: Org-scoped

Available categories:

  • Seller
  • Landlord
  • Buy-to-Let Investor
  • Active Tenant
  • (and eight additional predefined categories)
await trpc.contacts.assignCategory.mutate({
  id: 'contact_abc123',
  category: 'Landlord',
});

Note: Calling assignCategory triggers downstream AI sequence generation. The generated sequence will be presented for review and approval before any emails are sent.


contacts.bulkImport

Import multiple contacts at once from a CSV file.

Type: Mutation
Scope: Org-scoped

The endpoint accepts a CSV payload with headers matching the NurtureHub contact schema. Rows that pass validation are imported immediately. Rows that fail validation are returned in a rejected array on the response — the entire import is not rolled back on partial failure.

const result = await trpc.contacts.bulkImport.mutate({
  csv: rawCsvString,
});

// result.imported — number of successfully created contacts
// result.rejected — array of { row, reason } for failed rows

Expected CSV headers:

HeaderRequiredDescription
firstNameYesContact first name
lastNameYesContact last name
emailYesContact email address
phoneNoUK phone number
categoryNoPredefined contact category

contacts.list

Retrieve a paginated list of contacts with optional filters.

Type: Query
Scope: Org-scoped

Parameters:

ParameterTypeDescription
pagenumberPage number (1-indexed)
limitnumberResults per page
categorystring | undefinedFilter by contact category
scoreRange{ min: number, max: number } | undefinedFilter by intent score bounds
sequenceStatusstring | undefinedFilter by current sequence state
searchstring | undefinedFree-text search across contact fields
const page = await trpc.contacts.list.query({
  page: 1,
  limit: 25,
  category: 'Landlord',
  scoreRange: { min: 60, max: 100 },
  sequenceStatus: 'active',
  search: 'Okafor',
});

// page.contacts — array of contact records
// page.total    — total matching contacts (for pagination UI)

contacts.getDetail

Retrieve the full record for a single contact.

Type: Query
Scope: Org-scoped

const contact = await trpc.contacts.getDetail.query({
  id: 'contact_abc123',
});

contacts.getSequenceHistory

Return the complete email sequence history for a contact, including generation, approval, and send events.

Type: Query
Scope: Org-scoped

const history = await trpc.contacts.getSequenceHistory.query({
  id: 'contact_abc123',
});

// history.sequences — array of sequence records, each containing:
//   .generatedAt    — timestamp of AI generation
//   .approvedAt     — timestamp of agent approval (null if pending)
//   .emails         — array of email records with send status

contacts.getEngagementTimeline

Return the chronological engagement timeline for a contact, including email opens, link clicks, replies, and intent score changes.

Type: Query
Scope: Org-scoped

const timeline = await trpc.contacts.getEngagementTimeline.query({
  id: 'contact_abc123',
});

// timeline.events — array of engagement events, each containing:
//   .type      — 'open' | 'click' | 'reply' | 'score_change'
//   .timestamp — ISO 8601 timestamp
//   .detail    — event-specific metadata

Org Scoping

Every procedure in the contacts router is automatically scoped to the authenticated organisation derived from the caller's session. You do not need to pass an orgId parameter — it is inferred and enforced server-side. Attempts to access contacts belonging to a different organisation will result in a NOT_FOUND or FORBIDDEN error rather than leaking cross-org data.


Error Handling

All procedures use standard tRPC error codes:

CodeWhen thrown
BAD_REQUESTInvalid input, malformed CSV, unknown category
NOT_FOUNDContact ID does not exist within the organisation
FORBIDDENCaller does not have permission for the requested operation
INTERNAL_SERVER_ERRORUnexpected server-side failure

CRM Sync

The contacts router is the primary integration point for bidirectional CRM sync with agentOS, Reapit, Alto, Street, and Loop. CRM adapters should use contacts.create, contacts.update, and contacts.archive to mirror contact state, and contacts.list with appropriate filters to pull contacts for sync.