Settings
See Domain Model → Settings for the resolution model this resource implements: reserved keys fall through AppSettings → global Settings, declared keys fall through AppSettings → the Application's default.
- Endpoints
GET /v1/users/{id}/settingsPATCH /v1/users/{id}/settingsGET /v1/users/{id}/apps/{appId}/settingsPATCH /v1/users/{id}/apps/{appId}/settings- Errors specific to this resource
Endpoints
| Method | Path | Requires | Purpose |
|---|---|---|---|
GET |
/v1/users/{id}/settings |
self, or platform users.list |
Fetch global Settings. |
PATCH |
/v1/users/{id}/settings |
self or users.manage |
Update global Settings. |
GET |
/v1/users/{id}/apps/{appId}/settings |
self, the app’s own key, or platform users.list |
Fetch the fully resolved per-app settings. |
PATCH |
/v1/users/{id}/apps/{appId}/settings |
self, the app’s own key, or users.manage |
Write per-app overrides. |
As on Profiles, “the app’s own key” means any app-scoped API Key scoped to {appId}, with no extra permission needed. Self and app-key access to an Application’s settings requires the user to have active access to that Application. Otherwise the call returns 403 entitlement_required. Support bypasses that check for investigations: platform users.list (held by the support Role) can read regardless, and users.manage can read and write regardless.
GET /v1/users/{id}/settings
// Response — 200
{
"user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
"locale": "en-US",
"timezone": "America/Denver",
"theme": "dark",
"notifications": { "email": true, "sms": false, "push": true },
"updated_at": "2026-08-11T10:00:00Z"
}
PATCH /v1/users/{id}/settings
// Request
{ "timezone": "Europe/Dublin", "notifications": { "sms": true } }
// Response — 200, the full updated Settings — notifications merged one level deep
{
"user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
"locale": "en-US",
"timezone": "Europe/Dublin",
"theme": "dark",
"notifications": { "email": true, "sms": true, "push": true },
"updated_at": "2026-10-05T10:05:00Z"
}
| Field | Validation |
|---|---|
locale |
BCP 47 tag the API recognizes. |
timezone |
IANA zone name, for example America/Denver. |
theme |
light, dark, or system. |
notifications |
An object of channel name to boolean. Channel names match ^[a-z][a-z0-9_]{0,31}$, with at most 20 channels. The keys are merged one level deep, and a channel sent as null is removed. |
Defaults on user creation: locale: "en-US", timezone: "UTC", theme: "system", notifications: {"email": true}. This is the only place locale is written; the Profile’s locale mirrors it.
A failed validation lists every field at once:
// PATCH { "timezone": "Mountain Time", "theme": "blue", "notifications": { "Push-Alerts": true } }
{
"error": {
"code": "validation_failed",
"message": "3 fields failed validation.",
"details": {
"fields": [
{ "field": "timezone", "code": "unknown_value" },
{ "field": "theme", "code": "enum_mismatch", "allowed": ["light", "dark", "system"] },
{ "field": "notifications.Push-Alerts", "code": "invalid_format", "pattern": "^[a-z][a-z0-9_]{0,31}$" }
]
}
}
}
GET /v1/users/{id}/apps/{appId}/settings
Returns the resolved object, the override layer that produced it, and the source of each resolved value:
// Response — 200
{
"user_id": "usr_01JAG3Z9X8QS3F6K2M4N5P6R7S",
"application_id": "app_timetrack",
"resolved": {
"locale": "en-US",
"timezone": "America/Denver",
"theme": "dark",
"notifications": { "email": true, "sms": false, "push": false },
"default_billable": true,
"week_start": "monday"
},
"sources": {
"locale": "global",
"timezone": "global",
"theme": "global",
"notifications": "app_override",
"default_billable": "app_default",
"week_start": "app_override"
},
"overrides": { "week_start": "monday", "notifications": { "push": false } },
"stale_overrides": [],
"updated_at": "2026-09-20T14:15:00Z"
}
The exact rules are in Domain Model → Settings → Resolution rules. In short:
- The reserved global keys (
locale,timezone,theme,notifications) always appear. Each comes from the per-app override if one is set, and from global Settings otherwise. Fornotifications, an override merges over the global object one channel at a time. - Keys the Application declares in its
settings_schemacome from the override if one is set, and otherwise from the schema property’sdefault. A declared key with neither is omitted fromresolvedrather than returned asnull. stale_overrideslists override keys that no longer validate against the Application’s current schema. This happens when the schema changed after the value was written: the property was removed, or its type changed. Stale values are skipped during resolution, so the default applies instead, and they stay inoverridesuntil the next write to that key replaces or clears them.- If the user has no per-app record yet,
overridesis{}andupdated_atisnull. Nothing is written on a read.
PATCH /v1/users/{id}/apps/{appId}/settings
// Request — set one override
{ "overrides": { "week_start": "monday" } }
// Request — clear an override back to inherited
{ "overrides": { "week_start": null } }
// Response — 200, same shape as the GET above, reflecting the write
Each key in overrides is validated on its own before anything is written:
- A reserved global key must satisfy the same rules as on
PATCH /v1/users/{id}/settings. - Any other key must be declared in the Application’s
settings_schema, and its value must validate against that property’s schema. nullalways means “clear this override” and is never validated.- The serialized
overridesobject is limited to 16 KB after the merge.
A key the schema doesn’t declare, a value of the wrong type, or a reserved key that fails its platform rule returns 422 settings_schema_violation. The error lists every failure, not just the first:
{
"error": {
"code": "settings_schema_violation",
"message": "2 overrides failed validation against app_timetrack's settings_schema.",
"details": {
"fields": [
{ "field": "overrides.week_start", "code": "enum_mismatch", "allowed": ["sunday", "monday"] },
{ "field": "overrides.color", "code": "undeclared_key" }
]
}
}
}
A self-service write or an app-key write produces no Audit Event. An admin writing someone else’s settings produces settings.updated. Send If-Match to avoid two writers clobbering each other. See Conventions → Concurrency.
Errors specific to this resource
| Code | Status | When |
|---|---|---|
settings_schema_violation |
422 | An override fails the Application’s settings_schema. Per-field detail is in details.fields. |
entitlement_required |
403 | Self or app-key access for a user without active access to the Application. |
application_not_found |
404 | {appId} doesn’t resolve, or isn’t the calling app key’s own Application. |
validation_failed |
422 | A global Settings PATCH breaks a field rule; see the example above. (Per-app overrides report through settings_schema_violation instead, even for reserved keys.) |