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.
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.
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/contactsgives. - 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,consentSiteIdis 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"
}
| Field | Type | Notes |
|---|---|---|
id | string | Opaque company id. |
object | string | Always "company". |
name | string | Required. Trimmed, truncated to 200 characters. Writable. |
domain | string | null | The bare lowercase hostname — acme.com, never https://www.acme.com/. Normalized before storing, and unique within the organization. Writable. |
website | string | null | A web address. A bare acme.com is stored with https://. Writable. |
phone | string | null | E.164 (+15125550123). Normalized before storing; a number that cannot be normalized confidently is a 400, never a half-cleaned string. Writable. |
address | object | null | The 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. |
shippingAddress | object | null | The shipping address, read the same way as address. Writable. |
type, industry, rating, ownership | string | null | Salesforce'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. |
accountSource | string | null | Where the account came from: one of the organization's active lead source values, judged the same way. Writable. |
annualRevenueCents | integer | null | Annual revenue in the minor unit of currency (cents, for USD), 0 or more. Writable. |
currency | string | Lowercase ISO 4217 code of annualRevenueCents; usd when unset, as a deal's is. Writable. |
numberOfEmployees | integer | null | 0 to 99,999,999. Writable. |
fax | string | null | E.164, normalized like phone. Writable. |
accountNumber, site, tickerSymbol, sicCode | string | null | Trimmed 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. |
parentCompanyId | string | null | The 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. |
ownerUid | string | null | The team member responsible for the account. Must be a member of your organization. Writable. |
notes | string | null | Free text, 5,000 characters. Writable. |
custom | object | The 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. |
nextTaskAt | string | null | When 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. |
mediaIds | string[] | Files from the organization's media library attached to this record, by media id, at most 20. An empty array clears them. Writable. |
siteId | string | The site the company was created from — see above. Read-only. |
created / updated | string | null | ISO 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.
| Param | Notes |
|---|---|
domain | Exact 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. |
ownerUid | Companies owned by this member. |
updatedAfter | Companies updated after this instant, oldest change first — the sync filter. ISO 8601 with an offset, like 2026-09-01T00:00:00Z. |
limit, cursor | Standard 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
| Status | type | When |
|---|---|---|
400 | bad_request | code: "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. |
403 | plan_required | code: "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. |
403 | insufficient_scope | Key lacks crm:read / crm:write. Checked before the method. |
404 | not_found | "No such company". |
405 | method_not_allowed | Allow lists what is: GET, POST on /v1/companies, GET, PATCH, DELETE on one company. |
409 | conflict | code: "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.
Related
- Contacts — a contact's
companyIdpoints here, andGET /v1/contacts?companyId=lists the people at an account. - Deals, tasks and activities — each takes a
companyIdand filters by it.