All guides

Send member data from your backend

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

Reviewed Oct 8, 2026View .md

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:

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

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.