<!-- Documentation Index: https://docs.publora.com/llms.txt -->
<!-- Canonical: https://docs.publora.com/endpoints/test-connection -->
# Test Connection

Validate that a platform connection is working before scheduling posts.

## Endpoint

```
POST https://api.publora.com/api/v1/test-connection/:platformId
```

## Headers

| Header | Required | Description |
|--------|----------|-------------|
| `x-publora-key` | Yes | Your API key |
| `x-publora-user-id` | No | Managed user ID (workspace only) |
| `x-publora-client` | No | Client identifier (e.g., `mcp`). Used for MCP access gating. |

## Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `platformId` | string | The platform ID (e.g., `linkedin-Tz9W5i6ZYG`) |

## Response

### Successful Connection

```json
{
  "platform": "linkedin",
  "platformId": "linkedin-Tz9W5i6ZYG",
  "username": "John Doe",
  "status": "ok",
  "message": "Connected as John Doe",
  "permissions": ["w_member_social", "w_member_social_feed", "r_basicprofile", "r_member_postAnalytics", "r_member_profileAnalytics"],
  "tokenExpiresIn": "45d 3h",
  "lastSuccessfulPost": "2026-02-22T09:15:00.000Z",
  "lastError": null
}
```

### Failed Connection

```json
{
  "platform": "threads",
  "platformId": "threads-17841412345678",
  "username": "yourthreads",
  "status": "error",
  "message": "Error validating access token: Session has expired",
  "permissions": [],
  "tokenExpiresIn": "expired",
  "lastSuccessfulPost": "2026-02-15T14:30:00.000Z",
  "lastError": {
    "message": "Session has expired",
    "occurredAt": "2026-02-22T10:00:00.000Z"
  }
}
```

> **Important:** All validation results — both success and failure — return HTTP 200. The connection test outcome is indicated by the `status` field (`"ok"` or `"error"`), not the HTTP status code. Only infrastructure errors (404 connection not found, 500 server error) use non-200 status codes.

## Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `platform` | string | Platform name (twitter, linkedin, threads, etc.) |
| `platformId` | string | Full platform ID |
| `username` | string/null | Connected account username. `null` for a Telegram channel without a public username (a private channel). |
| `status` | string | `ok` or `error` |
| `message` | string | Human-readable status message |
| `permissions` | array | List of granted permissions/scopes. With the YouTube update in #490, these are Google's recorded full scope URIs; an unknown grant returns `[]`. |
| `permissionsKnown` | boolean | YouTube only, added in #490. Whether the granted scopes are recorded. `false` means unknown, not permission denied. |
| `tokenExpiresIn` | string/null | Time until token expiry. Possible values: `"7d 3h"` (days and hours), `"5h"` (hours only, when less than one day remains), `"< 1h"` (less than one hour remaining), `"expired"` (token already expired), or `null` (no expiration). |
| `lastSuccessfulPost` | string/null | Timestamp of last successful post |
| `lastError` | object/null | Last error details if any. When present, contains: `message` (string — error description) and `occurredAt` (string — ISO 8601 timestamp of when the error occurred). |

### YouTube permissions — #490 update

[Publora #490](https://github.com/publora/publora.com/issues/490) corrects the
previous fixed `["youtube.upload", "youtube.readonly"]` list. Since 2026-10-01,
YouTube responses return the actual scopes recorded from Google's OAuth response,
using their full URIs. For a canonical broad grant, the relevant response fields are:

```json
{
  "status": "ok",
  "permissions": ["https://www.googleapis.com/auth/youtube"],
  "permissionsKnown": true
}
```

A broad `youtube` grant covers uploading videos and adding them to a playlist;
it is not expanded into separately granted `youtube.upload` or `youtube.readonly`
entries. See Google's [scope definitions](https://developers.google.com/youtube/v3/guides/auth/installed-apps#identify-access-scopes)
and [playlist-write authorization](https://developers.google.com/youtube/v3/docs/playlistItems/insert#authorization).

Older connections without a recorded grant, or a reconnect where Google omits
scopes, return `permissions: []` and `permissionsKnown: false`. Do not interpret
that empty array as proof that permission was denied. If your integration gates
operations by scopes, handle unknown grants separately and recognize broad
`youtube` access; checking for the old short `youtube.upload` string is insufficient.
Use `status` for the connection health result. Other platforms' responses keep
their current format.

## Examples

### JavaScript (fetch)

```javascript
const platformId = 'linkedin-Tz9W5i6ZYG';
const response = await fetch(
  `https://api.publora.com/api/v1/test-connection/${platformId}`,
  {
    method: 'POST',
    headers: { 'x-publora-key': 'YOUR_API_KEY' }
  }
);
const result = await response.json();

if (result.status === 'ok') {
  console.log(`✅ ${result.message}`);
  console.log(`Token expires in: ${result.tokenExpiresIn}`);
} else {
  console.log(`❌ Connection failed: ${result.message}`);
}
```

### Python (requests)

```python
import requests

platform_id = 'linkedin-Tz9W5i6ZYG'
response = requests.post(
    f'https://api.publora.com/api/v1/test-connection/{platform_id}',
    headers={'x-publora-key': 'YOUR_API_KEY'}
)
result = response.json()

if result['status'] == 'ok':
    print(f"✅ {result['message']}")
    print(f"Permissions: {', '.join(result['permissions'])}")
else:
    print(f"❌ {result['message']}")
```

### cURL

```bash
curl -X POST https://api.publora.com/api/v1/test-connection/linkedin-Tz9W5i6ZYG \
  -H "x-publora-key: YOUR_API_KEY"
```

### Pre-flight Check Before Scheduling

```javascript
async function validateConnectionsBeforePost(platformIds) {
  const results = await Promise.all(
    platformIds.map(async (platformId) => {
      const response = await fetch(
        `https://api.publora.com/api/v1/test-connection/${platformId}`,
        {
          method: 'POST',
          headers: { 'x-publora-key': process.env.PUBLORA_API_KEY }
        }
      );
      return response.json();
    })
  );

  const failed = results.filter(r => r.status === 'error');
  if (failed.length > 0) {
    console.log('⚠️  Some connections need attention:');
    failed.forEach(r => console.log(`  - ${r.platformId}: ${r.message}`));
    return false;
  }

  console.log('✅ All connections healthy');
  return true;
}

// Usage
const platforms = ['twitter-123', 'linkedin-ABC', 'threads-456'];
const healthy = await validateConnectionsBeforePost(platforms);
if (healthy) {
  // Proceed with scheduling
}
```

### Telegram Connections

For a Telegram bot connection, the check verifies the bot token, that the bot can still read the saved channel, and that the bot is an administrator allowed to post there. A private channel without a public username returns `"username": null`.

| `status` | `message` | Meaning |
|----------|-----------|---------|
| `ok` | `Bot can post to <channel title>` | The token works and the bot is an administrator with posting rights |
| `error` | `Bot needs channel administrator posting rights` | The bot is no longer an administrator, or lost the **Post Messages** right in a channel |
| `error` | `Channel is unavailable` | The saved chat is not a channel or supergroup |
| `error` | `Invalid bot token` | Telegram answered without confirming the bot. A revoked token normally fails with `Request failed with status code 401` instead (see below) |
| `error` | `Invalid bot connection` | Not a usable bot connection (no bot token or chat ID). Every MTProto connection returns this |

Errors returned by the Telegram API itself, such as a revoked bot token or a private channel the bot was removed from, come back as `Request failed with status code <code>`.

> **Note:** Telegram MTProto connections will always fail the test-connection check because the validator only checks bot connections. They return `"Invalid bot connection"`, but still publish normally.

### Unknown Platform

If the platform has no test-connection validator implemented (e.g., Pinterest), the endpoint returns HTTP 200 with a full response structure but error status:

```json
{
  "platform": "pinterest",
  "platformId": "pinterest-abc123",
  "username": "unknown",
  "status": "error",
  "message": "Unknown platform: pinterest",
  "permissions": [],
  "tokenExpiresIn": null,
  "lastSuccessfulPost": null,
  "lastError": null
}
```

> **Note:** The public API route returns only `{ error: "Failed to test connection" }` for 500 errors without a `message` field. The internal dashboard endpoint may include an additional `message` field in error responses.

## Errors

| Status | Error | Cause |
|--------|-------|-------|
| 400 | `"Invalid platformId format"` | `platformId` doesn't contain a dash or is malformed. **Note:** Parsed the same way as in `create-post`: split at the first dash (so a Telegram bot ID such as `telegram--1001234567890` is valid), with a lowercase-letter prefix; the non-empty ID may not contain whitespace, `/`, `?`, or `#`; colons are allowed. |
| 400 | `"Invalid x-publora-user-id"` | `x-publora-user-id` header contains an invalid ObjectId |
| 401 | `"API key is required"` | Missing `x-publora-key` header |
| 401 | `"Invalid API key"` | `x-publora-key` value is not a valid key |
| 401 | `"Invalid API key owner"` | The API key's owner account could not be found |
| 403 | `"API access is not enabled for this account"` | Account does not have API access enabled |
| 403 | `"MCP access is not enabled for this account"` | MCP access is not enabled when `x-publora-client: mcp` is sent |
| 403 | `"Workspace access is not enabled for this key"` | API key does not have workspace permissions |
| 403 | `"User is not managed by key"` | The `x-publora-user-id` user is not managed by this API key |
| 500 | `"Internal server error"` | Unexpected error in middleware |
| 404 | `"Connection not found"` | Invalid platformId or connection doesn't exist |
| 500 | `"Failed to test connection"` | Server error during validation |


---

*[Publora](https://publora.com) is built by [Creative Content Crafts, Inc.](https://cccrafts.ai) Need AI-powered content creation for LinkedIn, Threads, and X? Try [Co.Actor](https://co.actor) — the best AI service for authentic thought leadership at scale.*
