Contacts and Tags
All routes here need a project API key: Authorization: Bearer gi_live_.... See Authentication.
Contacts#
Manage CRM records, custom properties, saved views, activity, journeys, next actions, stages, imports, and enrichment. Contacts are also created automatically when a form is submitted or a booking is made.
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:projectId/contacts |
List, filter, sort, and group contacts. |
| POST | /api/projects/:projectId/contacts |
Create a contact. |
| POST | /api/projects/:projectId/contacts/import |
Import mapped contact rows. |
| GET | /api/projects/:projectId/contacts/facets |
Lead source and company values with counts under the current filters. |
| POST | /api/projects/:projectId/contacts/bulk |
Add or remove a tag, set a property or lead source, or delete up to 500 contacts. |
| GET | /api/projects/:projectId/contact-properties |
List custom contact properties. |
| POST | /api/projects/:projectId/contact-properties |
Create a contact property. |
| PATCH | /api/projects/:projectId/contact-properties/:key |
Rename, reorder, or change a property's options. |
| DELETE | /api/projects/:projectId/contact-properties/:key |
Delete a property and its value on every contact. |
| GET | /api/projects/:projectId/contacts/:contactId |
Get a contact with tags. |
| PUT | /api/projects/:projectId/contacts/:contactId |
Update a contact. |
| DELETE | /api/projects/:projectId/contacts/:contactId |
Delete a contact. |
| GET | /api/projects/:projectId/contacts/:contactId/activities |
List the cursor-paginated contact timeline and category counts. |
| GET | /api/projects/:projectId/contacts/:contactId/journeys |
Where the contact's bookings and form responses came from: landing page, referrer, UTMs, ad click IDs, and steps (about 3 months kept). |
| GET | /api/projects/:projectId/journey |
The journey of one booking or form response (bookingId or formResponseId). |
| PUT | /api/projects/:projectId/contacts/:contactId/next-action |
Set, replace, or complete the contact's next action. |
| POST | /api/projects/:projectId/contacts/:contactId/stage |
Move a contact between stage tags. |
| POST | /api/projects/:projectId/contacts/:contactId/enrich |
Run contact research and return the enriched contact. |
| GET | /api/projects/:projectId/contact-views |
List saved contact views. |
| POST | /api/projects/:projectId/contact-views |
Create a saved contact view. |
| PUT | /api/projects/:projectId/contact-views/:viewId |
Update a saved contact view. |
| DELETE | /api/projects/:projectId/contact-views/:viewId |
Delete a saved contact view. |
| POST | /api/projects/:projectId/pipeline/seed |
Create the default pipeline stages when missing. |
- Lead source. Every contact has a
leadSource: what created it ("Booking: Intro call", "Form: Contact us", "Manual", "API", "MCP", "Import"), or a custom value you send.leadSourceTypeandleadSourceIdidentify the record. - Fields. Contact writes support
name,email,phone,notes,leadSource,properties,company,companyWebsite,position,companySize,estimatedRevenue, andlinkedinUrl. - Custom properties.
propertiesis an object keyed by property key. Updates merge it, andnullclears a key. PropertyPATCHwithoptionsadds or updates the listed options and keeps the rest; sendremoveOptionswith option values to delete them and clear them from every contact. - Next action. Set it with
{ text, deadline }; send both asnullto complete it.
Create a contact#
POST /api/projects/:projectId/contacts
| Property | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Contact's full name |
email |
string | No | Contact's email address |
phone |
string | No | Contact's phone number |
notes |
string | No | Internal notes about the contact |
leadSource |
string | No | Where the lead came from, e.g. "Partner referral". Defaults to "API". |
properties |
object | No | Custom property values keyed by property key |
curl -X POST "https://goodinbound.com/api/projects/proj_123/contacts" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer gi_live_your_api_key" \
-d '{
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+1-555-0123",
"notes": "Met at conference",
"leadSource": "Partner referral"
}'
{
"contact": {
"id": "ct_p4q5r6",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+1-555-0123",
"notes": "Met at conference",
"leadSource": "Partner referral",
"createdAt": "2026-03-24T14:00:00Z"
}
}
Update a contact#
PUT /api/projects/:projectId/contacts/:contactId
Only the fields you send change. properties is merged into the existing values; send null for a key to clear it.
curl -X PUT "https://goodinbound.com/api/projects/proj_123/contacts/ct_p4q5r6" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer gi_live_your_api_key" \
-d '{
"name": "Jane Smith-Doe",
"notes": "Updated: now a paying customer"
}'
List, filter, and group contacts#
GET /api/projects/:projectId/contacts returns { contacts, total }, 50 at a time by default. Use limit and offset to load more.
| Parameter | Type | Description |
|---|---|---|
search |
string | Match name, email, phone, or company |
tagIds |
string[] | Repeat the key to filter by multiple tags (tagId for one) |
matchAllTags |
boolean | Require every tagIds value instead of any |
stageTagId |
string | Require the current stage tag |
excludeStageTagIds |
string[] | Repeated stage IDs to exclude |
activityType |
enum | Filter by the latest supported activity type |
activitySinceDays |
number | Require activity within this many days |
noActivitySinceDays |
number | Require no activity within this many days |
bookingStatus |
enum | Filter by related booking status |
leadSource |
string[] | Repeat for each lead source label (up to 40) |
companyDomain |
string[] | Repeat for each company domain (up to 40); __none__ matches contacts without one |
createdFrom, createdTo |
string | Created date range, YYYY-MM-DD |
rules |
JSON | Array of { field, op, value }. field is a built-in field or property:<key>; op is is, is_not, contains, is_set, is_empty, gt, lt, or any_of. |
sort, dir |
string | name, email, company, leadSource, createdAt, lastActivityAt, nextActionDeadline, or property:<key>; asc or desc |
groupBy |
string | company, leadSource, stage, or property:<key>. Returns { groups: [{ key, label, count, contacts }], total, hiddenGroups } with the 100 largest groups. |
group |
string | With groupBy, page through one group |
limit |
integer | Page size; default 50, maximum 100 |
offset |
integer | Zero-based result offset |
curl --get "https://goodinbound.com/api/projects/proj_123/contacts" \
-H "Authorization: Bearer gi_live_your_api_key" \
--data-urlencode "tagIds=tag_lead" \
--data-urlencode "tagIds=tag_vip" \
--data-urlencode "matchAllTags=true" \
--data-urlencode "limit=50"
{
"contacts": [
{
"id": "ct_m1n2o3",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+1-555-0123",
"tags": [{ "id": "tag_lead", "name": "Lead", "color": "#1B4332" }],
"lastActivityAt": "2026-07-22T08:30:00.000Z",
"createdAt": "2026-03-20T10:00:00.000Z"
}
],
"total": 1
}
Contact activity#
Activity accepts category values all, bookings, form_responses, or workflows. The default page size is 20 and the maximum is 100. Pass the returned opaque nextCursor unchanged to load the next page.
curl --get "https://goodinbound.com/api/projects/proj_123/contacts/ct_123/activities" \
-H "Authorization: Bearer gi_live_your_api_key" \
--data-urlencode "category=bookings" \
--data-urlencode "limit=20"
Tags#
Create project tags and assign them to contacts. Names are unique within a project after trimming and case normalization.
| Method | Path | Description |
|---|---|---|
| GET | /api/projects/:projectId/tags |
List tags with search and cursor pagination. |
| POST | /api/projects/:projectId/tags |
Create a tag from a name and optional #RRGGBB color. |
| GET | /api/projects/:projectId/tags/:tagId |
Get a tag. |
| PATCH | /api/projects/:projectId/tags/:tagId |
Update a tag's name or color. |
| DELETE | /api/projects/:projectId/tags/:tagId |
Delete an unused tag. |
| PUT | /api/projects/:projectId/contacts/:contactId/tags/:tagId |
Assign a tag to a contact. |
| DELETE | /api/projects/:projectId/contacts/:contactId/tags/:tagId |
Remove a tag from a contact. |
| POST | /api/projects/:projectId/contacts/:contactId/tags |
Older assignment endpoint taking { tagId }; use PUT instead. |
| Parameter | Type | Description |
|---|---|---|
search |
string | Case-insensitive name search, 1–100 characters |
limit |
integer | Optional page size from 1 to 100 |
cursor |
string | Opaque nextCursor value; requires limit |
name |
string | Required on create. Project-unique name, 1–50 characters |
color |
string | Optional six-digit hex color such as #1B4332 |
curl -X POST "https://goodinbound.com/api/projects/proj_123/tags" \
-H "Authorization: Bearer gi_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Qualified lead","color":"#1B4332"}'
{
"tags": [
{
"id": "tag_123",
"projectId": "proj_123",
"name": "Qualified lead",
"color": "#1B4332",
"createdAt": "2026-07-22T09:00:00.000Z"
}
],
"nextCursor": null
}
Assigning and removing are idempotent. Assignment returns { tag, assigned } and removal returns { success, tag, removed }; repeating either succeeds and reports false when nothing changed.
curl -X PUT \
"https://goodinbound.com/api/projects/proj_123/contacts/ct_123/tags/tag_123" \
-H "Authorization: Bearer gi_live_your_api_key"
Deleting a tag a workflow uses returns 409:
{
"error": "Tag is referenced by one or more workflows",
"code": "TAG_IN_USE",
"workflows": [{ "id": "wf_123", "name": "Research new leads" }]
}