CRM API
Base Path: /api/v2
A trailing slash is optional: /companies and /companies/ reach the same endpoint.
Types
This section describes the data types used in the CRM API.
Company Type
Represents a company in the CRM system.
| Property | Type | Description |
|---|---|---|
id | string | Company ID |
name | string | Company name |
address | string | Company address: street number and name |
city | string | Company address: city name |
postal_code | string | Company address: postal code |
state | string | Company address: ISO 3166 province/state code |
country_code | string | Company address: ISO 3166 country code |
address_unlocode | string | UN/LOCODE (5-character code) representing the company's physical address location. When it's empty, Starboard can fill it in from the address on a write or a read (see Update Company and Read Company). |
formatted_address | string | Read-only. address, city, state, postal_code and country_code joined with , . Absent when they're all empty. |
phone | string | Company contact phone |
email | string | Company contact email address |
is_customer | bool | Is it a customer? |
is_origin_agent | bool | Is an origin agent? |
is_destination_agent | bool | Is a destination agent? |
is_carrier | bool | Is a carrier? |
ocean_fcl | bool | Provides Ocean FCL services |
ocean_lcl | bool | Provides Ocean LCL services |
drayage | bool | Provides Drayage services |
cartage | bool | Provides Cartage services |
air | bool | Provides Air services |
external_id | string | External identifier for the company (e.g., from a third-party system). Unique among your companies. Update Quote Customer finds a quote's customer by it. |
service_countries | string[] | An array of ISO 3166 codes of countries where the company provides their services. When unset, assumed to operate globally. Kept only for a carrier, origin agent or destination agent (see Update Company). |
service_provinces | string[] | An array of ISO 3166 codes of subdivisions of service_countries where the company provides their services. Relevant for trucking services. When unset, assumed to operate in all provinces of the specified countries. |
is_active | bool | Read-only. false when the company is archived in Starboard. Archived companies are still listed and readable. The API can't archive or reactivate a company. |
created_at | string | Read-only. ISO 8601 timestamp of when the company was created. |
updated_at | string | ISO 8601 timestamp of the last time this company or any of its contacts changed. Server-maintained: it advances on every write to the company, and whenever one of its contacts is created, edited, deleted, or moved to or from it (a write that leaves a contact exactly as stored doesn't count). Pair with the updated_since query parameter on List Companies for incremental sync. |
Company Roles
A company can have multiple roles in the system:
- Customer: Companies that request quotes and book shipments
- Origin Agent: Companies that handle cargo at the origin location
- Destination Agent: Companies that handle cargo at the destination location
- Carrier: Companies that provide transportation services
Company Service Types
Companies can offer various service types:
- Ocean FCL: Full Container Load ocean freight services
- Ocean LCL: Less than Container Load ocean freight services
- Drayage: Short-distance transportation of cargo, typically from ports to inland locations
- Cartage: Trucking for LCL and air shipments, at origin or destination
- Air: Air freight services
Country and Subdivision Codes
The service_countries array contains ISO 3166 two-letter country codes. Examples:
US: United StatesCA: CanadaGB: United KingdomCN: ChinaDE: Germany
The subdivision (state/province) contains the ISO 3166 two-letter code as well. Examples:
ON: Ontario, CanadaBC: British Columbia, CanadaFL: Florida, USA
Contact Type
Represents a contact of a company in the CRM system.
| Property | Type | Description |
|---|---|---|
id | string | Contact ID |
company_id | string | Company ID the contact belongs to |
name | string | Contact's full name |
title | string | Contact's title if available |
email | string | Contact's email if available |
phone | string | Contact's phone if available |
is_pricing_agent | bool | Contact is a pricing agent and should be contacted for rate inquiries |
updated_at | string | ISO 8601 timestamp of the last time this contact was modified. Server-maintained on every write. |
Example Company Object
{
"id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"created_at": "2025-11-02T09:00:00Z",
"updated_at": "2026-01-15T12:34:56Z",
"name": "Example Logistics Inc.",
"address": "123 Main Street",
"city": "New York",
"postal_code": "10001",
"state": "NY",
"country_code": "US",
"address_unlocode": "USNYC",
"formatted_address": "123 Main Street, New York, NY, 10001, US",
"phone": "+1-212-555-0100",
"email": "info@example.com",
"external_id": "GL-2024-001",
"is_customer": true,
"is_origin_agent": true,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": true,
"ocean_lcl": true,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": ["US", "CA", "MX"],
"service_provinces": null,
"is_active": true
}
Example Contact Object
{
"id": "e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+1-212-555-0110",
"title": "CMO",
"is_pricing_agent": false,
"updated_at": "2026-01-15T12:34:56Z"
}
Operations
Errors from CRM Writes
Some validation failures come back as 500 internal_server_error with a generic message, instead of 400. These failures aren't transient: the same request fails the same way every time, so don't retry it blindly. Fix the data instead. They include:
- Companies (Create and Update):
namemissing; a name that matches another of your companies, ignoring case, punctuation and suffixes such as Inc, Corp or Ltd; anexternal_idwith characters other than letters, numbers, hyphens (-), underscores (_) and slashes (/), or one that another of your companies already has; a company ID that isn't a UUID or belongs to another Starboard account. - Contacts (Create and Update):
namemissing; acompany_idthat isn't one of your CRM companies; a contact ID that isn't a UUID or belongs to another Starboard account; anemailthat another contact of the same company already has (an exact match). An omittedemailis saved as an empty string, and that counts too: a company can have only one contact without an email. - Delete Company: see that operation.
Invalid credentials, or an X-User-Company-ID the key can't act for, return 401. No CRM endpoint returns 403 or 409.
List Companies
Retrieve a list of all CRM companies, newest first by created_at. Archived companies are included, with is_active: false. Your own company (the one your key acts for) isn't listed.
Endpoint
GET /companies
Query Parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | A cursor string to continue iterating over the results (pagination) |
limit | integer | Optional. Maximum companies per page. Server-capped at 50 — larger values are silently clamped. Defaults to 20 when omitted. |
updated_since | string | Optional. RFC 3339 timestamp. When set, only companies whose updated_at is greater than or equal to this value are returned. Malformed values return 400 Bad Request. URL-encode it: a + in a time zone offset must be sent as %2B, or use Z. |
Response
Returns a paginated list of companies:
| Property | Type | Description |
|---|---|---|
data | Company[] | An array of Company objects. null means the page failed in Starboard: the response still has status 200 and "cursor": "", so treat it as an error and retry the page, not as the end of the list. |
cursor | string | A cursor for retrieving the next "page" of results; "" on the last page |
Error Responses
| Status Code | Description |
|---|---|
| 400 | Bad Request - Invalid updated_since |
| 401 | Unauthorized - Invalid credentials, or an X-User-Company-ID the key can't act for |
| 500 | Internal Server Error |
Example
Request:
curl https://<your-starboard-api-host>/api/v2/companies \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Response:
{
"data": [
{
"id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"created_at": "2025-11-02T09:00:00Z",
"updated_at": "2026-01-15T12:34:56Z",
"name": "Example Logistics Inc.",
"address": "123 Main Street",
"city": "New York",
"postal_code": "10001",
"state": "NY",
"country_code": "US",
"address_unlocode": "USNYC",
"formatted_address": "123 Main Street, New York, NY, 10001, US",
"phone": "+1-212-555-0100",
"email": "info@example.com",
"external_id": "GL-2024-001",
"is_customer": true,
"is_origin_agent": true,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": true,
"ocean_lcl": true,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": ["US", "CA", "MX"],
"service_provinces": null,
"is_active": true
},
{
"id": "8b2e4d6f-1a3c-4e5f-9b7d-2c4e6a8b0d13",
"created_at": "2025-10-20T14:30:00Z",
"updated_at": "2026-02-01T08:15:00Z",
"name": "Example Shipping Co.",
"address": "456 Harbor Blvd",
"city": "Los Angeles",
"postal_code": "90001",
"state": "CA",
"country_code": "US",
"address_unlocode": "USLAX",
"formatted_address": "456 Harbor Blvd, Los Angeles, CA, 90001, US",
"phone": "+1-310-555-0200",
"email": "contact@example.org",
"external_id": "ESC-2024-002",
"is_customer": false,
"is_origin_agent": true,
"is_destination_agent": true,
"is_carrier": true,
"ocean_fcl": true,
"ocean_lcl": false,
"drayage": true,
"cartage": false,
"air": false,
"service_countries": ["US", "CN", "JP", "KR"],
"service_provinces": null,
"is_active": true
}
],
"cursor": "eyJvIjoyMH0"
}
Pagination
To retrieve additional results, send the same request again (the same updated_since and limit, if you used them) with the cursor value from the previous response. See Pagination.
curl 'https://<your-starboard-api-host>/api/v2/companies?cursor=eyJvIjoyMH0' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Incremental Sync with updated_since
For integrators keeping a local mirror of the CRM in sync, use updated_since to fetch only the companies that have changed since your last poll. It's inclusive: a company whose updated_at equals updated_since is returned. The recommended polling pattern is:
- Before the walk starts, note the server's current time, for example from the
Dateheader of the first response. Subtract a small overlap (30–120 seconds): that's your nextupdated_since. Don't use the highestupdated_atyou see in the walk instead: a company on a page you've already read can be saved during the walk, earlier than one you read later, and would then fall outside your next window. - Walk every page of the current
updated_sincewindow, sending the sameupdated_sinceandlimiton each page, untilcursoris empty. If a page comes back with"data": null, the request failed in Starboard: retry that page instead of treating it as the end. - Deduplicate on
idin your local store: pages are positions, so a company can appear twice.
What the window doesn't tell you:
- Deletions and merges aren't reported. A company deleted in Starboard, or merged into another, simply stops appearing. Run a periodic full walk (no
updated_since) and remove local companies whoseidis no longer listed. The full walk also catches a company skipped because another one was deleted while you paged. - Archived companies stay in the list with
is_active: false. Archiving advancesupdated_at, so it shows up in the window. - Reading can move
updated_at. Read Company on a company with an emptyaddress_unlocodecan fill it in from the address, which advancesupdated_at.
Contact changes are included. Creating, editing, or deleting a contact advances its company's updated_at, and moving a contact to another company advances both companies — so the company shows up in the next updated_since window even if none of its own fields changed. Deleted contacts are not returned anywhere, so for each company in the window, re-fetch its full contact list and replace your local copy rather than merging into it. A few saves count even when a contact's visible details don't change: the first PUT of an unchanged contact can count once (empty fields are stored as empty strings), and some internal saves re-create a contact under a new id. Replacing rather than merging handles both. Page the contact list to an empty cursor first, and if it comes back empty, confirm the company still exists before clearing your copy: List Contacts returns an empty list for an unknown company too.
Results are ordered by created_at DESC — not by updated_at — so late writes to older rows appear alongside newer ones within the same window. Companies created while you walk push the rest of the results back by one place, which is why step 3 deduplicates.
Example — companies changed since your last poll:
curl 'https://<your-starboard-api-host>/api/v2/companies?updated_since=2026-01-14T12:00:00Z' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
The next page sends the same updated_since again, with the cursor from the previous response:
curl 'https://<your-starboard-api-host>/api/v2/companies?updated_since=2026-01-14T12:00:00Z&cursor=eyJvIjoyMH0' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
An invalid updated_since returns 400 Bad Request:
{
"error": "bad_request",
"message": "Invalid updated_since; expected RFC3339 timestamp (e.g. 2026-01-02T15:04:05Z)"
}
Read Company
Retrieve details of a specific company.
Endpoint
GET /companies/{company_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company to retrieve |
Response
Returns a single Company object.
If the company has no address_unlocode but has a country, reading it makes Starboard guess one from the address. When it finds one, it saves it on the company, which advances the company's updated_at.
Error Responses
| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid credentials |
| 404 | Not Found - Company doesn't exist in your CRM |
| 500 | Internal Server Error |
Example
Request:
curl https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Response:
{
"id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"created_at": "2025-11-02T09:00:00Z",
"updated_at": "2026-01-15T12:34:56Z",
"name": "Example Logistics Inc.",
"address": "123 Main Street",
"city": "New York",
"postal_code": "10001",
"state": "NY",
"country_code": "US",
"address_unlocode": "USNYC",
"formatted_address": "123 Main Street, New York, NY, 10001, US",
"phone": "+1-212-555-0100",
"email": "info@example.com",
"external_id": "GL-2024-001",
"is_customer": true,
"is_origin_agent": true,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": true,
"ocean_lcl": true,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": ["US", "CA", "MX"],
"service_provinces": null,
"is_active": true
}
Create Company
Create a company in your CRM. Starboard assigns its id.
Endpoint
POST /companies
Request Body
A Company object without id. name is required. Any id you send is replaced by a new one, and the read-only fields (is_active, created_at, updated_at, formatted_address) are ignored. Fields you omit are saved empty, and if address_unlocode is omitted Starboard guesses one from the address, as in Update Company.
Service coverage needs a coverage role, as in Update Company: without one, service_provinces is saved as null, and service_countries is kept at first but saved as null at the company's next change (an update, or a contact being added, edited or removed).
Response
200 OK with your request echoed back, plus the new id. It is not the stored record: fields you didn't send come back empty or zero (timestamps as 0001-01-01T00:00:00Z), and fields you sent, including read-only ones, come back as sent. So is_active comes back false unless you sent it, although the new company is active, and address_unlocode comes back as you sent it, even when Starboard filled one in. Read the company with Read Company to see what was stored.
Error Responses
| Status Code | Description |
|---|---|
| 400 | bad_request - The body isn't valid JSON, or Content-Type isn't application/json |
| 400 | invalid_unlocode - address_unlocode isn't in UN/LOCODE format (see Update Company) |
| 401 | Unauthorized - Invalid credentials |
| 500 | Internal Server Error, including validation failures such as a missing or duplicate name (see Errors from CRM Writes) |
Example
Request:
curl -X POST https://<your-starboard-api-host>/api/v2/companies \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}' \
-d '{
"name": "Example Freight Partners",
"city": "Rotterdam",
"country_code": "NL",
"email": "ops@example.net",
"is_customer": true,
"external_id": "EFP-001"
}'
Response (the request echoed back with the new id):
{
"id": "0c9d8e7f-6a5b-4c3d-8e2f-1a0b9c8d7e6f",
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"name": "Example Freight Partners",
"address": "",
"city": "Rotterdam",
"postal_code": "",
"state": "",
"country_code": "NL",
"address_unlocode": "",
"phone": "",
"email": "ops@example.net",
"external_id": "EFP-001",
"is_customer": true,
"is_origin_agent": false,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": false,
"ocean_lcl": false,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": null,
"service_provinces": null,
"is_active": false
}
Update Company
Replace a company's information. PUT replaces the whole record: it isn't a partial update.
Endpoint
PUT /companies/{company_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company to update. Must be a UUID. |
Request Body
A full Company object. name is required. The id comes from the path, and the read-only fields (is_active, created_at, updated_at, formatted_address) are ignored.
- Fields you omit are cleared (saved empty,
falseornull). Read the company first and send every field back, changing only what you mean to change. - Some fields are cleared on every
PUT, even if you send back exactly what you read, because the API doesn't expose them: the company's notes, email domain and country name, and its roles other than the ones in the Company type (vendor, prospect, agent, customs agent, insurance, air carrier, ocean carrier, bonded and non-bonded warehouse). If your team keeps these in Starboard, aPUTfrom the API erases them. - A
PUTto an ID that doesn't exist creates the company with that ID. - If
address_unlocodeis empty or omitted, Starboard guesses one fromcity,stateandcountry_code(no guess withoutcountry_code). - Service coverage needs a coverage role.
service_countriesandservice_provincesare kept only whenis_carrier,is_origin_agentoris_destination_agentistrue; otherwise both are saved asnull.service_provincesis also saved asnullwhenservice_countriesis empty. is_activecan't be changed through the API.
Response
200 OK with your request echoed back, not the stored record: fields you didn't send come back empty or zero (timestamps as 0001-01-01T00:00:00Z), and fields you sent, including read-only ones, come back as sent. So is_active comes back false unless you sent it, and address_unlocode as you sent it. Read the company with Read Company afterwards to see what was stored.
Error Responses
| Status Code | Description |
|---|---|
| 400 | bad_request - The body isn't valid JSON, or Content-Type isn't application/json |
| 400 | invalid_unlocode - address_unlocode isn't in UN/LOCODE format (see below) |
| 401 | Unauthorized - Invalid credentials, or an X-User-Company-ID the key can't act for |
| 500 | Internal Server Error, including validation failures such as a missing name, a duplicate name or external_id, or a company_id that isn't a UUID (see Errors from CRM Writes) |
Validation Errors
When the address_unlocode field is provided with an invalid format, the API returns a 400 Bad Request error:
{
"error": "invalid_unlocode",
"message": "Invalid UNLOCODE format. Expected format: 2-letter country code + 3-letter location code (e.g., USNYC, GBLON)"
}
Example
Changing the phone, email, external_id, LCL service and service countries of the company from Read Company. Every other field is sent back as read, so it isn't cleared, and the service countries are kept because the company is an origin agent. The read-only fields are left out of the request.
Request:
curl -X PUT https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}' \
-d '{
"name": "Example Logistics Inc.",
"address": "123 Main Street",
"city": "New York",
"postal_code": "10001",
"state": "NY",
"country_code": "US",
"address_unlocode": "USNYC",
"phone": "+1-212-555-0101",
"email": "ops@example.com",
"external_id": "GL-2024-001-UPDATED",
"is_customer": true,
"is_origin_agent": true,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": true,
"ocean_lcl": false,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": ["US", "CA", "MX", "GB"],
"service_provinces": null
}'
Response (the request echoed back):
{
"id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"name": "Example Logistics Inc.",
"address": "123 Main Street",
"city": "New York",
"postal_code": "10001",
"state": "NY",
"country_code": "US",
"address_unlocode": "USNYC",
"phone": "+1-212-555-0101",
"email": "ops@example.com",
"external_id": "GL-2024-001-UPDATED",
"is_customer": true,
"is_origin_agent": true,
"is_destination_agent": false,
"is_carrier": false,
"ocean_fcl": true,
"ocean_lcl": false,
"drayage": false,
"cartage": false,
"air": false,
"service_countries": ["US", "CA", "MX", "GB"],
"service_provinces": null,
"is_active": false
}
Delete Company
Permanently delete a company from your CRM.
This is a hard delete, and it can't be undone:
- Records linked to the company are deleted with it: its contacts and other records that point at it, which can include quotes where it is the customer. Companies it was the parent of are kept.
- If a linked record can't be removed, the whole delete fails with
500and nothing is deleted.
Don't delete customers that have quotes. Archive them in Starboard instead.
Endpoint
DELETE /companies/{company_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company to delete |
Response
Returns an empty response with status 204 No Content on success.
Error Responses
| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid credentials |
| 500 | Internal Server Error. Also returned when the company doesn't exist in your CRM (there is no 404), and when linked records block the delete. A 500 can also mean the delete took longer than about 30 seconds: it may still complete. Read the company with Read Company before retrying; 404 means it's gone. |
Example
Request:
curl -X DELETE https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Success Response:
HTTP/1.1 204 No Content
List Contacts
Retrieve the contacts of one CRM company.
Endpoint
GET /companies/{company_id}/contacts
Query Parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | A cursor string to continue iterating over the results (pagination) |
limit | integer | Optional. Maximum contacts per page. Defaults to 20; capped at 50 (larger values are treated as 50). |
Response
Returns a paginated list of the company's contacts, newest first by when they were created (then by name):
| Property | Type | Description |
|---|---|---|
data | Contact[] | An array of Contact objects |
cursor | string | A cursor for retrieving the next "page" of results; "" on the last page |
A company_id that doesn't exist, or isn't in your CRM, returns 200 with an empty list, not 404. Page until cursor is empty before acting on the result, and don't replace your local copy of a company's contacts with an empty first page until you've confirmed the company exists (for example with Read Company).
Error Responses
| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid credentials, or an X-User-Company-ID the key can't act for |
| 500 | Internal Server Error. Also returned when company_id isn't a UUID. |
Example
Request:
curl https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Response:
{
"data": [
{
"id": "e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+1-212-555-0110",
"title": "CMO",
"is_pricing_agent": false,
"updated_at": "2026-01-15T12:34:56Z"
},
{
"id": "e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f406",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Richard Roe",
"email": "richard.roe@example.com",
"phone": "+1-212-555-0111",
"title": "CCO",
"is_pricing_agent": false,
"updated_at": "2026-01-16T09:00:00Z"
}
],
"cursor": ""
}
Pagination
When cursor isn't empty, send the same request again (with the same limit, if you used one) plus the cursor value from the previous response:
curl 'https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts?cursor=eyJvIjoyMH0' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Read Contact
Retrieve details of a specific company contact.
Endpoint
GET /companies/{company_id}/contacts/{contact_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company |
contact_id | string | The ID of the contact to retrieve |
Response
Returns a single Contact object.
Error Responses
| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid credentials |
| 404 | Not Found - The contact doesn't exist in your CRM, or belongs to a different company than company_id |
| 500 | Internal Server Error |
Example
Request:
curl https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts/e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Response:
{
"id": "e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+1-212-555-0110",
"title": "CMO",
"is_pricing_agent": false,
"updated_at": "2026-01-15T12:34:56Z"
}
Create Contact
Create a contact on one of your CRM companies. Starboard assigns its id.
Endpoint
POST /companies/{company_id}/contacts
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company the contact belongs to |
Request Body
A Contact object without id. name is required. company_id comes from the path, any id you send is replaced by a new one, and is_pricing_agent defaults to false. Each contact of a company needs its own email: an omitted one is saved as an empty string, so a company can have only one contact without an email (see Errors from CRM Writes).
Response
200 OK with your request echoed back, plus the new id and the company_id from the path. It is not the stored record: fields you didn't send come back empty or zero (updated_at as 0001-01-01T00:00:00Z), and fields you sent, including updated_at, come back as sent. Read the contact with Read Contact to see what was stored.
Error Responses
| Status Code | Description |
|---|---|
| 400 | bad_request - The body isn't valid JSON, or Content-Type isn't application/json |
| 401 | Unauthorized - Invalid credentials |
| 500 | Internal Server Error, including validation failures such as a missing name, a company_id that isn't in your CRM, or an email another contact of the company already has (see Errors from CRM Writes) |
Example
Request:
curl -X POST https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}' \
-d '{
"name": "Mary Major",
"email": "mary.major@example.com",
"phone": "+1-212-555-0112",
"title": "Pricing Manager",
"is_pricing_agent": true
}'
Response:
{
"id": "3c1e5a7b-9d2f-4b6a-8e0c-5f7a9b1d3e2c",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Mary Major",
"email": "mary.major@example.com",
"phone": "+1-212-555-0112",
"title": "Pricing Manager",
"is_pricing_agent": true,
"updated_at": "0001-01-01T00:00:00Z"
}
Update Contact
Replace a contact's information. PUT replaces the whole contact: it isn't a partial update.
Endpoint
PUT /companies/{company_id}/contacts/{contact_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company. The contact is moved to this company if it's on another one. |
contact_id | string | The ID of the contact to update. Must be a UUID. |
Request Body
A full Contact object. name is required. id and company_id come from the path.
- Fields you omit are cleared:
title,emailandphoneare saved empty, andis_pricing_agentis saved asfalse. Read the contact first and send every field back. - Every API write turns off auto-sourcing for the contact in Starboard. Turn it back on in Starboard if you rely on it.
- The contact moves to the company in the path, if it was on another of your companies.
- A
PUTto an ID that doesn't exist creates the contact with that ID.
Response
200 OK with your request echoed back, with the id and company_id from the path. It is not the stored record: fields you didn't send come back empty or zero (updated_at as 0001-01-01T00:00:00Z), and fields you sent, including updated_at, come back as sent. Read the contact with Read Contact afterwards to see what was stored.
Error Responses
| Status Code | Description |
|---|---|
| 400 | bad_request - The body isn't valid JSON, or Content-Type isn't application/json |
| 401 | Unauthorized - Invalid credentials, or an X-User-Company-ID the key can't act for |
| 500 | Internal Server Error, including validation failures such as a missing name, a company_id that isn't in your CRM, or an email another contact of the company already has (see Errors from CRM Writes) |
Example
Request:
curl -X PUT https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts/e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}' \
-d '{
"name": "Jane Doe",
"title": "CMO",
"email": "jane.doe@example.org",
"phone": "+1-212-555-0101",
"is_pricing_agent": false
}'
Response (the request echoed back):
{
"id": "e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405",
"company_id": "5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10",
"name": "Jane Doe",
"email": "jane.doe@example.org",
"phone": "+1-212-555-0101",
"title": "CMO",
"is_pricing_agent": false,
"updated_at": "0001-01-01T00:00:00Z"
}
Delete Contact
Delete an existing contact from the CRM system.
Endpoint
DELETE /companies/{company_id}/contacts/{contact_id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
company_id | string | The ID of the company |
contact_id | string | The ID of the contact to delete |
Response
Returns an empty response with status 204 No Content on success. A contact that doesn't exist also returns 204, so repeating a delete is safe.
Error Responses
| Status Code | Description |
|---|---|
| 401 | Unauthorized - Invalid credentials |
| 404 | Not Found - The contact belongs to a different company than company_id |
| 500 | Internal Server Error |
Example
Request:
curl -X DELETE https://<your-starboard-api-host>/api/v2/companies/5f0c3a52-7d1e-4b8a-9c2f-1e6d4b3a2c10/contacts/e4b4a8c8-ce25-4df2-8b1d-ec7e5b45f405 \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic {base64-encoded-credentials}' \
-H 'X-User-Company-ID: {ID}'
Success Response:
HTTP/1.1 204 No Content