PostPost

Error Handling

This guide covers how to handle errors from the PostPost API, including HTTP status codes, error response formats, retry strategies, and dealing with partial failures.

How It Works

The PostPost API uses standard HTTP status codes to indicate success or failure. When an error occurs, the response body contains a JSON object with details about what went wrong.

HTTP Status Codes

CodeMeaningDescription
200OKRequest succeeded (GET, PUT, DELETE, and most POST endpoints including create-post)
201CreatedResource created successfully (used by specific POST endpoints such as workspace user creation)

Note: Most POST endpoints (including create-post) return 200 on success, but workspace user creation (POST /workspace/users) returns 201. Check for both 200 and 201 as success codes in your error handling logic. | 400 | Bad Request | Invalid input, missing required fields, or malformed data | | 401 | Unauthorized | Missing or invalid API key | | 403 | Forbidden | Valid API key but insufficient permissions or plan limits reached | | 404 | Not Found | Resource does not exist or does not belong to your account | | 409 | Conflict | Duplicate or conflicting operation | | 500 | Internal Server Error | Something went wrong on PostPost's side |

Error Response Format

Errors are returned as JSON in one of two formats:

{
  "error": "Human-readable error message"
}

or

{
  "message": "Detailed description of what went wrong"
}

Common Errors

StatusError MessageCauseResolution
401"API key is required"The x-api-key header is missingInclude a valid API key in the x-api-key header
401"Invalid API key"The x-api-key header contains an invalid or revoked keyVerify your API key is correct
401"Invalid API key owner"Workspace resolution failed for the API key (the key exists but its owner account could not be resolved)Verify the API key is associated with a valid account, or regenerate the key
403"API access is not enabled for this account"Your account is on the Starter plan, which does not include API accessUpgrade to Pro or Premium to use the API
403"Workspace access is not enabled for this key"The API key does not have workspace-level access enabledVerify the API key has the correct permissions, or generate a new key with workspace access
403"Subscription required"Your account does not have an active subscription. Response includes message: "No active plan entitlements found for this account." Note: This error is currently unreachable in practice because resolveEntitlementsForOwner() always falls back to the Starter plan when no paid subscription exists. The middleware code exists and is documented here for completeness, but with the current entitlement resolution logic this error cannot occur.Subscribe to a paid plan
403"Account on hold"Your account has been placed on hold. Response includes message: "Your account is temporarily on hold. Please contact support.", holdExpiresAt (ISO 8601 timestamp), and holdReason fields. Note: This error only applies to dashboard routes, not the external REST API. The checkHold middleware is not applied to external API endpoints.Wait until the hold expires or contact support to resolve the issue
403"Account inactive"Your account has been deactivated. Response includes message: "Your account is inactive." Note: This error is currently unreachable. The checkSubscription middleware is imported but never mounted on any route (dead code), so this error cannot occur on either dashboard or external API endpoints. It is documented here for completeness.Contact support@postpost.dev to reactivate your account
403Limit exceeded (structured)You have exceeded a usage limit for your plan. Response uses LimitExceededError format with fields: code, error (short label), message (long text), metric, limit, used, requested, remaining, periodStart, periodEnd, planName, plus context-specific fields (see below)Wait for the current period to reset, reduce usage, or upgrade your plan
403"MCP access is not enabled for this account"Your account does not have MCP access enabledContact support or upgrade your plan to enable MCP access
400"Invalid x-postpost-user-id"The x-postpost-user-id header contains a malformed user ID (not a valid ObjectId format). A valid-format but non-existent user ID returns 403 "User is not managed by key" insteadVerify the user ID is a valid ObjectId and belongs to the account associated with the MCP API key
400"Invalid scheduled time format"The scheduledTime is not a valid ISO 8601 stringUse a valid time in the format YYYY-MM-DDTHH:mm:ss.sssZ
404"Post group not found"The post group ID does not exist or does not belong to your accountVerify the ID and ensure you are using the correct API key
400"Cannot update post: post is currently in {status} status"Attempting to modify a post that is in a non-editable status (e.g., published, failed)Only draft and scheduled posts can be updated. The external API and dashboard have different update rules. The external API rejects any post not in draft or scheduled status. The dashboard controller rejects posts in published or failed status — meaning partially_published posts can be updated via the dashboard but not via the API. Neither checks processingStatus. Note: processing is not a value of the status field — it is tracked separately via the processingStatus field (values: pending, processing, finished). The status field values are: draft, scheduled, published, failed, partially_published.

Post Creation Errors

StatusError MessageCauseResolution
400"Content is required"The content field is empty or missing in a create-post requestProvide a non-empty content string
400"Platforms are required"The platforms field is missing from the request bodyInclude a platforms field in the request
400"At least one platform is required"The platforms array is present but emptyAdd at least one platform ID to the platforms array
400"Invalid platform ID format: {platformId}"A platform ID in the platforms array does not match the expected {platform}-{id} formatUse the correct format, e.g., "twitter-123456"
400"Invalid platforms JSON format"The platforms field could not be parsed as valid JSONEnsure platforms is a valid JSON array
400"Platforms must be an array"The platforms field is not an array (e.g., a string or object)Pass platforms as a JSON array of platform ID strings
400"Invalid platformSettings JSON"The platformSettings field could not be parsed as valid JSONEnsure platformSettings is a valid JSON object
400"platformSettings must be an object"The platformSettings field is not a JSON objectPass platformSettings as a JSON object, not an array or primitive

Post Update Errors

StatusError MessageCauseResolution
400"Either status or scheduledTime must be provided"An update-post request was sent without specifying status or scheduledTimeInclude at least one of status or scheduledTime in the request body
400"Status must be either 'draft' or 'scheduled'"The status field in an update-post request contains an invalid valueSet status to either "draft" or "scheduled"

Media Upload Errors

StatusError MessageCauseResolution
400"fileName, contentType, and postGroupId are required"A get-upload-url request is missing one or more required fieldsInclude all three fields: fileName, contentType, and postGroupId

LimitExceededError Codes

The code field in a LimitExceededError response identifies the specific limit that was exceeded:

CodeDescription
POST_LIMIT_REACHEDMonthly post limit for the plan has been reached
SCHEDULED_POST_LIMIT_REACHEDMaximum number of pending scheduled posts reached
SCHEDULE_HORIZON_REACHEDScheduled time exceeds the plan's maximum scheduling horizon
PLATFORM_NOT_AVAILABLEThe target platform is not available on the current plan
CONNECTIONS_OVER_LIMITNumber of connected accounts exceeds the plan limit
CHANNEL_LIMIT_REACHEDMaximum number of channels for the plan has been reached

Note on CHANNEL_LIMIT_REACHED: The checkConnectionLimit middleware returns a different response format from the standard LimitExceededError. When canExpand: true, the response includes planKey, planName, currentQuantity, targetQuantity, connectionCount, and pricing (expansion pricing details). When canExpand: false, the response varies depending on whether the user's billing is managed by a workspace owner. For billing-managed users, only success, code, error, and canExpand are returned. For non-managed users, connectionCount is also included in the response. If you are handling connection limit errors specifically, parse these fields rather than the standard LimitExceededError shape.

LimitExceededError Context Fields

In addition to the standard fields (code, error, message, metric, limit, used, requested, remaining, periodStart, periodEnd, planName), the response spreads ...this.context into the top-level response object. This means context-specific fields appear conditionally depending on the error code -- they are not guaranteed to be present on every LimitExceededError response:

Context FieldAppears WithDescription
overLimitByCONNECTIONS_OVER_LIMITHow many units over the limit the request would go
disallowedPlatformsPLATFORM_NOT_AVAILABLEArray of platforms in the request that are not available on the current plan
allowedPlatformsPLATFORM_NOT_AVAILABLEArray of platforms that are available on the current plan
projectedUsedCHANNEL_LIMIT_REACHEDThe projected usage count if the operation were to proceed
blockedPlatformsPOST_LIMIT_REACHED, SCHEDULED_POST_LIMIT_REACHEDArray of objects with platformSelection, used, and remaining fields. Present when scope is connection-level
scopeVariousThe scope of the limit (e.g., "account", "connection")
scheduledTimeSCHEDULE_HORIZON_REACHEDThe requested scheduled time that exceeded the horizon
maxScheduledDateSCHEDULE_HORIZON_REACHEDThe latest allowed scheduled date for the current plan

Threads-Specific Errors

Error MessageCauseResolution
"Multi-part threads (nested replies) are temporarily blocked while we wait for Meta to approve additional permissions for our app."Multi-part threads (content >500 chars or with --- separators) are temporarily disabled due to Threads API access requirementsKeep content under 500 characters without --- separators. Use carousel posts for multiple images. Contact support@postpost.dev for updates.

Note: Single posts (under 500 characters) and carousel posts on Threads continue to work normally. Only multi-part threads (where content is automatically split into multiple connected replies) are temporarily restricted.

Post-Level Errors (Partial Failures)

A post group can target multiple platforms. Even if some platforms succeed, others may fail. This results in a partially_published status on the post group.

When checking a post group's status, each individual platform post has its own status field:

{
  "success": true,
  "postGroupId": "664f1a2b3c4d5e6f7a8b9c0d",
  "posts": [
    {
      "platform": "twitter",
      "platformId": "123456",
      "status": "published",
      "content": "Check out our latest product launch!",
      "postedId": "1234567890123456789"
    },
    {
      "platform": "tiktok",
      "platformId": "789012",
      "status": "failed",
      "error": {
        "code": "PLATFORM_VALIDATION_ERROR",
        "message": "Video FPS is below minimum requirement",
        "platformStatusCode": null,
        "platformError": null,
        "failedAt": "2026-03-15T14:01:12.000Z",
        "retryable": false
      }
    },
    {
      "platform": "linkedin",
      "platformId": "ABCDEF",
      "status": "published",
      "content": "Check out our latest product launch!",
      "postedId": "urn:li:share:7000000000000000000"
    }
  ]
}

Note: The content and status fields exist on each individual post within the posts array, not at the post group level. The top-level response only contains success, postGroupId, and posts.

error field format: The error field on individual platform posts is a structured object, not a plain string. The get-post endpoint returns the full error object with fields: code, message, platformStatusCode, platformError, failedAt, and retryable.

platformId vs platform ID string: In the get-post response, platform and platformId are separate fields. The platform field contains the platform name (e.g., "twitter"), and platformId contains just the account identifier (e.g., "123456"). This is different from the {platform}-{id} combined format (e.g., "twitter-123456") used in the platforms array when creating posts.

Platform-Specific Validation Errors

Some platforms have strict media requirements that are checked before publishing:

  • TikTok: Minimum FPS requirement, specific video format requirements, duration limits
  • Instagram: Video aspect ratio and duration requirements for Reels
  • YouTube: Video format and size requirements

These errors appear at the individual platform post level, not at the HTTP response level.

Examples

Error Handling Wrapper (JavaScript)

class PostPostApiError extends Error {
  constructor(status, message, body) {
    super(message);
    this.name = 'PostPostApiError';
    this.status = status;
    this.body = body;
  }
}

async function postpostRequest(endpoint, options = {}) {
  const url = `https://api.postpost.dev/api/v1${endpoint}`;

  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': 'YOUR_API_KEY',
      ...options.headers
    }
  });

  const body = await response.json();

  if (!response.ok) {
    const message = body.error || body.message || 'Unknown API error';
    throw new PostPostApiError(response.status, message, body);
  }

  return body;
}

// Usage
try {
  const post = await postpostRequest('/create-post', {
    method: 'POST',
    body: JSON.stringify({
      content: 'Hello from PostPost!',
      platforms: ['twitter-123456'],
      scheduledTime: '2026-03-15T14:00:00.000Z'
    })
  });

  console.log('Post created:', post.postGroupId);
} catch (error) {
  if (error instanceof PostPostApiError) {
    switch (error.status) {
      case 401:
        console.error('Authentication failed. Check your API key.');
        break;
      case 403:
        console.error('Access denied:', error.message);
        // Could be "Subscription required", limit exceeded, or account hold
        break;
      case 400:
        console.error('Bad request:', error.message);
        // Could be "Invalid scheduled time" or validation errors
        break;
      case 404:
        console.error('Resource not found:', error.message);
        break;
      case 409:
        console.error('Conflict:', error.message);
        break;
      case 500:
        console.error('Server error. Retry later.');
        break;
      default:
        console.error(`API error (${error.status}):`, error.message);
    }
  } else {
    console.error('Network or unexpected error:', error.message);
  }
}

Error Handling Wrapper (Python)

import requests


class PostPostApiError(Exception):
    def __init__(self, status_code, message, body):
        super().__init__(message)
        self.status_code = status_code
        self.body = body


class PostPostClient:
    BASE_URL = 'https://api.postpost.dev/api/v1'

    def __init__(self, api_key):
        self.session = requests.Session()
        self.session.headers.update({
            'Content-Type': 'application/json',
            'x-api-key': api_key
        })

    def _request(self, method, endpoint, **kwargs):
        url = f'{self.BASE_URL}{endpoint}'
        response = self.session.request(method, url, **kwargs)

        try:
            body = response.json()
        except ValueError:
            body = {}

        if not response.ok:
            message = body.get('error') or body.get('message') or 'Unknown API error'
            raise PostPostApiError(response.status_code, message, body)

        return body

    def create_post(self, content, platform_ids, scheduled_time=None):
        payload = {
            'content': content,
            'platforms': platform_ids
        }
        if scheduled_time:
            payload['scheduledTime'] = scheduled_time

        return self._request('POST', '/create-post', json=payload)

    def get_post(self, post_group_id):
        return self._request('GET', f'/get-post/{post_group_id}')


# Usage
client = PostPostClient('YOUR_API_KEY')

try:
    post = client.create_post(
        content='Hello from PostPost!',
        platforms=['twitter-123456'],
        scheduled_time='2026-03-15T14:00:00.000Z'
    )
    print(f"Post created: {post['postGroupId']}")

except PostPostApiError as e:
    if e.status_code == 401:
        print('Authentication failed. Check your API key.')
    elif e.status_code == 403:
        print(f'Access denied: {e}')
    elif e.status_code == 400:
        print(f'Bad request: {e}')
    elif e.status_code == 404:
        print(f'Resource not found: {e}')
    elif e.status_code == 500:
        print('Server error. Retry later.')
    else:
        print(f'API error ({e.status_code}): {e}')

except requests.RequestException as e:
    print(f'Network error: {e}')

cURL with Error Checking

#!/bin/bash

API_KEY="YOUR_API_KEY"
BASE_URL="https://api.postpost.dev/api/v1"

# Make a request and capture both HTTP status and body
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/create-post" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -d '{
    "content": "Hello from PostPost!",
    "platforms": ["twitter-123456"],
    "scheduledTime": "2026-03-15T14:00:00.000Z"
  }')

# Split response body and status code
HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -1)

case $HTTP_STATUS in
  200|201)
    echo "Success:"
    echo "$HTTP_BODY" | jq .
    ;;
  400)
    echo "Bad request:"
    echo "$HTTP_BODY" | jq '.error // .message'
    ;;
  401)
    echo "Authentication failed. Check your x-api-key header."
    ;;
  403)
    echo "Access denied:"
    echo "$HTTP_BODY" | jq '.error // .message'
    ;;
  404)
    echo "Not found:"
    echo "$HTTP_BODY" | jq '.error // .message'
    ;;
  500)
    echo "Server error. Retry later."
    ;;
  *)
    echo "Unexpected status: $HTTP_STATUS"
    echo "$HTTP_BODY"
    ;;
esac

Node.js (axios) with Error Handling

const axios = require('axios');

const api = axios.create({
  baseURL: 'https://api.postpost.dev/api/v1',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
  }
});

// Add a response interceptor for centralized error handling
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response) {
      const { status, data } = error.response;
      const message = data.error || data.message || 'Unknown error';

      switch (status) {
        case 401:
          console.error('Authentication failed. Check your API key.');
          break;
        case 403:
          console.error('Access denied:', message);
          break;
        case 400:
          console.error('Bad request:', message);
          break;
        case 404:
          console.error('Not found:', message);
          break;
        case 500:
          console.error('Server error. Retry later.');
          break;
        default:
          console.error(`API error (${status}):`, message);
      }
    } else if (error.request) {
      console.error('No response received. Check your network connection.');
    } else {
      console.error('Request setup error:', error.message);
    }

    return Promise.reject(error);
  }
);

// Usage
try {
  const { data: post } = await api.post('/create-post', {
    content: 'Hello from PostPost!',
    platforms: ['twitter-123456'],
    scheduledTime: '2026-03-15T14:00:00.000Z'
  });

  console.log('Post created:', post.postGroupId);
} catch (error) {
  // Error already logged by the interceptor
  // Add any additional handling here
}

Retry Logic with Exponential Backoff

Server errors (500) and network failures are usually transient. Implement retry logic for these cases.

JavaScript (fetch)

async function postpostRequestWithRetry(endpoint, options = {}, maxRetries = 3) {
  const url = `https://api.postpost.dev/api/v1${endpoint}`;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'YOUR_API_KEY',
          ...options.headers
        }
      });

      const body = await response.json();

      if (response.ok) {
        return body;
      }

      // Do not retry client errors (4xx) -- they will not succeed on retry
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error ${response.status}: ${body.error || body.message}`);
      }

      // Retry on server errors (5xx)
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        console.log(`Server error (${response.status}). Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw new Error(`Server error ${response.status} after ${maxRetries} retries`);

    } catch (error) {
      // Retry on network errors
      if (error.name === 'TypeError' && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`Network error. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }
}

// Usage
try {
  const post = await postpostRequestWithRetry('/create-post', {
    method: 'POST',
    body: JSON.stringify({
      content: 'Retryable post!',
      platforms: ['twitter-123456'],
      scheduledTime: '2026-03-15T14:00:00.000Z'
    })
  });

  console.log('Post created:', post.postGroupId);
} catch (error) {
  console.error('Failed after retries:', error.message);
}

Python (requests)

import time
import requests


def postpost_request_with_retry(method, endpoint, max_retries=3, **kwargs):
    url = f'https://api.postpost.dev/api/v1{endpoint}'
    headers = {
        'Content-Type': 'application/json',
        'x-api-key': 'YOUR_API_KEY'
    }

    for attempt in range(max_retries + 1):
        try:
            response = requests.request(method, url, headers=headers, **kwargs)
            body = response.json()

            if response.ok:
                return body

            # Do not retry client errors (4xx)
            if 400 <= response.status_code < 500:
                message = body.get('error') or body.get('message') or 'Unknown error'
                raise Exception(f'Client error {response.status_code}: {message}')

            # Retry on server errors (5xx)
            if attempt < max_retries:
                delay = (2 ** attempt)  # 1s, 2s, 4s
                print(f'Server error ({response.status_code}). Retrying in {delay}s...')
                time.sleep(delay)
                continue

            raise Exception(f'Server error {response.status_code} after {max_retries} retries')

        except requests.ConnectionError:
            if attempt < max_retries:
                delay = (2 ** attempt)
                print(f'Network error. Retrying in {delay}s...')
                time.sleep(delay)
                continue
            raise


# Usage
try:
    post = postpost_request_with_retry(
        'POST', '/create-post',
        json={
            'content': 'Retryable post!',
            'platforms': ['twitter-123456'],
            'scheduledTime': '2026-03-15T14:00:00.000Z'
        }
    )
    print(f"Post created: {post['postGroupId']}")
except Exception as e:
    print(f'Failed after retries: {e}')

Node.js (axios)

const axios = require('axios');

async function postpostRequestWithRetry(method, endpoint, data = null, maxRetries = 3) {
  const config = {
    method,
    url: `https://api.postpost.dev/api/v1${endpoint}`,
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': 'YOUR_API_KEY'
    },
    data
  };

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios(config);
      return response.data;
    } catch (error) {
      const status = error.response?.status;

      // Do not retry client errors
      if (status && status >= 400 && status < 500) {
        const message = error.response.data?.error || error.response.data?.message;
        throw new Error(`Client error ${status}: ${message}`);
      }

      // Retry on server errors or network errors
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000;
        const reason = status ? `Server error (${status})` : 'Network error';
        console.log(`${reason}. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }
}

// Usage
try {
  const post = await postpostRequestWithRetry('POST', '/create-post', {
    content: 'Retryable post!',
    platforms: ['twitter-123456'],
    scheduledTime: '2026-03-15T14:00:00.000Z'
  });

  console.log('Post created:', post.postGroupId);
} catch (error) {
  console.error('Failed after retries:', error.message);
}

Checking for Partial Failures

After a post group has been processed, check whether all platforms succeeded or if some failed.

JavaScript (fetch)

async function checkPostGroupStatus(postGroupId) {
  const response = await fetch(
    `https://api.postpost.dev/api/v1/get-post/${postGroupId}`,
    {
      headers: { 'x-api-key': 'YOUR_API_KEY' }
    }
  );

  const postGroup = await response.json();

  // Determine overall status from individual posts
  const posts = postGroup.posts || [];
  const statuses = posts.map(p => p.status);
  const allPublished = statuses.every(s => s === 'published');
  const allFailed = statuses.every(s => s === 'failed');
  const overallStatus = allPublished ? 'published' : allFailed ? 'failed' : 'partially_published';

  console.log(`Post group ${postGroup.postGroupId}: ${overallStatus}`);

  if (overallStatus === 'partially_published') {
    console.log('Some platforms failed:');

    const failed = postGroup.posts.filter(p => p.status === 'failed');
    const succeeded = postGroup.posts.filter(p => p.status === 'published');

    console.log(`  Succeeded: ${succeeded.length}`);
    for (const p of succeeded) {
      console.log(`    - ${p.platform}: ${p.postedId}`);
    }

    console.log(`  Failed: ${failed.length}`);
    for (const p of failed) {
      console.log(`    - ${p.platform}: ${p.error}`);
    }

    return { status: 'partial', succeeded, failed };
  }

  if (overallStatus === 'failed') {
    console.log('All platforms failed.');
    for (const p of posts) {
      console.log(`  - ${p.platform}: ${p.error}`);
    }
    return { status: 'failed', succeeded: [], failed: posts };
  }

  if (overallStatus === 'published') {
    console.log('All platforms succeeded.');
    return { status: 'success', succeeded: posts, failed: [] };
  }

  // Still processing or scheduled (posts array may be empty or have pending statuses)
  console.log('Post is still being processed');
  return { status: 'pending', succeeded: [], failed: [] };
}

// Usage
const result = await checkPostGroupStatus('664f1a2b3c4d5e6f7a8b9c0d');
if (result.failed.length > 0) {
  // Take action: notify team, retry failed platforms, etc.
}

Python (requests)

import requests

API_URL = 'https://api.postpost.dev/api/v1'
HEADERS = {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_API_KEY'
}


def check_post_group_status(post_group_id):
    response = requests.get(
        f'{API_URL}/get-post/{post_group_id}',
        headers=HEADERS
    )

    post_group = response.json()
    posts = post_group.get('posts', [])

    # Determine overall status from individual posts
    statuses = [p['status'] for p in posts]
    all_published = all(s == 'published' for s in statuses)
    all_failed = all(s == 'failed' for s in statuses)

    if all_published:
        overall_status = 'published'
    elif all_failed:
        overall_status = 'failed'
    elif posts:
        overall_status = 'partially_published'
    else:
        overall_status = 'pending'

    print(f"Post group {post_group['postGroupId']}: {overall_status}")

    if overall_status == 'partially_published':
        print('Some platforms failed:')

        failed = [p for p in posts if p['status'] == 'failed']
        succeeded = [p for p in posts if p['status'] == 'published']

        print(f"  Succeeded: {len(succeeded)}")
        for p in succeeded:
            print(f"    - {p['platform']}: {p.get('postedId', 'N/A')}")

        print(f"  Failed: {len(failed)}")
        for p in failed:
            print(f"    - {p['platform']}: {p.get('error', 'Unknown error')}")

        return {'status': 'partial', 'succeeded': succeeded, 'failed': failed}

    if overall_status == 'failed':
        print('All platforms failed.')
        for p in posts:
            print(f"  - {p['platform']}: {p.get('error', 'Unknown error')}")
        return {'status': 'failed', 'succeeded': [], 'failed': posts}

    if overall_status == 'published':
        print('All platforms succeeded.')
        return {'status': 'success', 'succeeded': posts, 'failed': []}

    print(f'Post is still being processed')
    return {'status': 'pending', 'succeeded': [], 'failed': []}


# Usage
result = check_post_group_status('664f1a2b3c4d5e6f7a8b9c0d')
if result['failed']:
    # Take action: notify team, retry failed platforms, etc.
    pass

Best Practices

  1. Always check the HTTP status code. Do not assume every response is successful. Parse the error body for details.

  2. Only retry on 5xx errors and network failures. Client errors (4xx) indicate a problem with your request that retrying will not fix.

  3. Use exponential backoff for retries. Start at 1 second and double with each attempt. Cap at 3-5 retries to avoid infinite loops.

  4. Monitor for partial failures. A 200 response when creating a post does not guarantee all platforms will succeed. Poll the post group status after the scheduled time to detect partial failures.

  5. Log error responses in full. When debugging, log the entire response body, not just the error message. Additional fields may provide context.

  6. Handle 403 errors gracefully in your UI. If a user hits a plan limit (returned as a structured LimitExceededError), parse the metric, limit, used, and remaining fields to show actionable guidance rather than a raw error.

  7. Validate inputs before sending. Check that scheduledTime is in the future and in ISO 8601 format, platform IDs follow the {platform}-{id} format, and media counts are within limits. This avoids unnecessary 400 errors.

Common Issues

ProblemCauseSolution
401 on every requestAPI key not set or incorrectEnsure x-api-key header is present with a valid key
403 but key is validAccount has no active subscription, hit a usage limit, or account is on hold/inactiveCheck subscription status, review the structured error response for limit details, or contact support
400 when schedulingscheduledTime is in the past or malformedAlways send a future UTC time as YYYY-MM-DDTHH:mm:ss.sssZ
404 when fetching a postPost group ID is wrong or belongs to another accountVerify the ID and that you are using the correct API key
Post group shows partially_publishedSome platforms failed while others succeededInspect individual platform post statuses for error details
TikTok post fails with FPS errorVideo frame rate below TikTok's minimum requirementRe-encode the video with at least 24 FPS before uploading
500 intermittent errorsTemporary server issuesImplement retry logic with exponential backoff
Threads post fails with nested thread errorMulti-part nested threads are temporarily disabled pending Meta permissions approvalKeep content under 500 characters or use carousel posts. Contact support@postpost.dev for updates.

On this page