# Send member data from your backend

Session and upsert attributes, built-in email and username, CRM custom fields, errors, segments, exports and webhooks.

Reviewed: 2026-10-08
Page: https://harbor.gg/docs/sdk/member-data/

## Overview

# Send member data from your backend

Use @harbor/server with a project key on your backend. Never put the project key in a browser. Both session creation and member upsert create an external-ID member when needed and use the same atomic attribute writer.


## Built-in and custom fields



Settable built-ins are externalId (required, stable account lookup), username (the external identity's display username, up to 255 characters), and email (native EmailIdentity, up to 200 characters). Email is trimmed and lowercased. A host-supplied email is unverified: it does not grant verified email login or complete email-verification quests. The host may change its own unverified email; a different existing linked or verified email is protected. An email belonging to another member in this project returns built_in_conflict. No merging or reassignment occurs. Repeating an identical value is a no-op.

Discord/Steam/social IDs, provider usernames, verification flags, accountId, avatar, displayName and locale cannot be set through these endpoints. They return built_in_not_settable with the offending key. username does not overwrite linked provider names or the member's Harbor username. Built-in properties are optional; omission preserves existing data. Built-in null is rejected with wrong_type. externalId is case-sensitive, at most 512 characters; ambiguous legacy case/accent collisions return identity_conflict.

Phone, plan, region, source and level are project custom fields. Define each first under CRM custom fields in Harbor or with MCP create_custom_field, for example field_key=phone, display_name=Phone, data_type=String. get_custom_fields returns the real ID, key and type. Field keys must match exactly; inactive or unknown fields are rejected. Never send phone as a top-level field. No fields are created implicitly.


## Session creation



Your backend already authenticates the member. Derive externalId from that authenticated user and origin from trusted server configuration:

~~~ts
const session = await harbor.sessions.create({
  externalId: user.id,
  origin: 'https://your-app.example',
  username: 'Ana',
  email: 'ana@example.com',
  customFields: { phone: '+14155550123', plan: 'pro' }
});
~~~

This calls POST /external/sdk/session. origin and optional returnUrl apply only to sessions. The session response contains the SDK bearer and account binding, never email or custom values. Attribute validation finishes before a session is issued.


## Backfills and webhook-driven changes



~~~ts
const member = await harbor.members.upsert({
  externalId: user.id,
  username: 'Ana',
  email: 'ana@example.com',
  customFields: { phone: '+14155550123', plan: 'pro', level: 12 }
});
console.log(member.accountId);
await harbor.members.upsert({ externalId: user.id, customFields: { phone: null } });
~~~

This calls POST /external/sdk/members/upsert. It does not mint a session. The project key authorizes the write. Each request validates all supplied attributes and commits identity, email and custom values together. If validation or a conflict fails, no account, username, email or custom values are written. Retrying identical values is safe. Omitted custom keys remain unchanged; null removes the saved value.


## Types and limits



At most 50 custom keys per request. String accepts a JSON string of at most 1,000 UTF-16 code units. Integer accepts a JSON integer in the signed 32-bit range. Boolean accepts true or false, never strings or 0/1. Date accepts an ISO-8601 timestamp (use an explicit UTC Z or offset); values are stored in UTC. Null clears any custom type. Objects and arrays are not custom values. Duplicate JSON keys are rejected. Limits are checked before writes.


## Errors



Responses use error.code, error.key, error.expectedType (where applicable), error.message and error.requestId. @harbor/server preserves code, key, expectedType and status on HarborServerError.

| Code | Key | Meaning |
| --- | --- | --- |
| unknown_field | custom field key | Define an active field with that exact key first. |
| wrong_type | offending property or custom key | Value must match expectedType. |
| value_too_long | offending property or custom key | Shorten to the documented limit. |
| too_many_fields | customFields | Send at most 50 fields. |
| built_in_conflict | email | Email belongs to another member, or would overwrite a linked email. HTTP 409. |
| built_in_not_settable | top-level property | Move a project-defined attribute into customFields; login identities are protected. |
| invalid_request | offending property | Missing, duplicate, empty required field or invalid email. |
| identity_conflict | externalId | Existing identity conflicts with the exact external ID. |
| plan_limit_exceeded | — | Project cannot create another member. |


## Where the values appear



Native email and external username appear in the member profile. Custom values appear under the existing CRM custom fields, in custom-field segment/filter conditions (including has a value and plan equals pro), and in CRM exports when those custom columns are selected. MCP set_custom_field accepts account_id or exact externalId; get_user_details and search_users include a customFields object. search_users_by_custom_field reads these same typed values.

Account-create, quest-complete, balance-change and resource-sync webhooks add memberAttributes with username, email and customFields. Existing identity targets are retained. These are current values at delivery, not historical event snapshots; updating a field alone does not emit a new webhook. Existing account-create scheduling rules still apply.

Browser SDK inventory, quest and session responses do not include these attributes. This release requires the HostSuppliedEmail migration; harbor.capabilities().migrationsReady checks email provenance as well as SDK storage. Keep the migration and its column in rollback binaries.

