> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sublay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetch or Create User by Foreign ID

> Return the user with your external ID, or create them with a profile (service key only)

Returns the user with this `foreignId` — the external identifier from your own system — or creates them when there is none. **Service key only**: this endpoint creates users, so only your backend may call it. To read a user without creating one, use [Fetch User by Foreign ID](https://docs.sublay.io/v8/api-reference/users/fetch-user-by-foreign-id.md) (`GET`).

Everything goes in the JSON request body, so the profile, `secureMetadata` included, never appears in a URL. The whole request body can be at most 100 KB, `metadata` and `secureMetadata` included.

* **Existing user:** `200` with the user, unchanged. The profile fields are used only to create a user; they never update one.
* **No user:** the user is created with the body's profile, `201`.
* **Two concurrent calls for the same new `foreignId`:** one user is created. The call that loses the race answers `200` with that user.

> [!NOTE]
>
> If the user exists and has blocked the viewer — the user named in `actingUserId` — this returns `404`, the same response as the read. Nothing is created. Requires the `moderation` bundle.

## Body Parameters

**foreignId** `string` *required*

The external identifier from your system. It is stored and matched exactly as sent. A blank or whitespace-only value (`""`, `"   "`) returns `400 user/invalid-body` (`foreignId: Foreign ID is required`).

**name** `string`

Display name. Used only when the user is created.

**username** `string`

Username, unique within the project. Used only when the user is created. The same rules as on sign-up and [Update User](https://docs.sublay.io/v8/api-reference/users/update-user.md): 3 to 30 characters after trimming, only letters, numbers, underscores (`_`) and hyphens (`-`). Anything else returns `400 user/invalid-body`. A username another user holds returns `409`.

**avatar** `string | null`

Avatar URL, or `null` for none. Used only when the user is created.

**bio** `string`

Bio text. Used only when the user is created.

**metadata** `object`

Public custom data, a JSON object. Used only when the user is created. A JSON string is refused (`400 user/invalid-body`).

**secureMetadata** `object`

Private custom data, a JSON object. Never returned by the API. Used only when the user is created.

**include** `string | string[]`

Associations to include, as a comma-separated string or an array. The only value is:

* `files` — the processed `avatarFile` and `bannerFile` objects

Matching ignores case. An unknown value returns `400 user/invalid-body`.

**actingUserId** `string`

The user the response is read as: a user who has blocked them comes back as `404`, as on the read. It never changes who creates: a user this request creates is always created by your app. Leave it out for the anonymous view.

### Space-scoped reputation

This endpoint has no space in context, so the opt-in `spaceReputation` object accepts a `spaceId` of a space **`<uuid>`** or **`none`** only — `context` is rejected (400). It adds a `spaceReputation` field to the returned user, alongside the always-present `reputation` total. Send it in the body: `spaceReputationId` as a string, `spaceReputationDescendants` as a boolean (`"true"` and `"false"` are accepted too). Requires the `reputation` bundle. See the [Reputation data model](https://docs.sublay.io/v8/data-models/reputation.md).

**spaceReputation** `object`

Opt-in. Adds a `spaceReputation` number to each returned user,
scoped to a single space. Requires the `reputation` bundle.
The `reputation` total is always present regardless;
`spaceReputation` is purely additive and only appears when you
pass this object. **This object is the primary form** — it
replaces the deprecated flat props below. See the
[Reputation data model](https://docs.sublay.io/v8/data-models/reputation.md) for the
full contract.

**spaceReputation**

Which bucket to read. A space **`<uuid>`** reads that
space's bucket; **`"none"`** reads the
project-general (space-less) bucket.
**`"context"` is rejected (400)** on this
endpoint — there is no space context to derive. The empty string
and the legacy `general` / `null` aliases are
also rejected. Missing buckets read as `0`.
Only honored when `spaceId` is an explicit space `<uuid>`.
When `true`, `spaceReputation` is the
**subtree sum** — the named space plus all of its
descendant spaces (the root space's own bucket included). Ignored for
`"none"`; not allowed with `"context"`.

**spaceReputationId** `string` *deprecated*

**Deprecated** — use the `spaceReputation`
object instead. The flat form of `spaceReputation.spaceId`.
Still accepted (the object wins if both are passed); removal is a later
major version. Accepts a space `<uuid>` or `"none"` only —
`"context"` is rejected (400) on this endpoint.

**spaceReputationDescendants** `boolean` *deprecated*

**Deprecated** — use
`spaceReputation.includeDescendants` instead. The flat form,
only honored alongside an explicit `spaceReputationId` `<uuid>`.

> [!NOTE]
>
> Every field goes in the body. Any of them sent in the query string returns `400 user/wrong-location`.

## Response

Returns HTTP `200` (an existing user) or `201` (a user this request created), with the same user object as the read:

**id** `string`

Unique user ID (UUID).

**foreignId** `string | null`

The foreign ID you provided.

**projectId** `string`

Project this user belongs to.

**name** `string | null`

Display name.

**username** `string | null`

Unique username within the project.

**avatar** `string | null`

Avatar image URL.

**bio** `string | null`

Short bio text.

**metadata** `object | null`

Public custom key-value data.

**reputation** `number`

Reputation score.

**avatarFile** `object | null`

Processed avatar image with variants. Only present when `include=files` is
requested.

**bannerFile** `object | null`

Processed banner image with variants. Only present when `include=files` is
requested.

**createdAt** `string`

ISO timestamp of account creation.

> [!NOTE]
>
> **Monthly active users:** a request naming `actingUserId` counts that user, as on the read. Creating a user counts no one.

## Error Responses

> **Service Key Required — 403**
>
> A user token, even one naming an `actingUserId`, or a request with no credentials. Nothing is created.
>
> ```json theme={null}
> {
>   "error": "Service key required: `POST /users/by-foreign-id` creates users, so only your backend may call it. To read a user by foreign id, use `GET /users/by-foreign-id`.",
>   "code": "auth/elevated-auth-required"
> }
> ```

> **Invalid Body — 400**
>
> Returned for an invalid body: a missing or blank `foreignId`, a `username` that breaks the rules above, a `metadata` or `secureMetadata` that isn't an object, or an unknown `include` value. The `error` names the field, for example:
>
> * `foreignId: Foreign ID is required`
> * `username: Username must be at least 3 characters`
> * `username: Username cannot exceed 30 characters`
> * `username: Username can only contain letters, numbers, underscores, and hyphens`
>
> ```json theme={null}
> {
>   "error": "username: Username must be at least 3 characters",
>   "code": "user/invalid-body"
> }
> ```

> **Wrong Location — 400**
>
> A body field was sent in the query string (`foreignId`, `name`, `include`, `spaceReputationId`, `actingUserId`, …).
>
> ```json theme={null}
> {
>   "error": "`foreignId` goes in the request body on this endpoint.",
>   "code": "user/wrong-location",
>   "param": "foreignId"
> }
> ```

> **User Not Found — 404**
>
> The user exists and has blocked the `actingUserId` viewer. Nothing is created.
>
> ```json theme={null}
> {
>   "error": "User not found",
>   "code": "user/not-found"
> }
> ```

> **Acting User Not Found — 404**
>
> `actingUserId` names a user that isn't in this project.
>
> ```json theme={null}
> { "error": "No user with this actingUserId exists in this project.", "code": "user/user-not-found" }
> ```

> **Duplicate Field — 409**
>
> No user has this `foreignId`, and another user already holds the `username` (or another unique field) in this project. Nothing is created.
>
> ```json theme={null}
> {
>   "error": "A user with this username already exists in this project",
>   "field": "username",
>   "code": "DUPLICATE_USER_FIELD"
> }
> ```

## See Also

* [Fetch User by Foreign ID](https://docs.sublay.io/v8/api-reference/users/fetch-user-by-foreign-id.md)
* [`fetchOrCreateUserByForeignId` (Node SDK)](https://docs.sublay.io/v8/node-sdk/users), which returns `{ user, created }`, with `created` true on `201`
* [User data model](https://docs.sublay.io/v8/data-models/user.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.