Sign-up fields and custom metadata
Collect additional answers on your hosted create-account form — a company name, a team size, a marketing consent — without building a form. You define the fields; the form renders them and stores answers on the new user's record. Your application can read user-owned answers with the person's access token.
The hosted waitlist form uses the same field configuration and stores answers on the waitlist entry. Social sign-up follows a different path; see where fields are collected before relying on a required answer.
The two metadata buckets
Every user record carries two free-form JSON buckets, split by who may write them:
metadata— maintained by your administrators through the dashboard (and the management API; see below), and confidential from the person it describes: it is never returned to that person. Users cannot read or edit it through their account page, the account API, their profile, tokens or UserInfo. A configured sign-up field can still place the person's submitted answer in this bucket when the account is created, after which they cannot read it back.userMetadata— owned by the user. The hosted/profilepage shows each sign-up field you store here as its own control, so people can update their answers later; keys no field defines are left alone. Your application can write any key through the account API, and you can write it from the dashboard. The right home for preferences and self-descriptions.
Each bucket must be a JSON object at the root, capped at 16 KB serialized
and 8 levels deep. Keys and text values must contain valid Unicode and no
NUL characters. Neither bucket rides tokens. The dashboard's user page shows
both to authorized administrators; every response a person gets about
themselves carries only userMetadata.
Sign-up answers are user input in either bucket. Setting target: "metadata"
controls later editing; it does not verify the answer. Your application must
authorize access with
permission checks,
rather than treating a submitted answer as a role or permission grant.
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. Treat user-editable values as
preferences or self-descriptions, never as authorization grants.
Username and name
The hosted create-account form always asks for an email address and a password.
Whether it also asks for a username and a name is yours to choose, in the
dashboard under Settings → Sign-up → Account details, or as
signupProfile on the tenant through the Management API:
| Setting | Choices | Default |
|---|---|---|
username | required, optional (may be left blank), hidden | required |
name | required, optional (may be left blank), hidden | required |
nameFormat | first-last (two fields) or full (one full-name field) | first-last |
A tenant that never sets these asks for a required username and a required first and last name, as every tenant did before the setting existed.
A username left blank or not asked for is made at random,
user-and eight hexadecimal digits, so it reveals nothing of the email address. The person never sees it unless your application shows it.A username stays required while people sign in with one. If the login identifier is Username only or Email or username, the username must be
required; set the login identifier to Email only first. A save that breaks this is refused, keyed to the setting you changed.A full name is stored as the user's display name, and the first and last name stay empty. Tokens and UserInfo carry it as
namewith nogiven_nameorfamily_name.A name left blank or not asked for leaves the name empty. The dashboard and tokens show the username instead, and an empty
given_nameorfamily_nameclaim is left out rather than sent blank.A detail the tenant does not ask for is ignored if a request posts it anyway, so a form opened before you changed the setting cannot set it.
Invited people redeeming a waitlist invitation get the same form. Social sign-up ignores these settings: it keeps the name the provider sends whatever the profile says, and makes the username from the provider's email address, as before.
Defining sign-up fields
In the dashboard: Settings → Sign-up → 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. A new field may not use fullName, which
the full-name option uses; a field already saved with that key keeps working.
On the ordinary hosted create-account form and the waitlist join form, 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 that form's submission 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.
Social sign-up does not collect these fields or enforce their required answers. It creates the account from the provider's profile. Redeeming a waitlist invitation does not ask for custom fields again or copy the entry's answers into the user's metadata.
If your application needs an answer from every user, collect and check it after sign-in across every method you offer. User-owned answers can be saved through the account API. Test both the local form and social sign-up before relying on your onboarding requirements.
The field list is one tenant setting, edited in the dashboard. Through the
management API, listed below for reference, 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 offers
Editable by the user (userMetadata) and Admin only (metadata), and
Required is a checkbox, off by default. 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 |
Reference only — dashboard today.
const tenantId = process.env.UDIBO_TENANT_ID;
const response = await fetch(
`https://www.udibo.com/api/identity/tenants/${tenantId}`,
{
method: "PATCH",
headers: {
authorization: `Bearer ${process.env.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
An administrator edits either bucket on the user's page in the dashboard. The users API operation below is the management API equivalent, for reference: 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 |
Reference only — dashboard today.
const tenantId = process.env.UDIBO_TENANT_ID;
const userId = process.env.USER_ID;
const response = await fetch(
`https://www.udibo.com/api/identity/${tenantId}/users/${userId}`,
{
method: "PATCH",
headers: {
authorization: `Bearer ${process.env.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.
Dashboard only today. These management API operations are listed for
reference: they accept only the dashboard's own session or a token from Udibo's
own sign-in, which you cannot get outside the dashboard. A token your tenant
issues, including a client-credentials token, is refused with 401; see
the management API.
Last verified 2026-10-30.

