> For the complete documentation index, see [llms.txt](https://docs.harmony.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.harmony.io/integrations/saas-applications/bring-your-own-app/bring-your-own-app-users.md).

# Users Data

Field reference, snapshot semantics, and examples for sending user lists to Harmony

A users push tells Harmony **who has access** to an application. It answers questions like "who holds a paid seat", "who still has an account after leaving", and "which roles are granted where".

This page covers the users endpoint in full. For setup, see [Bring Your Own App](/integrations/saas-applications/bring-your-own-app.md).

### The most important rule: a push is a snapshot

Every users push is a **complete replacement** of the application's user list — not a list of changes.

Harmony compares each push against what it already knows and makes reality match your push. Users present in your push are created or updated. **Users absent from your push are removed from Harmony.**

```
Push 1:  alice, bob, carol      →  Harmony shows: alice, bob, carol
Push 2:  alice, carol           →  Harmony shows: alice, carol      (bob removed)
Push 3:  alice, bob, carol      →  Harmony shows: alice, bob, carol (bob restored)
```

This is what makes offboarding work — when someone loses access in the application, they disappear from your next push and Harmony reflects that automatically, with no delete call required.

{% hint style="warning" %}
**Always send every current user, every time.** Sending only the users who changed since your last push will remove everyone else. If your script filters, paginates, or short-circuits on an error, make sure the final result is still the complete list.
{% endhint %}

### Endpoint

```
POST https://external.harmony.io/custom-push/v1/users
Authorization: Bearer <access key>
Content-Type: application/json
```

### Request body

| Field             | Type   | Required | Description                                                                                                                        |
| ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `external_app_id` | string | **Yes**  | The App ID you registered in Harmony. 1–200 characters, `A–Z a–z 0–9 . _ -`. Must be identical in every push for this application. |
| `users`           | array  | **Yes**  | Between 1 and 100 user objects. See below.                                                                                         |
| `snapshot_token`  | string | No       | Only used when a single user list spans more than one request. See [Sending more than 100 users](#sending-more-than-100-users).    |
| `schema_version`  | string | No       | Defaults to `"1.0"`. Reserved for future versions of this API.                                                                     |

### User fields

Each object in the `users` array describes one account in the application.

| Field          | Type   | Required | Limit                      | Description                                                                                                                                                                                                                            |
| -------------- | ------ | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `external_id`  | string | **Yes**  | 1–255 chars                | The application's own identifier for this account. Use whatever the application calls its user ID — a UUID, a numeric ID, a username. It identifies the row across pushes, so it must be stable for a given person.                    |
| `email`        | string | **Yes**  | Valid email, max 254 chars | The user's email address. Harmony uses this to match the account to an employee record, which is what links this application to the rest of your data. Accounts without a usable email address cannot be matched and are not retained. |
| `name`         | string | No       | 255 chars                  | Display name, shown in Harmony's user lists. If omitted, Harmony shows the email address instead.                                                                                                                                      |
| `status`       | string | No       | See values below           | The account's state in the application. Defaults to `active`.                                                                                                                                                                          |
| `role`         | string | No       | 255 chars                  | The role this user holds, **in the application's own vocabulary** — `Billing Admin`, `Editor`, `read-only`. Do not translate it into Harmony terms; access reviews are more useful when they show what the application actually says.  |
| `license_tier` | string | No       | `paid`, `free`, `unknown`  | Whether this account consumes a billable seat. Omit if you genuinely do not know — do not guess.                                                                                                                                       |
| `plan_name`    | string | No       | 255 chars                  | The plan or SKU name attached to this user, for example `Enterprise` or `Business Plus`.                                                                                                                                               |
| `metadata`     | object | No       | 1 KB serialized            | Any additional key/value data you want to carry along. Free-form JSON.                                                                                                                                                                 |

#### `status` values

| Value       | Meaning                                                     |
| ----------- | ----------------------------------------------------------- |
| `active`    | The account is in normal use. This is the default.          |
| `inactive`  | The account exists but is deactivated or dormant.           |
| `pending`   | The account is invited or provisioned but not yet accepted. |
| `suspended` | The account is temporarily blocked by an administrator.     |
| `expired`   | The account's access has lapsed.                            |

Reporting a user as `inactive` is **not** the same as omitting them. An inactive user still appears in Harmony, still counts toward the application's user list, and still shows up in access reviews — which is usually what you want, since a dormant account is still an account. Omitting a user removes them entirely.

#### `license_tier` values

| Value     | Meaning                                                   |
| --------- | --------------------------------------------------------- |
| `paid`    | The account consumes a billable seat.                     |
| `free`    | The account is on a free or included tier.                |
| `unknown` | The tier is not determinable from the application's data. |

Reporting `license_tier` is what makes license optimization possible — Harmony can only tell you about wasted spend if it knows which seats cost money.

### Response

`202 Accepted`:

```json
{
  "batch_id": "8f14e45f-ce34-4f2b-9b3d-1a2c5e7d9f01",
  "snapshot_id": "01937f2a-9c4d-7e10-b8a3-5f6d7e8c9a0b",
  "accepted_rows": 2
}
```

| Field           | Description                                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `batch_id`      | A unique identifier for this request. Useful when contacting support about a specific push.                                                            |
| `snapshot_id`   | The identifier of the user list this push opened or contributed to. You only need it when [sending more than 100 users](#sending-more-than-100-users). |
| `accepted_rows` | How many user rows were accepted.                                                                                                                      |

For error responses, see the [API reference](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-api.md#errors).

### Examples

#### A minimal push

Only the two required fields per user:

```json
{
  "external_app_id": "acme-crm",
  "users": [
    { "external_id": "u-1001", "email": "alice@example.com" },
    { "external_id": "u-1002", "email": "bob@example.com" }
  ]
}
```

#### A fully populated push

Every field in use:

```json
{
  "external_app_id": "acme-crm",
  "users": [
    {
      "external_id": "u-1001",
      "email": "alice@example.com",
      "name": "Alice Chen",
      "status": "active",
      "role": "Billing Admin",
      "license_tier": "paid",
      "plan_name": "Enterprise",
      "metadata": {
        "department": "Finance",
        "created_at": "2024-03-11",
        "sso_enabled": true
      }
    },
    {
      "external_id": "u-1002",
      "email": "bob@example.com",
      "name": "Bob Ortiz",
      "status": "suspended",
      "role": "Editor",
      "license_tier": "free",
      "plan_name": "Starter"
    }
  ]
}
```

#### A complete sync script

```python
import os
import requests

BASE_URL = "https://external.harmony.io"
APP_ID = "acme-crm"
ACCESS_KEY = os.environ["HARMONY_ACCESS_KEY"]


def fetch_all_users_from_your_app():
    """Return every current user. Replace with your application's API."""
    ...


def push_users(rows):
    response = requests.post(
        f"{BASE_URL}/custom-push/v1/users",
        headers={"Authorization": f"Bearer {ACCESS_KEY}"},
        json={"external_app_id": APP_ID, "users": rows},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


users = [
    {
        "external_id": u["id"],
        "email": u["email"],
        "name": u.get("full_name"),
        "status": "active" if u["enabled"] else "inactive",
        "role": u.get("role_name"),
        "license_tier": "paid" if u["seat_type"] == "licensed" else "free",
    }
    for u in fetch_all_users_from_your_app()
    if u.get("email")
]

if not users:
    raise SystemExit("Refusing to push an empty user list")

result = push_users(users)
print(f"Accepted {result['accepted_rows']} users")
```

{% hint style="info" %}
Note the guard before pushing. If your application's API returns an empty list because of an outage or an expired credential, pushing that empty result would remove every user from Harmony. A check like this costs nothing and prevents the worst possible outcome.
{% endhint %}

### Sending more than 100 users

A single request carries at most 100 users. For larger applications, split the list across several requests that Harmony joins into **one** user list.

1. **First request** — send the first 100 users and omit `snapshot_token`. Harmony opens a new user list and returns its `snapshot_id`.
2. **Every following request** — send the next 100 users and set `snapshot_token` to the `snapshot_id` you received from the first request.
3. When the last request succeeds, the list is complete and replaces the previous one.

```python
def push_all_users(all_rows):
    token = None
    for i in range(0, len(all_rows), 100):
        body = {"external_app_id": APP_ID, "users": all_rows[i:i + 100]}
        if token:
            body["snapshot_token"] = token

        response = requests.post(
            f"{BASE_URL}/custom-push/v1/users",
            headers={"Authorization": f"Bearer {ACCESS_KEY}"},
            json=body,
            timeout=30,
        )
        response.raise_for_status()
        token = response.json()["snapshot_id"]
```

Rules for multi-batch pushes:

* `snapshot_token` must be a value Harmony gave you. You cannot invent one.
* Complete the full sequence within an hour. A user list left half-sent for longer is rejected when you try to continue it, which leaves it permanently incomplete.
* Send each user exactly once across the whole sequence. Sending the same user twice under one list inflates seat counts until the next clean push.

{% hint style="info" %}
**Large directories.** The one-hour window covers the whole sequence, not each request. A sequential loop is fine for most directories; for very large ones, send several requests concurrently so the sequence finishes comfortably inside the window. Requests are rate limited, so ramp up gradually rather than firing everything at once — a `429` means slow down, not stop.
{% endhint %}

{% hint style="warning" %}
**If any request in the sequence fails, do not resume.** Start over: send the complete user list again from the beginning, without a `snapshot_token`. This opens a fresh list that replaces the incomplete one.

Resuming after a failure — or abandoning a half-sent list — leaves Harmony holding a partial user list that it treats as complete, which removes every user that never made it.
{% endhint %}

### Recommended push frequency

Push whenever the application's user list changes, or on a fixed schedule if you cannot detect changes. Daily is a good default. Pushing more often is safe — each push simply replaces the previous list.

There is no penalty for pushing an unchanged list.

### See also

* [**Usage data**](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-usage.md) — reporting what users actually did
* [**API reference**](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-api.md) — errors, retries, and limits


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.harmony.io/integrations/saas-applications/bring-your-own-app/bring-your-own-app-users.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
