---
description: "User Profiles API: create a profile per end user, generate JWT connect links and connect or disconnect social accounts for white-label integrations."
title: User Profiles API
---

# User Profiles API

These endpoints allow you to integrate Upload-Post directly into your platform by managing user profiles and generating secure tokens for account linking.

See the [User Profile Integration Guide](../guides/user-profile-integration.md) for a conceptual overview and workflow.

## Authentication

All API requests require authentication using your API Key. Include it in the `Authorization` header for every request:

```
Authorization: Apikey YOUR_API_KEY
```

Replace `YOUR_API_KEY` with the actual API key provided to you.

---

## User Profile Management

Manage user profiles within Upload-Post that correspond to users on your platform.

### Endpoint

```
/api/uploadposts/users
```

---

### Create User Profile

Creates a new profile linked to a user on your platform.

*   **Method:** `POST`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name         | Type   | Required | Description                                                                 |
    |--------------|--------|----------|-----------------------------------------------------------------------------|
    | username     | String | Yes      | A unique identifier for the user on your platform (e.g., your internal ID). It is the `user` value of every API call and **cannot be changed later**. Letters, numbers, `_`, `@` and `-`. |
    | display_name | String | No       | A label shown in the dashboard instead of the username. Max 100 characters. Can be changed at any time, see [Rename a User Profile](#rename-user-profile). |

*   **Example Body:**
    ```json
    {
      "username": "your_platform_user_id_123",
      "display_name": "Acme Corp"
    }
    ```
*   **Success Response (201 Created):**
    ```json
    {
      "profile": {
        "created_at": "Fri, 02 May 2025 21:43:14 GMT",
        "social_accounts": {
          "tiktok": ""
          // Other platforms will appear here as they are connected
        },
        "username": "your_platform_user_id_123",
        "display_name": "Acme Corp"
      },
      "success": true
    }
    ```
    *   `profile`: Contains details of the newly created profile.
        *   `created_at`: Timestamp of profile creation.
        *   `social_accounts`: Object showing connected accounts (initially empty or with placeholders).
        *   `username`: The unique identifier provided.
        *   `display_name`: The label provided, or `null` when none was sent.
    *   `success`: Indicates successful creation.
*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`, or invalid `display_name` (`error_code: INVALID_DISPLAY_NAME`).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `403 Forbidden`: Profile limit reached for the current plan (`error_code: PROFILE_LIMIT_REACHED`).
    *   `409 Conflict`: A profile with the provided `username` already exists.

---

### Get User Profiles

Retrieves a list of all user profiles created under your API key.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
*   **Query Parameters:** None
*   **Success Response (200 OK):**
    ```json
    {
        "limit": 10,
        "plan": "default",
        "profiles": [
            {
                "created_at": "2025-04-02T17:44:33.229755",
                "social_accounts": {
                    "facebook": {
                        "display_name": "FB User",
                        "social_images": "url_to_fb_image"
                    },
                    "instagram": {
                        "display_name": "IG User",
                        "social_images": "url_to_ig_image"
                    }
                    // ... other connected platforms with their details
                },
                "username": "your_platform_user_id_1",
                "display_name": "Acme Corp"
            },
            {
                "created_at": "Fri, 02 May 2025 21:43:14 GMT",
                "social_accounts": {
                    "tiktok": "" // Example of a platform added but not yet connected
                },
                "username": "your_platform_user_id_2",
                "display_name": null
            }
        ],
        "success": true
    }
    ```
    *   `limit`: The maximum number of profiles allowed by the current plan.
    *   `plan`: The subscription plan associated with the API key.
    *   `profiles`: An array of user profile objects.
        *   `created_at`: Timestamp of profile creation.
        *   `social_accounts`: An object detailing connected social media accounts. Each key is the platform name (e.g., `facebook`, `instagram`, `tiktok`). The value can be an object with details (`username`, `handle`, `display_name`, `social_images`) or an empty string/null if not fully connected. See [Identifying a connected account](#identifying-a-connected-account).
        *   `capabilities`: On the accounts that expose it (today, `tiktok`), the list of optional features that **this particular connection** supports. See [Account capabilities](#account-capabilities).
        *   `reauth_required`: `true` when the connection's token has expired and the account must be reconnected before it can publish.
        *   `username`: The unique identifier for the profile. This is the value you pass as `user` when publishing.
        *   `display_name`: The profile's label in the dashboard, or `null` when it has none (the dashboard then shows the `username`). Not to be confused with `social_accounts.<platform>.display_name`, which is the connected account's own name.
    *   `success`: Indicates successful retrieval.
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid or missing API Key.

---

### Identifying a connected account

Every connected account in `social_accounts` carries two different strings, and
they answer two different questions:

| Field | What it is | Use it to |
| :--- | :--- | :--- |
| `username` | The **account identifier**: the platform's own id for the account, and the key this connection is stored under. Opaque by design. | Bind your own records to a destination. |
| `handle` | The public @name on the platform. | Show a human which account a post goes to. |
| `display_name` | The profile's display name. | Labels in your UI. |

For most platforms the identifier is a numeric id (Instagram's IG user id,
Facebook's page id, YouTube's channel id). For **TikTok** it is the account's
`open_id`, which looks like `-000g91dgwXgwtNwc-V8dolLsQJMUY3MQAfv`. That is the
identifier TikTok itself issues; it is **scoped to the application**, so the
same TikTok account has a different `open_id` in every app that connects to it,
including ours. Within Upload-Post it is stable:

*   it does **not** change when the user reconnects the same account,
*   it does **not** change when the user renames the account or changes its
    @handle — `handle` follows the rename on the next reconnection, `username`
    never moves,
*   it **does** differ if a different TikTok account is connected. A changed
    `username` therefore means "this is another account", which is exactly the
    signal you want before publishing.

:::note Reading it back
`GET /api/uploadposts/users` (all profiles) and
`GET /api/uploadposts/users/{profile}` (one profile) are the supported
read-only endpoints. Both return the same `social_accounts` shape, and neither
publishes or modifies anything.
:::

---

### Account capabilities

Not every connected account supports the same optional fields. A connected
account can report what it supports in a `capabilities` array, so you can check
it **before** sending optional fields instead of guessing:

```json
{
    "success": true,
    "profiles": [
        {
            "username": "your_profile",
            "social_accounts": {
                "tiktok": {
                    "display_name": "Fotoexamen",
                    "handle": "fotoexamen",
                    "social_images": "https://storage.googleapis.com/.../avatar.jpg",
                    "reauth_required": false,
                    "capabilities": [
                        "music",
                        "location",
                        "cover_image",
                        "cover_timestamp",
                        "draft",
                        "video_privacy",
                        "photo_privacy",
                        "profile_analytics",
                        "comments",
                        "trend_search"
                    ]
                }
            }
        }
    ]
}
```

**TikTok capability values**

| Capability | What it unlocks |
| :--- | :--- |
| `music` | Attach a Commercial Music Library track: `tiktok_music_id`, `tiktok_music_volume`, `tiktok_music_start`, `tiktok_music_end`, `tiktok_original_sound_volume`. See [Get TikTok Trending Music](./get-tiktok-music.md). |
| `location` | Tag a place on the post: `tiktok_location_id` + `tiktok_location_name`. See [Get TikTok Locations](./get-tiktok-locations.md). |
| `cover_image` | Set a custom cover image: `tiktok_cover_image_url` / `tiktok_cover_image`. |
| `cover_timestamp` | Pick the cover frame with `cover_timestamp` (milliseconds). |
| `draft` | Send the video to the account's drafts with `post_mode=MEDIA_UPLOAD` (alias `tiktok_upload_to_draft=true`). |
| `photo_privacy` | The connection accepts `privacy_level` on **photo** posts. |
| `video_privacy` | The connection accepts `privacy_level` on **video** posts. Connections without it always publish videos as public — use `draft` instead. |
| `inbox_fallback` | When TikTok's daily active-user cap is hit, the post is delivered to the account's TikTok inbox as a draft instead of failing. See [Reached Active User Cap](../guides/reached-active-user-cap-error.md). |
| `comments` | Everything in the [Comments API](./comments.md) with `platform=tiktok`: list, create, reply, delete, the [replies under a comment](./comments.md#list-comments) (`comment_id`) and the [hide / like / pin](./comments.md#hide-like-or-pin-a-comment) verbs — plus attaching a [`first_comment`](./upload-video.md#platform-specific-first-comments) to a post. **Only granted on reconnection** — see below. |
| `trend_search` | [`GET /suggestions?type=keywords`](./suggestions.md#typekeywords): the terms people search around a word. **Only granted on reconnection** — see below. |
| `profile_analytics` | Profile-level TikTok analytics in [Get Analytics](./get-analytics.md) — including the [full per-post breakdown](./get-analytics.md#tiktok-the-full-per-post-breakdown) (retention, impression sources, audience types) — plus [Audience Insights](./audience.md) with its category benchmark, and [`GET /suggestions?type=hashtags`](./suggestions.md#typehashtags). Granted by **any recent connection**; no reconnection needed. |

`video_privacy` and `inbox_fallback` on one side and `music` / `location` /
`cover_image` / `draft` / `profile_analytics` on the other are
mutually exclusive: a connection reports one group or the other, never both.

Neither group changes how you ask for a draft. `post_mode=MEDIA_UPLOAD` (and its
alias `tiktok_upload_to_draft=true`) works on every TikTok connection, whether it
reports `draft` or `inbox_fallback` — do not branch on the capability list.
`inbox_fallback` is not a mode you can request: it is what happens on its own
when TikTok's daily active-user cap is hit.

:::caution `comments` and `trend_search` only appear after a reconnection
Every other capability depends on how the account was connected. These two
depend on a permission TikTok grants **at the moment of connecting**, and it
only started issuing it recently. An account connected before that publishes
exactly as it always did — nothing about its uploads changes — but it will not
report `comments` or `trend_search`, and the endpoints that need them answer
`400` with `error_code: "tiktok_reconnect_required"`.

The fix is the same one-click reconnection as any other missing capability: the
account owner reconnects TikTok from
[Manage Users](https://app.upload-post.com/manage-users), or you send them
through a [white-label connect link](../guides/user-profile-integration.md).
Nothing in your integration changes.

This is also why `first_comment` on TikTok can come back as a warning instead of
a comment: without `comments` the post is still published, and the response says
the first comment was skipped.
:::

**Sending a field your connection does not support is safe.** The post is still
published: the upload response comes back with a `warnings` array of plain
strings naming the field that was ignored and telling you that reconnecting the
TikTok account enables it.

```json
{
    "success": true,
    "warnings": [
        "tiktok_music_id ignored: your current TikTok connection cannot attach a track from TikTok's music catalogue. Reconnect your TikTok account from Manage Users (https://app.upload-post.com/manage-users) to enable it."
    ]
}
```

To enable a missing capability, reconnect the account from
[Manage Users](https://app.upload-post.com/manage-users) (or send your end user
through a [white-label connect link](../guides/user-profile-integration.md)).
Nothing in your integration changes — same endpoints, same field names.

:::note Treat `capabilities` as an open list
New values can be added over time. Check whether the capability you need is
present rather than matching the array exactly, and fall back gracefully when a
capability is missing.
:::

---

### Get a Specific User Profile

Retrieves information for a single user profile using its username.

*   **Method:** `GET`
*   **Endpoint**: `/api/uploadposts/users/{username}`

**Path Parameters**

| Parameter  | Type   | Description                                            |
| :--------- | :----- | :----------------------------------------------------- |
| `username` | string | **Required**. The username of the profile to retrieve. |

**Success Response (200 OK)**

If the profile is found, the API will return a JSON object with the profile details.

```json
{
    "success": true,
    "profile": {
        "created_at": "2023-10-27T10:00:00Z",
            "social_accounts": {
                "tiktok": {
                    "username": "tiktok_user_123",
                    "display_name": "User Display Name",
                    "social_images": "https://example.com/image.jpg"
                },
                "bluesky": {
                     "username": "user.bsky.social",
                     "display_name": "Bluesky User",
                     "social_images": "https://example.com/avatar.jpg"
                },
                "instagram": null
            },
        "username": "specific_profile_name",
        "display_name": "Acme Corp"
    }
}
```

**Error Response (404 Not Found)**

If no profile is found with the specified `username`, the API will return:

```json
{
    "success": false,
    "message": "Profile not found"
}
```

---

### Rename a User Profile {#rename-user-profile}

Changes the name a profile shows in the dashboard. The `username` itself does
**not** change: it is the `user` value of every API call, and your upload
history, scheduled posts, analytics and integrations keep pointing at it. Rename
as often as you like; nothing that already works needs updating.

*   **Method:** `PATCH`
*   **Endpoint**: `/api/uploadposts/users/{username}`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name         | Type           | Required | Description |
    |--------------|----------------|----------|-------------|
    | display_name | String or null | Yes      | The new label. Leading and trailing spaces are trimmed. Max 100 characters, no line breaks. An empty string or `null` removes it, and the dashboard shows the `username` again. |

    `display_name` is the only editable field. Sending anything else (including
    `username`) returns `400` with `error_code: FIELD_NOT_EDITABLE`.

*   **Example:**
    ```bash
    curl -X PATCH https://api.upload-post.com/api/uploadposts/users/your_platform_user_id_123 \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"display_name": "Acme Corp"}'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "profile": {
        "username": "your_platform_user_id_123",
        "display_name": "Acme Corp"
      }
    }
    ```
*   **Error Responses:**
    *   `400 Bad Request`: Missing `display_name` (`INVALID_REQUEST`), invalid value (`INVALID_DISPLAY_NAME`) or a field that cannot be edited (`FIELD_NOT_EDITABLE`).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: No profile found with the provided `username`.

You can also rename profiles from [Manage Users](https://app.upload-post.com/manage-users)
in the dashboard (the pencil next to the profile name). Team members with access
to the account can rename its profiles too.

---

### Delete User Profile

Deletes an existing user profile and its associated data (like social connections).

*   **Method:** `DELETE`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name     | Type   | Required | Description                                      |
    |----------|--------|----------|--------------------------------------------------|
    | username | String | Yes      | The unique identifier of the profile to delete. |

*   **Example Body:**
    ```json
    {
      "username": "user_id_to_delete"
    }
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "message": "Perfil eliminado correctamente",
      "success": true
    }
    ```
*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`.
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: No profile found with the provided `username`.

---

### Disconnect a Social Account {#disconnect-social-account}

Unlinks **one** social platform from a profile, for example to remove a user's Instagram while keeping their TikTok and YouTube connected. The profile itself and its other connections are not touched. To delete the whole profile, use [Delete User Profile](#delete-user-profile) instead.

*   **Endpoint:** `/api/uploadposts/users/social`
*   **Method:** `DELETE`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`, or `Authorization: Bearer PROFILE_JWT` with the profile JWT from [Generate JWT URL](#endpoint-generate-jwt-url). A profile JWT only works for the profile it was issued for.
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name             | Type   | Required | Description |
    |------------------|--------|----------|-------------|
    | profile_username | String | Yes      | The profile to disconnect the account from. |
    | social_platform  | String | Yes      | The platform key as it appears in the profile's `social_accounts` object, e.g. `instagram`, `tiktok`, `facebook`, `youtube`, `linkedin`, `x`. |

*   **Example Request:**
    ```bash
    curl -X DELETE https://api.upload-post.com/api/uploadposts/users/social \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"profile_username": "user_id_123", "social_platform": "instagram"}'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "message": "Social platform instagram disconnected successfully from profile user_id_123"
    }
    ```
    If the platform was already disconnected, the call still returns `200` with `"success": true` and a message saying so, so it is safe to retry.
*   **What happens:**
    *   The platform is removed from the profile's `social_accounts`. The stored credentials are deleted only when no other profile of your account still uses the same social account.
    *   A [`social_account_disconnected`](./webhooks.md) webhook fires with `"reason": "manual_disconnect"` (if you subscribed to that event).
    *   To connect the platform again, send the user through a new [JWT connect link](#endpoint-generate-jwt-url).
*   **Error Responses:**
    *   `400 Bad Request`: Body missing, not JSON, or `profile_username` / `social_platform` missing.
    *   `401 Unauthorized`: Invalid or missing API key or token.
    *   `403 Forbidden`: The profile JWT belongs to a different profile than `profile_username`.
    *   `404 Not Found`: The profile does not exist, or that platform was never connected to it.

---

## JWT Management

Generate and validate JWTs for the secure social account linking process.

### Endpoint: Generate JWT URL

```
/api/uploadposts/users/generate-jwt
```

Generates a secure, single-use URL containing a JWT. Your user visits this URL to link their social media accounts.

*   **Method:** `POST`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name         | Type    | Required | Description                                                                                      |
    |--------------|---------|----------|--------------------------------------------------------------------------------------------------|
    | username     | String  | Yes      | The identifier for the user profile for which the JWT is being generated.                        |
    | redirect_url | String  | No       | (Optional) The URL to which the user will be redirected after linking their social account.      |
    | logo_image   | String  | No       | (Optional) A URL to a logo image to display on the linking page for branding purposes.           |
    | redirect_button_text | String | No | (Optional) The text to display on the redirect button after linking. Defaults to "Logout connection". |
    | connect_title | String | No | (Optional) Custom title text for the connection page. Defaults to "Connect Social Media Accounts". |
    | connect_description | String | No | (Optional) Custom description text for the connection page. Defaults to "Connect your social media accounts to manage your posts.". |
    | platforms    | Array   | No       | (Optional) List of platforms to show for connection. Possible values: 'tiktok', 'instagram', 'linkedin', 'youtube', 'facebook', 'x', 'threads', 'google_business'. Defaults to all supported platforms. |
    | show_calendar | Boolean | No       | (Optional) Whether to show the calendar view on the connection page. Defaults to `true`.         |
    | readonly_calendar | Boolean | No   | (Optional) When `true`, shows only a read-only calendar view. The user cannot edit, delete, or create posts, and cannot connect or disconnect social accounts. Ideal for sharing a content calendar with end clients. Defaults to `false`. |
    | language | String | No | (Optional) Forces the language of the connection page for this profile. Supported values: `en`, `es`, `de`, `fr`, `pt`, `pl`, `tr`. When omitted, the page automatically detects the visitor's browser language and falls back to English. |
    | ui_labels | Object | No | (Optional) Flat object of i18n key → replacement string, to override individual pieces of connect-page UI text. See [Custom UI Labels](#custom-ui-labels). |
    | connect_theme | String | No | (Optional) Forces the connection page to `light` or `dark` and hides the visitor's theme toggle. `auto` (or `null`) clears it so the page follows the visitor's own preference again. Stored on the profile: omit the key to keep the current value. |

*   **Supported languages:**

    | Value | Language   |
    |-------|------------|
    | `en`  | English    |
    | `es`  | Spanish    |
    | `de`  | German     |
    | `fr`  | French     |
    | `pt`  | Portuguese |
    | `pl`  | Polish     |
    | `tr`  | Turkish    |

*   **Example Body:**
    ```json
    {
      "username": "your_platform_user_id_123"
    }
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "access_url": "https://app.upload-post.com/connect?token=GENERATED_JWT_TOKEN",
      "success": true,
      "duration": "48h"
    }
    ```
    *   `access_url`: The secure URL your user needs to visit. Redirect your user to this URL.
    *   `success`: Always `true` if the request was successful.
    *   `duration`: The validity period of the generated JWT (48 hours).
*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123"}'
    ```
*   **Example Request with Calendar Disabled (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123", "show_calendar": false}'
    ```
*   **Example Request with Read-Only Calendar for Clients (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_platform_user_id_123", "readonly_calendar": true, "logo_image": "https://youragency.com/logo.png", "connect_title": "Your Content Calendar"}'
    ```
    This generates a link where the client only sees the calendar with scheduled posts (social channel, date/time, visual, text) but cannot edit anything or access other sections.
*   **Example Success Response (200 OK):**
    ```json
    {
      "access_url": "https://app.upload-post.com/connect?token=GENERATED_JWT_TOKEN_STRING",
      "duration": "48h",
      "success": true
    }
    ```
*   **Calendar Deep Link:** If you want users to land directly on the shared calendar view, replace the path with `/connect/calendar` while keeping the token intact, e.g. `https://app.upload-post.com/connect/calendar?token=GENERATED_JWT_TOKEN`. The page will automatically fall back to `/connect` when the profile has `show_calendar` disabled. When `readonly_calendar` is `true`, the user is automatically redirected to the calendar view regardless of the URL path.

*   **Error Responses:**
    *   `400 Bad Request`: Missing or invalid `username`, an invalid `ui_labels` payload (see [Custom UI Labels](#custom-ui-labels)), or a `connect_theme` other than `light`, `dark` or `auto`.
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `403 Forbidden`: Profile exists but is blocked by plan limits (`error_code: PROFILE_BLOCKED`).
    *   `404 Not Found`: No profile found with the provided `username` (`error_code: PROFILE_NOT_FOUND`).

*   **Integration tip:** If JWT generation returns `404`, call `GET /api/uploadposts/users` first to confirm the profile exists and that profile creation did not fail due to plan limits.

#### Custom UI Labels {#custom-ui-labels}

`connect_title`, `connect_description` and `redirect_button_text` cover the three most visible strings on the connection page. `ui_labels` goes further: it lets a white-label integration override **any individual piece of connect-page UI text**, in any language, without waiting for a translation to ship.

`ui_labels` is a **flat** object mapping the connect page's own i18n dot-path keys to the replacement strings:

```json
{
  "username": "your_platform_user_id_123",
  "language": "tr",
  "ui_labels": {
    "connect.connectButton": "Bağlan",
    "connect.notConnected": "Bağlı değil"
  }
}
```

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/generate-jwt \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "username": "your_platform_user_id_123",
        "language": "tr",
        "ui_labels": {
          "connect.connectButton": "Bağlan",
          "connect.notConnected": "Bağlı değil"
        }
      }'
    ```

**Validation rules**

| Rule | Limit |
|------|-------|
| Maximum number of entries | 100 |
| Key format | Must match `^[a-zA-Z0-9_.]+$` — letters, numbers, dots and underscores only |
| Value type | Must be a string |
| Value length | Maximum 300 characters |

A violation returns `400 Bad Request` with the specific reason:

```json
{
  "success": false,
  "message": "Invalid ui_labels key 'bad key!'. Keys may only contain letters, numbers, dots and underscores"
}
```

```json
{
  "success": false,
  "message": "ui_labels['connect.x'] must be a string"
}
```

**Update semantics**

`ui_labels` is stored on the profile and persists across JWT generations:

| You send | Result |
|----------|--------|
| The field is **omitted** | Previously stored labels are left **untouched** |
| `"ui_labels": { ... }` (non-empty) | Replaces the stored labels with what you sent |
| `"ui_labels": null` | **Clears** all stored labels |
| `"ui_labels": {}` | **Clears** all stored labels |

The stored labels are returned inside the `profile` object by [`GET /api/uploadposts/users/validate-jwt`](#endpoint-validate-jwt), so you can read back what is currently applied.

:::warning
The keys are the **connect page's own translation keys**, not free-form identifiers. An unknown key is simply never rendered. Override only keys you have confirmed exist — verify each one against the live connect page after setting it.
:::

#### Mobile OAuth Compatibility

When users open the connection page on a mobile device (iPhone, iPad or Android), the
operating system may intercept OAuth URLs (e.g. `instagram.com`, `accounts.google.com`)
and open the corresponding native app instead of keeping the flow in the browser.
Because native apps cannot handle the OAuth authorization URL, the connection fails.

Upload-Post automatically detects mobile browsers and routes OAuth redirects through
a secure intermediate page (`/api/uploadposts/oauth/bounce`) that performs the
redirect via JavaScript. This bypasses Universal Links (iOS) and App Links (Android)
interception so the OAuth flow stays entirely in the mobile browser. iPads are
included even though iPadOS Safari identifies itself as a desktop Mac by default.

**No action is required from API consumers** — the mobile-safe redirect is applied
automatically when the user accesses the `access_url` on a mobile device.

---

### Endpoint: Validate JWT

```
/api/uploadposts/users/validate-jwt
```

(Optional) Allows you to validate a JWT token. The primary validation occurs automatically when the user accesses the `access_url`.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Bearer YOUR_JWT_TOKEN`
*   **Body Parameters:** None. The token is read from the `Authorization` header, not from the request body.
*   **Example Request (curl):**
    ```bash
    # Replace YOUR_JWT_TOKEN with the actual token string
    curl -X GET https://api.upload-post.com/api/uploadposts/users/validate-jwt \
      -H "Authorization: Bearer YOUR_JWT_TOKEN"
    ```
*   **Success Response (200 OK - Valid Token):** Returns the profile details associated with the token.
    ```json
    {
      "profile": {
        "social_accounts": {
          "tiktok": null,
          "instagram": "connected_account_details",
          // ... other platforms
        },
        "username": "your_platform_user_id_123",
        "ui_labels": {
          "connect.connectButton": "Bağlan",
          "connect.notConnected": "Bağlı değil"
        }
      },
      "success": true
    }
    ```
    *   `profile`: Contains details about the user profile linked to the token.
        *   `social_accounts`: An object showing the connection status for various platforms (e.g., `null` if not connected, or details if connected).
        *   `username`: The unique identifier provided when the profile was created.
        *   `ui_labels`: The connect-page text overrides currently stored for this profile, as set via [`generate-jwt`](#custom-ui-labels). Empty or absent when no overrides are stored.
    *   `success`: Indicates the token is valid.
*   **Success Response (200 OK - Invalid Token):**
    ```json
    {
      "isValid": false,
      "reason": "Token expired or invalid signature" // Example reason
    }
    ```
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid, expired, or missing JWT token in the `Authorization` header.

---

## Facebook Pages

Retrieve Facebook page IDs associated with user profiles to enable posting to Facebook pages.

### Endpoint

```
/api/uploadposts/facebook/pages
```

---

### Get Facebook Pages

Fetches Facebook page IDs associated with a profile. You can use this endpoint to connect and start posting on Facebook pages.

*   **Method:** `GET`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
*   **Query Parameters:**

    | Name    | Type   | Required | Description                                                                          |
    |---------|--------|----------|--------------------------------------------------------------------------------------|
    | profile | String | No       | The unique identifier of the profile. If not specified, returns all pages for your account. |

*   **Example Request (curl):**
    ```bash
    curl 'https://api.upload-post.com/api/uploadposts/facebook/pages?profile=your_profile' \
      -H 'Authorization: Apikey YOUR_API_KEY'
    ```
*   **Example Request (without profile parameter):**
    ```bash
    curl 'https://api.upload-post.com/api/uploadposts/facebook/pages' \
      -H 'Authorization: Apikey YOUR_API_KEY'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "pages": [
        {
          "page_id": "123456789",
          "page_name": "My Business Page",
          "profile": "your_platform_user_id_123"
        },
        {
          "page_id": "987654321", 
          "page_name": "Another Page",
          "profile": "your_platform_user_id_123"
        }
      ],
      "success": true
    }
    ```
    *   `pages`: Array of Facebook page objects associated with the profile(s).
        *   `page_id`: The Facebook page ID that can be used for posting.
        *   `page_name`: The display name of the Facebook page.
        *   `profile`: The profile identifier associated with this page.
    *   `success`: Indicates successful retrieval.
*   **Error Responses:**
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: No profile found with the provided identifier (if profile parameter is specified).

---

## Manual Credential Connections

Most platforms are connected through the [JWT connection page](#endpoint-generate-jwt-url) (OAuth). A few platforms instead use a **manual credential** model: you submit the credentials directly to a dedicated endpoint, Upload-Post validates them against the platform, encrypts the secret at rest, and links it to the profile. There is **no OAuth flow and no browser redirect**. Tokens/webhooks do not expire, so no reconnection is required unless you revoke them.

### Connect Discord

Links a Discord channel **incoming webhook** to a profile. Upload-Post validates the webhook by performing a `GET` on the webhook URL (expecting a `200` with the webhook `id`, `channel_id`, and `guild_id`), then encrypts and stores the webhook URL.

*   **Method:** `POST`
*   **Endpoint:** `/api/uploadposts/users/discord/credentials`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name             | Type   | Required | Description                                                                 |
    |------------------|--------|----------|-----------------------------------------------------------------------------|
    | profile_username | String | Yes      | The profile to link the Discord webhook to.                                 |
    | webhook_url      | String | Yes      | The Discord channel incoming webhook URL (`https://discord.com/api/webhooks/...`). |
    | name             | String | No       | Optional display name for the connection. Defaults to the webhook's name.   |

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/discord/credentials \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "profile_username": "your_platform_user_id_123",
        "webhook_url": "https://discord.com/api/webhooks/123456789/abcdef...",
        "name": "My Server #announcements"
      }'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "message": "Discord credentials saved successfully"
    }
    ```
    On success, the profile's `social_accounts.discord` is set to the connection key (the supplied `name`, or the webhook id when `name` is omitted).
*   **Error Responses:**
    *   `400 Bad Request`: Missing `profile_username`/`webhook_url`, an invalid webhook URL format, or a webhook that fails validation (deleted/invalid).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: User or profile not found.

To find the webhook URL: open Discord → **Server Settings → Integrations → Webhooks → New Webhook**, pick the target channel, and click **Copy Webhook URL**.

### Connect Telegram

Links a **Telegram bot** (bring-your-own bot) and a target chat/channel to a profile. Upload-Post validates the bot token via `getMe` and the chat via `getChat` (the bot must be an **admin** of the chat), then encrypts and stores the bot token.

*   **Method:** `POST`
*   **Endpoint:** `/api/uploadposts/users/telegram/credentials`
*   **Headers:**
    *   `Authorization: Apikey YOUR_API_KEY`
    *   `Content-Type: application/json`
*   **Body Parameters:**

    | Name             | Type   | Required | Description                                                                                  |
    |------------------|--------|----------|----------------------------------------------------------------------------------------------|
    | profile_username | String | Yes      | The profile to link the Telegram bot to.                                                     |
    | bot_token        | String | Yes      | The bot token from @BotFather (e.g. `123456:ABC-DEF...`).                                     |
    | chat_id          | String | Yes      | The target chat: a public channel `@username`, or a numeric chat id (e.g. `-100123456789`).  |
    | name             | String | No       | Optional display name for the connection. Defaults to the bot's username.                    |

*   **Example Request (curl):**
    ```bash
    curl -X POST https://api.upload-post.com/api/uploadposts/users/telegram/credentials \
      -H "Authorization: Apikey YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "profile_username": "your_platform_user_id_123",
        "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
        "chat_id": "@my_channel",
        "name": "My Channel"
      }'
    ```
*   **Success Response (200 OK):**
    ```json
    {
      "success": true,
      "message": "Telegram credentials saved successfully"
    }
    ```
    On success, the profile's `social_accounts.telegram` is set to the connection key (the supplied `name`, or the bot username when `name` is omitted).
*   **Error Responses:**
    *   `400 Bad Request`: Missing `profile_username`/`bot_token`/`chat_id`, an invalid bot token, or a chat that is not reachable (often because the bot is not an admin of the chat).
    *   `401 Unauthorized`: Invalid or missing API Key.
    *   `404 Not Found`: User or profile not found.

To set up the bot: message [@BotFather](https://t.me/BotFather) → `/newbot` to get a `bot_token`, then add the bot to your target channel/group **as an administrator** so it can post.

---
