Developers · Custom Posting Integration
Custom Posting Integration API and quickstart
Connect your own iOS, Android, web or other application to Central Socials. Retrieve published text, images and videos, display them however you choose, and return numeric hourly engagement. The API accepts independent HTTPS requests; your application can call it directly or use any service you choose. Comment content, conversations and Inbox replies are outside this API.
For account-level publishing, Community, forms and landing-page automation with personal tokens or OAuth, see Public API, tokens and MCP. For the connection-key tools at the shared MCP endpoint, see Custom Posting consumer MCP tools.
Quickstart
- Open your profile’s Social Links → Custom Posting Integration. Select Custom posting integration, enter an integration name, and select Generate key. A profile owner or administrator manages the keys.
- You can upload an optional square PNG, JPEG or WebP logo, up to 1024 × 1024 pixels and 2 MB. It appears beside your integration in Posting and Engagement. Replace or remove it from the same dialog. Copy and save the key immediately. It is shown once. Closing the dialog removes the displayed copy. Each key reads only its connection’s published content and submits only that connection’s metrics.
- Copy the downloadable JavaScript helper into your project, or implement the HTTP operations below in your preferred language. Configure the application origin and key. Keep the key out of URLs, logs, screenshots and source control. Anyone possessing it can use this connection; a key embedded in a distributed app is extractable and is not proof that its reports are genuine.
- Select Test Connection in the dialog. Run the helper’s
testConnection()method within five minutes. It fetches sample content and a challenge, then sends the challenge and numeric test engagement back. Test figures never enter real analytics. Keep your integration running while the dialog waits. - After confirmation, the card turns green and shows LINKED. In Posting, select the named app as a destination. Publish now, save a draft, or schedule using your profile’s existing permissions and plan. Approval-required profiles retain their approval workflow.
- Fetch published posts. Use the returned post ID when reporting engagement. Open Engagement or Post Results to see custom posting integration figures in UTC hours and 6-, 24- or 72-hour windows.
import { createCustomAppClient } from './custom-app-client-v1.mjs';
const app = createCustomAppClient({
baseUrl: 'https://app.centralsocials.com',
key: 'YOUR_APP_KEY'
});
// First click Test Connection in Social Links.
await app.testConnection();
const { posts, nextCursor } = await app.getPosts();
// Render posts using your own application components.
// Example: numeric activity for an exact UTC hour.
const hour = new Date(Math.floor(Date.now() / 3600000) * 3600000).toISOString();
await app.reportEngagement([
{ postId: posts[0].id, hour, metrics: { views: 5, clicks: 1 } }
], { requestId: 'UNIQUE_SAVED_REQUEST_ID' });
Only report actual activity; the numbers above illustrate the format. Supply a new unique request ID for new activity. Save a pending batch and its ID before sending; reuse both unchanged after an uncertain response or app restart. The helper retries transient failures within a call, but your app owns durable offline storage and when to fetch or submit. JavaScript environments without standard fetch may supply an implementation. Native Swift, Kotlin and other languages can call the same HTTP API without this helper.
API reference
Production base URL: https://app.centralsocials.com/api/custom-apps/v1. For development, use your local application origin with the same path. Send Authorization: Bearer YOUR_APP_KEY on every API request. POST bodies use Content-Type: application/json. Browser callers can use the API’s CORS support without application cookies. Responses are JSON and must not be cached by shared caches.
| Operation | Purpose |
|---|---|
GET /posts | Retrieve published posts, optionally filtered or paginated. |
POST /engagement | Submit a batch of numeric hourly reports. |
GET /connection-test | Fetch the currently pending test, or {"test":null}. |
POST /connection-test | Return its challenge and a test engagement report. |
Retrieve published content
curl 'https://app.centralsocials.com/api/custom-apps/v1/posts?limit=25' \
-H "Authorization: Bearer $CENTRAL_SOCIALS_APP_KEY"
The response contains posts, nextCursor and serverTime. Every post has id, publicationId, title, text, publishedAt, updatedAt and a media array. Media entries contain id, type (image or video), mimeType, a signed download url, expiresAt and optional crop transform metadata. Media URLs normally expire after 15 minutes. Fetch the post again to get fresh links; retain content in your own app according to your needs. Applying crop metadata and choosing the final display are your responsibility.
limit is 1–50, default 25. When nextCursor is non-null, request the next page with after=nextCursor and keep the same filters. Use since with an ISO timestamp to fetch newly published content. Start each new sync without after, and use the first page’s serverTime as the next sync’s since only after all pages succeed. Upsert by post ID because boundary items can repeat. A pagination cursor is not a permanent sync checkpoint. Use postId to refetch one post and renew its media links.
Drafts, unapproved posts and future scheduled posts are unavailable to the external API. Published means available in this feed; it does not claim someone viewed it. Scheduled publication follows the existing publishing worker. App posts retain their attached media, so automatic deletion after publishing is unavailable for these destinations. Explicit media deletion can remove an attachment from later responses. Deleting the local post removes it from future feed reads; this version has no deletion notifications, so periodically reconcile against a complete feed if your app needs to remove cached content.
Submit hourly engagement
{
"requestId": "UNIQUE_SAVED_REQUEST_ID",
"reports": [{
"postId": "POST_ID_FROM_THE_FEED",
"hour": "2026-10-15T10:00:00.000Z",
"source": "default",
"mode": "increment",
"metrics": { "views": 20, "likes": 2, "clicks": 1 }
}]
}
Send this body to POST /engagement. Replace the example timestamp with the actual reporting hour. Each batch accepts 1–100 reports and a unique requestId of 8–128 letters, digits, dots, underscores, colons or hyphens. Each report identifies a post published to this connection and an exact UTC hour start. Hours within the past 90 days, including the current hour, are accepted; activity cannot precede publication. Hourly buckets are independent of upload frequency. You may send updates during an hour or later after reconnecting.
Optional metrics are views, likes, clicks, shares, saves, videoViews and comments. Include at least one. Values are nonnegative integer counts, at most 2,147,483,647 per metric/source/post/hour. Omitted metrics remain unchanged; zero is a reported value. These are activity counts within the hour, not lifetime balances, unique-person counts or conversation content. Report a view when content is actually displayed, and a click when its action is used.
Increment mode adds new activity and is the default when mode is omitted. Total mode replaces the figure for that post, hour, source and metric. Totals require an ISO UTC observedAt timestamp at or after the reporting hour. A later observation replaces an earlier one; older totals are ignored and counted in the receipt’s ignoredStaleTotals. Different totals with an identical observation time are rejected. You may correct a total downward using a later observation. Do not send a post’s lifetime total as an hourly total.
Choose one mode per post/hour/source/metric. Different metrics may use different modes, but mixing modes for the same counter is rejected. source defaults to default and accepts 1–128 letters, digits, dots, underscores, colons or hyphens. Independent reporters using totals must use distinct source IDs; otherwise their totals would replace each other. For example, use separate iOS/Android summary sources, or an app-generated installation reference for independent device totals. Source counts are summed in reporting. Avoid personal information in source IDs.
A successful receipt includes accepted, requestId, acceptedReports, ignoredStaleTotals and receivedAt. The entire batch is committed or rejected together. Repeating the same ID and data returns the saved receipt without changing metrics. Reusing an ID for different data returns a conflict. New activity requires a new ID. Existing profile Engagement entitlements apply to live submissions and reporting.
Connection test without the helper
Click Test Connection, then request GET /connection-test with the generated key. Read test.id, test.challenge, test.hour and test.post. Return:
{
"testId": "ID_FROM_TEST_RESPONSE",
"challenge": "CHALLENGE_FROM_TEST_RESPONSE",
"report": {
"postId": "TEST_POST_ID_FROM_RESPONSE",
"hour": "HOUR_FROM_TEST_RESPONSE",
"metrics": { "views": 1 }
}
}
POST this body to /connection-test. A successful response includes accepted:true, testId, connectedAt and testOnly:true. The dialog then marks the current key connected. You can instead test total mode by supplying mode:"total" and a current observedAt. Tests expire after five minutes and must match the current key. They exercise content receipt and reporting format; they do not certify your app’s rendering or the accuracy of real metrics. Complete this test before fetching live posts or submitting live engagement.
Keys, limits and errors
Generate and regenerate keys in Social Links. Regeneration immediately invalidates the previous key and clears connection verification; install the new key and test again. While waiting for a connection test, choose Cancel test to stop it. This preserves your key and any earlier successful verification; late responses cannot complete the cancelled test. You can start another test immediately. Revoke stops API access while preserving saved posts and engagement. The full key is never returned by connection-list or status reads. API keys are separate from account personal tokens and grant no account administration, publishing-write, draft-read or other-profile access. Frozen or deleted profiles cannot use the connection.
The API permits up to 120 authenticated requests per connection per minute. Engagement bodies are limited to 64 KiB and test acknowledgements to 16 KiB. Poll at a sensible interval while your app is active; no always-on connection is required. Respect Retry-After and use exponential backoff. Central Socials administrators can temporarily pause Custom Posting Integration through the existing service controls; data and keys remain saved.
| Status | Action |
|---|---|
| 400 / 415 | Correct the fields, UTC hour, content type or JSON body. |
| 401 | Configure the current valid app key; check whether it was regenerated or revoked. |
| 402 / 403 | Check the profile’s current feature access or permissions. |
| 404 | Check the operation or whether the post belongs to this connection and is published. |
| 409 | Complete the connection test, use a fresh request ID for changed data, or resolve a mode/observation conflict. |
| 413 | Split the batch into smaller requests, giving each new batch its own ID. |
| 429 / 503 | Wait for Retry-After, then retry the same pending batch unchanged. |
| 500 / network failure | Keep the original batch and ID and retry with backoff. |
Errors use {"error":{"code":"...","message":"..."}}. Receipt IDs and timestamps help diagnose a request without sharing your key. During a compatible release the feature can remain unavailable until its migration and activation gates complete.