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_idfor them, their status (active,locked,removed), and when they joined.
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_idis 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_bandcomputed fromdob+countryat 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 throughGET /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
- Create a user - the create/attach lifecycle in practice.
- Migrate an existing member base - the same create-or-attach semantics, in bulk.
- Tenants - how organizations and multi-team setups are isolated.