# Object tags API (beta)

Object tags store structured data about a user in one tag. A tag of the `Object` type holds one object with named fields, such as the last `order` with its `amount` and `status`. A tag of the `ObjectList` type holds an array of such objects, such as all `orders`. You define the fields once, write and read values through the tag API, and build segments by field values.

<Aside type="caution" title="Beta">
Object tags are in beta and available on request. Contact [Pushwoosh Support](https://help.pushwoosh.com/hc/en-us/requests/new) to enable them for your account. During the beta, object tags work only through the API:

- The Control Panel has no screen for object tags.
- Fields of object tags can't be inserted into message content.
- Customer Journey doesn't support object tags.
</Aside>

## How object tags work

An object tag has a schema and a value. The schema is the tag name and a list of fields, each with its own scalar type. The value is stored in the fields, separately for each user.

- **Field.** Each field is a tag named `<object>.<field>`, for example `order.amount`. You segment by fields, and you can write a field of an `Object` tag by this name.
- **`Object` value.** One object per user, for example `"order": {"amount": 120, "status": "paid"}`.
- **`ObjectList` value.** An array of objects with the same fields, for example `"orders": [{"amount": 120, "status": "paid"}, {"amount": 50}]`.

Object tags are always User-specific, which means the value belongs to the user, not to a single device, and is copied to all of the user's devices. See [Tags API](/developer/api-reference/tags/) for regular tags.

## Schema rules

Pushwoosh checks these rules when you create an object tag or add a field:

- Object and field names contain only Latin letters, digits, and `_`.
- The full field name `<object>.<field>` is 64 characters or shorter.
- An object tag has from 1 to 20 fields. Field names are case-insensitive, so `Amount` and `amount` are the same field.
- A field has one of these types: `INTEGER`, `STRING`, `LIST`, `DATE`, `BOOLEAN`, `PRICE`, `VERSION`. Timezone fields and nested objects aren't supported.
- A field can't take a name that another tag already has. If a tag `order.note` exists, adding the field `note` to `order` fails with `tag order.note already exists`.
- An object tag can't lose its last field. Delete the object tag instead.
- A field or an object tag that is in use can't be deleted. The error lists where it is used, for example `field is in use: segment Big buyers, in-app Spring sale`. Segments, in-apps, running Customer Journey campaigns, presets, email templates, and other items that reference the tag count as usages.

## Manage the schema

Create and change object tags with the REST methods below. Use `/json/1.3/addTag` only for regular tags, because it can't define fields.

### Base URL

```
https://rpc-api.svc-nue.pushwoosh.com
```

### Authentication

Every request must include an `Authorization` header with your [Server API token](/developer/api-reference/api-access-token/#server-api-token). A Device API token gets HTTP 403 `a device API token cannot call this method, use an account API token`.

```
Authorization: Api YOUR_API_TOKEN
```

### Endpoints

These methods manage the schema of object tags:

| Method | Path | Description |
| :---- | :---- | :---- |
| `POST` | `/api/tags` | Create an object tag with its fields |
| `GET` | `/api/tags/{name}` | Get an object tag with its fields |
| `GET` | `/api/tags` | List tags. An object tag comes with its `fields`, and the fields aren't listed as separate tags |
| `POST` | `/api/tags/{name}/fields` | Add a field |
| `DELETE` | `/api/tags/{name}/fields/{field}` | Delete a field |
| `DELETE` | `/api/tags/{name}` | Delete an object tag with all its fields |

`POST /api/tags` and both field methods return the object tag after the change, wrapped in `{"tag": {...}}`. `DELETE /api/tags/{name}` returns `{}`.

If object tags aren't enabled for your account, `POST /api/tags` with an object type and both field methods fail with HTTP 403 `object tags are not enabled for the account`. `GET /api/tags/{name}` and `DELETE /api/tags/{name}` return HTTP 404 `tag not found`, and `GET /api/tags` doesn't list object tags. Without object tags enabled you can't create one, so there is nothing to write values to.

### Create an object tag

`POST` `/api/tags`

##### Request body

| Name | Type | Description |
| :---- | :---- | :---- |
| name\* | string | Name of the object tag. |
| type\* | string | `OBJECT` for one object or `OBJECT_LIST` for a list of objects. |
| fields\* | array | Fields of the object, from 1 to 20. |
| fields[].name\* | string | Field name. The field becomes the tag `<name>.<fields[].name>`. |
| fields[].type\* | string | Field type: `INTEGER`, `STRING`, `LIST`, `DATE`, `BOOLEAN`, `PRICE`, or `VERSION`. |

```json title="Example"
{
  "name": "orders",
  "type": "OBJECT_LIST",
  "fields": [
    { "name": "amount", "type": "INTEGER" },
    { "name": "status", "type": "STRING" }
  ]
}
```

```json title="Response"
{
  "tag": {
    "name": "orders",
    "type": "OBJECT_LIST",
    "system": false,
    "user_specific": true,
    "has_right_ace_modify": true,
    "fields": [
      { "name": "amount", "type": "INTEGER" },
      { "name": "status", "type": "STRING" }
    ]
  }
}
```

The tag is created as User-specific regardless of the `user_specific` value. `GET /api/tags/{name}` returns the same object without the `tag` wrapper. Fields are sorted by name.

### Add a field

`POST` `/api/tags/{name}/fields`

```json title="Example: POST /api/tags/orders/fields"
{
  "field": { "name": "note", "type": "STRING" }
}
```

You can write the new field right away as part of the whole tag value. Adding a field fails if the object tag already has 20 fields or if a tag with the full field name exists, see [Errors](#errors).

### Delete a field

`DELETE` `/api/tags/{name}/fields/{field}`

For example, `DELETE /api/tags/orders/fields/note` deletes the field `orders.note`. You can't delete the last field or a field that is in use, see [Errors](#errors).

### Errors

Errors come in the format `{"code": <n>, "message": "...", "details": [...]}`. When you create an object tag, the 400 messages start with `validate: `.

| HTTP status | Message | Cause |
| :---- | :---- | :---- |
| 400 | `object tag and field names must contain only letters, digits and underscores` | A name has another character. |
| 400 | `an object tag needs 1 to 20 fields` | No fields or more than 20 fields on create. |
| 400 | `an object tag has at most 20 fields` | The object tag already has 20 fields. |
| 400 | `field <name> is given twice` | Two fields have the same name, ignoring case. |
| 400 | `<object>.<field>: object and field names together cannot be longer than 64 characters` | The full field name is too long. |
| 400 | `<object>.<field>: field type must be INTEGER, STRING, LIST, DATE, BOOLEAN, PRICE or VERSION` | Unsupported field type. |
| 400 | `an object tag needs at least one field; delete the object tag instead` | You tried to delete the last field. |
| 400 | `field is in use: ...`, `tag is in use: ...` | The field or the object tag is used in segments, in-apps, Customer Journey campaigns, or other items. |
| 403 | `object tags are not enabled for the account` | Object tags aren't enabled for the account. |
| 403 | `a device API token cannot call this method, use an account API token` | The request uses a Device API token. |
| 404 | `tag not found` | `GET` or `DELETE /api/tags/{name}`: no such tag, the name is a field, or object tags aren't enabled. |
| 404 | `object tag not found`, `field not found` | Field methods: no such object tag or field. |
| 409 | `tag already exists` | A tag with the object tag name exists. |
| 409 | `tag <object>.<field> already exists` | A tag with the field's full name exists. |

## Write values

Write object tag values with [`/setTags`](/developer/api-reference/device-api/#settags) and [`/bulkSetTags`](/developer/api-reference/audience-api/#bulksettags). These methods keep their own authentication: `/setTags` takes a Device API token in the `Authorization: Token` header, and `/bulkSetTags` takes the API access token in `auth`. Send an `Object` value as a JSON object and an `ObjectList` value as an array of objects:

```json title="Example: /json/1.3/setTags"
{
  "request": {
    "application": "XXXXX-XXXXX",
    "userId": "user_42",
    "tags": {
      "order": { "amount": 120, "status": "paid" },
      "orders": [
        { "amount": 120, "status": "paid" },
        { "amount": 50 }
      ]
    }
  }
}
```

If you set the value by HWID, it is stored on the user of that device.

Each field takes a value in the format of its type:

| Field type | Example value |
| :---- | :---- |
| `INTEGER` | `120` |
| `STRING` | `"paid"` |
| `LIST` | `["gift", "express"]` |
| `DATE` | `"2026-10-02 22:11"` (UTC), `"2026-10-02"`, or a Unix timestamp in seconds |
| `BOOLEAN` | `true` or `false` |
| `PRICE` | `19.95` |
| `VERSION` | `"1.0.0"` |

### Writing replaces the whole value

Each write replaces the stored value of the object tag:

- A field missing from the new value is deleted. A field sent as `null` is deleted too.
- `null`, `[]`, or `""` deletes the whole value.
- `{}` deletes all fields of an `Object` tag.
- A key that isn't a field of the object is skipped.
- If a field value doesn't match the field type, the whole value of the tag is rejected and the stored value stays as it was.

`/setTags` doesn't report a rejected value in its response. Check the result with [`/getTags`](#read-values) or use `/bulkSetTags`, which reports a result for each tag.

### Write one field of an Object

Write a single field of an `Object` tag by its full name. `increment` and the other operations of [`/setTags`](/developer/api-reference/device-api/#settags) work on fields:

```json
"tags": {
  "order.status": "shipped",
  "order.amount": { "operation": "increment", "value": 5 }
}
```

A field of an `ObjectList` tag can't be written on its own, because it isn't clear which element to change. `/setTags` ignores such a value. Send the whole list instead.

### Object tags aren't created automatically

A value with an object or an array of objects for a tag that doesn't exist is skipped. Tag auto-creation doesn't create object tags, so create the schema first with [`POST /api/tags`](#create-an-object-tag).

### bulkSetTags results

With `detailed=true`, [`/bulkSetTags` status](/developer/api-reference/audience-api/#bulksettags-status) returns one of these results for an object tag, in addition to `ok`, `tag not found`, and `wrong value. expect :<type>`:

| Result | Cause |
| :---- | :---- |
| `object tags are stored on the user only` | The object tag isn't User-specific. |
| `cannot load the fields of the object` | Pushwoosh couldn't read the fields of the object tag. |
| `wrong value. expect an object (a map) or, for a list of objects, an array of maps` | The value doesn't fit the object tag, or a field value doesn't match its type. |
| `a field of a list of objects cannot be set on its own, set the whole list` | The value is set on a field of an `ObjectList` tag. |

## Read values

[`/getTags`](/developer/api-reference/device-api/#gettags) returns an `Object` value as a JSON object and an `ObjectList` value as an array of objects:

```json title="Response"
{
  "status_code": 200,
  "status_message": "OK",
  "response": {
    "result": {
      "order": { "amount": 120, "status": "paid" },
      "orders": [
        { "amount": 120, "status": "paid" },
        { "amount": 50 }
      ]
    }
  }
}
```

Field values are returned the same way as regular tag values in `/getTags`: a Date as a Unix timestamp in seconds, a Price as a string such as `"120.00"`, and a Boolean as `1` or `0`.

## Segment by fields

Set segment conditions on fields, not on the object tag itself. In the [segmentation language](/developer/api-reference/segmentation-filters-api/segmentation-language/), a field is a tag with a dotted name, and it takes the operators of its own type:

```
AT("XXXXX-XXXXX", "order.amount", gte, 100)
```

A condition on the object tag itself fails with `object tag has no value of its own, use one of its fields`.

### Conditions on an ObjectList

A condition on a field of an `ObjectList` tag matches a user when at least one element of the list fits the condition. Conditions on fields of the same list joined with AND must match the same element:

```
AT("XXXXX-XXXXX", "orders.amount", gte, 100) * AT("XXXXX-XXXXX", "orders.status", eq, "paid")
```

This segment holds users with at least one order that is both 100 or more and paid. A user with one large unpaid order and one small paid order doesn't match. Conditions joined with OR don't have to match the same element. Conditions on two different lists are checked on each list separately.

Some operators work differently on a field of an `ObjectList` tag:

- **`notset`.** No element of the list has a value in this field. A user without the list matches too.
- **`noteq`.** At least one element has a value in this field, and that value differs from the given one. Elements without the field don't count. A user with orders in statuses `paid` and `new` matches `noteq "paid"`.
- **`notin` on a List field.** No element contains any of the given values.
- **`notin` on a field of another type.** At least one element has a value in this field that isn't among the given values.

`notset` and `notin` on a List field aren't tied to the same element as the other conditions joined with AND.

## Export segments with object tags

[`/exportSegment`](/developer/api-reference/segmentation-filters-api/#exportsegment) exports the fields of object tags:

- **CSV.** Each field is a separate column named `<object>.<field>`, and object tags themselves get no column. A field of an ObjectList holds all distinct values across the elements as a JSON array, the same way as a List tag: `["120","50"]`.
- **`json` and `json_each_line`.** Fields of an `Object` tag come as separate tags named `<object>.<field>`. An `ObjectList` tag comes as one entry in `tags` with the elements in `object_list.items`. Each element lists its fields with short names, and each field carries its value under a key named after its type: `integer`, `string`, `list`, `date`, `boolean`, `price`, or `version`. Integer values come as strings:

```json
{
  "name": "orders",
  "object_list": {
    "items": [
      { "fields": [{ "name": "amount", "integer": "120" }, { "name": "status", "string": "paid" }] },
      { "fields": [{ "name": "amount", "integer": "50" }] }
    ]
  }
}
```

If `tagsList` names an object tag, all of its fields are exported.