Best Practices

Tips and recommendations for using the PostCore API

Best Practices

Follow these best practices to get the most out of PostCore and avoid common pitfalls.

API Key Management

Use Environment Variables

Never hardcode API keys in your source code:

// ❌ Bad
const apiKey = "pc_live_abc123...";

// ✅ Good
const apiKey = process.env.POSTCORE_API_KEY;

Separate Keys for Environments

Use different API keys for development and production:

const apiKey =
  process.env.NODE_ENV === "production"
    ? process.env.POSTCORE_PROD_KEY
    : process.env.POSTCORE_DEV_KEY;

Rotate Keys Regularly

Create new API keys every 90 days:

  1. Create new key
  2. Update your application
  3. Test thoroughly
  4. Delete old key

Delete Unused Keys

Remove API keys immediately when:

  • A team member leaves
  • You stop using an integration
  • A key may have been compromised

Token Expiry Management

Monitor LinkedIn Tokens

LinkedIn tokens expire after 60 days. Check needs_reconnect regularly:

async function checkProfiles() {
  const profiles = await fetch("https://api.postcore.dev/profiles", {
    headers: { "x-api-key": apiKey },
  }).then((r) => r.json());

  const needsReconnect = profiles.filter((p) => p.needs_reconnect);

  if (needsReconnect.length > 0) {
    console.warn("These profiles need reconnection:", needsReconnect);
    // Send email/notification to user
  }
}

// Check weekly
setInterval(checkProfiles, 7 * 24 * 60 * 60 * 1000);

Proactive Reconnection

Don't wait for tokens to expire. Send reminders at 50 and 55 days:

const FIFTY_DAYS = 50 * 24 * 60 * 60 * 1000;

profiles.forEach((profile) => {
  if (profile.platform === "linkedin") {
    const age = Date.now() - new Date(profile.created_at).getTime();

    if (age > FIFTY_DAYS && !profile.needs_reconnect) {
      sendReminderEmail(profile, "Your LinkedIn connection expires in 10 days");
    }
  }
});

Rate Limit Handling

Respect Rate Limits

Don't spam the API. Use the rate limit headers:

async function makeRequest(url, options) {
  const response = await fetch(url, options);

  const remaining = response.headers.get("X-RateLimit-Remaining");
  const reset = response.headers.get("X-RateLimit-Reset");

  if (remaining < 10) {
    const waitTime = reset * 1000 - Date.now();
    console.warn(`Only ${remaining} requests left. Resets in ${waitTime}ms`);
  }

  return response;
}

Implement Backoff

When you hit a rate limit, wait before retrying:

async function schedulePostWithRetry(postData, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch("https://api.postcore.dev/posts", {
        method: "POST",
        headers: {
          "x-api-key": apiKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(postData),
      });

      if (response.status === 429) {
        const error = await response.json();

        if (error.next_available) {
          // Platform rate limit - use next_available time
          postData.scheduled_for = error.next_available;
          continue;
        } else if (error.retry_after) {
          // API rate limit - wait and retry
          await new Promise((resolve) =>
            setTimeout(resolve, error.retry_after * 1000)
          );
          continue;
        }
      }

      return await response.json();
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      await new Promise((resolve) =>
        setTimeout(resolve, 1000 * Math.pow(2, attempt))
      );
    }
  }
}

Batch Requests Wisely

Space out bulk operations:

async function scheduleManyPosts(posts) {
  const results = [];

  for (const post of posts) {
    results.push(await schedulePost(post));

    // Wait 100ms between requests to avoid hitting rate limits
    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  return results;
}

Error Handling

Handle All Error Cases

Don't assume requests will succeed:

async function createPost(content, platforms, scheduledFor) {
  try {
    const response = await fetch("https://api.postcore.dev/posts", {
      method: "POST",
      headers: {
        "x-api-key": apiKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ content, platforms, scheduled_for: scheduledFor }),
    });

    if (!response.ok) {
      const error = await response.json();

      switch (error.error) {
        case "MONTHLY_LIMIT_EXCEEDED":
          return { success: false, reason: "monthly_limit" };
        case "PLATFORMS_NOT_CONNECTED":
          return {
            success: false,
            reason: "missing_platforms",
            platforms: error.missing_platforms,
          };
        case "RATE_LIMIT_EXCEEDED":
          return {
            success: false,
            reason: "rate_limit",
            nextAvailable: error.next_available,
          };
        default:
          return { success: false, reason: "unknown", error };
      }
    }

    const post = await response.json();
    return { success: true, post };
  } catch (err) {
    console.error("Network error:", err);
    return { success: false, reason: "network_error", error: err };
  }
}

Provide User Feedback

Show clear, actionable error messages:

const result = await createPost(content, platforms, time);

if (!result.success) {
  switch (result.reason) {
    case "monthly_limit":
      showMessage(
        "Monthly limit reached. Upgrade to Pro for more posts!",
        "error"
      );
      break;
    case "missing_platforms":
      showMessage(
        `Connect these platforms first: ${result.platforms.join(", ")}`,
        "warning"
      );
      break;
    case "rate_limit":
      showMessage(
        `Too many posts scheduled. Try again at ${result.nextAvailable}`,
        "info"
      );
      break;
    default:
      showMessage("Something went wrong. Please try again.", "error");
  }
}

Monthly Usage Tracking

Monitor Usage

Track your usage to avoid surprises:

async function getMonthlyUsage() {
  const posts = await fetch("https://api.postcore.dev/posts", {
    headers: { "x-api-key": apiKey },
  }).then((r) => r.json());

  const now = new Date();
  const thisMonth = posts.filter((p) => {
    const created = new Date(p.created_at);
    return (
      created.getMonth() === now.getMonth() &&
      created.getFullYear() === now.getFullYear()
    );
  });

  return {
    used: thisMonth.length,
    limit: 10, // or 3000 for Pro
    remaining: 10 - thisMonth.length,
    percentage: (thisMonth.length / 10) * 100,
  };
}

Set Up Alerts

Warn users before they hit limits:

const usage = await getMonthlyUsage();

if (usage.percentage >= 80) {
  console.warn(
    `You've used ${usage.used} of ${usage.limit} posts (${usage.percentage}%)`
  );
  sendAlertEmail("You're running low on posts this month");
}

if (usage.percentage >= 100) {
  console.error("Monthly limit reached!");
  showUpgradePrompt();
}

Content Validation

Check Character Limits

Validate content length before sending:

function validateContent(content, platforms) {
  const errors = [];

  if (content.length > 3000) {
    errors.push("Content too long (max 3000 characters)");
  }

  if (platforms.includes("bluesky") && content.length > 300) {
    errors.push("Content too long for Bluesky (max 300 characters)");
  }

  return errors;
}

const errors = validateContent(post.content, post.platforms);
if (errors.length > 0) {
  console.error("Validation errors:", errors);
  return;
}

Validate Time Format

Ensure times are in correct format:

function validateScheduledTime(timeString) {
  try {
    const date = new Date(timeString);

    // Check if valid date
    if (isNaN(date.getTime())) {
      return { valid: false, error: "Invalid date format" };
    }

    // Check if in future
    if (date <= new Date()) {
      return { valid: false, error: "Time must be in the future" };
    }

    // Check if at least 1 minute from now
    const oneMinuteFromNow = new Date(Date.now() + 60000);
    if (date < oneMinuteFromNow) {
      return { valid: false, error: "Must schedule at least 1 minute ahead" };
    }

    return { valid: true };
  } catch (err) {
    return { valid: false, error: "Invalid date format" };
  }
}

Platform-Specific Considerations

LinkedIn Best Practices

  • Schedule posts during business hours for better engagement
  • Space posts at least 60 seconds apart
  • Monitor needs_reconnect status
  • Provide clear CTAs in your content

Bluesky Best Practices

  • Keep content under 300 characters
  • Use hashtags appropriately
  • Posts can be 1 second apart (but slower is better for engagement)
  • App passwords never expire unless revoked

Testing

Use Separate API Keys

Never test with production API keys:

const config = {
  development: {
    apiKey: process.env.DEV_API_KEY,
    baseUrl: "https://api.postcore.dev",
  },
  production: {
    apiKey: process.env.PROD_API_KEY,
    baseUrl: "https://api.postcore.dev",
  },
};

const env = process.env.NODE_ENV || "development";
const { apiKey, baseUrl } = config[env];

Test Error Scenarios

Test how your app handles errors:

// Test rate limits
// Test invalid credentials
// Test missing profiles
// Test monthly limits
// Test network failures

Performance

Cache Profile Data

Don't fetch profiles on every request:

let profilesCache = null;
let cacheTime = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

async function getProfiles() {
  if (profilesCache && Date.now() - cacheTime < CACHE_DURATION) {
    return profilesCache;
  }

  profilesCache = await fetch("https://api.postcore.dev/profiles", {
    headers: { "x-api-key": apiKey },
  }).then((r) => r.json());

  cacheTime = Date.now();
  return profilesCache;
}

Minimize API Calls

Fetch all posts once and filter locally:

// ❌ Bad - multiple API calls
const scheduled = await fetchPostsByStatus("scheduled");
const published = await fetchPostsByStatus("published");
const thisMonth = await fetchPostsByMonth("2025-01");

// ✅ Good - one API call
const allPosts = await fetch("https://api.postcore.dev/posts", {
  headers: { "x-api-key": apiKey },
}).then((r) => r.json());

const scheduled = allPosts.filter((p) => p.status === "scheduled");
const published = allPosts.filter((p) => p.status === "published");
const thisMonth = allPosts.filter((p) => p.created_at.startsWith("2025-01"));