Contacts
People your practice knows — leads, prospects, patients, churned. Contacts hold encrypted PHI and respect consent flags on outbound channels.
The contact object
All identifying fields (email, phone, address, dob, notes) are stored encrypted at rest. Reads decrypt server-side based on the caller's role. lifecycle is one of LEAD, PROSPECT, PATIENT, INACTIVE, CHURNED.
Create a contact
Create a new contact in the workspace. The authenticated user/service-account becomes the owner unless an explicit owner is set elsewhere.
contacts.writeRequest body
| Name | Type | Description |
|---|---|---|
firstNamerequired | string | Given name. 1–100 characters. |
lastNamerequired | string | Family name. 1–100 characters. |
email | string | Email address. Must be a valid format if provided. |
phone | string | E.164 phone number recommended. 7–40 chars. |
dob | string | Date of birth. ISO 8601 date string. |
address | string | Mailing address — free-form. |
notes | string | Free-text notes. ≤ 8000 chars. |
lifecycle | string | One of LEAD, PROSPECT, PATIENT, INACTIVE, CHURNED. |
source | string | Where the contact came from. ≤ 120 chars. |
tags | string[] | Up to 20 short tags. |
consentEmail | boolean | Patient consents to marketing email. |
consentSms | boolean | Patient consents to SMS. |
consentHipaa | boolean | HIPAA disclosure acknowledged. |
Request body
{
"firstName": "Avery",
"lastName": "Liu",
"email": "avery@example.com",
"phone": "+15555550123",
"lifecycle": "PROSPECT",
"consentEmail": true
}Responses
201 CreatedCreated.example { "id": "cln1f8s7p0001a8l1c2k3d4e5", "firstName": "Avery", "lastName": "Liu", "email": "avery@example.com", "phone": "+15555550123", "lifecycle": "PROSPECT", "source": null, "tags": [], "ownerId": "usr_8h2c3p…", "consentEmail": true, "consentSms": false, "consentHipaa": false, "createdAt": "2026-05-18T14:00:00Z", "updatedAt": "2026-05-18T14:00:00Z" }401 UnauthorizedBearer token missing or invalid.403 ForbiddenToken lacks contacts.write.422 Unprocessable EntityValidation failed.
Code samples
curl -X POST https://app.contactfollowup.com/api/contacts \
-H "Authorization: Bearer $HEARTHNOTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Avery",
"lastName": "Liu",
"email": "avery@example.com",
"phone": "+15555550123",
"lifecycle": "PROSPECT",
"consentEmail": true
}'List contacts
Return the contacts the caller can see, filtered by query / lifecycle. Results are scoped to the caller's role and any ownership rules.
contacts.readQuery parameters
| Name | Type | Description |
|---|---|---|
q | string | Full-text search over name / email / phone. |
lifecycle | string | Filter by lifecycle stage. |
limit | integer | Page size. Default 50, max 200. |
Responses
200 OKContacts the caller can see.example { "contacts": [ { "id": "cln1f8s7p0001a8l1c2k3d4e5", "firstName": "Avery", "lastName": "Liu", "email": "avery@example.com", "phone": "+15555550123", "lifecycle": "PROSPECT", "source": null, "tags": [], "ownerId": "usr_8h2c3p…", "consentEmail": true, "consentSms": false, "consentHipaa": false, "createdAt": "2026-05-18T14:00:00Z", "updatedAt": "2026-05-18T14:00:00Z" } ] }403 ForbiddenMissing contacts.read.
Code samples
curl -X GET https://app.contactfollowup.com/api/contacts \
-H "Authorization: Bearer $HEARTHNOTE_TOKEN" \
-H "Content-Type: application/json"Retrieve a contact
Fetch a single contact by id.
contacts.readPath parameters
| Name | Type | Description |
|---|---|---|
idrequired | string | Contact id. |
Responses
200 OKThe contact.example { "id": "cln1f8s7p0001a8l1c2k3d4e5", "firstName": "Avery", "lastName": "Liu", "email": "avery@example.com", "phone": "+15555550123", "lifecycle": "PROSPECT", "source": null, "tags": [], "ownerId": "usr_8h2c3p…", "consentEmail": true, "consentSms": false, "consentHipaa": false, "createdAt": "2026-05-18T14:00:00Z", "updatedAt": "2026-05-18T14:00:00Z" }404 Not FoundNot visible to the caller, or doesn't exist.
Code samples
curl -X GET https://app.contactfollowup.com/api/contacts/{id} \
-H "Authorization: Bearer $HEARTHNOTE_TOKEN" \
-H "Content-Type: application/json"Update a contact
Partial update. Only the fields you send change. PHI fields re-encrypt server-side.
contacts.writePath parameters
| Name | Type | Description |
|---|---|---|
idrequired | string | Contact id. |
Request body
| Name | Type | Description |
|---|---|---|
firstName | string | New given name. |
lastName | string | New family name. |
email | string | New email (use null to clear). |
phone | string | New phone. |
lifecycle | string | New lifecycle stage. |
tags | string[] | Replace the tag list. |
Request body
{ "lifecycle": "PATIENT", "tags": ["new-intake"] }Responses
200 OKUpdated record.example { "id": "cln1f8s7p0001a8l1c2k3d4e5", "firstName": "Avery", "lastName": "Liu", "email": "avery@example.com", "phone": "+15555550123", "lifecycle": "PROSPECT", "source": null, "tags": [], "ownerId": "usr_8h2c3p…", "consentEmail": true, "consentSms": false, "consentHipaa": false, "createdAt": "2026-05-18T14:00:00Z", "updatedAt": "2026-05-18T14:00:00Z" }422 Unprocessable EntityValidation failed.
Code samples
curl -X PATCH https://app.contactfollowup.com/api/contacts/{id} \
-H "Authorization: Bearer $HEARTHNOTE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "lifecycle": "PATIENT", "tags": ["new-intake"] }'Soft-delete a contact
Tombstones the contact: nulls all encrypted PHI columns, sets deletedAt, writes an immutable audit row. ADMIN only.
contacts.deletePath parameters
| Name | Type | Description |
|---|---|---|
idrequired | string | Contact id. |
Responses
204 No ContentTombstoned. No body returned.403 ForbiddenCaller is not an ADMIN.404 Not FoundContact doesn't exist.
Code samples
curl -X DELETE https://app.contactfollowup.com/api/contacts/{id} \
-H "Authorization: Bearer $HEARTHNOTE_TOKEN" \
-H "Content-Type: application/json"