Skip to main content

Users and members: a two-level identity

4D separates who a person is from their relationship with your organization:
  • A user is a person’s single, global 4D identity, keyed by email (matched case-insensitively). Core profile fields - name, country, date of birth, locale - live here, along with their wallet.
  • A member is that user’s relationship with one organization: your external_id for them, their status (active, locked, removed), and when they joined.
Members are the people your application tracks - they carry no roles or console access. Dashboard access for your own staff (administrators and role-bounded seats like a CMO or CFO) is a separate team relationship, managed in the console’s Team section; the same person can hold both. See Team. One person engaging with three 4D organizations is one user with three memberships. When you POST /v1/users with an email that already has a 4D account, the platform doesn’t create a duplicate - it attaches the existing user to your organization as a new member. Your view of them is your membership plus their profile; other organizations’ relationships with the same user are invisible to you. How that isolation works is covered in Tenants. Two consequences worth designing around:
  • external_id is yours. It’s unique per organization, so you can address members by the id your own systems already use.
  • Age is derived, never stored. The API returns a coarse age_band computed from dob + country at read time; the raw date of birth is redacted from partner reads. A member with unknown age is treated as a minor.

Wallets

Every user gets an EVM wallet, provisioned automatically in the background when they’re created (wallet.status moves from provisioning to active). It’s global to the user - one wallet across all their memberships - and it’s where XP settles on-chain. You never manage keys; wallets are platform-custodied.

Attributes: your org’s custom fields

Beyond the fixed core profile, each organization enables its own typed attribute registry - text, number, date, boolean, single_choice, multi_choice - and writes per-member values against it (PUT /v1/users/{userId}/attributes). Choice-typed attributes validate against your configured choice list. Every value carries provenance, so you can always tell whether a value came from the API, an admin, the member, or a bulk import.

Job title

Job title is an admin-managed descriptive attribute for organization members. It does not change access. Org keys and administrators can read its stored value through GET /v1/users/{userId}/attributes when the organization has enabled it. See Get user details.

Finding a member by name

GET /v1/users?q=<name> finds members of your organization when you do not have their id yet, for a check-in desk or a “find your name” step in your app. Every word of q (2 to 100 characters, at most five words) must appear in the member’s display name, first name or last name, in any order, ignoring case and accents (zoe finds Zoë); punctuation is compared as stored. It answers { members, has_more }, at most limit rows (1-50, default 20) ordered by display name, each with the user id, the names and the membership’s id, external id, status and join date. Removed memberships are left out and locked ones are listed with their status. There is no cursor: when has_more is true, narrow the query. The member’s email is not part of the response. Your organization key and tenant administrators can call it. The row cap bounds one response, not what a key can learn: repeated searches can list your whole member directory. That is intended, since the same key can already read, edit, lock and remove any member it holds an id for, so keep it on your server like every other organization key. When your own sign-in already knows the fan’s email, pass email=<address> instead of q: an exact match, ignoring case, that answers the same row shape with at most one member (an email belongs to one user). Send one of q or email, never both.

What you recorded about a member

Every event your site or app posts to the tracking rail is kept against the member it belongs to. GET /v1/users/{userId}/engagement/events reads one member’s events back, newest first, with limit (1-100, default 20) and an opaque next_cursor for paging. Each event carries its action, tags, your own metadata and the time it happened, so you can show a member what you observed rather than only what it earned them. Your organization key reads it for any member you can already read through GET /v1/users/{userId}; a user outside your organization answers 404 the same way. The member’s email is not part of the response.

Labels: lightweight admin grouping

Labels are free-form strings admins put on members (“u18-squad”, “season-ticket”) - normalized (trimmed, lowercased), capped at 100 per member. They’re deliberately not entities: no schema to manage, just consistent strings to filter and segment by in the console.

Where to go next