Keep another system in sync
Read, edit and archive contacts, write notes, set reminders for your team and manage who's in a group, from your own backend or an automation tool.
Use these endpoints when something other than a form talks to your Lahuta CRM: your own backend, an order system, a spreadsheet sync, or an automation tool like Zapier or n8n. They read the full contact record, so the key belongs on a server, never in a page's JavaScript.
| You want to | Use | Permission |
|---|---|---|
| List or search your contacts | GET /v1/contacts?kind=person | contacts:read |
| Read one contact | GET /v1/contacts/{contactId} | contacts:read |
| Change or clear a contact's details | PATCH /v1/contacts/{contactId} | contacts:write |
| Archive contacts | POST /v1/contacts/archive | contacts:write |
| Read a contact's notes | GET /v1/contacts/{contactId}/notes | contacts:read |
| Write a note | POST /v1/contacts/{contactId}/notes | contacts:write |
| See which people are on your team | GET /v1/members | contacts:read |
| List reminders | GET /v1/reminders | contacts:read |
| Set, change, finish or delete a reminder | POST /v1/reminders, PATCH and DELETE /v1/reminders/{reminderId} | contacts:write |
| List your groups, and read one group's columns | GET /v1/groups, GET /v1/groups/{group} | contacts:read |
| See which groups a contact is in | GET /v1/contacts/{contactId}/groups | contacts:read |
| Take people out of a group | POST /v1/groups/{group}/rows/remove | contacts:write |
To save new people from a form, use PUT /v1/contacts and PUT /v1/groups, described in Contacts and groups.
List your contacts
Without an email or phone, GET /v1/contacts lists your contacts a page at a time. Send kind, either person or company, and optionally q to search names, email addresses and phone numbers.
curl "https://api.lahuta.org/v1/contacts?kind=person&q=chen&limit=50" \
-H "X-Api-Key: $LAHUTA_API_KEY"The response is a page: items, total, limit and offset. Each item is a contact with the same fields as everywhere else in the API. Archived contacts are left out.
A lookup by email or phone still works as before and returns every match on one page, so its total and limit are the number of matches. Sending q with an email or phone, or leaving out kind when you send neither, is refused with a 400.
Read and edit one contact
GET /v1/contacts/{contactId} returns one contact. PATCH changes it. Send the contact's kind and only the fields you want to change:
curl -X PATCH https://api.lahuta.org/v1/contacts/0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61 \
-H "X-Api-Key: $LAHUTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "person",
"jobTitle": "Head buyer",
"description": null,
"phones": [{ "label": "Cell", "value": "+15035550148" }]
}'It answers with the contact as it now stands. PATCH works differently from PUT /v1/contacts:
- Fields you leave out stay as they are, and
nullclears a field. An upsert only fills in what's empty. - A list replaces the stored list. Sending
phonessets the contact's phone numbers to exactly those, in that order.[]removes them all. - The
kindmust be the contact's own. A person's id sent with"kind": "company"is a404.
An archived contact, or an id that isn't one of your contacts, is a 404 on both calls.
Archive contacts
POST /v1/contacts/archive takes up to 500 ids, people and companies mixed:
{ "contactIds": ["0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61"] }{
"archived": ["0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61"],
"groupRowsRemoved": 2,
"unsubscribed": 1
}Archiving does what Archive contact does in the dashboard. The contact leaves your address book and every group they're in, with their group rows' details, and stops receiving your email. Their notes, reminders and history are kept. groupRowsRemoved and unsubscribed count the group rows and newsletter memberships they lost.
- An id that was never one of your contacts refuses the whole call with a
404, and nothing is archived. - A contact who's already archived isn't in
archivedagain, so retrying a call is safe. - A contact can come back. If a form later sends their email or phone to
PUT /v1/contactsorPUT /v1/groups, the match brings them back, because the same address is the same person.
Notes
POST /v1/contacts/{contactId}/notes adds a note to a contact you already have, without sending their details again:
curl -X POST https://api.lahuta.org/v1/contacts/0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61/notes \
-H "X-Api-Key: $LAHUTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key": "order-1042:shipped",
"body": "Order #1042 shipped.\n\nTracking: 1Z999AA10123456784"
}'{
"id": "0197c0d1-2b3e-7a40-9c11-4d2e8f6a1b07",
"contactId": "0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61",
"body": "Order #1042 shipped.\n\nTracking: 1Z999AA10123456784",
"author": null,
"createdVia": "api",
"createdAt": "2026-10-12T09:30:00.000Z",
"updatedAt": "2026-10-12T09:30:00.000Z"
}bodyis plain text, up to 20,000 characters. Blank lines become paragraphs.- With a
key, a retry returns the note it already wrote. Keys are unique across your organization and shared withnote.keyonPUT /v1/contacts. See Idempotency. - In the dashboard the note shows via API where a person's name would be. Anyone on your team who can edit contacts can delete it, but nobody can edit it, so it keeps the text you sent.
GET /v1/contacts/{contactId}/notes returns a contact's notes in items, newest first, 50 by default and up to 200 with limit. Notes your team wrote come back as plain text too: bold and links are dropped, list items become lines starting with - or 1., and a mention reads as @ and the person's name. author is the team member who wrote the note. createdVia says where it was written: api for a key, dashboard, chat, mcp or cli for a member, or null for older notes and ones Lahuta wrote itself.
Reminders
A reminder is follow-up work on a contact, assigned to someone on your team, who gets an email when it comes due. It shows on the contact and in their reminders in the dashboard, marked via API.
A key isn't a person, so every reminder needs an assignedTo. GET /v1/members lists your team with each person's id, email, firstName, lastName and image. Match them to your own system's users by email.
curl -X POST https://api.lahuta.org/v1/reminders \
-H "X-Api-Key: $LAHUTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactId": "0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61",
"title": "Call Maya about the spring order",
"note": "She asked for samples of the new glaze.",
"dueAt": "2026-10-20T16:00:00.000Z",
"timezone": "America/Los_Angeles",
"assignedTo": "0196f1a2-3c4d-7e5f-8a9b-0c1d2e3f4a5b"
}'The response is the reminder, with its status, assignee and creator (null for a key). A contact or member who isn't in your organization is a 404.
| Call | What it does |
|---|---|
GET /v1/reminders | A page of open reminders, soonest first. Filter with contactId and assignedTo. status=completed lists finished ones, newest first |
PATCH /v1/reminders/{reminderId} | Change any of title, note, dueAt, timezone or assignedTo. Moving dueAt snoozes it. "status": "completed" finishes it, "pending" opens it again |
DELETE /v1/reminders/{reminderId} | Deletes it, and answers 204. Deleting one that's already gone also answers 204 |
A reminder can also be canceled: set from an inbox conversation, it's called off when the contact replies first. You can list those with status=canceled, but you can't set that status yourself.
Groups
GET /v1/groups lists your groups in sidebar order, with each one's id, name, type and rowCount. GET /v1/groups/{group} takes a name or an id and adds the group's columns and views. A select column lists the options it has seen so far.
{
"id": "0197a9f0-1d2e-7b3c-8d4e-5f6a7b8c9d0e",
"name": "Wholesale",
"type": "person",
"columns": [
{
"id": "0197a9f0-2a3b-7c4d-9e5f-6a7b8c9d0e1f",
"name": "Stage",
"type": "select",
"options": ["Inquiry", "Samples sent", "Ordering"]
}
],
"views": [
{ "id": "0197a9f0-3b4c-7d5e-8f6a-7b8c9d0e1f2a", "name": "Wholesale" }
]
}A name is matched the way PUT /v1/groups matches it, ignoring capital letters, spaces and punctuation. A group that doesn't exist is a 404. Reading never creates one.
GET /v1/contacts/{contactId}/groups lists the groups a contact is in, each with the rowId that holds them there.
To take people out of a group, send either their row ids or their contact ids, up to 500, to POST /v1/groups/{group}/rows/remove:
{ "contactIds": ["0197b2e4-6a1c-7f02-8d3e-5b9a1c4f2e61"] }The response is { "removed": 1 }. The contacts stay in your CRM. Only their row in this group goes, with its details and files. A row or contact that isn't in the group is skipped, so a retry removes nothing and still succeeds.
Errors
| Error | When |
|---|---|
400 | A field is missing or in the wrong shape, like a reminder without assignedTo or a list without kind |
403 Forbidden | The key doesn't have contacts:read or contacts:write. permission says which |
404 NotFound | The contact, group, reminder or member isn't in your organization, or the contact is archived |
For every field, see Contacts, Reminders, Members and Groups in the API reference.