---
title: "Update account profile"
method: PATCH
path: "/accounts/{id}"
tags: ["accounts"]
---

# Update account profile

`PATCH /accounts/{id}`

## Request

- Base URL: `https://api.instruxi.dev/api/v1/enforcer`
- URL: `https://api.instruxi.dev/api/v1/enforcer/accounts/{id}`
- Auth: one of:
  - API key in header `Authorization`
  - API key in header `X-API-Key`

## Path parameters

- `id` string, required

## Request body

- EnforcerV3InternalUsecaseAccountsProfileInput
  - `first_name` string
  - `is_discoverable` boolean: IsDiscoverable controls whether this account appears to other members in contact discovery (/contacts/suggestions, /contacts/lookup, /contacts/ resolve). It had no writer anywhere in the API: the column was stamped once at account creation from the tenant's allow_user_discovery and could never be changed, so "discovery is opt-out" was only true on paper. Self-service by design — it is the subject's own privacy setting, and this endpoint is already reachable by the owner (write on your own account is the authz baseline). It cannot be used to become MORE visible than the tenant permits: the tenant-wide allow_user_discovery gate is checked independently on every discovery read, so a false there hides everyone regardless of this flag.
  - `last_name` string
  - `username` string

## Response `200`

OK

- InternalDeliveryHttpHandlerAccountResponse
  - `data` InternalDeliveryHttpHandlerAccountView
    - `active` boolean
    - `authenticated_at` string
    - `created_at` string
    - `email` string
    - `email_verified` boolean
    - `first_name` string
    - `id` string
    - `invited_by` string
    - `is_discoverable` boolean
    - `kind` string: Kind is "human" or "service" (AGENTS_CONTRACT §8). Every row carries one; never empty on a response built from a DB-loaded account.
    - `last_active_at` string
    - `last_login_at` string
    - `last_name` string
    - `person` InternalDeliveryHttpHandlerPersonView
      - `first_name` string
      - `id` string
      - `last_name` string
      - `name` string
      - `phone_verified` boolean
      - `primary_phone` string: Phone lives on the person (persons.primary_phone) — surfaced here so admin lists can show phone + its verification without a second read.
      - `profile_image_url` string
    - `person_id` string: PersonID is a pointer — nil for a service account (AGENTS_CONTRACT §8), which has none. The zero UUID would read as a real person id to any consumer that doesn't know to check for it, exactly the trap account.Account.PersonID itself was made a pointer to avoid.
    - `privy_user_id` string
    - `profile_completed` boolean
    - `role` EnforcerV3InternalDomainRoleRole
      - `admin_managed` boolean
      - `created_at` string
      - `cross_tenant` boolean: Capability grants (additive; all false = baseline-only, like "user").
      - `description` string
      - `id` string
      - `mpp_ops` boolean: MPPOps: read-only MPP deposit-intent operator views (migration 000129).
      - `name` string
      - `slug` string
      - `tenant_manage` boolean
      - `tenant_read` boolean
      - `tenant_write` boolean
      - `updated_at` string
    - `role_id` string
    - `tenant` EnforcerV3InternalDomainTenantTenant
      - `account_abstraction` boolean
      - `allow_user_discovery` boolean: AllowUserDiscovery gates contact discovery for the whole tenant: when true, accounts may be discoverable (and new accounts default to discoverable); when false, discovery is off tenant-wide regardless of account.is_discoverable.
      - `app_url` string
      - `auth_provider` string: AuthProvider / WalletProvider: nil = inherit the instance default.
      - `banner_url` string
      - `billing_status` string: BillingStatus is the tenant's platform-billing dunning state, PROJECTED from enforcer-billing (migration 000087): "" | active | past_due | restricted | suspended | collections. enforcer-billing is the source of truth; this is a copy the billing.status_changed consumer keeps current so the value rides Accessor.billing_status on every resolve with no hop. "" means UNKNOWN and readers MUST treat it as active (fail open). `<-:false` makes GORM never write these two columns: Repository.Update is a whole-row Save, so without it an admin editing the tenant's name from a stale struct would silently roll the projection back. Only BillingStatusStore.ApplyBillingStatus writes them, by targeted UPDATE under the monotonic entered_at guard.
      - `billing_status_entered_at` string
      - `code` string
      - `code_expires_at` string: CodeExpiresAt is when the join code stops working everywhere a code is accepted, signing in and joining alike, as if it had been rotated away. Absent: the code never expires. Set by POST /tenants/{id}/code.
      - `created_at` string
      - `description` string
      - `id` string
      - `logo_url` string
      - `metadata` EnforcerV3InternalDomainTenantJSONBMap
      - `metered_sends_enabled` boolean: MeteredSendsEnabled turns on per-transaction limit metering: every value leg leaving the platform is measured against the caller's live cap before the server co-signs. Meaningful only while Web3Enabled is true — the pair (false, true) is refused on write; see ValidateWeb3Gates.
      - `modules` string[]: Modules is the tenant's MODULE entitlements as group slugs, prefixed `module_` (migration 000094, V3-02). Which HALO-Lego surfaces this tenant has bought — NOT which of them any given account may use, which is the role's question. It lives on the tenant rather than in account_groups because it is a fact about the TENANT: a module per account would mean auto-joining every new account and would make "remove one person from a module the tenant paid for" expressible. The resolver UNIONS this into Accessor.GroupSlugs, so every leaf's RequireModule reads it with no new field and no extra hop. Same `<-:false` guard and the same targeted-write rule as BillingStatus above: Repository.Update is a whole-row Save, so without it an admin editing the tenant's name from a stale struct would silently revoke every module. Only ModuleStore writes these two columns.
      - `modules_updated_at` string
      - `name` string
      - `payment_link_url` string
      - `privy_app_id` string
      - `self_join_policy` 'open' | 'invite_only'
      - `status` 'active' | 'inactive' | 'suspended'
      - `theme_colors` EnforcerV3InternalDomainTenantJSONBMap
      - `thumbnail_url` string
      - `updated_at` string
      - `wallet_provider` string
      - `web3_enabled` boolean: Web3Enabled is the tenant-wide web3 kill switch. False rejects EVERY web3 transaction for the tenant — guarded sends, signing, and RPC writes alike — so it is deliberately a gate rather than an enforcement mode. Defaults true (migration 000066) because false would disable web3 for every existing tenant on deploy.
    - `tenant_id` string
    - `updated_at` string
    - `username` string
    - `verification_status` string: VerificationStatus is the KYC rollup bucket — verified | pending | failed | none — decorated onto list rows, absent on payloads that do not resolve it. NOT the same thing as ProfileCompleted, which is an admin-set provisioner flag and says nothing about identity.
    - `wallet_address` string
  - `success` boolean

## Other responses

- `400`: Bad Request
- `401`: Unauthorized
- `403`: Forbidden
- `409`: Conflict

---

[API](https://skmtc.dev/instruxi/apis/enforcer-api.md) · [All operations](https://skmtc.dev/instruxi/apis/enforcer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/instruxi/apis/enforcer-api/revisions/fa82bc1becc0?raw)
