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
assignCategorytriggers 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:
| Header | Required | Description |
|---|---|---|
firstName | Yes | Contact first name |
lastName | Yes | Contact last name |
email | Yes | Contact email address |
phone | No | UK phone number |
category | No | Predefined contact category |
contacts.list
Retrieve a paginated list of contacts with optional filters.
Type: Query
Scope: Org-scoped
Parameters:
| Parameter | Type | Description |
|---|---|---|
page | number | Page number (1-indexed) |
limit | number | Results per page |
category | string | undefined | Filter by contact category |
scoreRange | { min: number, max: number } | undefined | Filter by intent score bounds |
sequenceStatus | string | undefined | Filter by current sequence state |
search | string | undefined | Free-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:
| Code | When thrown |
|---|---|
BAD_REQUEST | Invalid input, malformed CSV, unknown category |
NOT_FOUND | Contact ID does not exist within the organisation |
FORBIDDEN | Caller does not have permission for the requested operation |
INTERNAL_SERVER_ERROR | Unexpected 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.