---
title: "Set Attribute"
method: PUT
path: "/v1/contacts/{contact_id}/attributes/{key}"
tags: ["contacts"]
---

# Set Attribute

`PUT /v1/contacts/{contact_id}/attributes/{key}`

## Request

- Base URL: `https://api.elaan.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.elaan.io/v1/contacts/{contact_id}/attributes/{key}`
- Auth: none declared

## Path parameters

- `key` string, required
- `contact_id` string, required

## Headers

- `authorization` string, nullable

## Request body

- AttributeValueRequest
  - `value` string, required

## Response `200`

Successful Response

- ContactResponse
  - `id` string, required
  - `external_id` string, nullable, required
  - `language` string, nullable, required: The contact's preferred language tag (e.g. "es"), or null for the language-less default. Set at create or via PUT /v1/contacts/{id}/language — deliberately NOT part of the whole-resource PUT, so a partial-shaped update can't silently clear it.
  - `emails` string[], required
  - `phones` string[], required
  - `push_subscriptions` PushSubscriptionDTO[], required
    - `value` string, required
    - `provider` 'fcm' | 'apns' | 'expo' | 'onesignal' | 'webpush', required: The service a push subscription is delivered *through* — the routing key that ties a contact's `PushSubscription` to the tenant's configured `PushTransportConfig` and selects which sender adapter delivers it. Cross-cutting vocabulary (like `Channel`): it lives in `shared` because both `contact` (the destination side) and `push_transport` / `outbound_push` (the sending side) reference the same members, and they must not drift apart. Covers direct transports (FCM / APNs / Web Push) and aggregators that fan out to the native platforms themselves (Expo, OneSignal). Subclassing `str` keeps JSON and dict keys clean.
    - `platform` 'ios' | 'android' | 'web': The device platform a subscription targets. Optional metadata — used for payload shaping once the push channel lands (APNs, Android and Web bodies differ), never for routing (that is `PushProvider`'s job).
    - `auth` string, nullable
    - `p256dh` string, nullable
  - `attributes` object, required
  - `preferences` PreferenceDTO[], required
    - `notification_type_key` string, required
    - `channel` 'email' | 'inbox' | 'push' | 'web_push', required: A delivery channel a notification can be sent over. This is an enum (not a bare string) so adding SMS / WHATSAPP later is a one-line change and every per-channel mapping (notification-type defaults, contact preferences) extends automatically. Crucially, a newly-added channel is **opted out by default** everywhere: `NotificationType.default_for` returns False for a channel absent from `channel_defaults`, and a contact has no preference until they set one — so an existing install gains a channel that delivers to nobody until a tenant explicitly enables it. That's why adding a member needs no data migration. Subclassing `str` makes members compare/serialize as their value ("email"), which keeps JSON and dict keys clean.
    - `enabled` boolean, required

## Other responses

- `404`: No contact with this id
- `409`: The contact was modified concurrently; reload and retry
- `422`: Validation Error

---

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