Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/_partials/user-object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ The `User` object holds all of the information for a single user of your applica

A user can be contacted at their primary email address or primary phone number. They can have more than one registered email address or phone number, but only one of them will be their primary email address (`User.primaryEmailAddress`) or primary phone number (`User.primaryPhoneNumber`). At the same time, a user can also have one or more external accounts by connecting to [social providers](/docs/guides/configure/auth-strategies/social-connections/overview) such as Google, Apple, Facebook, and many more (`User.externalAccounts`).

Finally, a `User` object holds profile data like the user's name, profile picture, and a set of [metadata](/docs/guides/users/extending) that can be used internally to store arbitrary information. The metadata are split into `publicMetadata` and `privateMetadata`. Both types are set from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }}, but public metadata can also be accessed from the [Frontend API](/docs/reference/frontend-api){{ target: '_blank' }}.
Finally, a `User` object holds profile data like the user's name, profile picture, and a set of [metadata](/docs/guides/users/extending) that can be used internally to store arbitrary information. The metadata are split into `publicMetadata` and `privateMetadata`. Both types are set from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }} via [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata), but public metadata can also be accessed from the [Frontend API](/docs/reference/frontend-api){{ target: '_blank' }}.
2 changes: 1 addition & 1 deletion docs/guides/development/add-onboarding-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export const completeOnboarding = async (formData: FormData) => {
const client = await clerkClient()

try {
const res = await client.users.updateUser(userId, {
const res = await client.users.updateUserMetadata(userId, {
publicMetadata: {
onboardingComplete: true,
applicationName: formData.get('applicationName'),
Expand Down
66 changes: 66 additions & 0 deletions docs/guides/development/upgrading/upgrade-guides/2026-05-12.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Upgrade to API version 2026-05-12
description: Learn how to upgrade to version 2026-05-12 of Clerk's API.
---

**Version 2026-05-12** of Clerk's Frontend and Backend APIs removes user metadata fields from the general-purpose user update endpoints. Metadata must now be set through the dedicated metadata endpoints, which provide deep-merge semantics.

To use this new API version, refer to the [versioning guide](/docs/guides/development/upgrading/versioning). This guide documents all updates at the API level. Since implementation details may differ between SDKs, it will help you identify which parts of your SDK usage may require additional review in the documentation, and assist consumers using unofficial or custom API clients in managing the upgrade.

## Backend endpoint changes

`PATCH /v1/users/{user_id}` no longer accepts the following fields in the request body:

- `public_metadata`
- `private_metadata`
- `unsafe_metadata`

To update user metadata, use one of the dedicated endpoints below:

| Endpoint | Semantics |
| - | - |
| `PATCH /v1/users/{user_id}/metadata` | Deep-merges the provided values into the existing metadata. Any key set to `null` is removed. |
| `PUT /v1/users/{user_id}/metadata` | Replaces each provided top-level metadata field in full — no merging at any level. Top-level fields omitted from the request body are left untouched. Send `{}` to clear a field, or `null` to store a JSON `null` value. |

Choose `PATCH` when you want to update specific keys without affecting others, and `PUT` when you want to overwrite an entire metadata field.

If you are using a Clerk SDK, use the [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata) method (which wraps `PATCH`) or the [`replaceUserMetadata()`](/docs/reference/backend/user/replace-user-metadata) method (which wraps `PUT`) instead of passing metadata fields to `updateUser()`.

```ts {{ del: [[1, 5]], ins: [[6, 10], [12, 16]] }}
await clerkClient.users.updateUser(userId, {
publicMetadata: {
role: 'admin',
},
})
await clerkClient.users.updateUserMetadata(userId, {
publicMetadata: {
role: 'admin',
},
})

await clerkClient.users.replaceUserMetadata(userId, {
publicMetadata: {
role: 'admin',
},
})
```

## Frontend endpoint changes

`PATCH /v1/me` no longer accepts the `unsafe_metadata` field in the request body.

To update unsafe metadata from the frontend, use the dedicated endpoint `PATCH /v1/me/metadata` instead. This endpoint deep-merges the provided value with the existing `unsafeMetadata`, and any key set to `null` is removed.

If you are using a Clerk SDK, use the [`updateMetadata()`](/docs/reference/objects/user#update-metadata) method on the `User` object instead of passing `unsafeMetadata` to `update()`.

```ts {{ del: [[1, 6]], ins: [[7, 9]] }}
await user.update({
unsafeMetadata: {
...user.unsafeMetadata,
theme: 'dark',
},
})
await user.updateMetadata({
unsafeMetadata: { theme: 'dark' },
})
```
36 changes: 36 additions & 0 deletions docs/guides/development/upgrading/upgrade-guides/core-3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ The following deprecated APIs have been removed from all Clerk SDKs.
<Accordion
titles={[
"<code>@clerk/types</code> deprecated in favor of <code>@clerk/shared/types</code>",
"Metadata params on <code>user.update()</code> and <code>updateUser()</code> deprecated",
]}
>
<AccordionPanel>
Expand All @@ -559,6 +560,41 @@ The following deprecated APIs have been removed from all Clerk SDKs.

The `@clerk/types` package will continue to re-export types from `@clerk/shared/types` for backward compatibility, but new types will only be added to `@clerk/shared/types`.
</AccordionPanel>

<AccordionPanel>
The `unsafeMetadata` parameter on the frontend `user.update()` method and the `publicMetadata`, `privateMetadata`, and `unsafeMetadata` parameters on the backend `clerkClient.users.updateUser()` method are deprecated. The SDKs continue to accept them for backwards compatibility and route to the dedicated metadata endpoints internally, but they will be removed in a future major version.

Use the dedicated metadata methods instead. On the frontend, [`user.updateMetadata()`](/docs/reference/objects/user#update-metadata) and the backend [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata) method deep-merge with the existing metadata, and any key set to `null` is removed. If you need to fully replace the metadata on the backend, use [`replaceUserMetadata()`](/docs/reference/backend/user/replace-user-metadata).

### Frontend {{ toc: false }}

```ts {{ del: [[1, 6]], ins: [[7, 9]] }}
await user.update({
unsafeMetadata: {
...user.unsafeMetadata,
theme: 'dark',
},
})
await user.updateMetadata({
unsafeMetadata: { theme: 'dark' },
})
```

### Backend {{ toc: false }}

```ts {{ del: [[1, 5]], ins: [[6, 10]] }}
await clerkClient.users.updateUser(userId, {
publicMetadata: {
role: 'admin',
},
})
await clerkClient.users.updateUserMetadata(userId, {
publicMetadata: {
role: 'admin',
},
})
```
</AccordionPanel>
</Accordion>

## Version requirements
Expand Down
29 changes: 29 additions & 0 deletions docs/guides/development/upgrading/versioning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ When making direct API calls to an endpoint, there are two options to specify th

## API versions

### 2026-05-12

Removes user metadata fields (`public_metadata`, `private_metadata`, `unsafe_metadata`) from `PATCH /v1/users/{user_id}` and `unsafe_metadata` from `PATCH /v1/me`. Metadata must now be set through the dedicated metadata endpoints. See the [migration guide](/docs/guides/development/upgrading/upgrade-guides/2026-05-12) for more details.

The following SDKs are compatible with this version:

| SDK | Version |
| - | - |
| Next.js | TBD |
| React | TBD |
| JavaScript | TBD |
| Expo | TBD |
| TanStack React Start | TBD |
| React Router | TBD |
| Express | TBD |
| Astro | TBD |
| C# | TBD |
| Chrome Extension | TBD |
| Fastify | TBD |
| Go | TBD |
| Java | TBD |
| JavaScript Backend | TBD |
| Nuxt.js | TBD |
| PHP | TBD |
| Python | TBD |
| Remix | TBD |
| Ruby | TBD |
| Vue | TBD |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a comment to flag that TBDs need to be fixed before merge. From @brunol95:

On the day of the release - my plan is to update the docs with all the latest version once the sdk PRs are merged/released


### 2025-11-10

Brings more consistency and clarity to the Clerk Billing API endpoints. See the [migration guide](/docs/guides/development/upgrading/upgrade-guides/2025-11-10) for more details.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/how-clerk-works/system-limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ These limits differ based on whether you're using a development or production in
---

- Update a user's data or metadata
- `PATCH /v1/users/{user_id}` and `PATCH /v1/users/{user_id}/metadata`
- `PATCH /v1/users/{user_id}`, `PATCH /v1/users/{user_id}/metadata`, and `PUT /v1/users/{user_id}/metadata`

10 requests per 10 seconds per user
</Properties>
Expand Down
8 changes: 8 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,10 @@
{
"title": "API Version 2025-11-10",
"href": "/docs/guides/development/upgrading/upgrade-guides/2025-11-10"
},
{
"title": "API Version 2026-05-12",
"href": "/docs/guides/development/upgrading/upgrade-guides/2026-05-12"
}
]
]
Expand Down Expand Up @@ -2824,6 +2828,10 @@
"title": "`updateUserMetadata()`",
"href": "/docs/reference/backend/user/update-user-metadata"
},
{
"title": "`replaceUserMetadata()`",
"href": "/docs/reference/backend/user/replace-user-metadata"
},
{
"title": "`deleteUser()`",
"href": "/docs/reference/backend/user/delete-user"
Expand Down
84 changes: 84 additions & 0 deletions docs/reference/backend/user/replace-user-metadata.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: '`replaceUserMetadata()`'
description: Use the replaceUserMetadata() method to fully replace the metadata associated with the specified user.
---

Replaces the metadata associated with the specified user. Unlike [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata), which deep-merges into the existing metadata, this method uses replace semantics: when a metadata field is provided, its previous value is overwritten in full with no merging at any level.

The distinction is at two layers:

- **Top-level field omission preserves the existing value.** Each top-level field (`publicMetadata`, `privateMetadata`, `unsafeMetadata`) is handled independently. If you don't include a field in the request, the stored value for that field is left untouched.
- **The value inside a provided field is replaced in full.** When you do include a field, its previous content is discarded — any nested keys present before but absent in the new value are dropped. There is no merge.

For the provided field, you can also send:

- `{}` (empty object) to clear the field.
- `null` to overwrite the field with a JSON `null` value. Prefer `{}` unless you specifically need a stored `null`.

Returns a [`User`](/docs/reference/backend/types/backend-user) object.

```ts
function replaceUserMetadata(userId: string, params: UserMetadataParams): Promise<User>
```

## `UserMetadataParams`

<Properties>
- `userId`
- `string`

The ID of the user to update.

---

- `publicMetadata?`
- [`UserPublicMetadata`](/docs/reference/types/metadata#user-public-metadata)

Metadata that can be read from the Frontend API and [Backend API](/docs/reference/backend-api){{ target: '_blank' }} and can be set only from the Backend API. When provided, the entire stored `publicMetadata` is overwritten with this value. When omitted, the existing `publicMetadata` is preserved.

---

- `privateMetadata?`
- [`UserPrivateMetadata`](/docs/reference/types/metadata#user-private-metadata)

Metadata that can be read and set only from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }}. When provided, the entire stored `privateMetadata` is overwritten with this value. When omitted, the existing `privateMetadata` is preserved.

---

- `unsafeMetadata?`
- [`UserUnsafeMetadata`](/docs/reference/types/metadata#user-unsafe-metadata)

Metadata that can be read and set from the Frontend API. It's considered unsafe because it can be modified from the frontend. When provided, the entire stored `unsafeMetadata` is overwritten with this value. When omitted, the existing `unsafeMetadata` is preserved.
</Properties>

## Usage

<Include src="_partials/backend/usage" />

```tsx
const userId = 'user_123'

const response = await clerkClient.users.replaceUserMetadata(userId, {
publicMetadata: {
role: 'admin',
},
})
```

## Backend API (BAPI) endpoint

This method in the SDK is a wrapper around the BAPI endpoint `PUT/users/{user_id}/metadata`. See the [BAPI reference](/docs/reference/backend-api/tag/users/put/users/\{user_id}/metadata){{ target: '_blank' }} for more information.

Here's an example of making a request directly to the endpoint using cURL.

<SignedOut>
Replace `YOUR_SECRET_KEY` with your Clerk Secret Key. You can find your Secret Key on the [**API keys**](https://dashboard.clerk.com/~/api-keys) page in the Clerk Dashboard.
</SignedOut>

```bash {{ filename: 'curl.sh' }}
curl -XPUT -H 'Authorization: Bearer {{secret}}' -H "Content-type: application/json" -d '{
"public_metadata": {
"role": "admin"
}
}' 'https://api.clerk.com/v1/users/{user_id}/metadata'
```
14 changes: 12 additions & 2 deletions docs/reference/backend/user/update-user-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ Updates the metadata associated with the specified user by merging existing valu

A "deep" merge will be performed - "deep" means that any nested JSON objects will be merged as well. You can remove metadata keys at any level by setting their value to `null`.

> [!TIP]
> If you want to fully replace the existing metadata instead of merging, use [`replaceUserMetadata()`](/docs/reference/backend/user/replace-user-metadata).

Returns a [`User`](/docs/reference/backend/types/backend-user) object.

```ts
function updateUserMetadata(userId: string, params: UpdateUserMetadataParams): Promise<User>
function updateUserMetadata(userId: string, params: UserMetadataParams): Promise<User>
```

## `UpdateUserMetadataParams`
## `UserMetadataParams`

<Properties>
- `userId`
Expand All @@ -36,6 +39,13 @@ function updateUserMetadata(userId: string, params: UpdateUserMetadataParams): P
- [`UserPrivateMetadata`](/docs/reference/types/metadata#user-private-metadata)

Metadata that can be read and set only from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }}.

---

- `unsafeMetadata?`
- [`UserUnsafeMetadata`](/docs/reference/types/metadata#user-unsafe-metadata)

Metadata that can be read and set from the Frontend API. It's considered unsafe because it can be modified from the frontend.
</Properties>

## Usage
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/backend/user/update-user.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,24 @@ function updateUser(userId: string, params: UpdateUserParams): Promise<User>

---

- `publicMetadata?`
- `publicMetadata?` (deprecated)
- [`UserPublicMetadata`](/docs/reference/types/metadata#user-public-metadata)

Metadata that can be read from the Frontend API and [Backend API](/docs/reference/backend-api){{ target: '_blank' }} and can be set only from the Backend API. Updating this property will override the existing metadata. To merge metadata, use [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata).
Deprecated: use [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata) instead, which deep-merges rather than replacing. This parameter will be removed in a future major API version.

---

- `privateMetadata?`
- `privateMetadata?` (deprecated)
- [`UserPrivateMetadata`](/docs/reference/types/metadata#user-private-metadata)

Metadata that can be read and set only from the [Backend API](/docs/reference/backend-api){{ target: '_blank' }}. Updating this property will override the existing metadata. To merge metadata, use [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata).
Deprecated: use [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata) instead, which deep-merges rather than replacing. This parameter will be removed in a future major API version.

---

- `unsafeMetadata?`
- `unsafeMetadata?` (deprecated)
- [`UserUnsafeMetadata`](/docs/reference/types/metadata#user-unsafe-metadata)

Metadata that can be read and set from the Frontend API. It's considered unsafe because it can be modified from the frontend.
Deprecated: use [`updateUserMetadata()`](/docs/reference/backend/user/update-user-metadata) instead, which deep-merges rather than replacing. This parameter will be removed in a future major API version.

---

Expand Down
17 changes: 17 additions & 0 deletions docs/reference/native-mobile/user.ios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ let updatedUser = try await user.update(.init(
))
```

### Update user metadata

Use `updateMetadata` to update the user's `unsafeMetadata`. The submitted value is deep-merged into the existing metadata; use `JSON.null` for any key whose value should be removed at any nesting level.

```swift
let updatedUser = try await user.updateMetadata(unsafeMetadata: ["theme": "dark"])
```

A params overload is also available:

```swift
let updatedUser = try await user.updateMetadata(.init(unsafeMetadata: ["theme": "dark"]))
```

> [!NOTE]
> Passing `unsafeMetadata` to `user.update(...)` is deprecated. Use `user.updateMetadata(...)` for partial updates going forward.

### Create email address

```swift
Expand Down
Loading
Loading