> 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-api.md).

# API Reference

Complete technical reference for the Custom Push API. For an introduction, see [Bring Your Own App](/integrations/saas-applications/bring-your-own-app.md).

### Base URL

```
https://external.harmony.io
```

All requests must use HTTPS.

### Authentication

Every request carries your access key as a bearer token:

```
Authorization: Bearer <access key>
```

Access keys are provisioned by Harmony support — [contact support](mailto:support@harmony.io) to request one. The key is scoped to your workspace; you never send a workspace or tenant identifier, and any such value in the request is ignored.

{% hint style="warning" %}
Store the key in a secrets manager or an environment variable. Never commit it to source control, never place it in a URL, and never expose it in client-side code.
{% endhint %}

### Endpoints

| Method | Path                    | Purpose                                                                                                                                |
| ------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/custom-push/v1/users` | Replace the application's user list — see [Users data](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-users.md) |
| `POST` | `/custom-push/v1/usage` | Add activity records — see [Usage data](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-usage.md)                |

Both accept `Content-Type: application/json`.

### Limits

| Limit            | Value                                                    |
| ---------------- | -------------------------------------------------------- |
| Rows per request | 100 (`users` or `events`)                                |
| Metadata per row | 1 KB serialized as JSON                                  |
| Text fields      | 255 characters                                           |
| Email fields     | 254 characters                                           |
| App ID           | 1–200 characters, `A–Z a–z 0–9 . _ -`                    |
| Rate limiting    | Applied per workspace; a throttled request returns `429` |

Requests that exceed the row limit or the metadata limit are rejected with a `400 validation` error naming the offending field. Requests that are too large overall are rejected with `413`.

### Success responses

Both endpoints return `202 Accepted` on success. `202` means the request was **received and validated** — processing happens asynchronously, so the data will not be visible in Harmony immediately.

**Users:**

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

**Usage:**

```json
{
  "batch_id": "3d5e1f88-27ac-4b60-9de1-4f0c8a1b2e39",
  "accepted_rows": 480
}
```

| Field           | Description                                                                                                                             |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `batch_id`      | Unique identifier for this request. Quote it when contacting support.                                                                   |
| `snapshot_id`   | Users only. The identifier of the user list this push opened or joined. Echo it as `snapshot_token` to continue a list across requests. |
| `accepted_rows` | Number of rows accepted.                                                                                                                |

### Errors

Every error response has the same envelope. `error` is a stable machine-readable code — match on it rather than on the human-readable text, which may change.

```json
{
  "error": "unknown_app",
  "details": "'acme-crm' is not registered as a Custom Push integration for this tenant. Create it in Settings → Integrations → SaaS Applications before pushing."
}
```

#### Configuration errors — `400`

Your request was well-formed, but the application it refers to is not set up correctly. Fix the configuration in Harmony, then retry.

| Code            | Meaning                                                          | Fix                                                                                                             |
| --------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `unknown_app`   | No Bring Your Own App integration is registered with this App ID | Check that the App ID in **Settings** > **Integrations** matches your `external_app_id` exactly, including case |
| `app_disabled`  | An integration is registered for this App ID but is disabled     | Re-enable it in **Settings** > **Integrations**                                                                 |
| `ambiguous_app` | Two or more enabled integrations use the same App ID             | Disable the duplicate — one integration per application                                                         |

#### Validation errors — `400`

| Code         | Meaning                                                                            |
| ------------ | ---------------------------------------------------------------------------------- |
| `validation` | One or more fields failed validation. The `details` array identifies each problem. |

```json
{
  "error": "validation",
  "details": [
    { "loc": ["users", 0, "email"], "msg": "value is not a valid email address", "type": "value_error" },
    { "loc": ["users", 3, "external_id"], "msg": "String should have at least 1 character", "type": "string_too_short" }
  ]
}
```

Each entry gives the exact path to the offending value — `["users", 0, "email"]` means the `email` field of the first user in the array. **No rows are accepted when validation fails**: fix every reported problem and resend the whole request.

#### Snapshot token errors — `400`

Only apply to users pushes that set `snapshot_token`.

| Code                       | Meaning                                                         | Fix                                                                                             |
| -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `malformed_snapshot_token` | The token is not a value Harmony issued                         | Use the `snapshot_id` returned by the first request of the sequence. Tokens cannot be invented. |
| `stale_snapshot_token`     | The user list this token refers to was left incomplete too long | Start over: send the complete user list again with no `snapshot_token`                          |
| `future_snapshot_token`    | The token is not valid                                          | Start over with no `snapshot_token`                                                             |

#### Authorization errors — `403`

| Code                        | Meaning                                                          | Fix                                                                                                                                  |
| --------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `integration_access_denied` | Your access key does not carry the permissions this API requires | Contact Harmony support. This is a key provisioning issue, not a problem with your request — no change to your code will resolve it. |

#### Other status codes

| Status | Meaning                                      | What to do                                                           |
| ------ | -------------------------------------------- | -------------------------------------------------------------------- |
| `401`  | Missing or invalid access key                | Check the `Authorization` header format: `Bearer <key>`              |
| `404`  | Unrecognized path                            | Verify the endpoint URL                                              |
| `413`  | Request body too large                       | Send fewer rows per request, or reduce per-row `metadata`            |
| `429`  | Rate limited                                 | Wait, then retry with exponential backoff                            |
| `502`  | Harmony could not accept the batch           | Retry with backoff. See the warning below for users pushes.          |
| `503`  | A Harmony service is temporarily unavailable | Retry with backoff. Nothing was accepted, so no partial data landed. |

### Retry guidance

| Status       | Retryable? | Notes                                                                                            |
| ------------ | ---------- | ------------------------------------------------------------------------------------------------ |
| `400`        | No         | Fix your request or your Harmony configuration first. Retrying unchanged returns the same error. |
| `401`, `403` | No         | Credential problem. Contact support for `403 integration_access_denied`.                         |
| `413`        | No         | Reduce the request size.                                                                         |
| `429`        | Yes        | Back off, then retry.                                                                            |
| `502`        | Yes        | Retry with exponential backoff.                                                                  |
| `503`        | Yes        | Retry with exponential backoff. The request was rejected before anything was stored.             |

A reasonable policy is three attempts with exponential backoff and jitter, retrying only on `429`, `502`, and `503`.

{% hint style="warning" %}
**Retrying a failed users push:** if a users push fails partway through a multi-request sequence, do **not** resume from where it stopped. Start the whole sequence again from the beginning without a `snapshot_token`. This opens a fresh, complete user list that replaces the incomplete one.

Resuming — or abandoning a half-sent list — leaves Harmony holding a partial user list that it treats as complete, which removes every user that never arrived. See [Users data](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-users.md#sending-more-than-100-users).
{% endhint %}

Usage pushes carry no such risk. Because every record is deduplicated on your own `event_id`, retrying a usage push — in whole or in part — is always safe.

### Idempotency

| Endpoint | Behavior                                                                                                  |
| -------- | --------------------------------------------------------------------------------------------------------- |
| `/users` | Each complete push replaces the previous user list. Sending the same list twice produces the same result. |
| `/usage` | Records are deduplicated by `event_id`. Sending the same records twice counts them once.                  |

Neither endpoint uses an idempotency key header — idempotency comes from the data itself. On the usage side this depends entirely on deriving `event_id` deterministically; see [The event ID contract](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-usage.md#the-event-id-contract).

### Complete request examples

**Users:**

```bash
curl -X POST https://external.harmony.io/custom-push/v1/users \
  -H "Authorization: Bearer $HARMONY_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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"
      }
    ]
  }'
```

**Usage:**

```bash
curl -X POST https://external.harmony.io/custom-push/v1/usage \
  -H "Authorization: Bearer $HARMONY_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_app_id": "acme-crm",
    "events": [
      {
        "event_id": "alice@example.com:2026-07-20:login",
        "event_date": "2026-07-20",
        "actor_email": "alice@example.com",
        "actor_id": "u-1001",
        "action": "login",
        "metric_value": 1,
        "outcome_reason": "request_count"
      }
    ]
  }'
```

### Versioning

The API version is part of the path (`/v1/`). Request bodies also carry an optional `schema_version` field, currently `"1.0"`.

Additive changes — new optional fields, new enum values — may be introduced within `v1`. Build clients that ignore unrecognized fields in responses. Breaking changes would arrive under a new path version, with the existing one supported through a published transition period.

### See also

* [**Bring Your Own App**](/integrations/saas-applications/bring-your-own-app.md) — overview and setup
* [**Users data**](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-users.md) — user list reference
* [**Usage data**](/integrations/saas-applications/bring-your-own-app/bring-your-own-app-usage.md) — activity reference


---

# 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-api.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.
