---
title: "Create or update a custom field of a company"
method: PUT
path: "/v1/companies/{company_id}/custom-fields/{key}"
tags: ["CompanyCustomFields"]
---

# Create or update a custom field of a company

`PUT /v1/companies/{company_id}/custom-fields/{key}`

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/companies/{company_id}/custom-fields/{key}`
- Auth: HTTP bearer

Creates the custom field with this `key`, or updates it if the company already has one.
Repeating the request leaves the same state, so it is safe to run on every deployment to
seed a catalogue.

- **Status:** `201` when the field is created, `200` when it already existed.
- **Key:** it is the path, never the body, and it never changes. To stop using a field,
  archive it (`is_archived: true`); fields are never deleted.
- **Archive state:** omitting `is_archived` keeps the current one, so repeating the
  request never reactivates a field someone archived.
- **Suggestions:** omitting `suggest_values` keeps the current setting; a new field starts
  with `true`. Use `false` for a value that changes on every invoice, such as an order
  number: invoices store the text as sent and no options are created. Turning it off
  deletes no option, and turning it back on offers them again.
- **Options:** the response carries the common options of the field, or none when
  `suggest_values` is `false`.

## Headers

- `Idempotency-Key` string

## Request body

- UpsertCustomFieldRequest: The state of the custom field. `label` and `show_on_pdf` are always replaced. `suggest_values` and `is_archived`, when omitted, keep their current value (a new field is created with `suggest_values: true` and active), so repeating a request never undoes a change someone made to them.
  - `label` string, required: Name people see next to the value. Leading and trailing spaces are removed; a label that is empty after that answers `422`.
  - `show_on_pdf` boolean, required: Whether the PDF of invoices saved from now on prints this field.
  - `suggest_values` boolean: `true` offers the values already used as options and adds new ones as invoices are saved; `false` stores each value as sent and creates no options. Omit it to keep the current setting; a new field starts with `true`. Turning it off deletes no option.
  - `is_archived` boolean: `true` archives the field, `false` reactivates it. Omit it to keep the current state.

## Response `200`

The custom field already existed and was updated.

- CustomFieldResponse: The envelope every successful JSON response of the BeeL. API is wrapped in. There are no bare resources in v1 and none are planned: the payload always hangs off `data`. - A **single resource** is an object in `data`. - A **collection** hangs off a named key inside `data`, together with its `pagination`, also inside `data` — `data: {invoices: [...], pagination: {...}}`. - A collection carries `pagination` unless its operation declares itself a **closed catalogue**: a fixed, bounded list with nothing to page through. The declaration is explicit in the operation; a missing `pagination` is never something to infer. - `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a documented variant of pagination, not another envelope. Putting the array straight into `data` with `pagination` as its sibling is the shape a v2 would adopt; v1 is not being flipped to it.
  - `success` boolean, required
  - `data` object, required: A custom field of the company's invoices, with the options that apply to the query that returned it.
    - `id` string, uuid, required: Unique identifier of the custom field.
    - `key` string, required: Identifier of the custom field within the company, chosen when the field is created: lowercase letters, digits and `_`, up to 64 characters. It never changes. BeeL. gives it no meaning of its own.
    - `label` string, required: Name people see next to the value, on screen and, if `show_on_pdf`, on the PDF.
    - `show_on_pdf` boolean, required: Whether the PDF of the invoice prints this field, in a block of its own under the notes. It applies to invoices saved from then on: an invoice keeps the setting it was saved with.
    - `suggest_values` boolean, required: Whether the field offers the values already used as options. With `true`, saving an invoice reuses an equivalent option or adds the value as a new one. With `false`, for a value that changes on every invoice such as an order number, the invoice stores the text as sent (without leading and trailing spaces), no option is created, and `options` comes back empty. Turning it off deletes no option: the existing ones are kept, can still be listed and managed under `GET /v1/companies/{company_id}/custom-fields/{key}/options`, and are offered again when it is turned back on.
    - `is_archived` boolean, required: An archived field cannot be added to new invoices and is left out of listings unless `include_archived=true`. Invoices that already carry it keep it.
    - `options` CustomFieldOption[], required: The options of this field that apply to the query: the common ones plus, when the query names a `customer_id`, the ones of that customer. Archived options appear only with `include_archived=true`. Always empty when `suggest_values` is `false`. To page through every option of the field, of every customer, use `GET /v1/companies/{company_id}/custom-fields/{key}/options`.
      - `id` string, uuid, required: Unique identifier of the option.
      - `value` string, required: Text of the option, exactly as it is shown and copied onto an invoice.
      - `customer_id` string, uuid, nullable, required: Customer the option belongs to, or `null` for an option common to every customer. It is set when the option is created and never changes.
      - `is_archived` boolean, required: An archived option is no longer offered and is left out of listings unless `include_archived=true`. Invoices that already carry its text keep it.
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `201`: The custom field was created.
- `400`: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in `error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production. `BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not admit in its current state when that state has no code of its own. It carries no `details`; read the resource again before retrying.
- `401`: Missing or invalid authentication. Like every other error, `message`/`detail` is localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the header is missing or asks for none of those.
- `403`: Your account does not own or manage this company, or does not hold the required access over it. A company that does not exist answers the same way. That case answers `error.code` `ACTIVE_COMPANY_NOT_ACCESSIBLE`.
- `409`: The request is well formed but collides with the state that is already there. Three families, told apart by `error.code`: - **Uniqueness** — something with that identity already exists (`CLIENT_DUPLICATE`, `PRODUCT_DUPLICATE`, `SERIES_CODE_DUPLICATED`, `NIF_ALREADY_REGISTERED`, `MEMBER_ALREADY_IN_ACCOUNT`, `INVOICE_DUPLICATE_EXTERNAL_REFERENCE`). - **Lifecycle** — the transition does not apply from where the resource is (`INVOICE_ALREADY_VOIDED`, `PROFORMA_ALREADY_CONVERTED`, `CLIENT_HAS_INVOICES`, `RECURRING_ALREADY_ENDED`, `CUSTOM_FIELD_ARCHIVED` — the custom field is archived and cannot be assigned, `RECURRING_OCCURRENCE_ALREADY_CONSUMED` — the occurrence the recurring template points at has already been billed; read its history instead of retrying). Read the resource before retrying: the state you assumed is not its state. - **Concurrency and idempotency** — another write got there first (`CONCURRENT_MODIFICATION`), or the `Idempotency-Key` you sent is still in flight (`IDEMPOTENCY_KEY_PROCESSING`, retry after a moment) or was already used for a different body (`IDEMPOTENCY_KEY_MISMATCH`, use a new key). `CONFLICT` is the generic fallback of the uniqueness family, when the uniqueness that was violated has no code of its own.
- `422`: `VALIDATION_ERROR` — the body parses but a value is not acceptable. `details` is a flat map from the offending property's name (in the contract's `snake_case`, nested paths joined with a dot, e.g. `recipient.address.postal_code`) to a message describing what is wrong with it, one entry per property. When the rejected value is one outside an enum's vocabulary, `details` instead follows `FieldDeserializationError` (`field`, `invalid_value`, `allowed_values`). Two more codes share this status for a value that passes the contract but does not fit where it is stored: `VALUE_TOO_LONG` (a text longer than its column holds) and `VALUE_OUT_OF_RANGE` (a number outside what its column holds). Nothing was written.
- `429`: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because too many went out to that recipient in a short time. Either way, retry after the `Retry-After` seconds. `QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan for the current period (issued invoices, for instance). Retrying does not help until the quota resets or the plan changes. `error.details` carries `feature_code`, `current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until the reset when it is known.
- `500`: Internal server error
- `default`: Any status code the operation does not list above. Every operation declares it, so a generated client always has a branch to fall into and never loses the cause of a failure it did not anticipate. This is where the transport-level answers land — `405`, `406`, `415` and `429` — together with any status a future version of the API starts returning. All of them carry the same error envelope as the codes listed explicitly, so `error.code` is what tells them apart: switching on the status code alone is not enough. See «Transport-level errors» in the API description for when each one is produced. A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active subscription. `error.details` says why and what to do: `current_status` (absent if it never had one), `required_permission` and `action`. A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a dependency of BeeL itself (the database, the email provider) was momentarily unavailable and nothing was done. Retry after those seconds. A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the operation depends on failed or did not answer in time. It is a transient condition — retry with the same `Idempotency-Key` where the operation accepts one. One exception to the envelope: a failure of the network edge that never reaches the application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not BeeL's — it may not even be JSON. Treat those as "no answer", and retry.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
