Migrating from Klara
Klara doesn't expose a public REST API, so a migration is mostly a one-time CSV export plus a bulk import script.
1. Export your data
In Klara, head to Settings → Data export and grab CSVs for: contacts, conversations, appointments, and forms. Klara's exports are well-formed but use Klara's own column names — we map them in step 3.
2. Stand up a service account
Create one in Settings → Service accounts with role ADMIN(you'll downgrade once the migration is done). Save the token as HEARTHNOTE_TOKEN in your shell.
3. Map the columns
| Klara column | ContactFollowUp field |
|---|---|
| First Name | firstName |
| Last Name | lastName |
| Email Address | email |
| Cell Phone | phone |
| DOB | dob (ISO 8601) |
| Status | lifecycle (PATIENT for active) |
| Insurance Plan | insurance.payerName (separate POST) |
| Member ID | insurance.memberId |
| Notes | notes |
4. Bulk import
Loop the CSV with whatever tool you like and POST to /api/contacts. We'd recommend a 100-record batch with 250 ms of jitter between batches, to be a polite client.
import csv, os, time, requests
TOKEN = os.environ["HEARTHNOTE_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
with open("klara_contacts.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
body = {
"firstName": row["First Name"],
"lastName": row["Last Name"],
"email": row["Email Address"] or None,
"phone": row["Cell Phone"] or None,
"lifecycle": "PATIENT" if row["Status"] == "Active" else "INACTIVE",
"notes": row.get("Notes") or None,
}
r = requests.post("https://app.contactfollowup.com/api/contacts", json=body, headers=HEADERS, timeout=30)
if not r.ok:
print("FAIL", row["Email Address"], r.status_code, r.text)
time.sleep(0.25)5. Forward inbound messages during the cutover
Klara's web inbox can forward each conversation as email. Point that forward at the ContactFollowUp inbox webhook (POST /api/inbox/webhook) and set the shared-secret header. See /docs/webhooks for the auth contract.
6. Downgrade your migration account
Once you've verified counts and spot-checked records, drop the migration service account's role to READONLY (or revoke the token outright). The principle is the same as any rotation: the least-privileged token wins.