Sign-up fields and custom metadata
Purpose: collect what your application needs to know about a user at the moment they create their account — a company name, a team size, a marketing consent — without building a form. You define the fields; the hosted create-account page renders them; each answer lands on the new user's record, where your application can read user-owned answers with the person's access token.
The two metadata buckets
Every user record carries two free-form JSON buckets, split by who may write them:
metadata— owned by you. Written from the dashboard and by a management-API token; never editable by the user. The right home for anything your application relies on.userMetadata— owned by the user. They edit it themselves on the hosted/profilepage or through the account API; you can also write it from the dashboard and management API. The right home for preferences and self-descriptions.
Each bucket is capped at 16 KB serialized and 8 levels deep. Neither
bucket rides tokens. An authorized management-API user read returns both; the
caller's account API returns only userMetadata.
Read and update the signed-in person's metadata
Call GET /api/account on your tenant's own origin,
https://{tenantId}.udibo.com, with the person's bearer access token. The
response is { "userMetadata": { ... } }. It requires no management scope or
dashboard seat. The credential fixes the person: there is no user or tenant
selector, and machine credentials are refused.
Send PATCH /api/account with a JSON body such as
{ "userMetadata": { "theme": "dark", "oldPreference": null } } to update that
same bucket. Keys merge at the top level, a null value deletes its key, and a
nested object replaces the previous value of that key. Concurrent merges to the
same account apply one after another, so none of them loses another's keys. The
response has the same shape as the read. The size and depth limits apply to the
final bucket. Only userMetadata is accepted; naming another person or sending
account fields such as email, verification state or display name is refused.
Use target: "userMetadata" for sign-up answers your application needs to read
with the person's token. The admin-owned metadata bucket is absent from this
surface, including sign-up answers stored there. It remains available to
authorized administrators through the dashboard and management API. Treat
user-editable values as preferences or self-descriptions, never as authorization
grants.
Defining sign-up fields
In the dashboard: Settings → Custom sign-up fields. Each field is:
| Setting | Rules |
|---|---|
key | Where the answer lands in the bucket. Starts with a letter; letters, digits, underscore; up to 64 characters |
label | What the form shows. Up to 100 characters |
type | text, number, boolean, or select |
required | Off by default. A required boolean must be ticked |
options | For select: 1–50 choices, and the submitted value must be one of them |
target | Which bucket the answer lands in: userMetadata or metadata. Required, with no default |
Up to 50 fields per tenant. A text answer is capped at 1024 characters.
Keys the sign-up form already owns — username, email, password,
confirmPassword, firstName, lastName, displayName, type, redirect,
intent — are reserved and refused.
Values are validated and typed on submit: a number must parse, a select value must be one of the field's options, and a missing required field blocks the account creation with a per-field message. The create-account page's data also exposes the field configuration, so a client rendering its own form can offer the same inputs.
The field list is one tenant setting, editable in the dashboard or replaced
wholesale through the management API. A PATCH replaces the whole list, so
re-send every field you keep, each with its target. A field without a target
is refused with a 400 and nothing is written, so re-sending a field can never
quietly move its future answers into the user-editable bucket. To move a field,
send it with the other target. In the dashboard, Stored in is a required
choice with nothing preselected, so re-adding an existing key names its bucket
too.
| Method | Path | Scope |
|---|---|---|
PATCH | /api/identity/tenants/{id} | identity:tenants:write |
const tenantId = Deno.env.get("UDIBO_TENANT_ID");
const response = await fetch(
`https://www.udibo.com/api/identity/tenants/${tenantId}`,
{
method: "PATCH",
headers: {
authorization: `Bearer ${Deno.env.get("UDIBO_API_TOKEN")}`,
"content-type": "application/json",
},
body: JSON.stringify({
signupFields: [
{
key: "company",
label: "Company",
type: "text",
required: true,
target: "userMetadata",
},
{
key: "teamSize",
label: "Team size",
type: "select",
options: ["1", "2-10", "11-50", "50+"],
target: "metadata",
},
],
}),
},
);
if (!response.ok) throw new Error(`update failed: ${response.status}`);Changing the field list affects future sign-ups only; nothing rewrites the answers existing users already gave.
Writing metadata after sign-up
Your application updates either bucket on any user through the users API. The bucket you send replaces that bucket; a bucket you omit is untouched.
| Method | Path | Scope |
|---|---|---|
PATCH | /api/identity/{tenantId}/users/{id} | identity:users:write |
const tenantId = Deno.env.get("UDIBO_TENANT_ID");
const userId = Deno.env.get("USER_ID");
const response = await fetch(
`https://www.udibo.com/api/identity/${tenantId}/users/${userId}`,
{
method: "PATCH",
headers: {
authorization: `Bearer ${Deno.env.get("UDIBO_API_TOKEN")}`,
"content-type": "application/json",
},
body: JSON.stringify({ metadata: { plan: "starter", seats: 5 } }),
},
);
if (!response.ok) throw new Error(`update failed: ${response.status}`);{tenantId} / {id} is your tenant's id — the same one in your dashboard URL —
and the token must hold administrative authority over that tenant.
Administrator credentials required. These management calls use the
credential associated with an authorized Udibo dashboard session. A machine
credential issued to an application in your own tenant is not accepted on
/api/identity/…. Use the dashboard or an approved administrator workflow; keep
administrator credentials out of your application integration.
Last verified 2026-09-11.

