Skip to main content

Organizations are tenants

Your organization is a tenant of 4D. Your API key is bound to it, your data lives inside it, and isolation is enforced at the database layer with row-level security - not just in application code. There is no cross-tenant read path: a partner API call can only ever see its own organization’s rows.

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 role, their status (active, locked, removed), and when they joined.
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. 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.

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