---
title: "Update a customer"
method: PATCH
path: "/v1/customers/{id}"
tags: ["Customers"]
---

# Update a customer

`PATCH /v1/customers/{id}`

## Request

- Base URL: `https://api.officina.it`
- URL: `https://api.officina.it/v1/customers/{id}`
- Auth: HTTP bearer

Partial update following the design philosophy:

- **Omitted JSON keys** leave the corresponding entity fields
  untouched. The mobile UI can send only the keys it actually
  changed.
- **Explicit JSON `null`** clears the field (sets the column
  to SQL NULL in the database). Use this to wipe an optional
  field — there is no `?clear=phone` query param.
- **Concrete values** overwrite the entity field with the value.

`customer_type` is NOT NULL in the schema; sending it as
`null` is a 422 `parameter_invalid` error.

`custom_fields` replaces the whole values object (omitted = leave,
`null` = clear); a hidden field keeps its value unless its key is
sent. See `CustomerCustomFieldValues`.

The customer's generated agenda events follow its dates: changing
the birthday or a custom date moves its event, clearing it removes
the event, and a patch that touches no date leaves them alone.

Returns the fully materialized post-update customer.

Foreign-key references in the body (`price_list_id`, etc.) must
resolve to resources owned by the authenticated user's company.
Unreachable refs return 422 `parameter_invalid` pointing at the
offending field.

## Request body

- CustomerPatchRequest: Body of `PATCH /v1/customers/{id}`. Every field is optional on the wire and carries three-state semantics: - Field omitted → leave the entity field untouched. - Field present with value `null` → clear the entity field. - Field present with a value → set the entity field. Fields not listed here are intentionally NOT patchable through the API: `id`, `company_id`, `created_by`, system timestamps (`created_at`, `updated_at`, `deleted_at`), and computed/derived fields (`total_spent`, `next_appointment`, `last_vehicle_*`). Documents are managed via the documents sub-resource. `customer_type` is NOT NULL at the entity layer; sending it as explicit `null` is rejected with 422 `parameter_invalid`.
  - `customer_type` string
  - `full_name` string, nullable
  - `email` string, email, nullable
  - `phone` string, nullable: National number as typed (e.g. "333 1234567"). Combined with `phone_dial_code` and validated server-side via the web customer-form rules ("+39" allows Italian mobiles and landlines; other known dial codes get a basic E.164 check); stored and returned as one E.164 string. Invalid pairs are rejected with 422 `parameter_invalid`.
  - `phone_dial_code` string, nullable: E.164 dial code (default "+39"). Only read alongside `phone`.
  - `address` string, nullable
  - `city` string, nullable
  - `zip_code` string, nullable
  - `province` string, nullable
  - `tax_id` string, nullable: Partita IVA, or the VAT number of a business in another EU country. Checked on every revision against the country's format and check digits (see "Partita IVA e numeri IVA UE"): 422 `parameter_invalid` on `/tax_id` when it fails. Stored without country prefix, spaces or dots. Checked only when `tax_id` or `customer_type` changes.
  - `fiscal_code` string, nullable
  - `pec` string, nullable
  - `destination_code` string, nullable
  - `payment_method` string, nullable
  - `comments` string, nullable
  - `birthday` string, date-time, nullable: Nullable client-supplied RFC 3339 timestamp. Same 1900-01-01 … 2100-12-31 window as `WritableTimestamp`; `null` clears the field.
  - `price_list_id` string, nullable
  - `custom_fields` CustomerCustomFieldValues, nullable: The customer's custom field values, keyed by definition `id` from `GET /v1/customer-field-settings` (e.g. `cf_9b20d3`). Same rules as `VehicleCustomFieldValues`: every key is checked against the company's definitions and each value against its field's `type` (`string` trimmed; `integer` as a JSON integer or a string holding one; `datetime` as `YYYY-MM-DD` or RFC 3339, stored as `YYYY-MM-DD`, years 1900–2100). A date field whose definition sets `reminder_days_before` (and is visible) gets an agenda event on that date. A `null` or empty value clears that field. An unknown key or a value that does not fit its type is a 422 `parameter_invalid` with `param` pointing at it (`/custom_fields/<id>`). A value sent back exactly as the customer stores it is never refused. On PATCH the object replaces the stored one as a whole: a visible field left out is cleared, a hidden field keeps its stored value unless its key is sent. A stored value that no longer fits its field's type (text left under a field later turned into a date or number) is likewise kept unless its key is sent — clients cannot show it in a typed input, so leaving it out is not a request to clear it. Send the key with `null` to clear it.

## Response `200`

Customer updated. Returns the full post-update entity.

- Customer: A workshop's client. Multi-tenant: belongs to one company. Most descriptive fields are optional (nullable) because customer records are commonly created from a phone call with only a name and phone number.
  - `object` 'customer', required
  - `id` string, required
  - `customer_type` string, required: Tax-status discriminator (e.g. "private", "company").
  - `full_name` string, nullable
  - `email` string, email, nullable
  - `phone` string, nullable: Stored E.164 value, e.g. "+393331234567".
  - `phone_dial_code` string, nullable: `phone` split server-side for the edit form's dial-code selector (e.g. "+39"). Null when `phone` is null.
  - `phone_national` string, nullable: `phone` minus the dial code, for the edit form's national-number input. Null when `phone` is null.
  - `address` string, nullable
  - `city` string, nullable
  - `zip_code` string, nullable
  - `province` string, nullable
  - `tax_id` string, nullable: Partita IVA for an Italian customer; the VAT number without country prefix for a business elsewhere in the EU; free text otherwise. See "Partita IVA e numeri IVA UE".
  - `fiscal_code` string, nullable: Italian fiscal code (codice fiscale) for individuals.
  - `pec` string, nullable: Italian certified-email address for e-invoicing.
  - `destination_code` string, nullable: Italian SDI destination code for e-invoicing.
  - `payment_method` string, nullable
  - `comments` string, nullable
  - `birthday` string, date-time, nullable
  - `last_vehicle_name` string, nullable: Display string of the customer's most recent vehicle (e.g. "Fiat Panda"). Pre-computed on the entity for list-row rendering without a second request.
  - `last_vehicle_license` string, nullable
  - `total_spent` number, double: Lifetime spend (worksheet totals net of discounts), computed by the list query for row rendering. Money — render via the locale formatter; avoid arithmetic.
  - `documents` CustomerDocument[]: Attachments (Documenti Allegati). Present on the detail response only; omitted on lists.
    - `object` 'customer_document', required
    - `id` string, required
    - `name` string, required
    - `content_type` string, required
    - `description` string, nullable
    - `url` string, required
    - `file_size_bytes` integer, required
    - `created_at` string, date-time, required
  - `custom_fields` object, nullable: Single-customer responses only (GET, POST, PATCH). Raw stored values keyed by custom field definition `id` (see `CustomerCustomFieldValues` for the stored shapes). Includes hidden fields. Omitted on lists.
  - `custom_field_values` object[]: `GET /v1/customers/{id}` only. The company's visible custom field definitions (settings → Campi cliente) resolved against this customer: configured order, display-ready names, "" when the customer has no value.
    - `id` string, required
    - `name` string, required
    - `type` 'string' | 'integer' | 'datetime', required
    - `value` string, required
  - `field_visibility` object: `GET /v1/customers/{id}` only. The company's optional-field toggles (settings → Campi cliente): the edit form hides — and omits from its PATCH — exactly what the web form hides. All true when never configured.
    - `birthday` boolean
    - `comments` boolean
  - `next_appointment` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `deleted_at` string, date-time, nullable: Soft-deletion timestamp. Populated on the response from DELETE /v1/customers/{id}. Customers with a non-null deleted_at are not returned by list/detail endpoints — this field appears for the delete response only.

## Other responses

- `400`: Request body is unparseable or X-API-Version is missing.
- `401`: Missing, malformed, expired, or wrong-audience bearer token.
- `402`: The company has no active subscription, or its plan does not include API access. API access is included in the Business and Illimitato plans.
- `403`: The authenticated user lacks the required permission.
- `404`: Resource does not exist OR belongs to a different company. The two cases are intentionally indistinguishable.
- `415`: Content-Type is not `application/json`, or Accept rejects JSON.
- `422`: A request field is missing or invalid (fast-fail).
- `500`: Unhandled server error. Detail is logged, not exposed.

---

[API](https://skmtc.dev/officina/apis/officina-it-api.md) · [All operations](https://skmtc.dev/officina/apis/officina-it-api/llms.txt) · [OpenAPI document](https://skmtc.dev/officina/apis/officina-it-api/revisions/7da3fccfe551?raw)
