# Customer

A customer: the CRM record every project, contact, subscription and sales invoice hangs off. Soft-deleted, and deleting one cascades to its projects.

## Used by
- GET /api/v2/customers: List customers (CustomerList)
- POST /api/v2/customers: Create a customer (Customer)
- GET /api/v2/customers/{customer}: Read a customer (Customer)
- PATCH /api/v2/customers/{customer}: Update a customer (Customer)
- POST /api/v2/customers/{customer}/actions/restore: Restore a deleted customer (Customer)

## Customer fields
- id (integer, required): Unique identifier.
- name (string ≤ 255, required): Customer name. This is the default sort key and one of the fields `?q=` searches.
- code (string ≤ 255, required): Short customer code, e.g. ACM. Prefixes every work item's issue key, is enforced unique on write, and is one of the fields `?q=` searches.
- color (string ≤ 255, required): Chip colour as a CSS hex triplet or sextet, e.g. #1d4ed8. A stored value that is not valid hex reads back as #6b7280.
- status (string, required): Where the customer sits in the sales lifecycle.
- email (string or null ≤ 255, required): Primary email address. The third field `?q=` searches; not unique.
- phone (string or null ≤ 255, required): Phone number.
- address (string or null ≤ 255, required): Street address. Writing this, zip or city re-geocodes lat and lng.
- zip (string or null ≤ 255, required): Postal code.
- city (string or null ≤ 255, required): City.
- country (string or null ≤ 255, required): Country as an ISO 3166-1 alpha-2 code, e.g. NL, as plnnrly writes it. Older records may hold any string.
- lat (string or null ≤ 255, required, server-assigned): Latitude, geocoded from the address whenever address, zip or city changes. Not writable.
- lng (string or null ≤ 255, required, server-assigned): Longitude, geocoded from the address whenever address, zip or city changes. Not writable.
- founded_at (date (YYYY-MM-DD) or null, required): Day the customer's company was founded.
- customer_since (date (YYYY-MM-DD) or null, required): Day this customer became a customer.
- is_vat_exempt (boolean, required): Whether invoices to this customer are issued without VAT.
- payment_days (integer or null, required, server-assigned): Payment term in days, as held by the accounting integration. Read by the invoice composer; written only by the Exact sync.
- debtor_code (string or null ≤ 255, required): Debtor number in the accounting system. Writing it requires `billing_settings::update`.
- exact_account_id (string or null ≤ 255, required, server-assigned): Account identifier in Exact Online, written by the sync.
- exact_synced_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required, server-assigned): When the Exact sync last handled this customer, or `null` if never.
- exact_keep_local_address (boolean, required, server-assigned): Whether the Exact sync leaves this customer's local address alone although it differs from the one in Exact. Set when a user accepts the difference on the Exact customers screen in plnnrly; not writable.
- account_manager_id (integer or null, required): Responsible account manager. Required on create; unassigned when the user is deleted.
- customer_category_id (integer or null, required): Customer category, or `null`.
- company_type_id (integer or null, required): Company type, or `null`. Company types are your organisation's own list, maintained in plnnrly and not exposed as a collection.
- company_size_id (integer or null, required): Company size band, or `null`. Company sizes are your organisation's own list, maintained in plnnrly and not exposed as a collection.
- company_branch_id (integer or null, required): Industry branch, or `null`.
- created_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Creation timestamp.
- updated_at (timestamp (YYYY-MM-DDThh:mm:ssZ), required): Last-write timestamp; use for incremental sync.
- deleted_at (timestamp (YYYY-MM-DDThh:mm:ssZ) or null, required): Soft-delete timestamp, `null` while live.
- customer_category (Ref or CustomerCategorySummary or null, required): The customer's category. Rendered as {"id": N} until named in `?include=customer_category`, and `null` where none is assigned.
- company_branch (Ref or CompanyBranchSummary or null, required): The customer's industry branch. Rendered as {"id": N} until named in `?include=company_branch`, and `null` where none is assigned.
- projects_count (integer ≥ 0, required): Number of live projects for this customer. Soft-deleted projects are not counted.
- contacts_count (integer ≥ 0, required): Number of contacts on this customer.
- tags_count (integer ≥ 0, required): Number of tags on this customer.
- links (CustomerLinks, required)
  - self (url, required): The canonical URI of this record.
  - projects (url, required): Projects for this customer.
  - contacts (url, required): Contacts on this customer.
- tags (array of TagSummary ≤ 50, optional, only with include): Tags on this customer. A customer takes at most 50 tags, so the include lists them all. `tag_ids` replaces the whole set. Wrapped in `{ data, meta }`; `meta.truncated` says whether the cap was hit and `meta.count` is the full count.
  - id (integer, required): Unique identifier.
  - title (string ≤ 255, required): Tag title as shown in the UI.

## Variants

### CustomerList

What a collection returns, and what `?view=summary` returns on a single record.

36 fields.
Identical to the detail object above.

### CustomerSummary

The embedded form: what you get when another resource carries this one and you did not `include` it.

3 fields.
Omits: color, status, email, phone, address, zip, city, country, lat, lng, founded_at, customer_since, is_vat_exempt, payment_days, debtor_code, exact_account_id, exact_synced_at, exact_keep_local_address, account_manager_id, customer_category_id, company_type_id, company_size_id, company_branch_id, created_at, updated_at, deleted_at, customer_category, company_branch, tags, projects_count, contacts_count, tags_count, links

### CustomerLinks

The `links` object on this resource.

Keys: self, projects, contacts

## Columns deliberately not published
- exact_sync_issues: Holds free-form sync messages with no stable shape, so it has no honest published type. exact_synced_at is published instead, so a client can still tell whether a sync has run.
- location: Not stored on its own: it combines lat and lng, both of which are published as their own read-only fields.

## Example
```json
{
  "id": 1,
  "name": "Acme Industries",
  "code": "ACM",
  "color": "#1d4ed8",
  "status": "active",
  "email": "hello@acme.test",
  "phone": "…",
  "address": "Keizersgracht 1",
  "zip": "1015 CJ",
  "city": "Amsterdam",
  "country": "NL",
  "lat": "…",
  "lng": "…",
  "founded_at": "2026-03-04",
  "customer_since": "2026-01-15",
  "is_vat_exempt": false,
  "payment_days": 1,
  "debtor_code": "…",
  "exact_account_id": "…",
  "exact_synced_at": "2026-03-04T09:15:00Z",
  "exact_keep_local_address": false,
  "account_manager_id": 1,
  "customer_category_id": 1,
  "company_type_id": 1,
  "company_size_id": 1,
  "company_branch_id": 1,
  "created_at": "2026-03-04T09:15:00Z",
  "updated_at": "2026-03-04T09:15:00Z",
  "deleted_at": "2026-03-04T09:15:00Z",
  "customer_category": null,
  "company_branch": null,
  "tags": {
    "data": [
      {
        "…": "2 fields of TagSummary"
      }
    ],
    "meta": {
      "truncated": false,
      "…": "1 optional field left out"
    }
  },
  "projects_count": 1,
  "contacts_count": 1,
  "tags_count": 1,
  "links": {
    "self": "https://<your-url>/api/v2/work-items/1",
    "projects": "https://<your-url>/api/v2/work-items/1",
    "contacts": "https://<your-url>/api/v2/work-items/1"
  }
}
```
