Skip to main content

Product Custom Fields

additional_information lets a business attach custom fields to an individual product — a gift note, a size dropdown, a custom-length slider — and collect the customer’s answers during checkout. The full lifecycle spans four calls:
  1. Define the fields on the product — business side (create-product / update-product)
  2. Discover them from the storefront (GET /product/{id} or GET /products)
  3. Submit the customer’s answers on the cart line (/product-payment/intent)
  4. Read the answers back on the order (/order/{id})
Steps 2–4 are the checkout integration. Step 1 is how a merchant configures the product in the first place.
Availability: checkout-time validation and storage of these answers ship with techchak-backend branch feat/b2b-settings-exports-kyc-bypass, which is not yet merged. The database columns are already live, but nothing populates them until that branch is released. Remove this callout once it ships.
This is not the same as checkout_form_response. That is a business-wide form shared across every product. additional_information is defined directly on one product, and each product has its own field list.

1. Define fields on the product

POST /business/create-product and PATCH /business/update-product/{product_id} accept additional_information as an array of field definitions:
string
required
Stable key for the field. This is what checkout answers are keyed by, so it must be unique within the product and should not change once customers have submitted against it.
string
required
Display text shown to the customer. Safe to rename at any time — it is never used as a key.
string
required
One of text, number, date, email, phone, textarea, select, checkbox, range.
boolean
default:"false"
When true, checkout rejects a submission that omits this field or sends it blank.
string[]
Allowed values. Accepted only when type is select — sending it with any other type is rejected.
number
Lower bound. Accepted only when type is range.
number
Upper bound. Accepted only when type is range.
number
Step increment for the input control. Accepted only when type is range. Not enforced at checkout — only min and max are.
options is optional at save time, so a merchant can pick select before filling in the choices. A select field saved with no options accepts any string at checkout, because there is nothing to validate against.

2. Discover the fields from the storefront

Before rendering a checkout form you need to know what a product asks for. The public product endpoints return the definitions on the product itself:
Build one input per entry: type picks the control, label is the caption, required drives client-side validation, and options or min/max/step populate select and range inputs. Keep each field’s id — it is the key you submit the answer under in the next step.
If the business has attached a shared custom form to the product, its fields are merged into this same array, already converted to the same shape, and attached_custom_form on the product names the form. There is no second call and no second shape to handle.One caveat from that conversion: shared-form fields of type time arrive as text, because there is no product-level time type. The label and required flag survive; the time-picker semantics do not.
An empty array means the product has no custom fields — skip the form entirely, since any answers sent for it are discarded. For a multi-item cart, do this per line item: each product has its own field list, and answers are submitted per cart line.

3. Submit answers at checkout

Answers go on the cart line, as an object keyed by each field’s id:
Answers are keyed by id, not label, and a wrong key fails silently.Sending { "Size": "M" } for a field whose id is size does not error — the key is simply dropped and no answer is stored. If answers are missing from an order, check the key spelling before assuming a server fault.
Only the cart array path collects custom fields.A single-product purchase that does not wrap the item in a cart array has no code path that reads additional_information at all — the request succeeds and the customer’s answers are discarded with no error. A “Buy Now” flow that needs custom fields must send a one-item cart array.
For physical products, POST /cart/validate still has to run first to obtain cart_unique_id. additional_information is ignored at that step — send it only on the final /product-payment/intent call.

What gets stored

The stored answer object is rebuilt from the product’s own field definitions rather than copied from the request, which means:
  • Keys that do not match a defined field id are dropped
  • Optional fields left blank are omitted rather than stored as empty
  • If the product defines no fields at all, nothing is stored regardless of what was submitted

4. Validation errors

All four use the standard error envelope, so no separate error-handling branch is needed — only the message copy differs. An unrecognized key is not an error — it succeeds and the key is absent from what is stored.

5. Read answers back on the order

GET /order/{id} returns the answers directly on the order object. No second lookup is needed.
It is an array because one order can cover several products, each with its own field set. Match on product_id to find the entry for a given line item.

Legacy shape

Before this schema, additional_information on a product looked like this:
Nothing backfills it. A product that already had custom fields keeps the legacy shape in GET responses until it is re-saved through the update endpoint, so both shapes appear on the same API surface until every such product has been saved once. Rendering code that assumes the flat { id, label, type, required } shape universally will break on any product that has not been touched since this shipped. Detect the shape before rendering — the presence of information_type or information_lists identifies a legacy record. Note also that the legacy information_type enum is not the same set: it included tel, time and uuid, and lacked number and checkbox. tel corresponds to the new phone.