Send member data from your backend
Session and upsert attributes, built-in email and username, CRM custom fields, errors, segments, exports and webhooks.
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 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.