Skip to main content

Companies

The organizations behind your contacts — the accounts a sales team files people, deals and tasks under. Part of the CRM, alongside pipelines, deals, tasks and activities.

A company is keyed, in practice, by its domain: two contacts at acme.com work for one company, and the console files them under it by that string. So the API normalizes a domain exactly as the console stores it, refuses a second company on a domain already held, and offers ?domain= as the lookup a sync starts with.

The API is ahead of the console

These endpoints are live. The console's CRM pages are still rolling out, so until they open this API is the way to work with companies, deals and tasks.

Plan availability

Companies, pipelines, deals, tasks and activities are the CRM suite, included from Starter; a plan without it answers 403 plan_required with code: "crm" on every one of them, while contacts keep working. A company is also a CRM record: it counts with contacts and deals against the plan's records band, and on a plan that hard-bands (Free) a create past the band answers 403 plan_required with code: "crm_records_quota" — the key is given back, so the same retry lands once the band is raised. Paid plans meter the excess instead.

Scopes and sites​

Every CRM resource uses the same two scopes — crm:read for GET, crm:write for POST, PATCH and DELETE — and follows two rules worth knowing once:

  • Reads are organization-wide. An API key is an organization credential, so a list returns every company any of your sites knows, whichever site created it. That is the same answer GET /v1/contacts gives.
  • Writes may name a site. A create takes an optional consentSiteId — the same parameter a contact opt-in takes — and the record is stored exactly as that site's console would store it: recorded as created from it, and visible to every site. In an organization that keeps each site's records apart, consentSiteId is required and the record is visible to that site (and, if you have declared a consent group, to every site in it). There is no default site, for the reason the contacts endpoint gives: picking your only site works until you have two. A record created for the wrong site is a record that site's own team cannot open, which is why this is required rather than guessed.

The site is published on every record as siteId, read-only. It cannot be changed after creation; a record that belongs somewhere else is deleted and recreated there.

The company object​

{
"id": "k7d2b9f104",
"object": "company",
"name": "Acme",
"domain": "acme.com",
"website": "https://acme.com/",
"phone": "+15125550123",
"address": { "line1": "1 Main St", "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US" },
"industry": "Food & Beverage",
"type": "Customer",
"rating": "Hot",
"ownership": "Private",
"accountSource": "Trade show",
"annualRevenueCents": 125000000,
"currency": "usd",
"numberOfEmployees": 42,
"fax": "+15125550124",
"accountNumber": "ACME-001",
"site": "Headquarters",
"tickerSymbol": null,
"sicCode": "5149",
"shippingAddress": { "line1": "9 Dock Rd", "city": "Austin", "state": "TX", "country": "US" },
"parentCompanyId": null,
"ownerUid": "u_9f1c",
"notes": "Renews in March.",
"custom": { "tier": "gold", "seats": 12 },
"nextTaskAt": "2026-09-12T15:00:00.000Z",
"siteId": "site_a1b2c3",
"created": "2026-09-05T18:23:23.941Z",
"updated": "2026-09-05T18:23:23.941Z"
}
FieldTypeNotes
idstringOpaque company id.
objectstringAlways "company".
namestringRequired. Trimmed, truncated to 200 characters. Writable.
domainstring | nullThe bare lowercase hostname — acme.com, never https://www.acme.com/. Normalized before storing, and unique within the organization. Writable.
websitestring | nullA web address. A bare acme.com is stored with https://. Writable.
phonestring | nullE.164 (+15125550123). Normalized before storing; a number that cannot be normalized confidently is a 400, never a half-cleaned string. Writable.
addressobject | nullThe billing address: line1, line2, city, state, postalCode, country (two-letter ISO code). Blank parts are dropped; an address with nothing in it is stored as null. Writable.
shippingAddressobject | nullThe shipping address, read the same way as address. Writable.
type, industry, rating, ownershipstring | nullSalesforce's Account Type, Industry, Rating and Ownership: each one of the organization's active values for that list — kept on the Companies tab of Fields — matched without regard to case and stored as the list spells it. Any other value is a 400 naming the values allowed. A company keeps the value it already holds even after the value is deactivated — including an industry written before Industry was a list. A create that names none starts from the list's default, when one is set. Writable.
accountSourcestring | nullWhere the account came from: one of the organization's active lead source values, judged the same way. Writable.
annualRevenueCentsinteger | nullAnnual revenue in the minor unit of currency (cents, for USD), 0 or more. Writable.
currencystringLowercase ISO 4217 code of annualRevenueCents; usd when unset, as a deal's is. Writable.
numberOfEmployeesinteger | null0 to 99,999,999. Writable.
faxstring | nullE.164, normalized like phone. Writable.
accountNumber, site, tickerSymbol, sicCodestring | nullTrimmed text of at most 40, 80, 20 and 20 characters; longer is a 400. site is which of the company's locations this record is. Writable.
parentCompanyIdstring | nullThe company this one sits under. Must be another company of the organization, never this one or one already under it. Deleting the parent clears it. Writable.
ownerUidstring | nullThe team member responsible for the account. Must be a member of your organization. Writable.
notesstring | nullFree text, 5,000 characters. Writable.
customobjectThe organization's company custom fields, keyed by field key; {} when the company has none. Judged against the company definitions: a key that is not one, a retired field, or a value the type cannot hold is a 400 naming custom.<key>. A PATCH merges the keys it sends; null clears one. Writable.
nextTaskAtstring | nullWhen the earliest open task filed against the company is due, or null when none is — its next activity. Maintained by every task write, this resource included. Read-only.
mediaIdsstring[]Files from the organization's media library attached to this record, by media id, at most 20. An empty array clears them. Writable.
siteIdstringThe site the company was created from — see above. Read-only.
created / updatedstring | nullISO 8601. A fresh record's updated equals its created.

Every writable field is also returned, so you can read back what you wrote.

Endpoints​

List companies​

GET /v1/companies — scope crm:read. Paginated, ordered by company id (see ordering) — unless updatedAfter is given, which changes the order.

ParamNotes
domainExact lookup. Normalized the same way a stored domain is, so ?domain=https://www.Acme.com/about finds acme.com. Returns 0 or 1 company. A value that is not a domain at all is a 400.
ownerUidCompanies owned by this member.
updatedAfterCompanies updated after this instant, oldest change first — the sync filter. ISO 8601 with an offset, like 2026-09-01T00:00:00Z.
limit, cursorStandard pagination.
# the whole list, a page at a time
curl "https://app.aglyn.com/api/v1/companies?limit=50" \
-H "Authorization: Bearer aglyn_sk_…"

# one account, by the domain your own system keys on
curl "https://app.aglyn.com/api/v1/companies?domain=acme.com" \
-H "Authorization: Bearer aglyn_sk_…"

# what changed since the last sync
curl "https://app.aglyn.com/api/v1/companies?updatedAfter=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer aglyn_sk_…"

Only one filter narrows the query itself — domain first, then ownerUid — and any other is checked on the page, so a combined filter can return a short page. Check has_more, not the row count.

Retrieve a company​

GET /v1/companies/{companyId} — scope crm:read. Returns a company, or 404 not_found ("No such company").

Add a company​

POST /v1/companies — scope crm:write. Accepts an Idempotency-Key, scoped to the organization.

Body — name is required; every other writable field is optional. consentSiteId names the site the record came from; leave it out for a record from no site, which belongs to the whole organization. It is required only when your organization keeps each site's records apart.

curl -X POST "https://app.aglyn.com/api/v1/companies" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Idempotency-Key: 9a41f0c2-…" \
-H "Content-Type: application/json" \
-d '{"name":"Acme","domain":"acme.com","ownerUid":"u_9f1c","consentSiteId":"site_a1b2c3"}'

Returns 201 with the created company — or 200 with the original when an Idempotency-Key replays. The status is how you tell a fresh create from a replay.

The domain is already in use​

If a company with that domain already exists you get 409 conflict with code: "company_exists", and the message names the existing id:

{
"error": {
"type": "conflict",
"message": "A company with this domain already exists (k7d2b9f104). Update it instead.",
"code": "company_exists"
}
}

PATCH that id instead — or avoid the round trip by looking the domain up first with GET /v1/companies?domain=. Because the domain is normalized first, https://www.Acme.com/ and acme.com are the same company, the same rule the console's auto-association uses, so the API and the console cannot disagree about who is a duplicate.

That refusal releases the key, so the retry that should succeed once the duplicate is removed still can.

Update a company​

PATCH /v1/companies/{companyId} — scope crm:write. Takes no Idempotency-Key and doesn't need one: the same body twice lands the same state and returns the same 200.

Send only what changes. An omitted key is left alone; an explicit null clears it. A body of {} is a no-op that returns the current company.

curl -X PATCH "https://app.aglyn.com/api/v1/companies/k7d2b9f104" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Content-Type: application/json" \
-d '{"industry":"Food & Beverage","notes":null}'

Changing domain to one another company already holds is the same 409 company_exists as on create. consentSiteId, siteId and visibleTo are refused with a 400 naming the key — the site is set when the record is created.

Delete a company​

DELETE /v1/companies/{companyId} — scope crm:write. Accepts an Idempotency-Key, scoped to the organization.

{ "id": "k7d2b9f104", "object": "company", "deleted": true }

The company alone is removed. The deals, tasks and activities filed against it keep their companyId, and the contacts that worked there keep theirs — they are records of their own, and a delete that cascaded through them would erase a sales history because somebody removed a duplicate account. The companies under it keep everything but their parentCompanyId, which is cleared. Deleting a company that isn't there returns 404 not_found, unless the call carries the key of the delete that removed it, in which case the original receipt is replayed.

Errors​

StatustypeWhen
400bad_requestcode: "validation_failed" — a missing name or consentSiteId (or one naming a site the organization does not own), a domain or phone that does not normalize, a website that is not a web address, an ownerUid who is not a member, a type, industry, rating, ownership or accountSource outside the organization's list (the message names the values allowed), an account field that does not fit (annualRevenueCents, numberOfEmployees, fax, currency, a text field past its length), a parentCompanyId that is the company itself, one below it or none at all, a custom entry that is not a company field or does not fit its type (named as custom.<key>), or a key that is not writable. On the list, a ?domain= that is not a domain or a malformed ?updatedAfter=. fields names each offending key.
403plan_requiredcode: "crm" — the plan doesn't include the CRM suite. code: "crm_records_quota" — the CRM records band is full on a plan that doesn't meter the overage.
403insufficient_scopeKey lacks crm:read / crm:write. Checked before the method.
404not_found"No such company".
405method_not_allowedAllow lists what is: GET, POST on /v1/companies, GET, PATCH, DELETE on one company.
409conflictcode: "company_exists" — that domain is already a company. code: "idempotency_in_progress" — an earlier write with the same key is still running.

See Conventions → Errors for the shared envelope.

  • Contacts — a contact's companyId points here, and GET /v1/contacts?companyId= lists the people at an account.
  • Deals, tasks and activities — each takes a companyId and filters by it.