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:
- Create new key
- Update your application
- Test thoroughly
- 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_reconnectstatus - 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 failuresPerformance
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"));