Skip to content

Checking access...

User-Service API Reference ​

Canister: user-service Candid Interface: user_service.didReconciled against the live .did: 2026-08-25 (bl-1404 — merged the former user-service-api.md comprehensive guide and user-service.md Candid quick-ref into this single page; method names/signatures below are arbitrated against src/user_service.did on main)

Router note: the production deployment fronts user-service with the separate user-service-router canister (sharding). Its routing surface is documented at user-service-router. The methods below are the user-service canister's own interface and work identically in standalone and routed modes.


Overview ​

The user-service canister provides user management, authentication, address management, self-custody verification, KYC/duplicate-prevention support, GDPR data-deletion, and COPPA parental-consent functionality for the Hello World DAO ecosystem.

Key Features ​

  • Individual (user) registration and email verification
  • Multi-method authentication (Email/Password, Internet Identity, OIDC, OAuth attestation)
  • Address management with primary-address support
  • Session token issue + refresh
  • Self-custody wallet verification (Epic 2.2.3)
  • GDPR data deletion (Story 2.0.5) and COPPA parental consent (BL-012.3)
  • Duplicate-account prevention via ID-hash lifecycle (Story 2.0.7)
  • Controller-only administrative + backup functions

Account creation is off-chain. See create_account — REMOVED below. Registration runs through identity-service POST /register + the @hello-world-co-op/auth client per ADR-020; user-service does not create accounts.


Authentication & Access Levels ​

Methods authenticate the caller via ic_cdk::caller(). Some settings-write methods accept an opt principal "caller_principal" argument used only for oracle-bridge-forwarded calls (AUTH-007.1); direct Internet Identity calls pass null.

  • Public: any caller
  • Controller: caller must be in the canister CONTROLLERS list
  • Bridge/admin-gated: guarded by the oracle-bridge service principal (require_oracle_bridge) or is_admin (e.g. confirm_payment, admin_review_kyc)

Core Data Types ​

IndividualRecord ​

⚠️ The live record stores PII encrypted (*_encrypted : blob), not as plaintext text. Earlier editions of this page (and the old quick-ref) showed a plaintext email : text shape that no longer matches the deployed WASM. Authoritative shape (user_service.did):

candid
type IndividualRecord = record {
  id : text;

  // Encrypted PII (Epic 7)
  email_encrypted : blob;
  first_name_encrypted : blob;
  last_name_encrypted : blob;
  email_hash : text;                 // SHA-256 hex secondary index
  encryption_key_id : text;
  encryption_type : EncryptionType;

  // Recovery-key envelope (Epic 2.5)
  encrypted_recovery_key : blob;
  password_salt : blob;

  verification_token : text;
  verification_code : text;
  verified : bool;
  submitted_at : nat64;
  verified_at : opt nat64;
  code_expires_at : nat64;
  ip_hash : opt text;

  ii_principal : opt principal;
  auth_methods : vec AuthMethod;
  preferred_auth_method : opt AuthMethodType;

  // Membership-mint prerequisites (Epic 2.1.6)
  membership_token_id : opt nat64;
  kyc_verified : bool;
  payment_received : bool;
  tos_accepted_at : opt nat64;

  // Self-custody (Epic 2.2.3)
  self_custody_verified : bool;
  self_custody_verified_at : opt nat64;
  self_custody_expires_at : opt nat64;

  // Preferences / profile / notifications (Epic FOS-5.1)
  preferences : opt UserPreferences;
  profile : opt Profile;
  notification_preferences : opt NotificationPreferences;

  // COPPA (BL-011.2 / BL-012.3)
  dob_encrypted : blob;
  is_age_verified_18_plus : bool;
  parental_consent_status : opt ParentalConsentStatus;
  parent_email_hash : opt text;
  consent_token : opt text;
  consent_requested_at : opt nat64;
  consent_received_at : opt nat64;
};

Admin-safe view. IndividualAdminView (returned by the *_admin methods) excludes verification_token, verification_code, password_salt, encrypted_recovery_key, ip_hash, and dob_encrypted (FOS-5.6.10 — sensitive data never returned in API responses).

AuthMethod & Credential ​

candid
type AuthMethod = record {
  method_type : AuthMethodType;
  identifier : text;
  credential : opt Credential;
  verified : bool;
  linked_at : nat64;
  last_used : opt nat64;
};

type Credential = variant {
  PasswordHash : blob;
  OidcToken : OidcTokenData;
  Principal : principal;
};

type AuthMethodType = variant {
  EmailPassword; InternetIdentity; Google; Apple; Microsoft; GitHub; Discord;
};

The AuthMethodType variant enumerates every provider slot the type reserves; the shipped OAuth providers are Google and GitHub (others are reserved — see the FounderyOS OAuth hold).

AddressRequest / Address ​

candid
type AddressType = variant { Home; Work; Billing; Shipping; Other : text };

type AddressRequest = record {
  address_type : AddressType;
  country : text;                    // required
  state : opt text;
  city : text;                       // required
  postal_code : text;                // required
  street_address : opt text;
  street_address2 : opt text;
  is_primary : bool;
};

type Address = record {
  id : text;                         // addr_{timestamp}{random}
  individual_id : text;
  address_type : AddressType;
  country : text; state : opt text; city : text; postal_code : text;
  street_address : opt text; street_address2 : opt text;
  is_primary : bool;
  created_at : nat64; updated_at : nat64;
};

IndividualRequest / result types ​

candid
type EncryptionType = variant { UserDerived; Temporary };

type IndividualRequest = record {
  email_encrypted : text;            // base64
  first_name_encrypted : text;
  last_name_encrypted : text;
  email_hash : text;                 // SHA-256 hex
  encryption_key_id : text;
  encryption_type : EncryptionType;
  email_plaintext_for_verification : text;   // used only to send the code, not stored
};

type IndividualResult = record { success : bool; message : text; id : opt text };
type VerifyResult    = record { success : bool; message : text };
type Stats           = record { total_individuals : nat64; verified_individuals : nat64; pending_verifications : nat64 };

Registration & Verification ​

submit_individual ​

Register a new individual with an optional initial address (legacy / interest-form path).

candid
submit_individual : (IndividualRequest, opt AddressRequest) -> (IndividualResult);

Access: Public. Returns IndividualResult (id = ind_{timestamp}{random} on success). Errors: "Email already registered".

javascript
const result = await userService.submit_individual(
  {
    email_encrypted: btoa("user@example.com"),
    first_name_encrypted: btoa("John"),
    last_name_encrypted: btoa("Doe"),
    email_hash: sha256("user@example.com"),
    encryption_key_id: "key_001",
    encryption_type: { Temporary: null },
    email_plaintext_for_verification: "user@example.com"
  },
  [] // no initial address; or [{ address_type:{Home:null}, country:"US", city:"…", postal_code:"…", is_primary:true, state:[], street_address:[], street_address2:[] }]
);

register_email_password / register_email_password_verified ​

candid
register_email_password          : (RegisterEmailPasswordRequest)         -> (variant { Ok : AuthResponse; Err : text });
register_email_password_verified : (RegisterEmailPasswordVerifiedRequest) -> (variant { Ok : AuthResponse; Err : text });

register_email_password_verified (arch-007-3a-4 / ADR-014) is the off-chain-derivation variant: it accepts a pre-derived age_category : AgeCategory (Adult | Minor13To17 | Under13) instead of a raw DOB — the raw DOB never crosses the wire. Controller-gated. Under13 is rejected on apply (fail-closed COPPA).

verify_email ​

Verify email using the token from an email link.

candid
verify_email : (text) -> (VerifyResult);

verify_code ​

Verify email using the emailed code. Four parameters — (email, code, first_name, last_name) — the trailing names trigger the post-verification database sync (Story 2-5-2).

candid
verify_code : (text, text, text, text) -> (VerifyResult);

⚠️ Prior editions of this page documented a two-parameter verify_code : (text, text). That signature is stale; the live canister takes four parameters.

resend_verification_code ​

candid
resend_verification_code : (text) -> (VerifyResult);   // (email)

Authentication ​

authenticate_with_password ​

Authenticate with email + password. Returns an AuthResponse carrying access/refresh tokens and the recovery-key envelope + encrypted PII for client-side decryption (Epic 2.5).

candid
authenticate_with_password : (AuthRequest, text, opt text, opt text, opt text) -> (AuthResponse);
// (AuthRequest{email,password}, device_fingerprint, ip_address?, timezone?, user_agent?)

⚠️ Prior editions marked this method "commented out / pending auth_api update". It is live in the deployed canister.

authenticate_with_internet_identity ​

candid
authenticate_with_internet_identity : (text, opt text, opt text, opt text) -> (AuthResponse);
// (device_fingerprint, ip_address?, timezone?, user_agent?) — uses ic_cdk::caller() for the II principal

authenticate_with_oidc / authenticate_with_oauth ​

candid
authenticate_with_oidc  : (OidcAuthRequest, opt text)            -> (AuthResponse);
authenticate_with_oauth : (OAuthAttestationRequest, opt text)    -> (AuthResponse);

refresh_tokens ​

candid
refresh_tokens : (text, text, opt text, opt text, opt text) -> (RefreshTokenResponse);
// (refresh_token, device_fingerprint, ip_address?, timezone?, user_agent?)

AuthResponse.session_id is deprecated (kept for backward compatibility); use access_token / refresh_token.


Password Management ​

candid
set_password                   : (text, SetPasswordRequest)              -> (variant { Ok : null; Err : text });
initiate_password_reset        : (text)                                  -> (variant { Ok : text; Err : text });  // (email) — 15-min code
complete_password_reset        : (text, text, text, text, text)          -> (variant { Ok : text; Err : text });
complete_password_reset_simple : (text, text, text, text, text)          -> (variant { Ok : text; Err : text });

complete_password_reset* params: (email, reset_code, new_password, encrypted_recovery_key_base64, password_salt_base64). The _simple variant clears encrypted PII — the user must re-enter profile info after login.

candid
type SetPasswordRequest = record {
  current_password : opt text;
  new_password : text;
  encrypted_recovery_key : opt blob;   // re-encrypted with the NEW password-derived key (Epic 2.5)
  password_salt : opt blob;
};

Off-chain password support (controller-only, called by oracle-bridge): get_user_id_by_email_hash, get_user_credentials_by_email_hash, get_password_hash_by_email_hash, update_password, update_password_hash.


Individual & Address Management ​

candid
get_current_user             : ()                    -> (opt IndividualRecord) query;   // by ic_cdk::caller() → ii_principal
get_user_by_principal        : (principal)           -> (opt IndividualRecord) query;
get_member_dob_by_principal  : (principal)           -> (variant { Ok : opt text; Err : text }) query;  // controller-only (ADR-015)
delete_individual            : (text)                -> (variant { Ok : text; Err : text });
sever_individual             : (text, SeveranceMode) -> (variant { Ok : SeveranceReport; Err : text }); // COPPA/GDPR erasure (bl-1102)

add_address                  : (text, AddressRequest) -> (variant { Ok : text; Err : text });   // (individual_id, …) → address_id
update_address               : (text, AddressRequest) -> (variant { Ok : null; Err : text });   // (address_id, …)
delete_address               : (text)                 -> (variant { Ok : null; Err : text });
set_primary_address          : (text, text)           -> (variant { Ok : null; Err : text });   // (individual_id, address_id)
get_addresses_for_individual : (text)                 -> (vec Address) query;
get_primary_address          : (text)                 -> (opt Address) query;

Address validation: country, city, postal_code required; setting is_primary: true unsets primary on the individual's other addresses. sever_individual's SeveranceReport deliberately carries no identifier of the severed subject — field names only.


Internet Identity Linking ​

candid
link_ii_principal       : (text, principal) -> (variant { Ok : null; Err : text });          // (user_id, ii_principal)
create_user_ii          : (principal)       -> (variant { Ok : text; Err : text });          // II-first minimal record → UUID (idempotent)
update_user_ii_profile  : (record { user_id; email_encrypted; email_hash; display_name; dob_encrypted; is_age_verified_18_plus; parent_email_hash; requires_parental_consent }) -> (variant { Ok : null; Err : text });  // controller-only
get_linked_ii_principal : (text)            -> (variant { Ok : opt principal; Err : text }) query;  // (user_id)

Removed methods. unlink_ii_principal, relink_ii_to_account, get_ii_link_status, and get_user_by_ii_principal were removed (uid-001-13d/13e — caller-free after the identity-gateway callsites were dropped). Earlier editions of the quick-ref documented unlink_ii_principal and get_user_by_ii_principal; do not call them — they no longer exist. The II link/relink/unlink flow now targets the identity-gateway canister.


Self-Custody Verification (Epic 2.2.3) ​

candid
update_self_custody_status : (text, bool, nat64) -> (variant { Ok : null; Err : text });          // (user_id, verified, timestamp)
check_self_custody_status  : (text)              -> (variant { Ok : SelfCustodyStatus; Err : text }) query;      // Verified | Expired | NeverVerified
get_self_custody_status    : (text)              -> (variant { Ok : SelfCustodyStatusInfo; Err : text }) query;  // detailed, for governance

GDPR Data Deletion (Story 2.0.5) ​

candid
request_kyc_data_deletion   : () -> (variant { Ok : null; Err : text });                       // 30-day grace period
cancel_kyc_data_deletion    : () -> (variant { Ok : null; Err : text });
get_deletion_request_status : () -> (variant { Ok : opt DeletionRequest; Err : text }) query;

Duplicate-Account Prevention (Story 2.0.7) ​

candid
claim_temporary_id_hash : (text, text, text) -> (variant { Ok : null; Err : text });   // (id_type, country, id_number) — 24h claim
check_duplicate_id_hash : (text, text, text) -> (variant { Ok : bool; Err : text });    // user-safe, prevents enumeration
promote_to_permanent_hash : (principal)      -> (variant { Ok : null; Err : text });    // after KYC Verified (idempotent)

Admin (controller-only) override path: propose_admin_override (2-of-3 multi-sig, proposer auto-approves), approve_admin_override (auto-executes at threshold), list_pending_overrides, get_override_request, get_id_hash_status, get_hash_lifecycle, verify_pepper_integrity.


Membership Minting (Epic 2.1.6) ​

candid
check_and_mint_membership    : (text) -> (variant { Ok : nat64; Err : text });   // (user_id) → ICRC-7 token ID
upgrade_membership_to_active : (text) -> (variant { Ok : nat64; Err : text });
retry_failed_mints           : ()     -> (variant { Ok : nat32; Err : text });   // controller-only
get_mint_retry_queue         : ()     -> (vec MintRetryEntry) query;             // controller-only

⚠️ bl-1093 caveat (do not treat as the live mint path). This mint block has no reachable in-fleet caller and carries a known reply-decode defect against membership's variant { Ok : nat; Err : text }. The live registered-SBT mint bypasses user-service entirely (oracle-bridge routes/auth.ts → membership directly). These methods ship with the canister per the ADR-020 pre-decommission checklist; upgrade_membership_to_active additionally errors by construction (short-payload argument mismatch). Documented for completeness, not as the current mint entry point.

create_account — REMOVED ​

⚠️ This method no longer exists. create_account was removed from user-service (user-service#77). Account creation is handled off-chain by identity-service POST /register + the @hello-world-co-op/auth client, per ADR-020. user-service does not create accounts, and the former CreateAccountRequest type documented in the old quick-ref is orphaned — do not use it. See the Registration Flow guide for the current path.


Statistics & Admin ​

candid
get_stats                        : ()             -> (Stats) query;
list_individuals                 : ()             -> (variant { Ok : vec IndividualRecord; Err : text }) query;        // controller-only
list_individuals_paginated       : (nat64, nat64) -> (variant { Ok : PaginatedResult; Err : text }) query;            // controller-only
list_individuals_paginated_admin : (nat64, nat64) -> (variant { Ok : PaginatedAdminResult; Err : text }) query;       // admin-safe view
get_user_by_principal_admin      : (principal)    -> (opt IndividualAdminView) query;
get_user_by_id_admin             : (text)         -> (opt IndividualAdminView) query;
get_display_name                 : (text)         -> (opt text) query;                                                // controller-only (BL-028.2)

Preferences / profile / notifications (Epic FOS-5.1) — self methods take an opt principal for bridge-forwarded writes: get_preferences, update_preferences, register_username, get_profile_by_username, update_profile, get_notification_preferences, update_notification_preferences; controller-only per-user variants: get_notification_preferences_for_user, update_notification_preferences_for_user, update_preferences_for_user, register_username_for_user, update_profile_for_user.

KYC / payment / consent: confirm_payment (oracle-bridge-gated; drives the membership mint relay — unauthenticated-reachable is prevented by require_oracle_bridge), kyc_webhook (mock-KYC sink), admin_review_kyc (is_admin), approve_parental_consent, admin_verify_email, send_parental_consent_email (BL-012.3).

Audit / config / backup: get_all_audit_entries (deprecated — use get_audit_entries_paginated), get_audit_entries_paginated, set_oracle_bridge_principal / get_oracle_bridge_principal, set_principals_canister / clear_principals_canister / get_principals_canister (arch-007-3a defense-in-depth gate + kill switch), set_membership_canister, set_dao_admin_canister / get_dao_admin_canister, set_oracle_bridge_url / get_oracle_bridge_url, set_oracle_public_key, initialize_signing_key / reset_signing_key / get_signing_public_key, export_state / import_state (controller-only; state contains GDPR-protected PII — encrypt backups at rest).

Dev/test-only (absent from --features prod builds): get_verification_code_debug, set_test_pepper, clear_all_individuals.

Completeness note. The live user_service.did exposes ~100 methods. The sections above document the member- and integration-facing surface in detail and index the remaining administrative/internal methods by name and signature. The .did is the authoritative, exhaustive source — enumerate #[update]/#[query] in the Rust sources (or read user_service.did on main) for anything not shown here; the hand-maintained .did header itself warns against treating it as a complete census.


Error Handling ​

All fallible methods return variant { Ok : T; Err : text }. Common errors:

ErrorCauseResolution
Email already registeredDuplicate email_hashUse a different email / password reset
Invalid verification codeWrong or expired codeRequest a new code
Individual not foundInvalid individual_idVerify the ID exists
Address not foundInvalid address_idVerify the address ID
Address does not belong to this individualOwnership mismatchPass the owning individual_id
UnauthorizedCaller not a controller / not the gated principalCheck the caller principal
Duplicate ID detectedKYC ID hash already registeredContact support

Frontend Integration ​

javascript
import { Actor, HttpAgent } from '@dfinity/agent';
import { idlFactory } from './declarations/user-service';

const agent = new HttpAgent({ host: 'https://ic0.app' });
const userService = Actor.createActor(idlFactory, { agent, canisterId: 'your-canister-id' });

const stats = await userService.get_stats();

Authentication is handled out-of-band by oracle-bridge sessions (cookie-based); inter-canister auth calls are no longer used (auth-service decommissioned 2026-04-11).


Hello World DAO