---
title: "Batch update user properties"
method: POST
path: "/v0/profiles/properties"
tags: ["Profiles"]
---

# Batch update user properties

`POST /v0/profiles/properties`

## Request

- Base URL: `https://api.formo.so`
- URL: `https://api.formo.so/v0/profiles/properties`
- Auth: HTTP bearer

Set first-party properties for up to 100 wallets in one request. Each item is a flat object with a required `address` (literal EVM `0x...` or Solana; ENS names are NOT resolved here) plus any of the allowed profile keys. A `null` value unsets (deletes) that property (`user_id` cannot be unset); the reserved tombstone string `_is_deleted` is rejected as a value. Unknown keys are ignored; a row left with no valid keys, or with an invalid address, is quarantined (skipped and reported) rather than failing the batch. A request where every row is invalid returns 400. Requires profiles:write scope.

## Request body

- object[]
  - `address` string, required: Wallet address. Literal EVM (0x...) or Solana address only; ENS names are not resolved in batch requests.
  - `user_id` string
  - `display_name` string, nullable
  - `email` string, nullable
  - `farcaster` string, nullable
  - `discord` string, nullable
  - `twitter` string, nullable
  - `telegram` string, nullable
  - `instagram` string, nullable
  - `website` string, nullable
  - `github` string, nullable
  - `linkedin` string, nullable
  - `facebook` string, nullable
  - `tiktok` string, nullable
  - `youtube` string, nullable
  - `reddit` string, nullable
  - `avatar` string, nullable
  - `description` string, nullable
  - `location` string, nullable
  - `ens` string, nullable
  - `lens` string, nullable
  - `basenames` string, nullable
  - `linea` string, nullable

## Response `200`

Batch processed. Returns counts of forwarded vs quarantined rows, with a per-row `errors` entry for each quarantined row.

- BatchWriteResponse: Acknowledgement for a batch write. `successful_rows` counts rows accepted and forwarded to ingest after a 2xx (ingestion is async/eventually-consistent; there is no follow-up read). `quarantined_rows` counts rows skipped for an invalid address or no valid keys; `errors` (omitted when nothing was quarantined) maps each skipped row back to its request index.
  - `successful_rows` integer, required: Number of rows accepted and forwarded to ingest.
  - `quarantined_rows` integer, required: Number of rows skipped (invalid address, or no valid keys).
  - `errors` object[]: One entry per quarantined row. Omitted when quarantined_rows is 0.
    - `index` integer, required: Zero-based index of the row in the request array.
    - `address` string: The address as submitted, echoed back.
    - `reason` string, required: Why the row was quarantined.

## Other responses

- `400`: 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.
- `401`: Missing or invalid API key.
- `403`: The API key is valid but lacks the required scope for this endpoint.
- `404`: The requested resource does not exist or is not visible to this API key's workspace.
- `429`: Per-workspace rate limit exceeded. Inspect the `RateLimit-*` response headers and back off.
- `500`: An unexpected error occurred on the server. The error has been logged; retry with exponential backoff.

---

[API](https://skmtc.dev/formo/apis/formo-public-api.md) · [All operations](https://skmtc.dev/formo/apis/formo-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/formo/apis/formo-public-api/revisions/67ada57df63c?raw)
