Workspace / B2B API

View as Markdown

Note: Workspace access is not enabled by default. Contact Publora support at serge@publora.com to enable Workspace API access for your account.

This guide covers how to use the Publora Workspace API to manage multiple users under your account. This is designed for B2B integrations where you need to create and manage social media posting on behalf of your customers.

How It Works

The Workspace API lets you create managed users under your workspace. Each managed user gets their own isolated environment with their own social media connections and posts. You control everything through your workspace API key.

Core Concepts

  • Workspace: Your B2B account that can manage multiple users.
  • Managed User: A user created under your workspace. They have their own social connections and posts, but you control them via the API. User IDs are MongoDB ObjectIds (24-character hex strings, e.g., 6626a1f5e4b0c91a2d3f4567). The API returns user IDs as _id in response bodies (the MongoDB _id field).
  • Connection URL: A temporary OAuth link you generate and send to a managed user so they can connect their social media accounts.
  • Per-User API Key: An optional API key generated for a specific managed user, allowing direct API access scoped to that user.
  • Attaching an Existing User: A person who already has their own Publora account can be pulled into your workspace as a managed user — but only with their consent, proven by a valid access token belonging to that user. See Attach an Existing User.

Authentication Headers

Header Purpose
x-publora-key Your workspace API key (required on all requests)
x-publora-user-id The managed user's ID (required when acting on behalf of a user)

Rate Limits

Each managed user has a dailyPostsLeft field (default: 100) returned in the user object. This field is stored per user but not enforced as an actual posting limit. Enforced limits come from the workspace owner's entitlements and are checked at scheduling time. monthlyPosts follows the plan's scope: Starter is account-wide across the workspace, while paid plans count usage separately per connected channel. scheduledPosts is the maximum concurrent scheduled queue across the entire workspace, and scheduleHorizonDays controls how far ahead any workspace post may be scheduled. The dailyPostsLeft value does not gate or restrict posting.

Endpoints Overview

Method Endpoint Description
GET /api/v1/workspace/users List all managed users
POST /api/v1/workspace/users Create a new managed user
POST /api/v1/workspace/users/attach Attach an existing Publora user to your workspace (requires the target user's consent token)
DELETE /api/v1/workspace/users/:userId Detach a managed user (preserves user record, removes workspace association)
POST /api/v1/workspace/users/:userId/api-key Generate a per-user API key
POST /api/v1/workspace/users/:userId/connection-url Generate an OAuth connection link

Examples

Create a Managed User

Note: This endpoint returns HTTP 201 Created on success, not 200.

JavaScript (fetch)

const response = await fetch(
  'https://api.publora.com/api/v1/workspace/users',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
      username: 'client@example.com',
      displayName: 'Acme Corp'
    })
  }
);
 
const { user } = await response.json();
console.log('Managed user created:', user._id);
console.log('Username:', user.username);
console.log('Display name:', user.displayName);

Python (requests)

import requests
 
response = requests.post(
    'https://api.publora.com/api/v1/workspace/users',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    },
    json={
        'username': 'client@example.com',
        'displayName': 'Acme Corp'
    }
)
 
data = response.json()
user = data['user']
print(f"Managed user created: {user['_id']}")
print(f"Username: {user['username']}")
print(f"Display name: {user['displayName']}")

cURL

curl -X POST https://api.publora.com/api/v1/workspace/users \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY" \
  -d '{
    "username": "client@example.com",
    "displayName": "Acme Corp"
  }'

Node.js (axios)

const axios = require('axios');
 
const { data } = await axios.post(
  'https://api.publora.com/api/v1/workspace/users',
  {
    username: 'client@example.com',
    displayName: 'Acme Corp'
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    }
  }
);
 
const user = data.user;
console.log('Managed user created:', user._id);
console.log('Username:', user.username);
console.log('Display name:', user.displayName);

Attach an Existing User

Use this when the person you want to manage already has their own Publora account. Calling Create a Managed User with an email that already exists returns 409 with code: "email_exists" and a set of fields describing whether — and how — that user can be attached:

Field Type Description
userId string The existing user's ObjectId
reason string Why the email conflicts (see table below)
attachable boolean true if the user can be attached to your workspace
requiresConsent boolean true if attaching requires the target user's consent token
alreadyInWorkspace boolean true if the user is already managed by your workspace
managedByAnotherWorkspace boolean true if the user is managed by a different workspace

Example 409 response for a standalone user who can be attached:

{
  "error": "User with this email already exists",
  "code": "email_exists",
  "userId": "6626a1f5e4b0c91a2d3f4567",
  "alreadyInWorkspace": false,
  "managedByAnotherWorkspace": false,
  "reason": "standalone_user_exists",
  "attachable": true,
  "requiresConsent": true
}

reason values:

reason Meaning Attachable?
standalone_user_exists Independent Publora account, not in any workspace Yes — with the user's consent
already_in_workspace Already managed by your workspace No action needed (already yours)
managed_by_another_workspace Managed by a different workspace No
workspace_owner The user is itself a workspace owner No
inactive_user The account is deactivated No

When attachable: true and requiresConsent: true, attach the user with their consent. The consent proof is a valid access token belonging to that user: the user signs in to Publora (POST https://api.publora.com/auth/signin with their email and password) and the response's accessToken is passed as userAccessToken. The token must belong to the exact user being attached, or the request is rejected.

Endpoint: POST /api/v1/workspace/users/attach

Body parameters:

Parameter Type Required Description
email string One of email / userId The existing user's email (also accepted as username)
userId string One of email / userId The existing user's ObjectId (e.g. taken from the email_exists response)
userAccessToken string Yes (for a new attach) The target user's access token, proving their consent. Also accepted as attachToken in the body, or via the x-publora-user-token header. Not required when re-attaching a user who is already in your workspace.

If you pass both email and userId, they must refer to the same user (otherwise 400 identifier_mismatch).

JavaScript (fetch)

const response = await fetch(
  'https://api.publora.com/api/v1/workspace/users/attach',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
      email: 'user@example.com',                 // or userId: '6626a1f5e4b0c91a2d3f4567'
      userAccessToken: 'THE_USERS_ACCESS_TOKEN'  // the target user's access token (their consent)
    })
  }
);
 
const { user, alreadyInWorkspace } = await response.json();
console.log('Attached user:', user._id, '| alreadyInWorkspace:', alreadyInWorkspace);

Python (requests)

import requests
 
response = requests.post(
    'https://api.publora.com/api/v1/workspace/users/attach',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    },
    json={
        'email': 'user@example.com',                  # or 'userId': '6626a1f5e4b0c91a2d3f4567'
        'userAccessToken': 'THE_USERS_ACCESS_TOKEN'   # the target user's access token (their consent)
    }
)
 
data = response.json()
print(f"Attached user: {data['user']['_id']} | alreadyInWorkspace: {data['alreadyInWorkspace']}")

cURL

curl -X POST https://api.publora.com/api/v1/workspace/users/attach \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY" \
  -d '{
    "email": "user@example.com",
    "userAccessToken": "THE_USERS_ACCESS_TOKEN"
  }'

Node.js (axios)

const axios = require('axios');
 
const { data } = await axios.post(
  'https://api.publora.com/api/v1/workspace/users/attach',
  {
    email: 'user@example.com',                 // or userId: '6626a1f5e4b0c91a2d3f4567'
    userAccessToken: 'THE_USERS_ACCESS_TOKEN'  // the target user's access token (their consent)
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    }
  }
);
 
console.log('Attached user:', data.user._id, '| alreadyInWorkspace:', data.alreadyInWorkspace);

Success response (200):

{
  "user": {
    "_id": "6626a1f5e4b0c91a2d3f4567",
    "username": "user@example.com",
    "displayName": "John Doe",
    "role": "managed",
    "dailyPostsLeft": 100,
    "isActive": true
  },
  "alreadyInWorkspace": false
}

alreadyInWorkspace is true when the user was already managed by your workspace — the call is idempotent, so re-attaching is safe and does not require a consent token.

Attach errors:

Status code Meaning
400 missing_identifier Neither email nor userId was provided
400 invalid_email email is not a valid email address
400 invalid_user_id userId is not a valid ObjectId
400 identifier_mismatch email and userId refer to different users
400 cannot_attach_self The identifier is the workspace owner's own account
403 attach_consent_required userAccessToken was missing
403 invalid_attach_consent The consent token is invalid, expired, or revoked
403 attach_consent_mismatch The consent token belongs to a different user than the target
404 user_not_found No active user matches the identifier
409 user_inactive The target account is deactivated
409 user_is_workspace_owner The target is itself a workspace owner and cannot be managed
409 user_in_another_workspace The target already belongs to a different workspace
409 user_has_active_subscription The target has an active paid subscription; it must be cancelled before attaching
403 CHANNEL_LIMIT_REACHED Attaching the user's existing social connections would exceed your plan's connection limit

List All Managed Users

Each user object in the response includes the following fields:

Field Type Description
_id string The user's MongoDB ObjectId
displayName string The user's display name
username string The user's email/username
role string The user's role in the workspace
dailyPostsLeft number Remaining daily post quota
connectionsPageUrl string The user's connections page URL (if generated)
connectionsPageTokenExpiresAt string ISO 8601 expiry timestamp for the connections page token

JavaScript (fetch)

const response = await fetch(
  'https://api.publora.com/api/v1/workspace/users',
  {
    headers: { 'x-publora-key': 'YOUR_API_KEY' }
  }
);
 
const { users } = await response.json();
 
console.log(`Total managed users: ${users.length}`);
for (const user of users) {
  console.log(`  - ${user._id}: ${user.displayName} (${user.username}), role: ${user.role}, dailyPostsLeft: ${user.dailyPostsLeft}`);
}

Python (requests)

import requests
 
response = requests.get(
    'https://api.publora.com/api/v1/workspace/users',
    headers={'x-publora-key': 'YOUR_API_KEY'}
)
 
data = response.json()
users = data['users']
 
print(f"Total managed users: {len(users)}")
for user in users:
    print(f"  - {user['_id']}: {user['displayName']} ({user['username']})")

cURL

curl -s https://api.publora.com/api/v1/workspace/users \
  -H "x-publora-key: YOUR_API_KEY" | jq '.users[] | "\(._id): \(.displayName) (\(.username))"'

Node.js (axios)

const axios = require('axios');
 
const { data } = await axios.get(
  'https://api.publora.com/api/v1/workspace/users',
  {
    headers: { 'x-publora-key': 'YOUR_API_KEY' }
  }
);
 
const users = data.users;
console.log(`Total managed users: ${users.length}`);
for (const user of users) {
  console.log(`  - ${user._id}: ${user.displayName} (${user.username})`);
}

Generate a Connection URL

A connection URL is a temporary OAuth link that you send to your managed user. When they open it, they can authorize their social media accounts (Twitter, LinkedIn, Instagram, etc.) to be managed through your workspace.

Optional body parameters:

Parameter Type Default Description
expiresInDays number 90 Number of days until the connection URL expires. Must be a positive number. No upper bound is enforced by the API.
rotate boolean false If true, invalidates any previously generated connection URL for this user and generates a fresh one

JavaScript (fetch)

const userId = '6626a1f5e4b0c91a2d3f4567'; // The managed user's ID
 
const response = await fetch(
  `https://api.publora.com/api/v1/workspace/users/${userId}/connection-url`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
      expiresInDays: 30,  // Optional: default is 90
      rotate: true         // Optional: invalidate previous URL
    })
  }
);
 
const data = await response.json();
console.log('Connection URL:', data.url);
console.log('Expires:', data.tokenExpiresAt); // Default TTL: 90 days
 
// Send this URL to your user via email, in-app notification, etc.
// When they open it, they will be guided through connecting their social accounts.

Python (requests)

import requests
 
user_id = '6626a1f5e4b0c91a2d3f4567'
 
response = requests.post(
    f'https://api.publora.com/api/v1/workspace/users/{user_id}/connection-url',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    }
)
 
data = response.json()
print(f"Connection URL: {data['url']}")
print(f"Expires: {data['tokenExpiresAt']}")  # Default TTL: 90 days
 
# Send this URL to your user via email, in-app notification, etc.

cURL

curl -X POST "https://api.publora.com/api/v1/workspace/users/6626a1f5e4b0c91a2d3f4567/connection-url" \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY"

Node.js (axios)

const axios = require('axios');
 
const userId = '6626a1f5e4b0c91a2d3f4567';
 
const { data } = await axios.post(
  `https://api.publora.com/api/v1/workspace/users/${userId}/connection-url`,
  {},
  {
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    }
  }
);
 
console.log('Connection URL:', data.url);
console.log('Expires:', data.tokenExpiresAt); // Default TTL: 90 days
 
// Send this URL to your user via email, in-app notification, etc.

Generate a Per-User API Key

If you want a managed user to have their own API key (scoped only to their data), you can generate one.

Requirement: The workspace owner must have the apiAccess entitlement. The apiAccess check is performed against the workspace owner, not the managed user. If the owner has API access disabled, this endpoint will return a 403 error. Contact Publora support to ensure the appropriate entitlements are configured.

JavaScript (fetch)

const userId = '6626a1f5e4b0c91a2d3f4567';
 
const response = await fetch(
  `https://api.publora.com/api/v1/workspace/users/${userId}/api-key`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    }
  }
);
 
const data = await response.json();
console.log('Per-user API key:', data.apiKey);
console.log('User ID:', data.userId);
console.log('Message:', data.message);
 
// This key can be used in the x-publora-key header
// and will only have access to this specific user's data.

Python (requests)

import requests
 
user_id = '6626a1f5e4b0c91a2d3f4567'
 
response = requests.post(
    f'https://api.publora.com/api/v1/workspace/users/{user_id}/api-key',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    }
)
 
data = response.json()
print(f"Per-user API key: {data['apiKey']}")
print(f"User ID: {data['userId']}")
print(f"Message: {data['message']}")

cURL

curl -X POST "https://api.publora.com/api/v1/workspace/users/6626a1f5e4b0c91a2d3f4567/api-key" \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY"

Node.js (axios)

const axios = require('axios');
 
const userId = '6626a1f5e4b0c91a2d3f4567';
 
const { data } = await axios.post(
  `https://api.publora.com/api/v1/workspace/users/${userId}/api-key`,
  {},
  {
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY'
    }
  }
);
 
console.log('Per-user API key:', data.apiKey);
console.log('User ID:', data.userId);
console.log('Message:', data.message);

Post on Behalf of a Managed User

To create posts for a managed user, include the x-publora-user-id header with the managed user's ID alongside your workspace API key.

JavaScript (fetch)

const userId = '6626a1f5e4b0c91a2d3f4567';
 
const response = await fetch('https://api.publora.com/api/v1/create-post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-publora-key': 'YOUR_API_KEY',
    'x-publora-user-id': userId
  },
  body: JSON.stringify({
    content: 'Exciting update from Acme Corp! We just hit 10,000 customers.',
    platforms: ['twitter-123456', 'linkedin-ABCDEF'],
    scheduledTime: '2026-03-15T14:00:00.000Z'
  })
});
 
const post = await response.json();
console.log(`Post created for user ${userId}:`, post.postGroupId);

Python (requests)

import requests
 
user_id = '6626a1f5e4b0c91a2d3f4567'
 
response = requests.post(
    'https://api.publora.com/api/v1/create-post',
    headers={
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY',
        'x-publora-user-id': user_id
    },
    json={
        'content': 'Exciting update from Acme Corp! We just hit 10,000 customers.',
        'platforms': ['twitter-123456', 'linkedin-ABCDEF'],
        'scheduledTime': '2026-03-15T14:00:00.000Z'
    }
)
 
post = response.json()
print(f"Post created for user {user_id}: {post['postGroupId']}")

cURL

curl -X POST https://api.publora.com/api/v1/create-post \
  -H "Content-Type: application/json" \
  -H "x-publora-key: YOUR_API_KEY" \
  -H "x-publora-user-id: 6626a1f5e4b0c91a2d3f4567" \
  -d '{
    "content": "Exciting update from Acme Corp! We just hit 10,000 customers.",
    "platforms": ["twitter-123456", "linkedin-ABCDEF"],
    "scheduledTime": "2026-03-15T14:00:00.000Z"
  }'

Node.js (axios)

const axios = require('axios');
 
const userId = '6626a1f5e4b0c91a2d3f4567';
 
const { data: post } = await axios.post(
  'https://api.publora.com/api/v1/create-post',
  {
    content: 'Exciting update from Acme Corp! We just hit 10,000 customers.',
    platforms: ['twitter-123456', 'linkedin-ABCDEF'],
    scheduledTime: '2026-03-15T14:00:00.000Z'
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'x-publora-key': 'YOUR_API_KEY',
      'x-publora-user-id': userId
    }
  }
);
 
console.log(`Post created for user ${userId}:`, post.postGroupId);

Detach a Managed User

On success, the response body is { "success": true }.

JavaScript (fetch)

const userId = '6626a1f5e4b0c91a2d3f4567';
 
const response = await fetch(
  `https://api.publora.com/api/v1/workspace/users/${userId}`,
  {
    method: 'DELETE',
    headers: { 'x-publora-key': 'YOUR_API_KEY' }
  }
);
 
if (response.ok) {
  const data = await response.json();
  console.log(`User ${userId} has been detached. Success:`, data.success);
} else {
  const error = await response.json();
  console.error('Failed to detach user:', error.error || error.message);
}

Python (requests)

import requests
 
user_id = '6626a1f5e4b0c91a2d3f4567'
 
response = requests.delete(
    f'https://api.publora.com/api/v1/workspace/users/{user_id}',
    headers={'x-publora-key': 'YOUR_API_KEY'}
)
 
if response.ok:
    print(f"User {user_id} has been detached.")
else:
    error = response.json()
    print(f"Failed to detach user: {error.get('error') or error.get('message')}")

cURL

curl -X DELETE "https://api.publora.com/api/v1/workspace/users/6626a1f5e4b0c91a2d3f4567" \
  -H "x-publora-key: YOUR_API_KEY"

Node.js (axios)

const axios = require('axios');
 
const userId = '6626a1f5e4b0c91a2d3f4567';
 
try {
  await axios.delete(
    `https://api.publora.com/api/v1/workspace/users/${userId}`,
    {
      headers: { 'x-publora-key': 'YOUR_API_KEY' }
    }
  );
 
  console.log(`User ${userId} has been detached.`);
} catch (error) {
  console.error('Failed to detach user:', error.response?.data?.error);
}

Full B2B Onboarding Workflow

This example shows the complete workflow: create a managed user, generate a connection URL, wait for them to connect, then post on their behalf.

JavaScript (fetch)

async function onboardClient(username, displayName) {
  const headers = {
    'Content-Type': 'application/json',
    'x-publora-key': 'YOUR_API_KEY'
  };
 
  // Step 1: Create the managed user
  const createResponse = await fetch(
    'https://api.publora.com/api/v1/workspace/users',
    {
      method: 'POST',
      headers,
      body: JSON.stringify({ username, displayName })
    }
  );
 
  const { user } = await createResponse.json();
  console.log(`1. Created user: ${user._id} (${user.displayName})`);
 
  // Step 2: Generate a connection URL for them to auth their social accounts
  const connectionResponse = await fetch(
    `https://api.publora.com/api/v1/workspace/users/${user._id}/connection-url`,
    {
      method: 'POST',
      headers
    }
  );
 
  const connectionData = await connectionResponse.json();
  console.log(`2. Connection URL: ${connectionData.url}`);
  console.log(`   Expires: ${connectionData.tokenExpiresAt}`);
  console.log('   Send this link to the client so they can connect their social accounts.');
 
  // Step 3: (After user connects accounts) Check their connections
  // In production, you would wait for a webhook or poll periodically.
  const connectionsResponse = await fetch(
    'https://api.publora.com/api/v1/platform-connections',
    {
      headers: {
        'x-publora-key': 'YOUR_API_KEY',
        'x-publora-user-id': user._id
      }
    }
  );
 
  const { connections } = await connectionsResponse.json();
  console.log(`3. User has ${connections.length} connected accounts.`);
 
  if (connections.length === 0) {
    console.log('   User has not connected any accounts yet.');
    return user;
  }
 
  // Step 4: Post on their behalf
  const platforms = connections.map(c => c.platformId);
 
  const postResponse = await fetch('https://api.publora.com/api/v1/create-post', {
    method: 'POST',
    headers: {
      ...headers,
      'x-publora-user-id': user._id
    },
    body: JSON.stringify({
      content: `Welcome to ${displayName}'s social presence, powered by our platform!`,
      platforms,
      scheduledTime: '2026-03-15T14:00:00.000Z'
    })
  });
 
  const post = await postResponse.json();
  console.log(`4. Post scheduled for user ${user._id}: ${post.postGroupId}`);
 
  return user;
}
 
// Usage
const client = await onboardClient('acme-corp', 'Acme Corp');

Python (requests)

import requests
 
 
def onboard_client(username, display_name):
    api_url = 'https://api.publora.com/api/v1'
    headers = {
        'Content-Type': 'application/json',
        'x-publora-key': 'YOUR_API_KEY'
    }
 
    # Step 1: Create the managed user
    user_response = requests.post(
        f'{api_url}/workspace/users',
        headers=headers,
        json={'username': username, 'displayName': display_name}
    )
    user = user_response.json()['user']
    print(f"1. Created user: {user['_id']} ({user['displayName']})")
 
    # Step 2: Generate a connection URL
    connection_response = requests.post(
        f"{api_url}/workspace/users/{user['_id']}/connection-url",
        headers=headers
    )
    connection_data = connection_response.json()
    print(f"2. Connection URL: {connection_data['url']}")
    print(f"   Expires: {connection_data['tokenExpiresAt']}")
    print('   Send this link to the client.')
 
    # Step 3: Check their connections (after they connect)
    user_headers = {**headers, 'x-publora-user-id': user['_id']}
    connections_response = requests.get(
        f'{api_url}/platform-connections',
        headers=user_headers
    )
    connections = connections_response.json()['connections']
    print(f"3. User has {len(connections)} connected accounts.")
 
    if not connections:
        print('   User has not connected any accounts yet.')
        return user
 
    # Step 4: Post on their behalf
    platform_ids = [c['platformId'] for c in connections]
    post_response = requests.post(
        f'{api_url}/create-post',
        headers=user_headers,
        json={
            'content': f"Welcome to {display_name}'s social presence, powered by our platform!",
            'platforms': platform_ids,
            'scheduledTime': '2026-03-15T14:00:00.000Z'
        }
    )
    post = post_response.json()
    print(f"4. Post scheduled for user {user['_id']}: {post['postGroupId']}")
 
    return user
 
 
# Usage
client = onboard_client('acme-corp', 'Acme Corp')

Best Practices

  1. Store managed user IDs. After creating a managed user, persist their id in your own database. You will need it for all subsequent operations on their behalf.

  2. Send connection URLs promptly. Connection URLs expire after 90 days by default (see tokenExpiresAt in the response). Send them to your users as soon as they are generated, and regenerate if needed.

  3. Use per-user API keys for client-side integrations. If your managed users need to interact with the API directly (e.g., from their own dashboard), generate per-user API keys instead of sharing your workspace key.

  4. Never expose your workspace API key. Your workspace key has full access to all managed users. Keep it server-side only. Use per-user API keys for any client-facing scenarios.

  5. Monitor plan entitlements. The actual posting limits come from the workspace owner's plan (monthlyPosts, scheduledPosts, scheduleHorizonDays). The dailyPostsLeft field is stored per user but not currently enforced. If you are building a high-volume integration, track your plan's monthly and scheduled post limits and queue posts accordingly.

  6. Detach unused users. If a managed user is no longer needed (e.g., they cancel their subscription with you), detach them via the DELETE endpoint. This removes the workspace association but preserves the user record.

  7. Always include both headers when acting on behalf of a user. For any post, connection, or media operation on a managed user's behalf, you need both x-publora-key (your workspace key) and x-publora-user-id (the managed user's ID).

Common Issues

Problem Cause Solution
400 "Email is required" The username field is missing or empty in the create-user request body Include a non-empty username field in the request body
409 "User with this email already exists" (code: "email_exists") An account with that email already exists. Inspect attachable / reason in the response: it may be your own managed user, another workspace's user, or a standalone account you can attach If attachable: true, attach it via Attach an Existing User; if alreadyInWorkspace: true, no action is needed; otherwise use a different email
403 attach_consent_required on attach The userAccessToken (target user's consent token) was missing Have the target user sign in (POST /auth/signin) and pass the returned accessToken as userAccessToken
409 user_has_active_subscription on attach The target user has their own active paid subscription The user must cancel their subscription before they can be attached as a managed user
409 user_in_another_workspace on attach The target user is already managed by a different workspace They must be detached from that workspace first
403 "Workspace access is not enabled for this key" Workspace feature not enabled for your account Contact serge@publora.com to enable Workspace API access
401 when using workspace endpoints Invalid workspace API key Verify the x-publora-key value is your workspace-level API key
403 "User is not managed by key" when posting on behalf of a user The x-publora-user-id refers to a user whose parentUser is not set to your user ID Ensure the managed user has parentUser set to your user ID (i.e., they belong to your workspace)
404 "User not found or not managed by this key" when detaching a user The user ID does not exist or is not managed by your workspace Verify the user ID is correct and belongs to your workspace via GET /api/v1/workspace/users
User has no connections They have not opened the connection URL yet, or the URL expired Generate a new connection URL and send it to the user
Connection URL expired The URL has a limited lifespan Generate a fresh connection URL via POST /workspace/users/:userId/connection-url
403 limit reached Monthly or scheduled post limit exceeded per the workspace owner's plan entitlements Upgrade the plan or wait for the next billing cycle; contact Publora to discuss higher limits
Posts not appearing for a managed user Using your own API key without the x-publora-user-id header Include the x-publora-user-id header so the post is created under the managed user's account
403 "API access is not enabled for this workspace owner" The workspace owner's plan does not include the apiAccess entitlement, which is required for generating per-user API keys Contact Publora support to ensure the workspace owner's plan includes API access
Per-user API key does not work Key was regenerated, invalidating the old one Use the latest generated key; generating a new key invalidates previous keys

Publora is built by Creative Content Crafts, Inc. Need AI-powered content creation for LinkedIn, Threads, and X? Try Co.Actor -- the best AI service for authentic thought leadership at scale.