> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formo.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Update Properties

> Merge-update profile properties for a wallet. Set display name, email, socials, avatar, location, and other identity fields, or send null to delete a property, via a single PUT request.

## Deleting properties

Send `null` as a property value to delete (unset) it. Set and unset compose in a single call, and every key you did not mention is left untouched:

```bash theme={null}
curl -sS -X PUT \
  -H "Authorization: Bearer <your_workspace_api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "display_name": "alice.eth", "email": null }' \
  "https://api.formo.so/v0/profiles/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/properties"
```

A deleted property reads as `null` everywhere (profile reads, the Users table, search, and filters such as `notEmpty`), including over any globally-enriched fallback value, until a new value is set. One exception: a deleted `location` falls back to device geo rather than reading as absent.

<Note>
  `user_id` cannot be unset; it participates in identity stitching, so `{ "user_id": null }` fails validation with [`400 BAD_REQUEST`](/api/errors#bad_request). The literal string `_is_deleted` is a reserved internal value and is rejected wherever a property value is accepted.
</Note>

<Note>
  Wallet-enrichment snapshots are versioned, but first-party property overrides (and their deletions) are always current. A point-in-time read with `timestamp` reflects the latest overrides, so a deletion also masks reads of snapshots taken before it.
</Note>


## OpenAPI

````yaml PUT /v0/profiles/{address}/properties
openapi: 3.1.0
info:
  title: Formo Public API
  description: >-
    REST API for managing Formo projects, analytics, alerts, boards, charts,
    contracts, segments, and AI chat.


    **Auth.** Every endpoint on `api.formo.so` requires a workspace API key with
    the appropriate scopes (see `x-api-scopes`). The one exception is `POST
    /v0/raw_events`, which runs on `events.formo.so` and authenticates with the
    project SDK write key instead.


    **Response shape.** Successful responses return the resource directly (or `{
    data: [...], total, page, size, has_more }` for paginated lists). HTTP
    status carries success/failure; there is no envelope wrapping success
    bodies.


    **Errors.** Non-2xx responses from `api.formo.so` use the `Error` envelope,
    with two exceptions: some rate-limit rejections reply in plain text, and
    `POST /v0/raw_events` returns a plain `{ "error": "..." }` object. Treat the
    HTTP status as authoritative and parse defensively: `{ error: { code,
    message, doc_url, param?, details? } }`. Branch on the machine-readable
    `code` (see `ErrorCode` enum) and follow `doc_url` to the matching section
    of the [errors reference](https://docs.formo.so/api/errors).


    **Idempotency.** Pass an `Idempotency-Key` header on write requests to
    alerts, boards, charts, contracts, segments, and import to make retries
    safe; the response is cached for 24 h and replayed on duplicate keys.
    Profile writes and `POST /v0/query` do not support it.
  version: 0.1.0
  contact:
    name: Formo
    url: https://formo.so
servers:
  - url: https://api.formo.so
    description: API Server (boards, alerts, contracts, segments, profiles, query, import)
  - url: https://events.formo.so
    description: Events Server (event ingestion)
security:
  - WorkspaceApiKey: []
tags:
  - name: Alerts
    description: Manage project alerts and notifications
  - name: Boards
    description: Manage dashboard boards
  - name: Charts
    description: Manage charts within boards
  - name: Contracts
    description: Manage blockchain contract monitoring
  - name: Segments
    description: Manage user segments
  - name: Profiles
    description: Wallet profiles and import
  - name: Query
    description: >-
      Execute SQL queries and call pre-built analytics endpoints (KPIs, top
      pages, lifecycle, retention, revenue). Requires the query:read scope.
  - name: Events
    description: Event ingestion API (events.formo.so)
paths:
  /v0/profiles/{address}/properties:
    put:
      tags:
        - Profiles
      summary: Update user properties
      description: >-
        Set or unset first-party properties for a wallet profile. Override
        display name, email, socials, avatar, location, and other identity
        fields. Send `null` as a value to delete (unset) that property: the
        field then reads as null everywhere, including over any
        globally-enriched fallback value, until a new value is set. `user_id`
        cannot be unset (it participates in identity stitching). Set-and-unset
        compose in a single call. Note: profile enrichment snapshots are
        versioned, but first-party property overrides (and their deletions) are
        always current; a point-in-time read (`timestamp`) reflects the latest
        overrides, so a deletion also masks historical reads.
      operationId: updateUserProperties
      parameters:
        - name: address
          in: path
          required: true
          schema:
            type: string
          description: >-
            Wallet address. Accepts an EVM (0x...) or Solana address, or an ENS
            name (e.g. vitalik.eth) which is resolved to an address.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: >-
                Merge-update of profile properties. Only the listed keys are
                accepted; unknown keys are rejected. At least one key must be
                provided. A `null` value unsets the property (not allowed for
                `user_id`). The literal string `_is_deleted` is a reserved
                internal tombstone and is rejected as a value.
              minProperties: 1
              additionalProperties: false
              properties:
                user_id:
                  type: string
                display_name:
                  type:
                    - string
                    - 'null'
                email:
                  type:
                    - string
                    - 'null'
                farcaster:
                  type:
                    - string
                    - 'null'
                discord:
                  type:
                    - string
                    - 'null'
                twitter:
                  type:
                    - string
                    - 'null'
                telegram:
                  type:
                    - string
                    - 'null'
                instagram:
                  type:
                    - string
                    - 'null'
                website:
                  type:
                    - string
                    - 'null'
                github:
                  type:
                    - string
                    - 'null'
                linkedin:
                  type:
                    - string
                    - 'null'
                facebook:
                  type:
                    - string
                    - 'null'
                tiktok:
                  type:
                    - string
                    - 'null'
                youtube:
                  type:
                    - string
                    - 'null'
                reddit:
                  type:
                    - string
                    - 'null'
                avatar:
                  type:
                    - string
                    - 'null'
                description:
                  type:
                    - string
                    - 'null'
                location:
                  type:
                    - string
                    - 'null'
                ens:
                  type:
                    - string
                    - 'null'
                lens:
                  type:
                    - string
                    - 'null'
                basenames:
                  type:
                    - string
                    - 'null'
                linea:
                  type:
                    - string
                    - 'null'
            examples:
              updateProperties:
                summary: Set display name and email
                value:
                  display_name: alice.eth
                  email: alice@example.com
                  twitter: alice
              unsetProperties:
                summary: Unset email, set a new display name
                value:
                  display_name: alice.eth
                  email: null
      responses:
        '200':
          description: >-
            Properties updated. Returns the merged identity (the keys actually
            set, plus address + updated_at) so callers can cache without a
            follow-up read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateUserPropertiesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  schemas:
    UpdateUserPropertiesResponse:
      type: object
      description: >-
        Echo of the wallet identity after the merge-update applied. Synthesised
        from the request payload; analytics ingestion is eventually consistent,
        so the response reflects the applied write rather than a follow-up read.
      required:
        - address
        - updated_at
      properties:
        address:
          type: string
        updated_at:
          type: string
          format: date-time
        user_id:
          type: string
          nullable: true
        display_name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        farcaster:
          type: string
          nullable: true
        discord:
          type: string
          nullable: true
        twitter:
          type: string
          nullable: true
        telegram:
          type: string
          nullable: true
        instagram:
          type: string
          nullable: true
        website:
          type: string
          nullable: true
        github:
          type: string
          nullable: true
        linkedin:
          type: string
          nullable: true
        facebook:
          type: string
          nullable: true
        tiktok:
          type: string
          nullable: true
        youtube:
          type: string
          nullable: true
        reddit:
          type: string
          nullable: true
        avatar:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        ens:
          type: string
          nullable: true
        lens:
          type: string
          nullable: true
        basenames:
          type: string
          nullable: true
        linea:
          type: string
          nullable: true
    Error:
      type: object
      description: >-
        Standard error envelope returned by every public API endpoint for any
        non-2xx response. The HTTP status code carries success/failure; the body
        provides a machine-readable `code`, a human-readable `message`, and a
        `doc_url` pointing at the matching section of the docs so agents can
        fetch context on the fly.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - doc_url
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: >-
                Human-readable error description. Wording may change between
                releases, so branch on `code`, not `message`.
            doc_url:
              type: string
              format: uri
              description: >-
                Link to the matching section of the errors reference at
                https://docs.formo.so/api/errors.
            param:
              type: string
              description: >-
                When the error pertains to a specific request field, the dotted
                path to that field (e.g. `body.trigger_filters.0.value`).
            details:
              type: object
              additionalProperties: true
              description: >-
                Code-specific extra context. For `INVALID_VALIDATION_REQUEST`
                this is a `{ fieldPath: message }` map of every Zod validation
                failure.
      required:
        - error
    ErrorCode:
      type: string
      description: >-
        Stable, enumerated error codes. New codes may be added in any release;
        clients should treat unknown codes as the closest matching HTTP status
        family.
      enum:
        - INTERNAL_SERVER_ERROR
        - INVALID_VALIDATION_REQUEST
        - UNAUTHORIZED
        - BAD_REQUEST
        - FORBIDDEN
        - NOT_FOUND
        - CONFLICT
        - INVALID_CHAIN_ID
        - CONTEXT_LIMIT_EXCEEDED
        - SERVICE_UNAVAILABLE
        - TOO_MANY_REQUESTS
        - IDEMPOTENCY_IN_PROGRESS
        - INVALID_IDEMPOTENCY_KEY
  responses:
    BadRequest:
      description: >-
        The request was rejected. `code` is either `INVALID_VALIDATION_REQUEST`
        (Zod schema mismatch; `details` carries a `{ fieldPath: message }` map)
        or `BAD_REQUEST` (semantic validation failure outside Zod, e.g.
        mismatched IDs, business-rule violations). Branch on `code`, not status,
        to tell the two apart.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            validation:
              summary: Zod schema mismatch
              value:
                error:
                  code: INVALID_VALIDATION_REQUEST
                  message: Invalid request data
                  doc_url: https://docs.formo.so/api/errors#invalid_validation_request
                  details:
                    body.name: String must contain at least 1 character(s)
            semantic:
              summary: Semantic validation failure
              value:
                error:
                  code: BAD_REQUEST
                  message: Target board must be different from the current board
                  doc_url: https://docs.formo.so/api/errors#bad_request
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Invalid API key
              doc_url: https://docs.formo.so/api/errors#unauthorized
    Forbidden:
      description: The API key is valid but lacks the required scope for this endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: FORBIDDEN
              message: 'API key missing required scope: alerts:write'
              doc_url: https://docs.formo.so/api/errors#forbidden
    NotFound:
      description: >-
        The requested resource does not exist or is not visible to this API
        key's workspace.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Alert not found
              doc_url: https://docs.formo.so/api/errors#not_found
    TooManyRequests:
      description: >-
        Per-workspace rate limit exceeded. Inspect the `RateLimit-*` response
        headers and back off.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: TOO_MANY_REQUESTS
              message: Too many requests
              doc_url: https://docs.formo.so/api/errors#too_many_requests
    InternalServerError:
      description: >-
        An unexpected error occurred on the server. The error has been logged;
        retry with exponential backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL_SERVER_ERROR
              message: Internal Server Error
              doc_url: https://docs.formo.so/api/errors#internal_server_error
  securitySchemes:
    WorkspaceApiKey:
      type: http
      scheme: bearer
      description: >-
        Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard
        under Team Settings > API.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.