Users

A user is a person (or service-bound identity) who can sign into the Huudis dashboard and administer your workspace. The Go SDK exposes the user-flavoured operations under client.IAM; every method takes context.Context first and a huudis.RequestAuth last for the per-call bearer override. Group membership, role assignment, and invites have their own pages. For HTTP shapes and the full field table, see API → Users.

End users — the people who sign into your product through Huudis OIDC — are a different surface; see End users.

Field on the Client

client.IAM — type *huudis.IamResource. Installed on *huudis.Client returned by NewClient; shares the underlying *http.Client, issuer, and audience. There is no per-namespace state — methods are safe to call concurrently from many goroutines.

Methods

ListUsers

Signature. func (r *IamResource) ListUsers(ctx context.Context, p ListUsersParams, auth RequestAuth) (*ListPage[IamUser], error)

Cursor-paginated. Default page size 25, max 100. The active workspace is determined by the bearer token; switch with client.Workspaces.Switch(...).

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

page, err := client.IAM.ListUsers(ctx, huudis.ListUsersParams{Limit: 50}, huudis.RequestAuth{})
if err != nil {
    return err
}
for _, u := range page.Items {
    state := "active"
    if u.Disabled {
        state = "disabled"
    }
    fmt.Println(u.ID, u.Email, state)
}

if page.NextCursor != "" {
    next, err := client.IAM.ListUsers(ctx,
        huudis.ListUsersParams{Limit: 50, Cursor: page.NextCursor},
        huudis.RequestAuth{})
    _ = next; _ = err
}

CreateUser

Signature. func (r *IamResource) CreateUser(ctx context.Context, in CreateUserInput, auth RequestAuth) (*IamUser, error)

Directly creates a workspace member, bypassing the email invite flow. Useful when seeding a new workspace from a script. Email is required; Name, Password, and Role are optional. Omitting Password forces the user through a password reset before first sign-in.

u, err := client.IAM.CreateUser(ctx, huudis.CreateUserInput{
    Email: "alice@example.com",
    Name:  "Alice Tan",
    Role:  "admin",
}, huudis.RequestAuth{})
if err != nil {
    return err
}
log.Printf("created %s", u.ID) // "usr_01HX..."

Prefer invites for humans. For real onboarding, use client.IAM.SendInvite(...) — see Invites. CreateUser is the bypass for service principals and migration scripts.

UpdateUser

Signature. func (r *IamResource) UpdateUser(ctx context.Context, id string, patch map[string]any, auth RequestAuth) (*IamUser, error)

PATCH semantics — the patch map carries only the fields you want to change. Common updates: name, disabled. To change role, see Roles — workspace role assignment is a separate endpoint.

_, err := client.IAM.UpdateUser(ctx, "usr_01HX...", map[string]any{
    "name": "Alice T. Wijaya",
}, huudis.RequestAuth{})

// Soft-disable an account.
_, _ = client.IAM.UpdateUser(ctx, "usr_01HX...", map[string]any{"disabled": true}, huudis.RequestAuth{})

DeleteUser

Signature. func (r *IamResource) DeleteUser(ctx context.Context, id string, auth RequestAuth) error

Hard-removes the user from this workspace. Their global Huudis identity stays — workspace membership is the join table. The very last owner of a workspace cannot delete themselves; you'll get *huudis.Error{Code: "state_conflict"} with Message containing cannot_remove_last_owner.

if err := client.IAM.DeleteUser(ctx, "usr_01HX...", huudis.RequestAuth{}); err != nil {
    return err
}

ResetUserPassword

Signature. func (r *IamResource) ResetUserPassword(ctx context.Context, id string, auth RequestAuth) (*PasswordResetResult, error)

Sends a password-reset email. The user receives a tokenised link valid for one hour. Returns &PasswordResetResult{Sent: true} on dispatch.

res, err := client.IAM.ResetUserPassword(ctx, "usr_01HX...", huudis.RequestAuth{})
if err != nil {
    return err
}
if !res.Sent {
    log.Println("mailer returned non-success — check audit log")
}

Types

type IamUser struct {
    ID            string `json:"id"`            // "usr_..."
    AccountID     string `json:"accountId"`     // workspace
    Email         string `json:"email"`
    Name          string `json:"name,omitempty"`
    EmailVerified bool   `json:"emailVerified"`
    MfaEnrolled   bool   `json:"mfaEnrolled"`
    Disabled      bool   `json:"disabled"`
    CreatedAt     string `json:"createdAt"`     // ISO-8601
}

type ListPage[T any] struct {
    Items      []T    `json:"items"`
    NextCursor string `json:"nextCursor,omitempty"`
}

For per-field validation rules see API → Users.

Common patterns

Find a user by email

The list endpoint has no email filter — walk pages and match client-side. For sub-thousand-member workspaces this is fine; for larger orgs, cache the lookup or maintain a side index keyed by your IdP's subject claim.

func userByEmail(ctx context.Context, c *huudis.Client, email string) (*huudis.IamUser, error) {
    var cursor string
    for {
        page, err := c.IAM.ListUsers(ctx,
            huudis.ListUsersParams{Limit: 100, Cursor: cursor},
            huudis.RequestAuth{})
        if err != nil {
            return nil, err
        }
        for i := range page.Items {
            if page.Items[i].Email == email {
                return &page.Items[i], nil
            }
        }
        if page.NextCursor == "" {
            return nil, nil
        }
        cursor = page.NextCursor
    }
}

Bulk-disable on offboarding

func offboard(ctx context.Context, c *huudis.Client, email string) error {
    workspaces, err := c.Workspaces.List(ctx, huudis.RequestAuth{})
    if err != nil {
        return err
    }
    for _, ws := range workspaces {
        if _, err := c.Workspaces.Switch(ctx, ws.ID, huudis.RequestAuth{}); err != nil {
            return err
        }
        u, err := userByEmail(ctx, c, email)
        if err != nil {
            return err
        }
        if u != nil {
            _, _ = c.IAM.UpdateUser(ctx, u.ID, map[string]any{"disabled": true}, huudis.RequestAuth{})
        }
    }
    return nil
}

Sync from your HRIS

type HrisUser struct{ Email, Name, Role string }

func syncFromHris(ctx context.Context, c *huudis.Client, active []HrisUser) error {
    present := map[string]*huudis.IamUser{}
    var cursor string
    for {
        page, err := c.IAM.ListUsers(ctx,
            huudis.ListUsersParams{Limit: 100, Cursor: cursor},
            huudis.RequestAuth{})
        if err != nil {
            return err
        }
        for i := range page.Items {
            present[page.Items[i].Email] = &page.Items[i]
        }
        if page.NextCursor == "" {
            break
        }
        cursor = page.NextCursor
    }
    wanted := map[string]bool{}
    for _, u := range active {
        wanted[u.Email] = true
        if _, ok := present[u.Email]; !ok {
            _, _ = c.IAM.CreateUser(ctx, huudis.CreateUserInput{
                Email: u.Email, Name: u.Name, Role: u.Role,
            }, huudis.RequestAuth{})
        }
    }
    for email, u := range present {
        if !wanted[email] && !u.Disabled {
            _, _ = c.IAM.UpdateUser(ctx, u.ID, map[string]any{"disabled": true}, huudis.RequestAuth{})
        }
    }
    return nil
}

Errors

Code Status Cause
validation_error 400 Bad email format, role doesn't exist, password too weak.
conflict 409 Email already a member of this workspace.
not_found 404 User ID doesn't exist or lives in another workspace.
state_conflict 409 Tried to delete the last owner; tried to disable yourself.
forbidden 403 Caller isn't an owner or admin.

Branch with errors.As:

var he *huudis.Error
if errors.As(err, &he) {
    switch he.Code {
    case "conflict":         // duplicate email
    case "not_found":        // missing user
    case "validation_error":
        log.Printf("bad input: %s", he.Message)
    }
}

Next

  • Invites — the email-driven onboarding flow.
  • Groups — bundle users for policy attachment.
  • Roles — assignable workspace permissions.
  • API → Users — HTTP-level reference.