openapi: 3.1.0
info:
title: Zernio API
version: "1.0.1"
description: |
API reference for Zernio. Authenticate with a Bearer API key.
Base URL: https://zernio.com/api
termsOfService: https://zernio.com/tos
contact:
name: Zernio Support
url: https://zernio.com
email: support@zernio.com
x-logo:
url: https://zernio.com/icon.png?v=3
x-long-description: |
Zernio is the social media API that replaces 14 integrations. Schedule posts, retrieve analytics,
manage DMs, comments, and reviews across Twitter/X, Instagram, TikTok, LinkedIn, Facebook,
YouTube, Threads, Reddit, Pinterest, Bluesky, Telegram, Google Business, and Snapchat, all
from a single REST API.
Key features: Unified posting to 14 platforms, aggregated analytics, unified inbox (DMs, comments, reviews), webhooks, OAuth connect, queue scheduling, and white-label support for agencies managing 5,000+ accounts.
Supported platforms: Twitter/X, Instagram, WhatsApp, Facebook, LinkedIn, TikTok, YouTube, Pinterest, Reddit, Bluesky, Threads, Google Business, Telegram, Snapchat.
x-category: Social
x-website: https://zernio.com
x-thumbnail: https://rapidapi-prod-apis.s3.amazonaws.com/b24d3df5-563c-4a50-9e1e-1ad3eb1fce69.png
x-version-lifecycle: ACTIVE
x-badges:
- name: "social media"
value: "social media"
- name: "scheduling"
value: "scheduling"
- name: "instagram"
value: "instagram"
- name: "tiktok"
value: "tiktok"
- name: "twitter"
value: "twitter"
- name: "linkedin"
value: "linkedin"
- name: "facebook"
value: "facebook"
- name: "youtube"
value: "youtube"
- name: "social media api"
value: "social media api"
- name: "posting"
value: "posting"
x-documentation:
readme: |
# Zernio API
The social media API that replaces 14 integrations. Build social media features into your app in minutes, not months.
## Quick Start
**Base URL:** `https://zernio.com/api/v1`
**Authentication:** All requests require a Bearer API key in the `Authorization` header.
```bash
curl https://zernio.com/api/v1/user \
-H "Authorization: Bearer YOUR_API_KEY"
```
Get your API key at [zernio.com/dashboard/api-keys](https://zernio.com/dashboard/api-keys).
## Core Concepts
| Concept | Description |
|---------|-------------|
| **Profiles** | Containers that organize social accounts into brands or projects |
| **Accounts** | Connected social media accounts belonging to a profile |
| **Posts** | Content scheduled or published to one or more accounts |
| **Queue** | Recurring time slots for automatic post scheduling |
## Create a Post
```bash
curl -X POST https://zernio.com/api/v1/post \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "your-profile-id",
"text": "Hello from Zernio API!",
"socialAccountIds": ["account-1", "account-2"],
"scheduledAt": "2025-01-15T10:00:00Z"
}'
```
This single call publishes or schedules the post to all selected accounts across any platform.
## Supported Platforms
| Platform | Post | Stories/Reels | Analytics | Inbox |
|----------|------|---------------|-----------|-------|
| Twitter/X | Yes | - | Yes | Yes |
| Instagram | Yes | Yes | Yes | Yes |
| Facebook | Yes | Stories | Yes | Yes |
| LinkedIn | Yes | - | Yes | - |
| TikTok | Yes | - | Yes | - |
| YouTube | Yes | Shorts | Yes | Yes |
| Pinterest | Yes | - | Yes | - |
| Reddit | Yes | - | - | Yes |
| Bluesky | Yes | - | - | Yes |
| Threads | Yes | - | Yes | Yes |
| Google Business | Yes | - | - | Yes |
| Telegram | Yes | - | - | - |
| Snapchat | Yes | - | - | - |
## Rate Limits
| Plan | Requests/min | Posts/month |
|------|-------------|-------------|
| Free | 60 | 20 |
| Build | 120 | 120 |
| Accelerate | 600 | Unlimited |
| Unlimited | 1,200 | Unlimited |
All responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers.
## Webhooks
Receive real-time notifications for post status changes, account events, and incoming messages:
- `post.scheduled` - Post successfully scheduled
- `post.published` - Post successfully published
- `post.failed` - Post failed on all platforms
- `post.partial` - Post published to some platforms, failed on others
- `post.recycled` - A new scheduled copy was created from a recycling post
- `account.connected` - Social account connected
- `account.disconnected` - Social account disconnected (token expired)
- `message.received` - New DM received
- `comment.received` - New comment received on a post
Webhook payloads are signed with HMAC-SHA256 via the `X-Zernio-Signature` header.
## Full Documentation
For complete guides, platform-specific details, and SDK references, visit [docs.zernio.com](https://docs.zernio.com).
## SDKs
Official SDKs available for: [Node.js](https://www.npmjs.com/package/@zernio/node), [Python](https://pypi.org/project/zernio-sdk), Go, Ruby, Java, PHP, .NET, and Rust.
servers:
- url: https://zernio.com/api
description: Production
- url: http://localhost:3000/api
description: Local
tags:
- name: Posts
- name: Users
- name: Usage
- name: Profiles
- name: Accounts
- name: Account Groups
- name: API Keys
- name: Invites
- name: Connect
- name: Media
- name: Reddit Search
- name: Facebook
- name: GMB Reviews
- name: GMB Food Menus
- name: GMB Location Details
- name: GMB Media
- name: GMB Attributes
- name: GMB Place Actions
- name: LinkedIn Mentions
- name: Pinterest
- name: TikTok
- name: Queue
- name: Analytics
- name: Inbox Access
description: |
Check and manage inbox feature access.
- name: Messages
description: |
Unified inbox API for managing conversations and direct messages across all connected accounts.
All endpoints aggregate data from multiple social accounts in a single API call.
Requires Inbox addon.
- name: Comments
description: |
Unified inbox API for managing comments on posts across all connected accounts.
Supports commenting on third-party posts for platforms that allow it (YouTube, Twitter, Reddit, Bluesky, Threads).
All endpoints aggregate data from multiple social accounts in a single API call.
Requires Inbox addon.
- name: Reviews
description: |
Unified inbox API for managing reviews on Facebook Pages and Google Business accounts.
All endpoints aggregate data from multiple social accounts in a single API call.
Requires Inbox addon.
- name: Twitter Engagement
description: |
X/Twitter-specific engagement endpoints for retweeting, bookmarking, and following.
Rate limits: 50 requests per 15-min window per user. Retweets share the 300/3hr creation limit with tweet creation.
- name: Tools
description: |
Media download and utility tools. Available to paid plans only.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
All responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
- name: Validate
description: |
Pre-flight validation endpoints. Check post content, character limits, media URLs, and subreddit existence before publishing.
- name: Account Settings
description: |
Platform-specific account settings: Facebook persistent menu, Instagram ice breakers, and Telegram bot commands.
- name: Webhooks
description: |
Configure webhooks for real-time notifications. Events: post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received.
Security: optional HMAC-SHA256 signature in X-Zernio-Signature header. Configure a secret key to enable verification. Custom headers supported.
- name: Logs
description: |
Publishing logs for transparency and debugging. Each log includes the platform API endpoint, HTTP status code, request/response bodies, duration, and retry attempts. Logs are automatically deleted after 7 days.
- name: WhatsApp
description: |
WhatsApp Business API for sending messages, managing contacts, templates, broadcasts, and conversations.
All endpoints require an accountId parameter identifying the WhatsApp-connected social account.
- name: WhatsApp Phone Numbers
description: |
Manage WhatsApp phone numbers: purchase, verify, and release numbers for your WhatsApp Business account.
Requires a paid plan.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: API key authentication - use your Zernio API key as a Bearer token
connectToken:
type: apiKey
in: header
name: X-Connect-Token
description: |
Short-lived connect token for API users during OAuth flows.
Automatically generated when initiating OAuth without a browser session.
Valid for 15 minutes. Used to authenticate Facebook page selection API calls.
parameters:
PageParam:
name: page
in: query
description: Page number (1-based)
schema: { type: integer, minimum: 1, default: 1 }
LimitParam:
name: limit
in: query
description: Page size
schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
responses:
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: Unauthorized
NotFound:
description: Resource not found
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: Not found
schemas:
ErrorResponse:
type: object
properties:
error:
type: string
details:
type: object
additionalProperties: true
FoodMenuLabel:
type: object
required: [displayName]
properties:
displayName: { type: string, description: Display name of the item/section/menu }
description: { type: string, description: Optional description }
languageCode: { type: string, description: "BCP-47 language code (e.g. en, es)" }
Money:
type: object
required: [currencyCode, units]
properties:
currencyCode: { type: string, description: "ISO 4217 currency code (e.g. USD, EUR)" }
units: { type: string, description: Whole units of the amount }
nanos: { type: integer, description: Nano units (10^-9) of the amount }
FoodMenuItemAttributes:
type: object
properties:
price: { $ref: '#/components/schemas/Money' }
spiciness: { type: string, description: "Spiciness level (e.g. MILD, MEDIUM, HOT)" }
allergen:
type: array
items: { type: string }
description: "Allergens (e.g. DAIRY, GLUTEN, SHELLFISH)"
dietaryRestriction:
type: array
items: { type: string }
description: "Dietary labels (e.g. VEGETARIAN, VEGAN, GLUTEN_FREE)"
servesNumPeople: { type: integer, description: Number of people the item serves }
preparationMethods:
type: array
items: { type: string }
description: "Preparation methods (e.g. GRILLED, FRIED)"
mediaKeys:
type: array
items: { type: string }
description: Media references for item photos
FoodMenuItem:
type: object
required: [labels]
properties:
labels:
type: array
items: { $ref: '#/components/schemas/FoodMenuLabel' }
attributes: { $ref: '#/components/schemas/FoodMenuItemAttributes' }
options:
type: array
items:
type: object
properties:
labels:
type: array
items: { $ref: '#/components/schemas/FoodMenuLabel' }
attributes: { $ref: '#/components/schemas/FoodMenuItemAttributes' }
description: Item variants/options (e.g. sizes, preparations)
FoodMenuSection:
type: object
required: [labels]
properties:
labels:
type: array
items: { $ref: '#/components/schemas/FoodMenuLabel' }
items:
type: array
items: { $ref: '#/components/schemas/FoodMenuItem' }
FoodMenu:
type: object
required: [labels]
properties:
labels:
type: array
items: { $ref: '#/components/schemas/FoodMenuLabel' }
sections:
type: array
items: { $ref: '#/components/schemas/FoodMenuSection' }
cuisines:
type: array
items: { type: string }
description: "Cuisine types (e.g. AMERICAN, ITALIAN, JAPANESE)"
sourceUrl:
type: string
description: URL of the original menu source
YouTubeDailyViewsResponse:
type: object
properties:
success:
type: boolean
example: true
videoId:
type: string
description: The YouTube video ID
dateRange:
type: object
properties:
startDate:
type: string
format: date
endDate:
type: string
format: date
totalViews:
type: integer
description: Sum of views across all days in the range
dailyViews:
type: array
items:
type: object
properties:
date:
type: string
format: date
views:
type: integer
estimatedMinutesWatched:
type: number
averageViewDuration:
type: number
description: Average view duration in seconds
subscribersGained:
type: integer
subscribersLost:
type: integer
likes:
type: integer
comments:
type: integer
shares:
type: integer
lastSyncedAt:
type: string
format: date-time
nullable: true
description: When the data was last synced from YouTube
scopeStatus:
type: object
properties:
hasAnalyticsScope:
type: boolean
YouTubeScopeMissingResponse:
type: object
properties:
success:
type: boolean
example: false
error:
type: string
example: "To access daily video analytics, please reconnect your YouTube account to grant the required permissions."
code:
type: string
example: youtube_analytics_scope_missing
scopeStatus:
type: object
properties:
hasAnalyticsScope:
type: boolean
example: false
requiresReauthorization:
type: boolean
example: true
reauthorizeUrl:
type: string
format: uri
description: URL to redirect user for reauthorization
Webhook:
type: object
description: Individual webhook configuration for receiving real-time notifications
properties:
_id:
type: string
description: Unique webhook identifier
name:
type: string
description: Webhook name (for identification)
maxLength: 50
url:
type: string
format: uri
description: Webhook endpoint URL
secret:
type: string
description: Secret key for HMAC-SHA256 signature (not returned in responses for security)
events:
type: array
items:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received]
description: Events subscribed to
isActive:
type: boolean
description: Whether webhook delivery is enabled
lastFiredAt:
type: string
format: date-time
description: Timestamp of last successful webhook delivery
failureCount:
type: integer
description: Consecutive delivery failures (resets on success, webhook disabled at 10)
customHeaders:
type: object
additionalProperties:
type: string
description: Custom headers included in webhook requests
WebhookLog:
type: object
description: Webhook delivery log entry
properties:
_id:
type: string
webhookId:
type: string
description: ID of the webhook that was triggered
webhookName:
type: string
description: Name of the webhook that was triggered
event:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test]
url:
type: string
format: uri
status:
type: string
enum: [success, failed]
statusCode:
type: integer
description: HTTP status code from webhook endpoint
requestPayload:
type: object
description: Payload sent to webhook endpoint
responseBody:
type: string
description: Response body from webhook endpoint (truncated to 10KB)
errorMessage:
type: string
description: Error message if delivery failed
attemptNumber:
type: integer
description: Delivery attempt number (max 3 retries)
responseTime:
type: integer
description: Response time in milliseconds
createdAt:
type: string
format: date-time
WebhookPayloadPost:
type: object
description: Webhook payload for post events
properties:
event:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled]
post:
type: object
properties:
id:
type: string
content:
type: string
status:
type: string
scheduledFor:
type: string
format: date-time
publishedAt:
type: string
format: date-time
platforms:
type: array
items:
type: object
properties:
platform:
type: string
status:
type: string
publishedUrl:
type: string
error:
type: string
timestamp:
type: string
format: date-time
WebhookPayloadAccountConnected:
type: object
description: Webhook payload for account connected events
properties:
event:
type: string
enum: [account.connected]
account:
type: object
properties:
accountId:
type: string
description: The account's unique identifier (same as used in /v1/accounts/{accountId})
profileId:
type: string
description: The profile's unique identifier this account belongs to
platform:
type: string
username:
type: string
displayName:
type: string
timestamp:
type: string
format: date-time
WebhookPayloadAccountDisconnected:
type: object
description: Webhook payload for account disconnected events
properties:
event:
type: string
enum: [account.disconnected]
account:
type: object
properties:
accountId:
type: string
description: The account's unique identifier (same as used in /v1/accounts/{accountId})
profileId:
type: string
description: The profile's unique identifier this account belongs to
platform:
type: string
username:
type: string
displayName:
type: string
disconnectionType:
type: string
enum: [intentional, unintentional]
description: Whether the disconnection was intentional (user action) or unintentional (token expired/revoked)
reason:
type: string
description: Human-readable reason for the disconnection
timestamp:
type: string
format: date-time
WebhookPayloadComment:
type: object
description: Webhook payload for comment received events (Instagram, Facebook, Twitter/X, YouTube, LinkedIn, Bluesky, Reddit)
properties:
event:
type: string
enum: [comment.received]
comment:
type: object
properties:
id:
type: string
description: Platform comment ID
postId:
type: string
description: Internal post ID
platformPostId:
type: string
description: Platform's post ID
platform:
type: string
enum: [instagram, facebook, twitter, youtube, linkedin, bluesky, reddit]
text:
type: string
description: Comment text content
author:
type: object
properties:
id:
type: string
description: Author's platform ID
username:
type: string
name:
type: string
picture:
type: string
nullable: true
createdAt:
type: string
format: date-time
isReply:
type: boolean
description: Whether this is a reply to another comment
parentCommentId:
type: string
nullable: true
description: Parent comment ID if this is a reply
post:
type: object
properties:
id:
type: string
description: Internal post ID
platformPostId:
type: string
description: Platform's post ID
account:
type: object
properties:
id:
type: string
description: Social account ID
platform:
type: string
username:
type: string
timestamp:
type: string
format: date-time
WebhookPayloadMessage:
type: object
description: Webhook payload for message received events (DMs from Instagram, Facebook, Telegram, Bluesky, Reddit)
properties:
event:
type: string
enum: [message.received]
message:
type: object
properties:
id:
type: string
description: Internal message ID
conversationId:
type: string
description: Internal conversation ID
platform:
type: string
enum: [instagram, facebook, telegram, bluesky, reddit]
platformMessageId:
type: string
description: Platform's message ID
direction:
type: string
enum: [incoming]
text:
type: string
nullable: true
description: Message text content
attachments:
type: array
items:
type: object
properties:
type:
type: string
description: Attachment type (image, video, file, sticker, audio)
url:
type: string
description: Attachment URL (may expire for Meta platforms)
payload:
type: object
description: Additional attachment metadata
sender:
type: object
properties:
id:
type: string
name:
type: string
username:
type: string
picture:
type: string
instagramProfile:
type: object
nullable: true
description: Instagram profile data for the sender. Only present for Instagram conversations.
properties:
isFollower:
type: boolean
nullable: true
description: Whether the sender follows your Instagram business account
isFollowing:
type: boolean
nullable: true
description: Whether your Instagram business account follows the sender
followerCount:
type: integer
nullable: true
description: The sender's follower count on Instagram
isVerified:
type: boolean
nullable: true
description: Whether the sender is a verified Instagram user
sentAt:
type: string
format: date-time
isRead:
type: boolean
conversation:
type: object
properties:
id:
type: string
platformConversationId:
type: string
participantId:
type: string
participantName:
type: string
participantUsername:
type: string
participantPicture:
type: string
status:
type: string
enum: [active, archived]
account:
type: object
properties:
id:
type: string
description: Social account ID
platform:
type: string
username:
type: string
displayName:
type: string
metadata:
type: object
nullable: true
description: Interactive message metadata (present when message is a quick reply tap, postback button tap, or inline keyboard callback)
properties:
quickReplyPayload:
type: string
description: Payload from a quick reply tap (Meta platforms)
postbackPayload:
type: string
description: Payload from a postback button tap (Meta platforms)
postbackTitle:
type: string
description: Title of the tapped postback button (Meta platforms)
callbackData:
type: string
description: Callback data from an inline keyboard button tap (Telegram)
timestamp:
type: string
format: date-time
PostLog:
type: object
description: Publishing log entry showing details of a post publishing attempt
properties:
_id:
type: string
postId:
oneOf:
- type: string
- type: object
description: Populated post reference
properties:
_id:
type: string
content:
type: string
status:
type: string
userId:
type: string
profileId:
type: string
platform:
type: string
enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat]
accountId:
type: string
accountUsername:
type: string
action:
type: string
enum: [publish, retry, media_upload, rate_limit_pause, token_refresh, cancelled]
description: "Type of action logged: publish (initial attempt), retry (after failure), media_upload, rate_limit_pause, token_refresh, cancelled"
status:
type: string
enum: [success, failed, pending, skipped]
statusCode:
type: integer
description: HTTP status code from platform API
endpoint:
type: string
description: Platform API endpoint called
request:
type: object
properties:
contentPreview:
type: string
description: First 200 chars of caption
mediaCount:
type: integer
mediaTypes:
type: array
items:
type: string
mediaUrls:
type: array
items:
type: string
description: URLs of media items sent to platform
scheduledFor:
type: string
format: date-time
rawBody:
type: string
description: Full request body JSON (max 5000 chars)
response:
type: object
properties:
platformPostId:
type: string
description: ID returned by platform on success
platformPostUrl:
type: string
description: URL of published post
errorMessage:
type: string
description: Error message on failure
errorCode:
type: string
description: Platform-specific error code
rawBody:
type: string
description: Full response body JSON (max 5000 chars)
durationMs:
type: integer
description: How long the operation took in milliseconds
attemptNumber:
type: integer
description: Attempt number (1 for first try, 2+ for retries)
createdAt:
type: string
format: date-time
ConnectionLog:
type: object
description: Connection event log showing account connection/disconnection history
properties:
_id:
type: string
userId:
type: string
description: User who owns the connection (may be null for early OAuth failures)
profileId:
type: string
accountId:
type: string
description: The social account ID (present on successful connections and disconnects)
platform:
type: string
enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat]
eventType:
type: string
enum: [connect_success, connect_failed, disconnect, reconnect_success, reconnect_failed]
description: "Type of connection event: connect_success, connect_failed, disconnect, reconnect_success, reconnect_failed"
connectionMethod:
type: string
enum: [oauth, credentials, invitation]
description: How the connection was initiated
error:
type: object
description: Error details (present on failed events)
properties:
code:
type: string
description: Error code (e.g., oauth_denied, token_exchange_failed)
message:
type: string
description: Human-readable error message
rawResponse:
type: string
description: Raw error response (truncated to 2000 chars)
success:
type: object
description: Success details (present on successful events)
properties:
displayName:
type: string
username:
type: string
profilePicture:
type: string
permissions:
type: array
items:
type: string
description: OAuth scopes/permissions granted
tokenExpiresAt:
type: string
format: date-time
accountType:
type: string
description: Account type (personal, business, organization)
context:
type: object
description: Additional context about the connection attempt
properties:
isHeadlessMode:
type: boolean
hasCustomRedirectUrl:
type: boolean
isReconnection:
type: boolean
isBYOK:
type: boolean
description: Using bring-your-own-keys
invitationToken:
type: string
connectToken:
type: string
durationMs:
type: integer
description: How long the operation took in milliseconds
metadata:
type: object
description: Additional metadata
createdAt:
type: string
format: date-time
MediaItem:
type: object
description: Media referenced in posts. URLs must be publicly reachable over HTTPS. Use POST /v1/media/presign for uploads up to 5GB. Zernio auto-compresses images and videos that exceed platform limits (videos over 200 MB may not be compressed).
properties:
type:
type: string
enum: [image, video, gif, document]
url:
type: string
format: uri
title:
type: string
description: Optional title for the media item. Used as the document title for LinkedIn PDF/carousel posts. If omitted, falls back to the post title, then the filename.
filename:
type: string
size:
type: integer
description: Optional file size in bytes
mimeType:
type: string
description: Optional MIME type (e.g. image/jpeg, video/mp4)
thumbnail:
type: string
format: uri
description: Optional custom thumbnail/cover image URL for videos. Supported for Facebook video posts, Facebook Reels, and regular video uploads. Max 10MB, JPG/PNG recommended.
instagramThumbnail:
type: string
format: uri
description: Optional custom cover image URL for Instagram Reels
tiktokProcessed:
type: boolean
description: Internal flag indicating the image was resized for TikTok
PlatformTarget:
type: object
properties:
platform:
type: string
example: twitter
description: "Supported values: twitter, threads, instagram, youtube, facebook, linkedin, pinterest, reddit, tiktok, bluesky, googlebusiness, telegram"
accountId:
oneOf:
- type: string
- $ref: '#/components/schemas/SocialAccount'
customContent:
type: string
description: Platform-specific text override. When set, this content is used instead of the top-level post content for this platform. Useful for tailoring captions per platform (e.g. keeping tweets under 280 characters).
customMedia:
type: array
items:
$ref: '#/components/schemas/MediaItem'
scheduledFor:
type: string
format: date-time
description: Optional per-platform scheduled time override (uses post.scheduledFor when omitted)
platformSpecificData:
description: Platform-specific overrides and options.
oneOf:
- $ref: '#/components/schemas/TwitterPlatformData'
- $ref: '#/components/schemas/ThreadsPlatformData'
- $ref: '#/components/schemas/FacebookPlatformData'
- $ref: '#/components/schemas/InstagramPlatformData'
- $ref: '#/components/schemas/LinkedInPlatformData'
- $ref: '#/components/schemas/PinterestPlatformData'
- $ref: '#/components/schemas/YouTubePlatformData'
- $ref: '#/components/schemas/GoogleBusinessPlatformData'
- $ref: '#/components/schemas/TikTokPlatformData'
- $ref: '#/components/schemas/TelegramPlatformData'
- $ref: '#/components/schemas/SnapchatPlatformData'
- $ref: '#/components/schemas/RedditPlatformData'
- $ref: '#/components/schemas/BlueskyPlatformData'
additionalProperties: true
status:
type: string
example: pending
description: "Platform-specific status: pending, publishing, published, failed"
platformPostId:
type: string
description: The native post ID on the platform (populated after successful publish)
example: "1234567890123456789"
platformPostUrl:
type: string
format: uri
description: Public URL of the published post. Included in the response for immediate posts; for scheduled posts, fetch via GET /v1/posts/{postId} after publish time.
example: "https://twitter.com/acmecorp/status/1234567890123456789"
publishedAt:
type: string
format: date-time
description: Timestamp when the post was published to this platform
errorMessage:
type: string
description: Human-readable error message when status is failed. Contains platform-specific error details explaining why the publish failed.
errorCategory:
type: string
enum: [auth_expired, user_content, user_abuse, account_issue, platform_rejected, platform_error, system_error, unknown]
description: "Error category for programmatic handling: auth_expired (token expired/revoked), user_content (wrong format/too long), user_abuse (rate limits/spam), account_issue (config problems), platform_rejected (policy violation), platform_error (5xx/maintenance), system_error (Zernio infra), unknown"
errorSource:
type: string
enum: [user, platform, system]
description: "Who caused the error: user (fix content/reconnect), platform (outage/API change), system (Zernio issue, rare)"
Post:
type: object
properties:
_id: { type: string }
userId:
oneOf:
- type: string
- $ref: '#/components/schemas/User'
title:
type: string
description: |
YouTube: title must be ≤ 100 characters.
content: { type: string }
mediaItems:
type: array
items: { $ref: '#/components/schemas/MediaItem' }
platforms:
type: array
items: { $ref: '#/components/schemas/PlatformTarget' }
scheduledFor: { type: string, format: date-time }
timezone: { type: string }
status: { type: string, enum: [draft, scheduled, publishing, published, failed, partial] }
tags:
type: array
description: "YouTube constraints: each tag max 100 chars, combined max 500 chars, duplicates removed."
items: { type: string }
hashtags:
type: array
items: { type: string }
mentions:
type: array
items: { type: string }
visibility: { type: string, enum: [public, private, unlisted] }
metadata:
type: object
additionalProperties: true
recycling:
$ref: '#/components/schemas/RecyclingState'
recycledFromPostId:
type: string
description: ID of the original post if this post was created via recycling
queuedFromProfile:
type: string
description: Profile ID if the post was scheduled via the queue
queueId:
type: string
description: Queue ID if the post was scheduled via a specific queue
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
RecyclingConfig:
type: object
description: |
Configure automatic post recycling (reposting at regular intervals).
After the post is published, the system creates new scheduled copies at the
specified interval until expiration conditions are met. Supports weekly or
monthly intervals. Maximum 10 active recycling posts per account.
YouTube and TikTok platforms are excluded from recycling.
Content variations are recommended for Twitter and Pinterest to avoid duplicate flags.
properties:
enabled:
type: boolean
default: true
description: Set to false to disable recycling on this post
gap:
type: integer
minimum: 1
description: Number of interval units between each repost. Required when enabling recycling.
example: 2
gapFreq:
type: string
enum: [week, month]
default: month
description: Interval unit for the gap. Defaults to 'month'.
startDate:
type: string
format: date-time
description: When to start the recycling cycle. Defaults to the post's scheduledFor date.
expireCount:
type: integer
minimum: 1
description: Stop recycling after this many copies have been created
example: 5
expireDate:
type: string
format: date-time
description: Stop recycling after this date, regardless of count
contentVariations:
type: array
items:
type: string
maxItems: 20
description: |
Array of content variations for recycled copies. On each recycle, the next
variation is used in round-robin order. Recommended for Twitter and Pinterest
to avoid duplicate content flags. If omitted, the original post content is
used for all recycled copies. Send an empty array [] to clear existing
variations. Must have 2+ entries when setting variations. Platform-level
customContent still overrides the base content per platform.
RecyclingState:
type: object
description: Current recycling configuration and state on a post
properties:
enabled:
type: boolean
description: Whether recycling is currently active
gap:
type: integer
description: Number of interval units between reposts
gapFreq:
type: string
enum: [week, month]
description: Interval unit (week or month)
startDate: { type: string, format: date-time }
expireCount: { type: integer }
expireDate: { type: string, format: date-time }
contentVariations:
type: array
items:
type: string
description: Content variations for recycled copies (if configured)
contentVariationIndex:
type: integer
description: Current position in the content variations rotation (read-only)
recycleCount:
type: integer
description: How many recycled copies have been created so far (read-only)
nextRecycleAt:
type: string
format: date-time
description: When the next recycled copy will be created (read-only)
lastRecycledAt:
type: string
format: date-time
description: When the last recycled copy was created (read-only)
TwitterPlatformData:
type: object
properties:
replyToTweetId:
type: string
description: ID of an existing tweet to reply to. The published tweet will appear as a reply in that tweet's thread. For threads, only the first tweet replies to the target; subsequent tweets chain normally.
replySettings:
type: string
enum: [following, mentionedUsers, subscribers, verified]
description: Controls who can reply to the tweet. "following" allows only people you follow, "mentionedUsers" allows only mentioned users, "subscribers" allows only subscribers, "verified" allows only verified users. Omit for default (everyone can reply). For threads, applies to the first tweet only. Cannot be combined with replyToTweetId.
threadItems:
type: array
description: Sequence of tweets in a thread. First item is the root tweet.
items:
type: object
properties:
content: { type: string }
mediaItems:
type: array
items: { $ref: '#/components/schemas/MediaItem' }
ThreadsPlatformData:
type: object
properties:
threadItems:
type: array
description: Sequence of posts in a Threads thread (root then replies in order).
items:
type: object
properties:
content: { type: string }
mediaItems:
type: array
items: { $ref: '#/components/schemas/MediaItem' }
description: Up to 10 images per carousel (no videos). Videos must be H.264/AAC MP4, max 5 min. Images JPEG/PNG, max 8 MB. Use threadItems for reply chains.
FacebookPlatformData:
type: object
properties:
contentType:
type: string
enum: [story, reel]
description: Set to 'story' for Page Stories (24h ephemeral) or 'reel' for Reels (short vertical video). Defaults to feed post if omitted.
title:
type: string
description: Reel title (only for contentType=reel). Separate from the caption/content field.
firstComment:
type: string
description: Optional first comment to post immediately after publishing (feed posts only, not stories or reels)
pageId:
type: string
description: Target Facebook Page ID for multi-page posting. If omitted, uses the default page. Use GET /v1/accounts/{id}/facebook-page to list pages.
description: Feed posts support up to 10 images (no mixed video+image). Stories require single media (24h, no captions). Reels require single vertical video (9:16, 3-60s).
InstagramPlatformData:
type: object
properties:
contentType:
type: string
enum: [story]
description: Set to 'story' to publish as a Story. Default posts become Reels or feed depending on media.
shareToFeed:
type: boolean
default: true
description: For Reels only. When true (default), the Reel appears on both the Reels tab and your main profile feed. Set to false to post to the Reels tab only.
collaborators:
type: array
items: { type: string }
description: Up to 3 Instagram usernames to invite as collaborators (feed/Reels only)
firstComment:
type: string
description: Optional first comment to add after the post is created (not applied to Stories)
trialParams:
type: object
description: Trial Reels configuration. Trial reels are shared to non-followers first and can later be graduated to regular reels manually or automatically based on performance. Only applies to Reels.
properties:
graduationStrategy:
type: string
enum: [MANUAL, SS_PERFORMANCE]
description: "MANUAL (graduate from Instagram app) or SS_PERFORMANCE (auto-graduate if performs well with non-followers)"
userTags:
type: array
description: Tag Instagram users in photos by username and position. Not supported for stories or videos. For carousels, use mediaIndex to target specific slides (defaults to 0). Tags on video items are silently skipped.
items:
type: object
required: [username, x, y]
properties:
username:
type: string
description: Instagram username (@ symbol is optional and will be removed automatically)
example: friend_username
x:
type: number
minimum: 0
maximum: 1
description: X coordinate position from left edge (0.0 = left, 0.5 = center, 1.0 = right)
example: 0.5
y:
type: number
minimum: 0
maximum: 1
description: Y coordinate position from top edge (0.0 = top, 0.5 = center, 1.0 = bottom)
example: 0.5
mediaIndex:
type: integer
minimum: 0
description: Zero-based index of the carousel item to tag. Defaults to 0. Tags on video items or out-of-range indices are ignored.
example: 0
audioName:
type: string
description: Custom name for original audio in Reels. Replaces the default "Original Audio" label. Can only be set once.
example: "My Podcast Intro"
thumbOffset:
type: integer
minimum: 0
description: Millisecond offset from video start for the Reel thumbnail. Ignored if a custom thumbnail URL is provided. Defaults to 0.
example: 5000
description: Feed aspect ratio 0.8-1.91, carousels up to 10 items, stories require media (no captions). User tag coordinates 0.0-1.0 from top-left. Images over 8 MB and videos over platform limits are auto-compressed.
LinkedInPlatformData:
type: object
properties:
documentTitle:
type: string
description: Title displayed on LinkedIn document (PDF/carousel) posts. Required by LinkedIn for document posts. If omitted, falls back to the media item title, then the filename.
organizationUrn:
type: string
description: Target LinkedIn Organization URN (e.g. "urn:li:organization:123456789"). If omitted, uses the default org. Use GET /v1/accounts/{id}/linkedin-organizations to list orgs.
firstComment:
type: string
description: Optional first comment to add after the post is created
disableLinkPreview:
type: boolean
description: Set to true to disable automatic link previews for URLs in the post content (default is false)
description: Up to 20 images, no multi-video. Single PDF supported (max 100MB). Link previews auto-generated when no media attached. Use organizationUrn for multi-org posting.
PinterestPlatformData:
type: object
properties:
title:
type: string
maxLength: 100
description: Pin title. Defaults to first line of content or "Pin". Must be ≤ 100 characters.
boardId:
type: string
description: Target Pinterest board ID. If omitted, the first available board is used.
link:
type: string
format: uri
description: Destination link (pin URL)
coverImageUrl:
type: string
format: uri
description: Optional cover image for video pins
coverImageKeyFrameTime:
type: integer
description: Optional key frame time in seconds for derived video cover
YouTubePlatformData:
type: object
properties:
title:
type: string
maxLength: 100
description: Video title. Defaults to first line of content or "Untitled Video". Must be ≤ 100 characters.
visibility:
type: string
enum: [public, private, unlisted]
default: public
description: "Video visibility: public (default, anyone can watch), unlisted (link only), private (invite only)"
madeForKids:
type: boolean
default: false
description: COPPA compliance flag. Set true for child-directed content (restricts comments, notifications, ad targeting). Defaults to false. YouTube may block views if not explicitly set.
firstComment:
type: string
maxLength: 10000
description: Optional first comment to post immediately after video upload. Up to 10,000 characters (YouTube's comment limit).
containsSyntheticMedia:
type: boolean
default: false
description: AI-generated content disclosure. Set true if the video contains synthetic content that could be mistaken for real. YouTube may add a label.
categoryId:
type: string
default: '22'
description: "YouTube video category ID. Defaults to 22 (People & Blogs). Common: 1 (Film), 2 (Autos), 10 (Music), 15 (Pets), 17 (Sports), 20 (Gaming), 23 (Comedy), 24 (Entertainment), 25 (News), 26 (Howto), 27 (Education), 28 (Science & Tech)."
description: Videos under 3 min auto-detected as Shorts. Custom thumbnails for regular videos only. Scheduled videos are uploaded immediately with the specified visibility.
GoogleBusinessPlatformData:
type: object
properties:
locationId:
type: string
description: Target GBP location ID (e.g. "locations/123456789"). If omitted, uses the default location. Use GET /v1/accounts/{id}/gmb-locations to list locations.
languageCode:
type: string
description: BCP 47 language code (e.g. "en", "de", "es"). Auto-detected if omitted. Set explicitly for short or mixed-language posts.
example: "de"
callToAction:
type: object
description: Optional call-to-action button displayed on the post
properties:
type:
type: string
enum: [LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL]
description: "Button action type: LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL"
url:
type: string
format: uri
description: Destination URL for the CTA button (required when callToAction is provided)
required: [type, url]
description: Text and single image only (no videos). Optional call-to-action button. Posts appear on GBP, Google Search, and Maps. Use locationId for multi-location posting.
TikTokPlatformData:
type: object
description: Photo carousels up to 35 images. Video titles up to 2200 chars, photo titles truncated to 90 chars. privacyLevel must match creator_info options. Both camelCase and snake_case accepted.
properties:
draft:
type: boolean
description: When true, sends the post to the TikTok Creator Inbox as a draft instead of publishing immediately.
privacyLevel:
type: string
description: One of the values returned by the TikTok creator info API for the account
allowComment:
type: boolean
description: Allow comments on the post
allowDuet:
type: boolean
description: Allow duets (required for video posts)
allowStitch:
type: boolean
description: Allow stitches (required for video posts)
commercialContentType:
type: string
enum: [none, brand_organic, brand_content]
description: Type of commercial content disclosure
brandPartnerPromote:
type: boolean
description: Whether the post promotes a brand partner
isBrandOrganicPost:
type: boolean
description: Whether the post is a brand organic post
contentPreviewConfirmed:
type: boolean
description: User has confirmed they previewed the content
expressConsentGiven:
type: boolean
description: User has given express consent for posting
mediaType:
type: string
enum: [video, photo]
description: Optional override. Defaults based on provided media items.
videoCoverTimestampMs:
type: integer
description: Optional for video posts. Timestamp in milliseconds to select which frame to use as thumbnail (defaults to 1000ms/1 second). Ignored when videoCoverImageUrl is provided.
minimum: 0
videoCoverImageUrl:
type: string
format: uri
description: Optional for video posts. URL of a custom thumbnail image (JPG, PNG, or WebP, max 20MB). The image is prepended as a 1-second still frame to the video and used as the cover. Overrides videoCoverTimestampMs when provided.
photoCoverIndex:
type: integer
description: Optional for photo carousels. Index of image to use as cover, 0-based (defaults to 0/first image).
minimum: 0
autoAddMusic:
type: boolean
description: When true, TikTok may add recommended music (photos only)
videoMadeWithAi:
type: boolean
description: Set true to disclose AI-generated content
description:
type: string
maxLength: 4000
description: Optional long-form description for photo posts (max 4000 chars). Recommended when content exceeds 90 chars, as photo titles are auto-truncated.
TelegramPlatformData:
type: object
properties:
parseMode:
type: string
enum: [HTML, Markdown, MarkdownV2]
description: Text formatting mode for the message (default is HTML)
disableWebPagePreview:
type: boolean
description: Disable link preview generation for URLs in the message
disableNotification:
type: boolean
description: Send the message silently (users will receive notification without sound)
protectContent:
type: boolean
description: Protect message content from forwarding and saving
description: Text, images (up to 10), videos (up to 10), and mixed media albums. Captions up to 1024 chars for media, 4096 for text-only.
SnapchatPlatformData:
type: object
properties:
contentType:
type: string
enum: [story, saved_story, spotlight]
default: story
description: "Content type: story (ephemeral 24h, default), saved_story (permanent on Public Profile), spotlight (video feed)"
description: "Requires a Public Profile. Single media item only. Content types: story (ephemeral 24h), saved_story (permanent, title max 45 chars), spotlight (video, max 160 chars)."
RedditPlatformData:
type: object
properties:
subreddit:
type: string
description: Target subreddit name (without "r/" prefix). Overrides the default. Use GET /v1/accounts/{id}/reddit-subreddits to list options.
example: socialmedia
title:
type: string
maxLength: 300
description: Post title. Defaults to the first line of content, truncated to 300 characters.
url:
type: string
format: uri
description: URL for link posts. If provided (and forceSelf is not true), creates a link post instead of a text post.
forceSelf:
type: boolean
description: When true, creates a text/self post even when a URL or media is provided.
flairId:
type: string
description: Flair ID for the post. Required by some subreddits. Use GET /v1/accounts/{id}/reddit-flairs?subreddit=name to list flairs.
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
description: Posts are either link (with URL/media) or self (text-only). Use forceSelf to override. Subreddit defaults to the account's configured one. Some subreddits require a flair.
BlueskyPlatformData:
type: object
properties:
threadItems:
type: array
description: Sequence of posts in a Bluesky thread (root then replies in order).
items:
type: object
properties:
content:
type: string
mediaItems:
type: array
items:
$ref: '#/components/schemas/MediaItem'
description: |
Bluesky post settings. Supports text posts with up to 4 images or a single video. threadItems creates a reply chain (Bluesky thread). Images exceeding 1MB are automatically compressed. Alt text supported via mediaItem properties.
QueueSlot:
type: object
properties:
dayOfWeek:
type: integer
description: Day of week (0=Sunday, 6=Saturday)
minimum: 0
maximum: 6
time:
type: string
description: Time in HH:mm format (24-hour)
pattern: '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'
QueueSchedule:
type: object
properties:
_id:
type: string
description: Unique queue identifier
profileId:
type: string
description: Profile ID this queue belongs to
name:
type: string
description: Queue name (e.g., "Morning Posts", "Evening Content")
timezone:
type: string
description: IANA timezone (e.g., America/New_York)
slots:
type: array
items:
$ref: '#/components/schemas/QueueSlot'
active:
type: boolean
description: Whether the queue is active
isDefault:
type: boolean
description: Whether this is the default queue for the profile (used when no queueId specified)
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
Pagination:
type: object
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
pages: { type: integer }
Profile:
type: object
properties:
_id: { type: string }
userId: { type: string }
name: { type: string }
description: { type: string }
color: { type: string }
isDefault: { type: boolean }
isOverLimit:
type: boolean
description: Only present when includeOverLimit=true. Indicates if this profile exceeds the plan limit.
createdAt: { type: string, format: date-time }
SocialAccount:
type: object
properties:
_id: { type: string }
platform: { type: string }
profileId:
oneOf:
- type: string
- $ref: '#/components/schemas/Profile'
username: { type: string }
displayName: { type: string }
profileUrl:
type: string
description: Full profile URL for the connected account on its platform.
isActive: { type: boolean }
followersCount:
type: number
description: Follower count (only included if user has analytics add-on)
followersLastUpdated:
type: string
format: date-time
description: Last time follower count was updated (only included if user has analytics add-on)
AccountWithFollowerStats:
allOf:
- $ref: '#/components/schemas/SocialAccount'
- type: object
properties:
profilePicture: { type: string }
currentFollowers: { type: number, description: Current follower count }
lastUpdated: { type: string, format: date-time }
growth: { type: number, description: Follower change over period }
growthPercentage: { type: number, description: Percentage growth }
dataPoints: { type: number, description: Number of historical snapshots }
accountStats:
type: object
description: |
Platform-specific account stats from the latest daily snapshot.
Fields vary by platform. Only present if metadata has been captured.
properties:
followingCount: { type: number, description: Number of accounts being followed }
mediaCount: { type: number, description: Total media posts (Instagram) }
videoCount: { type: number, description: Total videos (YouTube, TikTok) }
tweetCount: { type: number, description: Total tweets (X/Twitter) }
postsCount: { type: number, description: Total posts (Bluesky) }
pinCount: { type: number, description: Total pins (Pinterest) }
totalViews: { type: number, description: Total channel views (YouTube) }
likesCount: { type: number, description: Total likes received (TikTok) }
monthlyViews: { type: number, description: Monthly profile views (Pinterest) }
listedCount: { type: number, description: Lists the user appears on (X/Twitter) }
boardCount: { type: number, description: Total boards (Pinterest) }
ApiKey:
type: object
properties:
id: { type: string }
name: { type: string }
keyPreview: { type: string }
expiresAt: { type: string, format: date-time }
createdAt: { type: string, format: date-time }
key:
type: string
description: Returned only once, on creation
scope:
type: string
enum: [full, profiles]
description: "'full' grants access to all profiles, 'profiles' restricts to specific profiles"
default: full
profileIds:
type: array
items:
type: object
properties:
_id: { type: string }
name: { type: string }
color: { type: string }
description: Profiles this key can access (populated with name and color). Only present when scope is 'profiles'.
permission:
type: string
enum: [read-write, read]
description: "'read-write' allows all operations, 'read' restricts to GET requests only"
default: read-write
UsageStats:
type: object
properties:
planName: { type: string }
billingPeriod: { type: string, enum: [monthly, yearly] }
signupDate: { type: string, format: date-time }
billingAnchorDay: { type: integer, description: "Day of month (1-31) when the billing cycle resets" }
limits:
type: object
properties:
uploads: { type: integer }
profiles: { type: integer }
usage:
type: object
properties:
uploads: { type: integer }
profiles: { type: integer }
lastReset: { type: string, format: date-time }
PostAnalytics:
type: object
properties:
impressions: { type: integer, example: 0 }
reach: { type: integer, example: 0 }
likes: { type: integer, example: 0 }
comments: { type: integer, example: 0 }
shares: { type: integer, example: 0 }
saves: { type: integer, example: 0, description: 'Number of saves/bookmarks (Instagram, Pinterest)' }
clicks: { type: integer, example: 0 }
views: { type: integer, example: 0 }
engagementRate: { type: number, example: 0 }
lastUpdated: { type: string, format: date-time }
PlatformAnalytics:
type: object
properties:
platform: { type: string }
status: { type: string, enum: [published, failed] }
accountId: { type: string }
accountUsername: { type: string, nullable: true }
analytics:
nullable: true
$ref: '#/components/schemas/PostAnalytics'
syncStatus: { type: string, enum: [synced, pending, unavailable], description: 'Sync state of analytics for this platform' }
platformPostUrl: { type: string, format: uri, nullable: true }
errorMessage: { type: string, nullable: true, description: 'Error details when status is failed' }
AnalyticsOverview:
type: object
properties:
totalPosts: { type: integer }
publishedPosts: { type: integer }
scheduledPosts: { type: integer }
lastSync: { type: string, format: date-time, nullable: true }
dataStaleness:
type: object
properties:
staleAccountCount: { type: integer, description: 'Number of accounts with stale analytics data' }
syncTriggered: { type: boolean, description: 'Whether a background sync was triggered for stale accounts' }
AnalyticsSinglePostResponse:
type: object
properties:
postId: { type: string }
latePostId: { type: string, nullable: true, description: 'Original Late post ID if scheduled via Late' }
status: { type: string, enum: [published, failed, partial], description: 'Overall post status. "partial" when some platforms published and others failed.' }
content: { type: string }
scheduledFor: { type: string, format: date-time }
publishedAt: { type: string, format: date-time, nullable: true }
analytics:
$ref: '#/components/schemas/PostAnalytics'
platformAnalytics:
type: array
items:
$ref: '#/components/schemas/PlatformAnalytics'
platform: { type: string }
platformPostUrl: { type: string, format: uri, nullable: true }
isExternal: { type: boolean }
syncStatus: { type: string, enum: [synced, pending, partial, unavailable], description: 'Overall sync state across all platforms' }
message: { type: string, nullable: true, description: 'Human-readable status message for pending, partial, or failed states' }
thumbnailUrl: { type: string, format: uri, nullable: true }
mediaType: { type: string, enum: [image, video, carousel, text], nullable: true }
mediaItems:
type: array
description: All media items for this post. Carousel posts contain one entry per slide.
items:
type: object
properties:
type: { type: string, enum: [image, video] }
url: { type: string, format: uri, description: Direct URL to the media }
thumbnail: { type: string, format: uri, description: Thumbnail URL (same as url for images) }
AnalyticsListResponse:
type: object
properties:
overview:
$ref: '#/components/schemas/AnalyticsOverview'
posts:
type: array
items:
type: object
properties:
_id: { type: string }
latePostId: { type: string, nullable: true, description: 'Original Late post ID if scheduled via Late' }
content: { type: string }
scheduledFor: { type: string, format: date-time }
publishedAt: { type: string, format: date-time }
status: { type: string }
analytics:
$ref: '#/components/schemas/PostAnalytics'
platforms:
type: array
items:
$ref: '#/components/schemas/PlatformAnalytics'
platform: { type: string }
platformPostUrl: { type: string, format: uri }
isExternal: { type: boolean }
profileId: { type: string, nullable: true }
thumbnailUrl: { type: string, format: uri }
mediaType: { type: string, enum: [image, video, gif, document, carousel, text] }
mediaItems:
type: array
description: All media items for this post. Carousel posts contain one entry per slide.
items:
type: object
properties:
type: { type: string, enum: [image, video] }
url: { type: string, format: uri, description: Direct URL to the media }
thumbnail: { type: string, format: uri, description: Thumbnail URL (same as url for images) }
pagination:
$ref: '#/components/schemas/Pagination'
accounts:
type: array
description: Connected social accounts (followerCount and followersLastUpdated only included if user has analytics add-on)
items:
$ref: '#/components/schemas/SocialAccount'
hasAnalyticsAccess:
type: boolean
description: Whether user has analytics add-on access
LinkedInAggregateAnalyticsTotalResponse:
type: object
description: Response for TOTAL aggregation (lifetime totals)
properties:
accountId: { type: string }
platform: { type: string, example: linkedin }
accountType: { type: string, example: personal }
username: { type: string }
aggregation: { type: string, enum: [TOTAL] }
dateRange:
type: object
nullable: true
properties:
startDate: { type: string, format: date }
endDate: { type: string, format: date }
analytics:
type: object
properties:
impressions: { type: integer, description: Total impressions across all posts }
reach: { type: integer, description: Unique members reached across all posts }
reactions: { type: integer, description: Total reactions across all posts }
comments: { type: integer, description: Total comments across all posts }
shares: { type: integer, description: Total reshares across all posts }
engagementRate: { type: number, description: Overall engagement rate as percentage }
note: { type: string }
lastUpdated: { type: string, format: date-time }
LinkedInAggregateAnalyticsDailyResponse:
type: object
description: Response for DAILY aggregation (time series breakdown)
properties:
accountId: { type: string }
platform: { type: string, example: linkedin }
accountType: { type: string, example: personal }
username: { type: string }
aggregation: { type: string, enum: [DAILY] }
dateRange:
type: object
nullable: true
properties:
startDate: { type: string, format: date }
endDate: { type: string, format: date }
analytics:
type: object
description: Daily breakdown of each metric as date/count pairs. Reach not available with DAILY aggregation.
properties:
impressions:
type: array
items:
type: object
properties:
date: { type: string, format: date }
count: { type: integer }
reactions:
type: array
items:
type: object
properties:
date: { type: string, format: date }
count: { type: integer }
comments:
type: array
items:
type: object
properties:
date: { type: string, format: date }
count: { type: integer }
shares:
type: array
items:
type: object
properties:
date: { type: string, format: date }
count: { type: integer }
skippedMetrics:
type: array
description: Metrics that were skipped due to API limitations
items: { type: string }
note: { type: string }
lastUpdated: { type: string, format: date-time }
PostsListResponse:
type: object
properties:
posts:
type: array
items:
$ref: '#/components/schemas/Post'
pagination:
$ref: '#/components/schemas/Pagination'
PostGetResponse:
type: object
properties:
post:
$ref: '#/components/schemas/Post'
PostCreateResponse:
type: object
properties:
message:
type: string
post:
$ref: '#/components/schemas/Post'
PostUpdateResponse:
type: object
properties:
message:
type: string
post:
$ref: '#/components/schemas/Post'
PostDeleteResponse:
type: object
properties:
message:
type: string
PostRetryResponse:
type: object
properties:
message:
type: string
post:
$ref: '#/components/schemas/Post'
ProfilesListResponse:
type: object
properties:
profiles:
type: array
items:
$ref: '#/components/schemas/Profile'
ProfileGetResponse:
type: object
properties:
profile:
$ref: '#/components/schemas/Profile'
ProfileCreateResponse:
type: object
properties:
message:
type: string
profile:
$ref: '#/components/schemas/Profile'
ProfileUpdateResponse:
type: object
properties:
message:
type: string
profile:
$ref: '#/components/schemas/Profile'
ProfileDeleteResponse:
type: object
properties:
message:
type: string
AccountsListResponse:
type: object
properties:
accounts:
type: array
items:
$ref: '#/components/schemas/SocialAccount'
hasAnalyticsAccess:
type: boolean
description: Whether user has analytics add-on access
AccountGetResponse:
type: object
properties:
account:
$ref: '#/components/schemas/SocialAccount'
FollowerStatsResponse:
type: object
properties:
accounts:
type: array
items:
$ref: '#/components/schemas/AccountWithFollowerStats'
dateRange:
type: object
properties:
from:
type: string
format: date-time
to:
type: string
format: date-time
aggregation:
type: string
enum: [daily, weekly, monthly]
UploadedFile:
type: object
properties:
type:
type: string
enum: [image, video, document]
url:
type: string
format: uri
filename:
type: string
size:
type: integer
mimeType:
type: string
MediaUploadResponse:
type: object
properties:
files:
type: array
items:
$ref: '#/components/schemas/UploadedFile'
UploadTokenResponse:
type: object
properties:
token:
type: string
uploadUrl:
type: string
format: uri
expiresAt:
type: string
format: date-time
status:
type: string
enum: [pending, completed, expired]
UploadTokenStatusResponse:
type: object
properties:
token:
type: string
status:
type: string
enum: [pending, completed, expired]
files:
type: array
items:
$ref: '#/components/schemas/UploadedFile'
createdAt:
type: string
format: date-time
expiresAt:
type: string
format: date-time
completedAt:
type: string
format: date-time
QueueSlotsResponse:
type: object
properties:
exists:
type: boolean
schedule:
$ref: '#/components/schemas/QueueSchedule'
nextSlots:
type: array
items:
type: string
format: date-time
QueueUpdateResponse:
type: object
properties:
success:
type: boolean
schedule:
$ref: '#/components/schemas/QueueSchedule'
nextSlots:
type: array
items:
type: string
format: date-time
reshuffledCount:
type: integer
QueueDeleteResponse:
type: object
properties:
success:
type: boolean
deleted:
type: boolean
QueuePreviewResponse:
type: object
properties:
profileId:
type: string
count:
type: integer
slots:
type: array
items:
type: string
format: date-time
QueueNextSlotResponse:
type: object
properties:
profileId:
type: string
nextSlot:
type: string
format: date-time
timezone:
type: string
DownloadFormat:
type: object
properties:
formatId:
type: string
ext:
type: string
resolution:
type: string
filesize:
type: integer
quality:
type: string
DownloadResponse:
type: object
properties:
url:
type: string
format: uri
title:
type: string
thumbnail:
type: string
format: uri
duration:
type: integer
formats:
type: array
items:
$ref: '#/components/schemas/DownloadFormat'
TranscriptSegment:
type: object
properties:
text:
type: string
start:
type: number
duration:
type: number
TranscriptResponse:
type: object
properties:
transcript:
type: string
segments:
type: array
items:
$ref: '#/components/schemas/TranscriptSegment'
language:
type: string
HashtagInfo:
type: object
properties:
hashtag:
type: string
status:
type: string
enum: [safe, banned, restricted, unknown]
postCount:
type: integer
HashtagCheckResponse:
type: object
properties:
hashtags:
type: array
items:
$ref: '#/components/schemas/HashtagInfo'
CaptionResponse:
type: object
properties:
caption:
type: string
User:
type: object
properties:
_id:
type: string
email:
type: string
name:
type: string
role:
type: string
createdAt:
type: string
format: date-time
UsersListResponse:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
UserGetResponse:
type: object
properties:
user:
$ref: '#/components/schemas/User'
security:
- bearerAuth: []
paths:
/v1/tools/youtube/download:
get:
operationId: downloadYouTubeVideo
tags: [Tools]
summary: Download YouTube video
description: |
Download YouTube videos or audio. Returns available formats or direct download URL.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: YouTube video URL or video ID
schema:
type: string
example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
- name: action
in: query
description: "Action to perform: 'download' returns download URL, 'formats' lists available formats"
schema:
type: string
enum: [download, formats]
default: download
- name: format
in: query
description: Desired format (when action=download)
schema:
type: string
enum: [video, audio]
default: video
- name: quality
in: query
description: Desired quality (when action=download)
schema:
type: string
enum: [hd, sd]
default: hd
- name: formatId
in: query
description: Specific format ID from formats list
schema:
type: string
responses:
"200":
description: Success
headers:
X-RateLimit-Limit:
schema: { type: string }
description: Daily rate limit
X-RateLimit-Remaining:
schema: { type: string }
description: Remaining calls today
X-RateLimit-Reset:
schema: { type: string }
description: Unix timestamp when limit resets
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
formats:
type: array
items:
type: object
properties:
id: { type: string }
label: { type: string }
ext: { type: string }
type: { type: string }
height: { type: integer }
width: { type: integer }
"401":
$ref: '#/components/responses/Unauthorized'
"403":
description: Tools API not available on free plan
"429":
description: Daily rate limit exceeded
/v1/tools/youtube/transcript:
get:
operationId: getYouTubeTranscript
tags: [Tools]
summary: Get YouTube transcript
description: |
Extract transcript/captions from a YouTube video.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: YouTube video URL or video ID
schema:
type: string
- name: lang
in: query
description: Language code for transcript
schema:
type: string
default: en
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
videoId: { type: string }
language: { type: string }
fullText: { type: string }
segments:
type: array
items:
type: object
properties:
text: { type: string }
start: { type: number }
duration: { type: number }
"404":
description: No transcript available
"503":
description: Transcript service temporarily unavailable
/v1/tools/instagram/download:
get:
operationId: downloadInstagramMedia
tags: [Tools]
summary: Download Instagram media
description: |
Download Instagram reels, posts, or photos.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: Instagram reel or post URL
schema:
type: string
example: "https://www.instagram.com/reel/ABC123/"
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
/v1/tools/instagram/hashtag-checker:
post:
operationId: checkInstagramHashtags
tags: [Tools]
summary: Check IG hashtag bans
description: |
Check if Instagram hashtags are banned, restricted, or safe to use.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [hashtags]
properties:
hashtags:
type: array
maxItems: 20
items:
type: string
example: ["travel", "followforfollow", "fitness"]
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
results:
type: array
items:
type: object
properties:
hashtag: { type: string }
status:
type: string
enum: [banned, restricted, safe, unknown]
reason: { type: string }
confidence: { type: number }
summary:
type: object
properties:
banned: { type: integer }
restricted: { type: integer }
safe: { type: integer }
/v1/tools/tiktok/download:
get:
operationId: downloadTikTokVideo
tags: [Tools]
summary: Download TikTok video
description: |
Download TikTok videos with or without watermark.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: TikTok video URL or ID
schema:
type: string
- name: action
in: query
description: "'formats' to list available formats"
schema:
type: string
enum: [download, formats]
default: download
- name: formatId
in: query
description: Specific format ID (0 = no watermark, etc.)
schema:
type: string
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
formats:
type: array
items:
type: object
properties:
id: { type: string }
label: { type: string }
ext: { type: string }
/v1/tools/twitter/download:
get:
operationId: downloadTwitterMedia
tags: [Tools]
summary: Download Twitter/X media
description: |
Download videos from Twitter/X posts.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: Twitter/X post URL
schema:
type: string
example: "https://x.com/user/status/123456789"
- name: action
in: query
schema:
type: string
enum: [download, formats]
default: download
- name: formatId
in: query
schema:
type: string
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
/v1/tools/facebook/download:
get:
operationId: downloadFacebookVideo
tags: [Tools]
summary: Download Facebook video
description: |
Download videos and reels from Facebook.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: Facebook video or reel URL
schema:
type: string
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
thumbnail: { type: string, format: uri }
/v1/tools/linkedin/download:
get:
operationId: downloadLinkedInVideo
tags: [Tools]
summary: Download LinkedIn video
description: |
Download videos from LinkedIn posts.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: LinkedIn post URL
schema:
type: string
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
downloadUrl: { type: string, format: uri }
/v1/tools/bluesky/download:
get:
operationId: downloadBlueskyMedia
tags: [Tools]
summary: Download Bluesky media
description: |
Download videos from Bluesky posts.
Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited).
security:
- bearerAuth: []
parameters:
- name: url
in: query
required: true
description: Bluesky post URL
schema:
type: string
example: "https://bsky.app/profile/user.bsky.social/post/abc123"
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
title: { type: string }
text: { type: string }
downloadUrl: { type: string, format: uri }
thumbnail: { type: string, format: uri }
/v1/tools/validate/post-length:
post:
operationId: validatePostLength
tags: [Validate]
summary: Validate post character count
description: |
Check weighted character count per platform and whether the text is within each platform's limit.
Twitter/X uses weighted counting (URLs = 23 chars via t.co, emojis = 2 chars). All other platforms use plain character length.
Returns counts and limits for all 15 supported platform variants.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [text]
properties:
text:
type: string
description: The post text to check
example: "Check out https://zernio.com for scheduling posts!"
responses:
"200":
description: Character counts per platform
content:
application/json:
schema:
type: object
properties:
text: { type: string }
platforms:
type: object
additionalProperties:
type: object
properties:
count: { type: integer, description: "Character count for this platform" }
limit: { type: integer, description: "Maximum allowed characters" }
valid: { type: boolean, description: "Whether the text is within the limit" }
example:
twitter: { count: 51, limit: 280, valid: true }
twitterPremium: { count: 51, limit: 25000, valid: true }
instagram: { count: 51, limit: 2200, valid: true }
bluesky: { count: 51, limit: 300, valid: true }
snapchat: { count: 51, limit: 160, valid: true }
/v1/tools/validate/post:
post:
operationId: validatePost
tags: [Validate]
summary: Validate post content
description: |
Dry-run the full post validation pipeline without publishing. Catches issues like missing media for Instagram/TikTok/YouTube, hashtag limits, invalid thread formats, Facebook Reel requirements, and character limit violations.
Accepts the same body as `POST /v1/posts`. Does NOT validate accounts, process media, or track usage. This is content-only validation.
Returns errors for failures and warnings for near-limit content (>90% of character limit).
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [platforms]
properties:
content:
type: string
description: Post text content
example: "Check out this video!"
platforms:
type: array
description: Target platforms (same format as POST /v1/posts)
items:
type: object
required: [platform]
properties:
platform:
type: string
enum: [twitter, instagram, tiktok, youtube, facebook, linkedin, bluesky, threads, reddit, pinterest, telegram, snapchat, googlebusiness]
customContent: { type: string }
platformSpecificData: { type: object }
customMedia:
type: array
items:
type: object
properties:
url: { type: string }
type: { type: string, enum: [image, video] }
example:
- platform: youtube
- platform: twitter
mediaItems:
type: array
description: Root media items shared across platforms
items:
type: object
properties:
url: { type: string, format: uri }
type: { type: string, enum: [image, video] }
responses:
"200":
description: Validation result
content:
application/json:
schema:
oneOf:
- type: object
description: Valid post
properties:
valid: { type: boolean }
message: { type: string, example: "No validation issues found." }
warnings:
type: array
items:
type: object
properties:
platform: { type: string }
warning: { type: string }
- type: object
description: Invalid post
properties:
valid: { type: boolean }
errors:
type: array
items:
type: object
properties:
platform: { type: string }
error: { type: string }
warnings:
type: array
items:
type: object
properties:
platform: { type: string }
warning: { type: string }
/v1/tools/validate/media:
post:
operationId: validateMedia
tags: [Validate]
summary: Validate media URL
description: |
Check if a media URL is accessible and return metadata (content type, file size) plus per-platform size limit comparisons.
Performs a HEAD request (with GET fallback) to detect content type and size. Rejects private/localhost URLs for SSRF protection.
Platform limits are sourced from each platform's actual upload constraints.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [url]
properties:
url:
type: string
format: uri
description: Public media URL to validate
example: "https://example.com/image.jpg"
responses:
"200":
description: Media validation result
content:
application/json:
schema:
type: object
properties:
valid: { type: boolean }
url: { type: string, format: uri }
error: { type: string, description: "Error message if valid is false" }
contentType: { type: string, example: "image/jpeg" }
size: { type: integer, nullable: true, description: "File size in bytes" }
sizeFormatted: { type: string, example: "245 KB" }
type: { type: string, enum: [image, video, unknown] }
platformLimits:
type: object
description: Per-platform size limit comparison (only present when size and type are known)
additionalProperties:
type: object
properties:
limit: { type: integer, description: "Platform size limit in bytes" }
limitFormatted: { type: string }
withinLimit: { type: boolean }
example:
instagram: { limit: 8388608, limitFormatted: "8.0 MB", withinLimit: true }
twitter: { limit: 5242880, limitFormatted: "5.0 MB", withinLimit: true }
bluesky: { limit: 1000000, limitFormatted: "977 KB", withinLimit: true }
/v1/tools/validate/subreddit:
get:
operationId: validateSubreddit
tags: [Validate]
summary: Check subreddit existence
description: |
Check if a subreddit exists and return basic info (title, subscriber count, NSFW status, post types allowed).
Uses Reddit's public JSON API (no Reddit auth needed). Returns `exists: false` for private, banned, or nonexistent subreddits.
security:
- bearerAuth: []
parameters:
- name: name
in: query
required: true
description: Subreddit name (with or without "r/" prefix)
schema:
type: string
example: "programming"
responses:
"200":
description: Subreddit lookup result
content:
application/json:
schema:
oneOf:
- type: object
description: Subreddit exists
properties:
exists: { type: boolean }
subreddit:
type: object
properties:
name: { type: string, example: "programming" }
title: { type: string, example: "programming" }
description: { type: string, example: "Computer Programming" }
subscribers: { type: integer, example: 6844284 }
isNSFW: { type: boolean }
type: { type: string, enum: [public, private, restricted], example: "public" }
allowImages: { type: boolean }
allowVideos: { type: boolean }
- type: object
description: Subreddit not found
properties:
exists: { type: boolean }
error: { type: string }
/v1/analytics:
get:
operationId: getAnalytics
tags: [Analytics]
summary: Get post analytics
description: |
Returns analytics for posts. With postId, returns a single post. Without it, returns a paginated list with overview stats.
Accepts both Zernio Post IDs and External Post IDs (auto-resolved). fromDate defaults to 90 days ago if omitted, max range 366 days.
Single post lookups may return 202 (sync pending) or 424 (all platforms failed). For follower stats, use /v1/accounts/follower-stats.
parameters:
- name: postId
in: query
schema: { type: string }
description: Returns analytics for a single post. Accepts both Zernio Post IDs and External Post IDs. Zernio IDs are auto-resolved to External Post analytics.
- name: platform
in: query
schema: { type: string }
description: Filter by platform (default "all")
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID (default "all")
- name: source
in: query
schema: { type: string, enum: [all, late, external], default: all }
description: "Filter by post source: late (posted via Zernio API), external (synced from platform), all (default)"
- name: fromDate
in: query
schema: { type: string, format: date }
description: Inclusive lower bound (YYYY-MM-DD). Defaults to 90 days ago if omitted. Max range is 366 days.
- name: toDate
in: query
schema: { type: string, format: date }
description: Inclusive upper bound (YYYY-MM-DD). Defaults to today if omitted.
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
description: Page size (default 50)
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
description: Page number (default 1)
- name: sortBy
in: query
schema: { type: string, enum: [date, engagement, impressions, reach, likes, comments, shares, saves, clicks, views], default: date }
description: Sort by date, engagement, or a specific metric
- name: order
in: query
schema: { type: string, enum: [asc, desc], default: desc }
description: Sort order
responses:
'200':
description: Analytics result
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/AnalyticsSinglePostResponse'
- $ref: '#/components/schemas/AnalyticsListResponse'
examples:
singlePost:
summary: Single post analytics (Late post with synced analytics)
value:
postId: "65f1c0a9e2b5af0012ab34cd"
latePostId: null
status: "published"
content: "Check out our new product launch!"
scheduledFor: "2024-11-01T10:00:00Z"
publishedAt: "2024-11-01T10:00:05Z"
analytics:
impressions: 15420
reach: 12350
likes: 342
comments: 28
shares: 45
saves: 0
clicks: 189
views: 0
engagementRate: 2.78
lastUpdated: "2024-11-02T08:30:00Z"
platformAnalytics:
- platform: "twitter"
status: "published"
accountId: "64e1f0a9e2b5af0012ab34cd"
accountUsername: "@acmecorp"
analytics:
impressions: 15420
reach: 12350
likes: 342
comments: 28
shares: 45
saves: 0
clicks: 189
views: 0
engagementRate: 2.78
lastUpdated: "2024-11-02T08:30:00Z"
syncStatus: "synced"
platformPostUrl: "https://twitter.com/acmecorp/status/123456789"
errorMessage: null
platform: "twitter"
platformPostUrl: "https://twitter.com/acmecorp/status/123456789"
isExternal: false
syncStatus: "synced"
message: null
thumbnailUrl: "https://storage.example.com/image.jpg"
mediaType: "image"
mediaItems:
- type: "image"
url: "https://storage.example.com/image.jpg"
thumbnail: "https://storage.example.com/image.jpg"
postList:
summary: Paginated analytics list
description: |
Note: The list endpoint returns External Post IDs. Posts originally
scheduled via Zernio will have isExternal: true in this response.
Use platformPostUrl to correlate with your original Zernio Post IDs.
value:
overview:
totalPosts: 156
publishedPosts: 156
scheduledPosts: 0
lastSync: "2024-11-02T08:30:00Z"
dataStaleness:
staleAccountCount: 0
syncTriggered: false
posts:
- _id: "65f1c0a9e2b5af0012ab34cd"
latePostId: "65f1c0a9e2b5af0012ab34ab"
content: "Check out our new product launch!"
scheduledFor: "2024-11-01T10:00:00Z"
publishedAt: "2024-11-01T10:00:05Z"
status: "published"
analytics:
impressions: 15420
reach: 12350
likes: 342
comments: 28
shares: 45
saves: 0
clicks: 189
views: 0
engagementRate: 2.78
lastUpdated: "2024-11-02T08:30:00Z"
platforms:
- platform: "instagram"
status: "published"
accountId: "64e1f0a9e2b5af0012ab34cd"
accountUsername: "@acmecorp"
analytics:
impressions: 15420
reach: 12350
likes: 342
comments: 28
shares: 45
saves: 0
clicks: 189
views: 0
engagementRate: 2.78
lastUpdated: "2024-11-02T08:30:00Z"
syncStatus: "synced"
platformPostUrl: "https://www.instagram.com/reel/ABC123xyz/"
errorMessage: null
platform: "instagram"
platformPostUrl: "https://www.instagram.com/reel/ABC123xyz/"
isExternal: true
profileId: "64e1f0a9e2b5af0012ab34cd"
thumbnailUrl: "https://storage.example.com/thumb.jpg"
mediaType: "carousel"
mediaItems:
- type: "image"
url: "https://storage.example.com/slide1.jpg"
thumbnail: "https://storage.example.com/slide1.jpg"
- type: "image"
url: "https://storage.example.com/slide2.jpg"
thumbnail: "https://storage.example.com/slide2.jpg"
pagination:
page: 1
limit: 50
total: 156
pages: 4
accounts:
- _id: "64e1f0..."
platform: "twitter"
username: "@acmecorp"
displayName: "Acme Corp"
isActive: true
hasAnalyticsAccess: true
'202':
description: Analytics are being synced from the platform (single post lookup only). The response body matches AnalyticsSinglePostResponse with syncStatus "pending" and a message.
content:
application/json:
schema:
$ref: '#/components/schemas/AnalyticsSinglePostResponse'
'400':
description: Validation error
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: Invalid query parameters }
details: { type: object, description: 'Detailed validation errors' }
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: Analytics add-on required }
code: { type: string, example: analytics_addon_required }
'404': { $ref: '#/components/responses/NotFound' }
'424':
description: Post failed to publish on all platforms. Analytics are unavailable. (single post lookup only)
content:
application/json:
schema:
$ref: '#/components/schemas/AnalyticsSinglePostResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/v1/analytics/youtube/daily-views:
get:
operationId: getYouTubeDailyViews
tags: [Analytics]
summary: Get YouTube daily views
description: |
Returns daily view counts for a YouTube video including views, watch time, and subscriber changes.
Requires yt-analytics.readonly scope (re-authorization may be needed). Data has a 2-3 day delay. Max 90 days, defaults to last 30 days.
parameters:
- name: videoId
in: query
required: true
schema: { type: string }
description: The YouTube video ID (e.g., "dQw4w9WgXcQ")
- name: accountId
in: query
required: true
schema: { type: string }
description: The Zernio account ID for the YouTube account
- name: startDate
in: query
schema: { type: string, format: date }
description: Start date (YYYY-MM-DD). Defaults to 30 days ago.
- name: endDate
in: query
schema: { type: string, format: date }
description: End date (YYYY-MM-DD). Defaults to 3 days ago (YouTube data latency).
responses:
'200':
description: Daily views breakdown
content:
application/json:
schema:
$ref: '#/components/schemas/YouTubeDailyViewsResponse'
examples:
success:
summary: Successful response with daily views
value:
success: true
videoId: "dQw4w9WgXcQ"
dateRange:
startDate: "2025-01-01"
endDate: "2025-01-12"
totalViews: 12345
dailyViews:
- date: "2025-01-12"
views: 1234
estimatedMinutesWatched: 567.5
averageViewDuration: 45.2
subscribersGained: 10
subscribersLost: 2
likes: 89
comments: 12
shares: 5
- date: "2025-01-11"
views: 987
estimatedMinutesWatched: 432.1
averageViewDuration: 43.8
subscribersGained: 8
subscribersLost: 1
likes: 67
comments: 8
shares: 3
lastSyncedAt: "2025-01-15T12:00:00Z"
scopeStatus:
hasAnalyticsScope: true
'400':
description: Bad request (missing or invalid parameters)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
examples:
missingVideoId:
value:
error: "videoId is required"
invalidDate:
value:
error: "Invalid startDate format. Use YYYY-MM-DD."
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
code: { type: string, example: "analytics_addon_required" }
'403':
description: Access denied to this account
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Access denied to this account" }
'412':
description: Missing YouTube Analytics scope
content:
application/json:
schema:
$ref: '#/components/schemas/YouTubeScopeMissingResponse'
examples:
scopeMissing:
summary: YouTube Analytics scope not granted
value:
success: false
error: "To access daily video analytics, please reconnect your YouTube account to grant the required permissions."
code: "youtube_analytics_scope_missing"
scopeStatus:
hasAnalyticsScope: false
requiresReauthorization: true
reauthorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=..."
'500':
description: Internal server error
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: false }
error: { type: string }
/v1/analytics/daily-metrics:
get:
operationId: getDailyMetrics
tags: [Analytics]
summary: Get daily aggregated metrics
description: |
Returns daily aggregated analytics metrics and a per-platform breakdown.
Each day includes post count, platform distribution, and summed metrics (impressions, reach, likes, comments, shares, saves, clicks, views).
Defaults to the last 180 days. Requires the Analytics add-on.
parameters:
- name: platform
in: query
schema: { type: string }
description: Filter by platform (e.g. "instagram", "tiktok"). Omit for all platforms.
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID. Omit for all profiles.
- name: fromDate
in: query
schema: { type: string, format: date-time }
description: Inclusive start date (ISO 8601). Defaults to 180 days ago.
- name: toDate
in: query
schema: { type: string, format: date-time }
description: Inclusive end date (ISO 8601). Defaults to now.
- name: source
in: query
schema:
type: string
enum: [all, late, external]
default: all
description: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.
responses:
'200':
description: Daily metrics and platform breakdown
content:
application/json:
schema:
type: object
properties:
dailyData:
type: array
items:
type: object
properties:
date: { type: string, example: "2025-12-01" }
postCount: { type: integer, example: 3 }
platforms:
type: object
additionalProperties: { type: integer }
example: { instagram: 2, twitter: 1 }
metrics:
type: object
properties:
impressions: { type: integer }
reach: { type: integer }
likes: { type: integer }
comments: { type: integer }
shares: { type: integer }
saves: { type: integer }
clicks: { type: integer }
views: { type: integer }
platformBreakdown:
type: array
items:
type: object
properties:
platform: { type: string, example: "instagram" }
postCount: { type: integer, example: 142 }
impressions: { type: integer }
reach: { type: integer }
likes: { type: integer }
comments: { type: integer }
shares: { type: integer }
saves: { type: integer }
clicks: { type: integer }
views: { type: integer }
examples:
success:
value:
dailyData:
- date: "2025-12-01"
postCount: 3
platforms: { instagram: 2, twitter: 1 }
metrics:
impressions: 4520
reach: 3200
likes: 312
comments: 45
shares: 28
saves: 67
clicks: 89
views: 1560
platformBreakdown:
- platform: "instagram"
postCount: 142
impressions: 89400
reach: 62100
likes: 8930
comments: 1204
shares: 567
saves: 2103
clicks: 3402
views: 45200
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
code: { type: string, example: "analytics_addon_required" }
/v1/analytics/best-time:
get:
operationId: getBestTimeToPost
tags: [Analytics]
summary: Get best times to post
description: |
Returns the best times to post based on historical engagement data.
Groups all published posts by day of week and hour (UTC), calculating average engagement per slot.
Use this to auto-schedule posts at optimal times. Requires the Analytics add-on.
parameters:
- name: platform
in: query
schema: { type: string }
description: Filter by platform (e.g. "instagram", "tiktok"). Omit for all platforms.
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID. Omit for all profiles.
- name: source
in: query
schema:
type: string
enum: [all, late, external]
default: all
description: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.
responses:
'200':
description: Best time slots
content:
application/json:
schema:
type: object
properties:
slots:
type: array
items:
type: object
properties:
day_of_week: { type: integer, description: "0=Monday, 6=Sunday", minimum: 0, maximum: 6 }
hour: { type: integer, description: "Hour in UTC (0-23)", minimum: 0, maximum: 23 }
avg_engagement: { type: number, description: "Average engagement (likes + comments + shares + saves)" }
post_count: { type: integer, description: "Number of posts in this slot" }
examples:
success:
value:
slots:
- day_of_week: 2
hour: 18
avg_engagement: 510.3
post_count: 15
- day_of_week: 0
hour: 9
avg_engagement: 342.5
post_count: 12
- day_of_week: 4
hour: 12
avg_engagement: 289.1
post_count: 8
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
requiresAddon: { type: boolean, example: true }
/v1/analytics/content-decay:
get:
operationId: getContentDecay
tags: [Analytics]
summary: Get content performance decay
description: |
Returns how engagement accumulates over time after a post is published.
Each bucket shows what percentage of the post's total engagement had been reached by that time window.
Useful for understanding content lifespan (e.g. "posts reach 78% of total engagement within 24 hours").
Requires the Analytics add-on.
parameters:
- name: platform
in: query
schema: { type: string }
description: Filter by platform (e.g. "instagram", "tiktok"). Omit for all platforms.
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID. Omit for all profiles.
- name: source
in: query
schema:
type: string
enum: [all, late, external]
default: all
description: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.
responses:
'200':
description: Content decay buckets
content:
application/json:
schema:
type: object
properties:
buckets:
type: array
items:
type: object
properties:
bucket_order: { type: integer, description: "Sort order (0 = earliest, 6 = latest)" }
bucket_label: { type: string, description: "Human-readable label" }
avg_pct_of_final: { type: number, description: "Average % of final engagement reached (0-100)" }
post_count: { type: integer, description: "Number of posts with data in this bucket" }
examples:
success:
value:
buckets:
- bucket_order: 0
bucket_label: "0-6h"
avg_pct_of_final: 45.2
post_count: 89
- bucket_order: 1
bucket_label: "6-12h"
avg_pct_of_final: 18.7
post_count: 89
- bucket_order: 2
bucket_label: "12-24h"
avg_pct_of_final: 14.1
post_count: 85
- bucket_order: 3
bucket_label: "1-2d"
avg_pct_of_final: 9.3
post_count: 82
- bucket_order: 4
bucket_label: "2-7d"
avg_pct_of_final: 8.1
post_count: 78
- bucket_order: 5
bucket_label: "7-30d"
avg_pct_of_final: 3.8
post_count: 64
- bucket_order: 6
bucket_label: "30d+"
avg_pct_of_final: 0.8
post_count: 41
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
requiresAddon: { type: boolean, example: true }
/v1/analytics/posting-frequency:
get:
operationId: getPostingFrequency
tags: [Analytics]
summary: Get posting frequency vs engagement
description: |
Returns the correlation between posting frequency (posts per week) and engagement rate, broken down by platform.
Helps find the optimal posting cadence for each platform. Each row represents a specific (platform, posts_per_week) combination
with the average engagement rate observed across all weeks matching that frequency.
Requires the Analytics add-on.
parameters:
- name: platform
in: query
schema: { type: string }
description: Filter by platform (e.g. "instagram", "tiktok"). Omit for all platforms.
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID. Omit for all profiles.
- name: source
in: query
schema:
type: string
enum: [all, late, external]
default: all
description: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.
responses:
'200':
description: Posting frequency data
content:
application/json:
schema:
type: object
properties:
frequency:
type: array
items:
type: object
properties:
platform: { type: string, example: "instagram" }
posts_per_week: { type: integer, description: "Number of posts published that week" }
avg_engagement_rate: { type: number, description: "Average engagement rate as percentage (0-100)" }
avg_engagement: { type: number, description: "Average raw engagement (likes+comments+shares+saves)" }
weeks_count: { type: integer, description: "Number of calendar weeks observed at this frequency" }
examples:
success:
value:
frequency:
- platform: "instagram"
posts_per_week: 2
avg_engagement_rate: 44.4
avg_engagement: 512
weeks_count: 18
- platform: "instagram"
posts_per_week: 4
avg_engagement_rate: 5.9
avg_engagement: 203
weeks_count: 6
- platform: "facebook"
posts_per_week: 3
avg_engagement_rate: 12.5
avg_engagement: 87
weeks_count: 10
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
requiresAddon: { type: boolean, example: true }
/v1/analytics/post-timeline:
get:
operationId: getPostTimeline
tags: [Analytics]
summary: Get post analytics timeline
description: |
Returns a daily timeline of analytics metrics for a specific post, showing how impressions, likes,
and other metrics evolved day-by-day since publishing. Each row represents one day of data per platform.
For multi-platform Zernio posts, returns separate rows for each platform. Requires the Analytics add-on.
parameters:
- name: postId
in: query
required: true
schema: { type: string }
description: |
The post to fetch timeline for. Accepts an ExternalPost ID, a platformPostId, or a Zernio Post ID.
- name: fromDate
in: query
schema: { type: string, format: date-time }
description: Start of date range (ISO 8601). Defaults to 90 days ago.
- name: toDate
in: query
schema: { type: string, format: date-time }
description: End of date range (ISO 8601). Defaults to now.
responses:
'200':
description: Daily analytics timeline
content:
application/json:
schema:
type: object
properties:
postId:
type: string
description: The postId that was requested
timeline:
type: array
items:
type: object
properties:
date: { type: string, format: date, description: "Date in YYYY-MM-DD format" }
platform: { type: string, description: "Platform name (e.g. instagram, tiktok)" }
platformPostId: { type: string, description: "Platform-specific post ID" }
impressions: { type: integer, description: "Total impressions on this date" }
reach: { type: integer, description: "Total reach on this date" }
likes: { type: integer, description: "Total likes on this date" }
comments: { type: integer, description: "Total comments on this date" }
shares: { type: integer, description: "Total shares on this date" }
saves: { type: integer, description: "Total saves on this date" }
clicks: { type: integer, description: "Total clicks on this date" }
views: { type: integer, description: "Total views on this date" }
examples:
single_platform:
summary: Single-platform post timeline
value:
postId: "6507a1b2c3d4e5f6a7b8c9d0"
timeline:
- date: "2025-01-15"
platform: "instagram"
platformPostId: "17902345678901234"
impressions: 1200
reach: 980
likes: 45
comments: 3
shares: 12
saves: 8
clicks: 25
views: 0
- date: "2025-01-16"
platform: "instagram"
platformPostId: "17902345678901234"
impressions: 2400
reach: 1850
likes: 92
comments: 7
shares: 21
saves: 15
clicks: 48
views: 0
'400':
description: Missing required postId parameter
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Missing required parameter: postId" }
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Analytics add-on required" }
code: { type: string, example: "analytics_addon_required" }
'403':
description: Forbidden (post belongs to another user or API key scope violation)
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Forbidden" }
'404':
description: Post not found
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "Post not found" }
/v1/account-groups:
get:
operationId: listAccountGroups
tags: [Account Groups]
summary: List groups
description: Returns all account groups for the authenticated user, including group names and associated account IDs.
responses:
'200':
description: Groups
content:
application/json:
schema:
type: object
properties:
groups:
type: array
items:
type: object
properties:
_id: { type: string }
name: { type: string }
accountIds:
type: array
items: { type: string }
examples:
example:
value:
groups:
- _id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Marketing Accounts"
accountIds:
- "64e1f0a9e2b5af0012ab34cd"
- "64e1f0a9e2b5af0012ab34ce"
- _id: "6507a1b2c3d4e5f6a7b8c9d1"
name: "Personal Brand"
accountIds:
- "64e1f0a9e2b5af0012ab34cf"
'401': { $ref: '#/components/responses/Unauthorized' }
post:
operationId: createAccountGroup
tags: [Account Groups]
summary: Create group
description: Creates a new account group with a name and a list of social account IDs.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name, accountIds]
properties:
name: { type: string }
accountIds:
type: array
items: { type: string }
example:
name: "Marketing Accounts"
accountIds:
- "64e1f0a9e2b5af0012ab34cd"
- "64e1f0a9e2b5af0012ab34ce"
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
message: { type: string }
group:
type: object
properties:
_id: { type: string }
name: { type: string }
accountIds:
type: array
items: { type: string }
example:
message: "Account group created successfully"
group:
_id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Marketing Accounts"
accountIds:
- "64e1f0a9e2b5af0012ab34cd"
- "64e1f0a9e2b5af0012ab34ce"
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { description: Group name already exists }
/v1/account-groups/{groupId}:
put:
operationId: updateAccountGroup
tags: [Account Groups]
summary: Update group
description: Updates the name or account list of an existing group. You can rename the group, change its accounts, or both.
parameters:
- name: groupId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
accountIds:
type: array
items: { type: string }
example:
name: "Updated Marketing Accounts"
accountIds:
- "64e1f0a9e2b5af0012ab34cd"
- "64e1f0a9e2b5af0012ab34ce"
- "64e1f0a9e2b5af0012ab34cf"
responses:
'200':
description: Updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
group:
type: object
example:
message: "Account group updated successfully"
group:
_id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Updated Marketing Accounts"
accountIds:
- "64e1f0a9e2b5af0012ab34cd"
- "64e1f0a9e2b5af0012ab34ce"
- "64e1f0a9e2b5af0012ab34cf"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { description: Group name already exists }
delete:
operationId: deleteAccountGroup
tags: [Account Groups]
summary: Delete group
description: Permanently deletes an account group. The accounts themselves are not affected.
parameters:
- name: groupId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Deleted
content:
application/json:
schema:
type: object
properties:
message: { type: string }
example:
message: "Account group deleted successfully"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/media/presign:
post:
operationId: getMediaPresignedUrl
tags: [Media]
summary: Get presigned upload URL
description: Get a presigned URL to upload files directly to cloud storage (up to 5GB). Returns an uploadUrl and publicUrl. PUT your file to the uploadUrl, then use the publicUrl in your posts.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [filename, contentType]
properties:
filename:
type: string
description: Name of the file to upload
example: "my-video.mp4"
contentType:
type: string
description: MIME type of the file
enum:
- image/jpeg
- image/jpg
- image/png
- image/webp
- image/gif
- video/mp4
- video/mpeg
- video/quicktime
- video/avi
- video/x-msvideo
- video/webm
- video/x-m4v
- application/pdf
example: "video/mp4"
size:
type: integer
description: Optional file size in bytes for pre-validation (max 5GB)
example: 15234567
responses:
'200':
description: Presigned URL generated successfully
content:
application/json:
schema:
type: object
properties:
uploadUrl:
type: string
format: uri
description: Presigned URL to PUT your file to (expires in 1 hour)
publicUrl:
type: string
format: uri
description: Public URL where the file will be accessible after upload
key:
type: string
description: Storage key/path of the file
type:
type: string
enum: [image, video, document]
description: Detected file type based on content type
example:
uploadUrl: "<presigned-upload-url>"
publicUrl: "https://media.zernio.com/temp/1234567890_abc123_my-video.mp4"
key: "temp/1234567890_abc123_my-video.mp4"
type: "video"
'400':
description: Invalid request (missing filename, contentType, or unsupported content type)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
examples:
missing_filename:
value: { error: "filename and contentType are required" }
invalid_type:
value: { error: "Content type not allowed: text/plain" }
file_too_large:
value: { error: "File too large. Maximum size is 5GB." }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/reddit/search:
get:
operationId: searchReddit
tags: [Reddit Search]
summary: Search posts
description: Search Reddit posts using a connected account. Optionally scope to a specific subreddit.
parameters:
- name: accountId
in: query
required: true
schema: { type: string }
- name: subreddit
in: query
schema: { type: string }
- name: q
in: query
required: true
schema: { type: string }
- name: restrict_sr
in: query
schema: { type: string, enum: ['0','1'] }
- name: sort
in: query
schema: { type: string, enum: [relevance, hot, top, new, comments], default: new }
- name: limit
in: query
schema: { type: integer, default: 25, maximum: 100 }
- name: after
in: query
schema: { type: string }
responses:
'200':
description: Search results
content:
application/json:
schema:
type: object
properties:
posts:
type: array
items:
type: object
properties:
id: { type: string }
title: { type: string }
selftext: { type: string }
author: { type: string }
subreddit: { type: string }
score: { type: integer }
num_comments: { type: integer }
created_utc: { type: number }
permalink: { type: string }
after: { type: string }
example:
posts:
- id: "1abc234"
title: "How to grow on social media in 2025"
selftext: "Here are my tips..."
author: "marketingpro"
subreddit: "socialmedia"
score: 156
num_comments: 42
created_utc: 1730000000
permalink: "/r/socialmedia/comments/1abc234/how_to_grow/"
after: "t3_1abc234"
'400': { description: Missing params }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/reddit/feed:
get:
operationId: getRedditFeed
tags: [Reddit Search]
summary: Get subreddit feed
description: Fetch posts from a subreddit feed. Supports sorting, time filtering, and cursor-based pagination.
parameters:
- name: accountId
in: query
required: true
schema: { type: string }
- name: subreddit
in: query
schema: { type: string }
- name: sort
in: query
schema: { type: string, enum: [hot, new, top, rising], default: hot }
- name: limit
in: query
schema: { type: integer, default: 25, maximum: 100 }
- name: after
in: query
schema: { type: string }
- name: t
in: query
schema: { type: string, enum: [hour, day, week, month, year, all] }
responses:
'200':
description: Feed items
content:
application/json:
schema:
type: object
properties:
posts:
type: array
items:
type: object
after: { type: string }
example:
posts:
- id: "1xyz789"
title: "Top marketing trends this week"
author: "trendwatcher"
subreddit: "marketing"
score: 892
num_comments: 134
created_utc: 1730100000
permalink: "/r/marketing/comments/1xyz789/top_marketing_trends/"
- id: "1def456"
title: "My social media strategy that worked"
author: "growthexpert"
subreddit: "marketing"
score: 567
num_comments: 89
created_utc: 1730050000
permalink: "/r/marketing/comments/1def456/my_social_media_strategy/"
after: "t3_1def456"
'400': { description: Missing params }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/usage-stats:
get:
operationId: getUsageStats
tags: [Usage]
summary: Get plan and usage stats
description: Returns the current plan name, billing period, plan limits, and usage counts.
responses:
'200':
description: Usage stats
content:
application/json:
schema:
$ref: '#/components/schemas/UsageStats'
example:
planName: "Pro"
billingPeriod: "monthly"
signupDate: "2024-01-15T10:30:00Z"
billingAnchorDay: 15
limits:
uploads: 500
profiles: 10
usage:
uploads: 127
profiles: 3
lastReset: "2024-11-01T00:00:00Z"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/posts:
get:
operationId: listPosts
tags: [Posts]
summary: List posts
description: Returns a paginated list of posts. Published posts include platformPostUrl with the public URL on each platform.
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/LimitParam'
- name: status
in: query
schema: { type: string, enum: [draft, scheduled, published, failed] }
- name: platform
in: query
schema: { type: string, example: twitter }
- name: profileId
in: query
schema: { type: string }
- name: createdBy
in: query
schema: { type: string }
- name: dateFrom
in: query
schema: { type: string, format: date }
- name: dateTo
in: query
schema: { type: string, format: date }
- name: includeHidden
in: query
schema: { type: boolean, default: false }
- name: search
in: query
schema: { type: string }
description: Search posts by text content.
- name: sortBy
in: query
schema:
type: string
enum: [scheduled-desc, scheduled-asc, created-desc, created-asc, status, platform]
default: scheduled-desc
description: Sort order for results.
responses:
'200':
description: Paginated posts
content:
application/json:
schema:
$ref: '#/components/schemas/PostsListResponse'
examples:
scheduledPost:
summary: Scheduled post (pending publish)
value:
posts:
- _id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "scheduled"
scheduledFor: "2024-11-01T10:00:00Z"
timezone: "UTC"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0..."
platform: "twitter"
username: "@acme"
displayName: "Acme Corp"
isActive: true
status: "pending"
tags: ["launch"]
createdAt: "2024-10-01T12:00:00Z"
updatedAt: "2024-10-01T12:00:00Z"
pagination:
page: 1
limit: 10
total: 1
pages: 1
publishedPost:
summary: Published post with platformPostUrl
value:
posts:
- _id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "published"
scheduledFor: "2024-11-01T10:00:00Z"
publishedAt: "2024-11-01T10:00:05Z"
timezone: "UTC"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0a9e2b5af0012ab34de"
platform: "twitter"
username: "@acmecorp"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platformPostId: "1852634789012345678"
platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
- platform: "linkedin"
accountId:
_id: "64e1f0a9e2b5af0012ab34ef"
platform: "linkedin"
username: "acme-corporation"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:06Z"
platformPostId: "urn:li:share:7123456789012345678"
platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
tags: ["launch"]
createdAt: "2024-10-01T12:00:00Z"
updatedAt: "2024-11-01T10:00:06Z"
pagination:
page: 1
limit: 10
total: 1
pages: 1
'401': { $ref: '#/components/responses/Unauthorized' }
post:
operationId: createPost
tags: [Posts]
summary: Create post
description: |
Create and optionally publish a post. Immediate posts (publishNow: true) include platformPostUrl in the response.
Content is optional when media is attached or all platforms have customContent. See each platform's schema for media constraints.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title: { type: string }
content:
type: string
description: Post caption/text. Optional when media is attached or all platforms have customContent. Required for text-only posts.
mediaItems:
type: array
items:
type: object
properties:
type: { type: string, enum: [image, video, gif, document] }
url: { type: string, format: uri }
platforms:
type: array
items:
type: object
properties:
platform: { type: string, example: twitter }
accountId: { type: string }
customContent:
type: string
description: Platform-specific text override. When set, this content is used instead of the top-level post content for this platform. Useful for tailoring captions per platform (e.g. keeping tweets under 280 characters).
customMedia:
type: array
items:
type: object
properties:
type: { type: string, enum: [image, video, gif, document] }
url: { type: string, format: uri }
scheduledFor:
type: string
format: date-time
description: Optional per-platform scheduled time override. When omitted, the top-level scheduledFor is used.
platformSpecificData:
oneOf:
- $ref: '#/components/schemas/TwitterPlatformData'
- $ref: '#/components/schemas/ThreadsPlatformData'
- $ref: '#/components/schemas/FacebookPlatformData'
- $ref: '#/components/schemas/InstagramPlatformData'
- $ref: '#/components/schemas/LinkedInPlatformData'
- $ref: '#/components/schemas/PinterestPlatformData'
- $ref: '#/components/schemas/YouTubePlatformData'
- $ref: '#/components/schemas/GoogleBusinessPlatformData'
- $ref: '#/components/schemas/TikTokPlatformData'
- $ref: '#/components/schemas/TelegramPlatformData'
- $ref: '#/components/schemas/SnapchatPlatformData'
- $ref: '#/components/schemas/RedditPlatformData'
- $ref: '#/components/schemas/BlueskyPlatformData'
scheduledFor: { type: string, format: date-time }
publishNow: { type: boolean, default: false }
isDraft:
type: boolean
default: false
description: When true, saves the post as a draft. When none of scheduledFor, publishNow, or queuedFromProfile are provided, the post defaults to draft automatically.
timezone: { type: string, default: UTC }
tags:
type: array
description: "Tags/keywords. YouTube constraints: each tag max 100 chars, combined max 500 chars, duplicates auto-removed."
items: { type: string }
hashtags:
type: array
items: { type: string }
mentions:
type: array
items: { type: string }
crosspostingEnabled: { type: boolean, default: true }
metadata: { type: object, additionalProperties: true }
tiktokSettings:
$ref: '#/components/schemas/TikTokPlatformData'
description: Root-level TikTok settings applied to all TikTok platforms. Merged into each platform's platformSpecificData, with platform-specific settings taking precedence.
recycling:
$ref: '#/components/schemas/RecyclingConfig'
queuedFromProfile:
type: string
description: Profile ID to schedule via queue. When provided without scheduledFor, the post is auto-assigned to the next available slot. Do not call /v1/queue/next-slot and use that time in scheduledFor, as that bypasses queue locking.
queueId:
type: string
description: |
Specific queue ID to use when scheduling via queue.
Only used when queuedFromProfile is also provided.
If omitted, uses the profile's default queue.
examples:
recyclingPost:
summary: Post with weekly recycling and content variations
value:
content: "Check out our evergreen guide!"
platforms:
- platform: twitter
accountId: "64e1f0a9e2b5af0012ab34cd"
scheduledFor: "2025-06-01T10:00:00Z"
recycling:
gap: 2
gapFreq: week
expireCount: 6
contentVariations:
- "Check out our evergreen guide!"
- "Don't miss our essential guide!"
- "Our most popular guide, updated!"
tiktokPhotoCarousel:
summary: TikTok photo carousel with draft mode
value:
content: "Check out these photos!"
mediaItems:
- type: image
url: "https://example.com/photo1.jpg"
- type: image
url: "https://example.com/photo2.jpg"
platforms:
- platform: tiktok
accountId: "64e1f0a9e2b5af0012ab34cd"
tiktokSettings:
draft: true
privacyLevel: "PUBLIC_TO_EVERYONE"
allowComment: true
contentPreviewConfirmed: true
expressConsentGiven: true
tiktokVideo:
summary: TikTok video post (direct publish)
value:
content: "New video is live!"
mediaItems:
- type: video
url: "https://example.com/video.mp4"
platforms:
- platform: tiktok
accountId: "64e1f0a9e2b5af0012ab34cd"
tiktokSettings:
privacyLevel: "PUBLIC_TO_EVERYONE"
allowComment: true
allowDuet: true
allowStitch: true
commercialContentType: "none"
contentPreviewConfirmed: true
expressConsentGiven: true
multiPlatform:
summary: Multi-platform post (Twitter + LinkedIn)
value:
content: "We just launched our new product!"
mediaItems:
- type: image
url: "https://example.com/launch.jpg"
platforms:
- platform: twitter
accountId: "64e1f0a9e2b5af0012ab34cd"
- platform: linkedin
accountId: "64e1f0a9e2b5af0012ab34ef"
scheduledFor: "2024-11-01T10:00:00Z"
timezone: "America/New_York"
responses:
'201':
description: Post created
content:
application/json:
schema:
$ref: '#/components/schemas/PostCreateResponse'
examples:
scheduled:
summary: Scheduled post (URLs populated after publish time)
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "scheduled"
scheduledFor: "2024-11-01T10:00:00Z"
timezone: "UTC"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0..."
platform: "twitter"
username: "@acme"
displayName: "Acme Corp"
isActive: true
status: "pending"
message: "Post scheduled successfully"
immediatePublish:
summary: Immediate post with publishNow=true (URLs included)
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
timezone: "UTC"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0a9e2b5af0012ab34de"
platform: "twitter"
username: "@acmecorp"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platformPostId: "1852634789012345678"
platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
- platform: "linkedin"
accountId:
_id: "64e1f0a9e2b5af0012ab34ef"
platform: "linkedin"
username: "acme-corporation"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:06Z"
platformPostId: "urn:li:share:7123456789012345678"
platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
message: "Post published successfully"
queueScheduled:
summary: Post scheduled via queue (using queuedFromProfile)
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "Scheduled via queue!"
status: "scheduled"
scheduledFor: "2024-11-01T09:00:00Z"
timezone: "America/New_York"
queuedFromProfile: "64f0a1b2c3d4e5f6a7b8c9d0"
queueId: "64f0a1b2c3d4e5f6a7b8c9d1"
platforms:
- platform: "linkedin"
accountId:
_id: "64e1f0..."
platform: "linkedin"
username: "acme-corp"
displayName: "Acme Corp"
isActive: true
status: "pending"
message: "Post scheduled successfully"
'400':
description: Validation error
content:
application/json:
schema:
type: object
properties:
error: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
content:
application/json:
schema:
type: object
properties:
error: { type: string }
'409':
description: Duplicate content detected
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: "This exact content was already posted to this account within the last 24 hours." }
details:
type: object
properties:
accountId: { type: string }
platform: { type: string }
existingPostId: { type: string }
'429':
description: "Rate limit exceeded. Possible causes: API rate limit, velocity limit (15 posts/hour per account), account cooldown, or daily platform limits."
content:
application/json:
schema:
type: object
properties:
error: { type: string }
details:
type: object
description: Additional context about the rate limit
headers:
Retry-After:
description: Seconds until the rate limit resets (for API rate limits)
schema: { type: integer }
X-RateLimit-Limit:
description: The rate limit ceiling
schema: { type: integer }
X-RateLimit-Remaining:
description: Requests remaining in current window
schema: { type: integer }
/v1/posts/{postId}:
get:
operationId: getPost
tags: [Posts]
summary: Get post
description: |
Fetch a single post by ID. For published posts, this returns platformPostUrl for each platform.
parameters:
- name: postId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Post
content:
application/json:
schema:
$ref: '#/components/schemas/PostGetResponse'
examples:
scheduledPost:
summary: Scheduled post (pending)
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "scheduled"
scheduledFor: "2024-11-01T10:00:00Z"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0..."
platform: "twitter"
username: "@acme"
displayName: "Acme Corp"
isActive: true
status: "pending"
publishedPost:
summary: Published post with platformPostUrl
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
title: "Launch post"
content: "We just launched!"
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0a9e2b5af0012ab34de"
platform: "twitter"
username: "@acmecorp"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platformPostId: "1852634789012345678"
platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
- platform: "linkedin"
accountId:
_id: "64e1f0a9e2b5af0012ab34ef"
platform: "linkedin"
username: "acme-corporation"
displayName: "Acme Corporation"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:06Z"
platformPostId: "urn:li:share:7123456789012345678"
platformPostUrl: "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678"
failedPost:
summary: Failed post with error details
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "This post failed to publish"
status: "failed"
platforms:
- platform: "instagram"
accountId:
_id: "64e1f0a9e2b5af0012ab34de"
platform: "instagram"
username: "acmecorp"
isActive: false
status: "failed"
errorMessage: "Instagram access token has expired. Please reconnect your account."
errorCategory: "auth_expired"
errorSource: "user"
partialPost:
summary: Partial success (some platforms failed)
value:
post:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "Launch announcement!"
status: "partial"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0a9e2b5af0012ab34de"
platform: "twitter"
username: "@acmecorp"
isActive: true
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platformPostId: "1852634789012345678"
platformPostUrl: "https://twitter.com/acmecorp/status/1852634789012345678"
- platform: "threads"
accountId:
_id: "64e1f0a9e2b5af0012ab34ef"
platform: "threads"
username: "acmecorp"
isActive: true
status: "failed"
errorMessage: "Post text exceeds the 500 character limit for Threads."
errorCategory: "user_content"
errorSource: "user"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: updatePost
tags: [Posts]
summary: Update post
description: |
Update an existing post. Only draft, scheduled, failed, and partial posts can be edited.
Published, publishing, and cancelled posts cannot be modified.
parameters:
- name: postId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
content: { type: string }
scheduledFor: { type: string, format: date-time }
tiktokSettings:
$ref: '#/components/schemas/TikTokPlatformData'
description: Root-level TikTok settings applied to all TikTok platforms. Merged into each platform's platformSpecificData, with platform-specific settings taking precedence.
recycling:
$ref: '#/components/schemas/RecyclingConfig'
additionalProperties: true
example:
content: "Updated content for our launch post!"
scheduledFor: "2024-11-02T14:00:00Z"
responses:
'200':
description: Post updated
content:
application/json:
schema:
$ref: '#/components/schemas/PostUpdateResponse'
example:
message: "Post updated successfully"
post:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "Updated content for our launch post!"
status: "scheduled"
scheduledFor: "2024-11-02T14:00:00Z"
'207':
description: Partial publish success
'400':
description: Invalid request
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deletePost
tags: [Posts]
summary: Delete post
description: Delete a draft or scheduled post from Zernio. Published posts cannot be deleted; use the Unpublish endpoint instead. Upload quota is automatically refunded.
parameters:
- name: postId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/PostDeleteResponse'
example:
message: "Post deleted successfully"
'400':
description: Cannot delete published posts
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
/v1/posts/bulk-upload:
post:
operationId: bulkUploadPosts
tags: [Posts]
summary: Bulk upload from CSV
description: Create multiple posts by uploading a CSV file. Use dryRun=true to validate without creating posts.
parameters:
- name: dryRun
in: query
schema: { type: boolean, default: false }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'200':
description: Bulk upload results
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
totalRows: { type: integer }
created: { type: integer }
failed: { type: integer }
errors:
type: array
items:
type: object
properties:
row: { type: integer }
error: { type: string }
posts:
type: array
items: { $ref: '#/components/schemas/Post' }
example:
success: true
totalRows: 10
created: 8
failed: 2
errors:
- row: 3
error: "Invalid date format"
- row: 7
error: "Account not found"
posts:
- _id: "65f1c0a9e2b5af0012ab34cd"
content: "First bulk post"
status: "scheduled"
scheduledFor: "2024-11-01T10:00:00Z"
'207':
description: Partial success
'400':
description: Invalid CSV or validation errors
'401': { $ref: '#/components/responses/Unauthorized' }
'429':
description: |
Rate limit exceeded. Possible causes: API rate limit (requests per minute) or account cooldown (one or more accounts for platforms specified in the CSV are temporarily rate-limited).
content:
application/json:
schema:
type: object
properties:
error: { type: string }
details:
type: object
/v1/posts/{postId}/retry:
post:
operationId: retryPost
tags: [Posts]
summary: Retry failed post
description: Immediately retries publishing a failed post. Returns the updated post with its new status.
parameters:
- name: postId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Retry successful
content:
application/json:
schema:
$ref: '#/components/schemas/PostRetryResponse'
example:
message: "Post published successfully"
post:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "Check out our new product!"
status: "published"
publishedAt: "2024-11-01T10:00:05Z"
platforms:
- platform: "twitter"
accountId:
_id: "64e1f0..."
platform: "twitter"
username: "@acme"
displayName: "Acme Corp"
isActive: true
status: "published"
platformPostId: "1234567890"
platformPostUrl: "https://twitter.com/acme/status/1234567890"
'207':
description: Partial success
'400':
description: Invalid state
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: Post is currently publishing
'429':
description: |
Rate limit exceeded. Possible causes: API rate limit (requests per minute), velocity limit (15 posts/hour per account), or account cooldown (temporarily rate-limited due to repeated errors).
content:
application/json:
schema:
type: object
properties:
error: { type: string }
details:
type: object
/v1/posts/{postId}/unpublish:
post:
operationId: unpublishPost
tags: [Posts]
summary: Unpublish post
description: |
Deletes a published post from the specified platform. The post record in Zernio is kept but its status is updated to cancelled.
Not supported on Instagram, TikTok, or Snapchat. Threaded posts delete all items. YouTube deletion is permanent.
parameters:
- name: postId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [platform]
properties:
platform:
type: string
description: The platform to delete the post from
enum:
- threads
- facebook
- twitter
- linkedin
- youtube
- pinterest
- reddit
- bluesky
- googlebusiness
- telegram
example:
platform: "threads"
responses:
'200':
description: Post deleted from platform
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Post deleted from threads successfully"
'400':
description: "Invalid request: platform not supported for deletion, post not on that platform, not published, no platform post ID, or no access token."
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
'500':
description: Platform API deletion failed
/v1/users:
get:
operationId: listUsers
tags: [Users]
summary: List users
description: Returns all users in the workspace including roles and profile access. Also returns the currentUserId of the caller.
responses:
'200':
description: Users
content:
application/json:
schema:
type: object
properties:
currentUserId: { type: string }
users:
type: array
items:
type: object
properties:
_id: { type: string }
name: { type: string }
email: { type: string }
role: { type: string }
isRoot: { type: boolean }
profileAccess:
type: array
items: { type: string }
createdAt: { type: string, format: date-time }
example:
currentUserId: "6507a1b2c3d4e5f6a7b8c9d0"
users:
- _id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "John Doe"
email: "john@example.com"
role: "owner"
isRoot: true
profileAccess: ["all"]
createdAt: "2024-01-15T10:30:00Z"
- _id: "6507a1b2c3d4e5f6a7b8c9d1"
name: "Jane Smith"
email: "jane@example.com"
role: "member"
isRoot: false
profileAccess:
- "64f0a1b2c3d4e5f6a7b8c9d0"
- "64f0a1b2c3d4e5f6a7b8c9d1"
createdAt: "2024-03-20T14:45:00Z"
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/users/{userId}:
get:
operationId: getUser
tags: [Users]
summary: Get user
description: Returns a single user's details by ID, including name, email, and role.
parameters:
- name: userId
in: path
required: true
schema: { type: string }
responses:
'200':
description: User
content:
application/json:
schema:
type: object
properties:
user:
type: object
properties:
_id: { type: string }
name: { type: string }
email: { type: string }
role: { type: string }
isRoot: { type: boolean }
profileAccess:
type: array
items: { type: string }
example:
user:
_id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "John Doe"
email: "john@example.com"
role: "owner"
isRoot: true
profileAccess: ["all"]
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden
'404': { $ref: '#/components/responses/NotFound' }
/v1/profiles:
get:
operationId: listProfiles
tags: [Profiles]
summary: List profiles
description: Returns profiles sorted by creation date. Use includeOverLimit=true to include profiles that exceed the plan limit.
parameters:
- name: includeOverLimit
in: query
required: false
schema:
type: boolean
default: false
description: "When true, includes over-limit profiles (marked with isOverLimit: true)."
responses:
'200':
description: Profiles
content:
application/json:
schema:
$ref: '#/components/schemas/ProfilesListResponse'
examples:
example:
value:
profiles:
- _id: "64f0..."
name: "Personal Brand"
color: "#ffeda0"
isDefault: true
'401': { $ref: '#/components/responses/Unauthorized' }
post:
operationId: createProfile
tags: [Profiles]
summary: Create profile
description: Creates a new profile with a name, optional description, and color.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name: { type: string }
description: { type: string }
color: { type: string, example: '#ffeda0' }
example:
name: "Marketing Team"
description: "Profile for marketing campaigns"
color: "#4CAF50"
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/ProfileCreateResponse'
example:
message: "Profile created successfully"
profile:
_id: "64f0a1b2c3d4e5f6a7b8c9d0"
userId: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Marketing Team"
description: "Profile for marketing campaigns"
color: "#4CAF50"
isDefault: false
createdAt: "2024-11-01T10:00:00Z"
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: Profile limit exceeded }
/v1/profiles/{profileId}:
get:
operationId: getProfile
tags: [Profiles]
summary: Get profile
description: Returns a single profile by ID, including its name, color, and default status.
parameters:
- name: profileId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Profile
content:
application/json:
schema:
type: object
properties:
profile: { $ref: '#/components/schemas/Profile' }
example:
profile:
_id: "64f0a1b2c3d4e5f6a7b8c9d0"
userId: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Marketing Team"
description: "Profile for marketing campaigns"
color: "#4CAF50"
isDefault: false
createdAt: "2024-11-01T10:00:00Z"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: updateProfile
tags: [Profiles]
summary: Update profile
description: Updates a profile's name, description, color, or default status.
parameters:
- name: profileId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
description: { type: string }
color: { type: string }
isDefault: { type: boolean }
example:
name: "Marketing Team (Updated)"
color: "#2196F3"
isDefault: true
responses:
'200':
description: Updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
profile: { $ref: '#/components/schemas/Profile' }
example:
message: "Profile updated successfully"
profile:
_id: "64f0a1b2c3d4e5f6a7b8c9d0"
userId: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Marketing Team (Updated)"
description: "Profile for marketing campaigns"
color: "#2196F3"
isDefault: true
createdAt: "2024-11-01T10:00:00Z"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteProfile
tags: [Profiles]
summary: Delete profile
description: Permanently deletes a profile by ID.
parameters:
- name: profileId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Deleted
content:
application/json:
schema:
type: object
properties:
message: { type: string }
example:
message: "Profile deleted successfully"
'400': { description: Has connected accounts }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: Forbidden }
'404': { $ref: '#/components/responses/NotFound' }
/v1/accounts:
get:
operationId: listAccounts
tags: [Accounts]
summary: List accounts
description: Returns connected social accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on.
parameters:
- name: profileId
in: query
schema: { type: string }
description: Filter accounts by profile ID
- name: platform
in: query
schema: { type: string }
description: Filter accounts by platform (e.g. "instagram", "twitter").
- name: includeOverLimit
in: query
required: false
schema:
type: boolean
default: false
description: When true, includes accounts from over-limit profiles.
responses:
'200':
description: Accounts
content:
application/json:
schema:
type: object
properties:
accounts:
type: array
items: { $ref: '#/components/schemas/SocialAccount' }
hasAnalyticsAccess:
type: boolean
description: Whether user has analytics add-on access
examples:
example:
value:
accounts:
- _id: "64e1..."
platform: "twitter"
profileId:
_id: "64f0..."
name: "My Brand"
slug: "my-brand"
username: "@acme"
displayName: "Acme"
profileUrl: "https://x.com/acme"
isActive: true
hasAnalyticsAccess: false
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/accounts/follower-stats:
get:
operationId: getFollowerStats
tags: [Accounts, Analytics]
summary: Get follower stats
description: |
Returns follower count history and growth metrics for connected social accounts.
Requires analytics add-on subscription. Follower counts are refreshed once per day.
parameters:
- name: accountIds
in: query
schema: { type: string }
description: Comma-separated list of account IDs (optional, defaults to all user's accounts)
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID
- name: fromDate
in: query
schema: { type: string, format: date }
description: Start date in YYYY-MM-DD format (defaults to 30 days ago)
- name: toDate
in: query
schema: { type: string, format: date }
description: End date in YYYY-MM-DD format (defaults to today)
- name: granularity
in: query
schema: { type: string, enum: [daily, weekly, monthly], default: daily }
description: Data aggregation level
responses:
'200':
description: Follower stats
content:
application/json:
schema:
type: object
properties:
accounts:
type: array
items:
$ref: '#/components/schemas/AccountWithFollowerStats'
stats:
type: object
additionalProperties:
type: array
items:
type: object
properties:
date: { type: string, format: date }
followers: { type: number }
dateRange:
type: object
properties:
from: { type: string, format: date-time }
to: { type: string, format: date-time }
granularity: { type: string }
examples:
example:
value:
accounts:
- _id: "64e1..."
platform: "twitter"
username: "@acme"
currentFollowers: 1250
growth: 50
growthPercentage: 4.17
dataPoints: 30
stats:
"64e1...":
- date: "2024-01-01"
followers: 1200
- date: "2024-01-02"
followers: 1250
dateRange:
from: "2024-01-01T00:00:00.000Z"
to: "2024-01-31T23:59:59.999Z"
granularity: "daily"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string, example: Analytics add-on required }
message: { type: string, example: Follower stats tracking requires the Analytics add-on. Please upgrade to access this feature. }
requiresAddon: { type: boolean, example: true }
/v1/accounts/{accountId}:
put:
operationId: updateAccount
tags: [Accounts]
summary: Update account
description: Updates a connected social account's display name or username override.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
username: { type: string }
displayName: { type: string }
example:
displayName: "Acme Corporation Official"
responses:
'200':
description: Updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
username: { type: string }
displayName: { type: string }
example:
message: "Account updated successfully"
username: "@acmecorp"
displayName: "Acme Corporation Official"
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteAccount
tags: [Accounts]
summary: Disconnect account
description: Disconnects and removes a connected social account.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Disconnected
content:
application/json:
schema:
type: object
properties:
message: { type: string }
example:
message: "Account disconnected successfully"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/accounts/health:
get:
operationId: getAllAccountsHealth
tags: [Accounts]
summary: Check accounts health
description: Returns health status of all connected accounts including token validity, permissions, and issues needing attention.
parameters:
- name: profileId
in: query
description: Filter by profile ID
schema: { type: string }
- name: platform
in: query
description: Filter by platform
schema:
type: string
enum: [facebook, instagram, linkedin, twitter, tiktok, youtube, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat]
- name: status
in: query
description: Filter by health status
schema:
type: string
enum: [healthy, warning, error]
responses:
'200':
description: Account health summary
content:
application/json:
schema:
type: object
properties:
summary:
type: object
properties:
total: { type: integer, description: Total number of accounts }
healthy: { type: integer, description: Number of healthy accounts }
warning: { type: integer, description: Number of accounts with warnings }
error: { type: integer, description: Number of accounts with errors }
needsReconnect: { type: integer, description: Number of accounts needing reconnection }
accounts:
type: array
items:
type: object
properties:
accountId: { type: string }
platform: { type: string }
username: { type: string }
displayName: { type: string }
profileId: { type: string }
status: { type: string, enum: [healthy, warning, error] }
canPost: { type: boolean }
canFetchAnalytics: { type: boolean }
tokenValid: { type: boolean }
tokenExpiresAt: { type: string, format: date-time }
needsReconnect: { type: boolean }
issues: { type: array, items: { type: string } }
example:
summary:
total: 5
healthy: 3
warning: 1
error: 1
needsReconnect: 1
accounts:
- accountId: "abc123"
platform: "instagram"
username: "myaccount"
status: "healthy"
canPost: true
canFetchAnalytics: true
tokenValid: true
tokenExpiresAt: "2025-06-15T00:00:00Z"
needsReconnect: false
issues: []
- accountId: "def456"
platform: "twitter"
username: "mytwitter"
status: "error"
canPost: false
canFetchAnalytics: false
tokenValid: false
needsReconnect: true
issues: ["Token expired"]
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/accounts/{accountId}/health:
get:
operationId: getAccountHealth
tags: [Accounts]
summary: Check account health
description: Returns detailed health info for a specific account including token status, permissions, and recommendations.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The account ID to check
responses:
'200':
description: Account health details
content:
application/json:
schema:
type: object
properties:
accountId: { type: string }
platform: { type: string }
username: { type: string }
displayName: { type: string }
status:
type: string
enum: [healthy, warning, error]
description: Overall health status
tokenStatus:
type: object
properties:
valid: { type: boolean, description: Whether the token is valid }
expiresAt: { type: string, format: date-time }
expiresIn: { type: string, description: Human-readable time until expiry }
needsRefresh: { type: boolean, description: Whether token expires within 24 hours }
permissions:
type: object
properties:
posting:
type: array
items:
type: object
properties:
scope: { type: string }
granted: { type: boolean }
required: { type: boolean }
analytics:
type: array
items:
type: object
properties:
scope: { type: string }
granted: { type: boolean }
required: { type: boolean }
optional:
type: array
items:
type: object
properties:
scope: { type: string }
granted: { type: boolean }
required: { type: boolean }
canPost: { type: boolean }
canFetchAnalytics: { type: boolean }
missingRequired: { type: array, items: { type: string } }
issues:
type: array
items: { type: string }
description: List of issues found
recommendations:
type: array
items: { type: string }
description: Actionable recommendations to fix issues
example:
accountId: "abc123"
platform: "instagram"
username: "myaccount"
displayName: "My Account"
status: "healthy"
tokenStatus:
valid: true
expiresAt: "2025-06-15T00:00:00Z"
expiresIn: "180 days"
needsRefresh: false
permissions:
posting:
- scope: "instagram_basic"
granted: true
required: true
- scope: "instagram_content_publish"
granted: true
required: true
analytics:
- scope: "instagram_manage_insights"
granted: true
required: false
optional: []
canPost: true
canFetchAnalytics: true
missingRequired: []
issues: []
recommendations: []
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/accounts/{accountId}/tiktok/creator-info:
get:
operationId: getTikTokCreatorInfo
tags: [Accounts]
summary: Get TikTok creator info
description: Returns TikTok creator details, available privacy levels, posting limits, and commercial content options for a specific TikTok account. Only works with TikTok accounts.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The TikTok account ID
- name: mediaType
in: query
required: false
schema:
type: string
enum: [video, photo]
default: video
description: The media type to get creator info for (affects available interaction settings)
responses:
'200':
description: TikTok creator info and posting options
content:
application/json:
schema:
type: object
properties:
creator:
type: object
properties:
nickname: { type: string, description: Creator display name }
avatarUrl: { type: string, description: Creator avatar URL }
isVerified: { type: boolean, description: Whether the creator is verified }
canPostMore: { type: boolean, description: Whether the creator can publish more posts right now }
privacyLevels:
type: array
description: Available privacy level options for this creator
items:
type: object
properties:
value: { type: string, description: "Privacy level value to use when creating posts (e.g. PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)" }
label: { type: string, description: Human-readable label }
postingLimits:
type: object
properties:
maxVideoDurationSec: { type: integer, description: Maximum video duration in seconds }
interactionSettings:
type: object
description: Available interaction toggles (comment, duet, stitch) and their defaults
commercialContentTypes:
type: array
description: Available commercial content disclosure options
items:
type: object
properties:
value: { type: string }
label: { type: string }
requires:
type: array
items: { type: string }
example:
creator:
nickname: "myaccount"
avatarUrl: "https://example.com/avatar.jpg"
isVerified: false
canPostMore: true
privacyLevels:
- value: "PUBLIC_TO_EVERYONE"
label: "Public To Everyone"
- value: "MUTUAL_FOLLOW_FRIENDS"
label: "Mutual Follow Friends"
- value: "SELF_ONLY"
label: "Self Only"
postingLimits:
maxVideoDurationSec: 600
interactionSettings:
comment: true
duet: true
stitch: true
commercialContentTypes:
- value: "none"
label: "No Commercial Content"
- value: "brand_organic"
label: "Your Brand"
requires: ["is_brand_organic_post"]
'400':
description: Account is not a TikTok account
content:
application/json:
schema:
type: object
properties:
error: { type: string }
example:
error: "This endpoint is only available for TikTok accounts"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'429':
description: Creator has reached TikTok daily posting limit
content:
application/json:
schema:
type: object
properties:
error: { type: string }
example:
error: "TikTok creator has reached the daily posting limit. Please try again later."
/v1/api-keys:
get:
operationId: listApiKeys
tags: [API Keys]
summary: List keys
description: Returns all API keys for the authenticated user. Keys are returned with a preview only, not the full key value.
responses:
'200':
description: API keys
content:
application/json:
schema:
type: object
properties:
apiKeys:
type: array
items: { $ref: '#/components/schemas/ApiKey' }
example:
apiKeys:
- id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Production API Key"
keyPreview: "sk_12345678...abcdef01"
expiresAt: "2025-12-31T23:59:59Z"
createdAt: "2024-01-15T10:30:00Z"
scope: "full"
profileIds: []
permission: "read-write"
- id: "6507a1b2c3d4e5f6a7b8c9d1"
name: "Analytics Read-Only"
keyPreview: "sk_87654321...12345678"
expiresAt: null
createdAt: "2024-03-20T14:45:00Z"
scope: "profiles"
profileIds:
- _id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Main Brand"
color: "#ffeda0"
permission: "read"
'401': { $ref: '#/components/responses/Unauthorized' }
post:
operationId: createApiKey
tags: [API Keys]
summary: Create key
description: Creates a new API key with an optional expiry. The full key value is only returned once in the response.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name: { type: string }
expiresIn:
type: integer
description: Days until expiry
scope:
type: string
enum: [full, profiles]
description: "'full' grants access to all profiles (default), 'profiles' restricts to specific profiles"
default: full
profileIds:
type: array
items: { type: string }
description: Profile IDs this key can access. Required when scope is 'profiles'.
permission:
type: string
enum: [read-write, read]
description: "'read-write' allows all operations (default), 'read' restricts to GET requests only"
default: read-write
example:
name: "Analytics Read-Only Key"
scope: "profiles"
profileIds: ["6507a1b2c3d4e5f6a7b8c9d0"]
permission: "read"
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
message: { type: string }
apiKey: { $ref: '#/components/schemas/ApiKey' }
example:
message: "API key created successfully"
apiKey:
id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Analytics Read-Only Key"
keyPreview: "sk_12345678...90abcdef"
key: "sk_1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
expiresAt: null
createdAt: "2024-01-15T10:30:00Z"
scope: "profiles"
profileIds:
- _id: "6507a1b2c3d4e5f6a7b8c9d0"
name: "Main Brand"
color: "#ffeda0"
permission: "read"
'400': { description: Invalid request (missing name, invalid scope/permission, or missing profileIds when scope is 'profiles') }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/api-keys/{keyId}:
delete:
operationId: deleteApiKey
tags: [API Keys]
summary: Delete key
description: Permanently revokes and deletes an API key.
parameters:
- name: keyId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Deleted
content:
application/json:
schema:
type: object
properties:
message: { type: string }
example:
message: "API key deleted successfully"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/invite/tokens:
post:
operationId: createInviteToken
tags: [Invites]
summary: Create invite token
description: |
Generate a secure invite link to grant team members access to your profiles.
Invites expire after 7 days and are single-use.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [scope]
properties:
scope:
type: string
enum: [all, profiles]
description: "'all' grants access to all profiles, 'profiles' restricts to specific profiles"
profileIds:
type: array
items: { type: string }
description: Required if scope is 'profiles'. Array of profile IDs to grant access to.
example:
scope: "profiles"
profileIds:
- "64f0a1b2c3d4e5f6a7b8c9d0"
- "64f0a1b2c3d4e5f6a7b8c9d1"
responses:
'201':
description: Invite token created
content:
application/json:
schema:
type: object
properties:
token: { type: string }
scope: { type: string }
invitedProfileIds:
type: array
items: { type: string }
expiresAt: { type: string, format: date-time }
inviteUrl: { type: string, format: uri }
example:
token: "inv_abc123def456ghi789"
scope: "profiles"
invitedProfileIds:
- "64f0a1b2c3d4e5f6a7b8c9d0"
- "64f0a1b2c3d4e5f6a7b8c9d1"
expiresAt: "2024-11-08T10:30:00Z"
inviteUrl: "https://zernio.com/invite/inv_abc123def456ghi789"
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: One or more profiles not found or not owned }
/v1/connect/{platform}:
get:
operationId: getConnectUrl
tags: [Connect]
summary: Get OAuth connect URL
description: |
Initiate an OAuth connection flow. Returns an authUrl to redirect the user to.
Standard flow: Zernio hosts the selection UI, then redirects to your redirect_url. Headless mode (headless=true): user is redirected to your redirect_url with OAuth data for custom UI. Use the platform-specific selection endpoints to complete.
parameters:
- name: platform
in: path
required: true
schema:
type: string
enum: [facebook, instagram, linkedin, twitter, tiktok, youtube, threads, reddit, pinterest, bluesky, googlebusiness, telegram, snapchat]
description: Social media platform to connect
- name: profileId
in: query
required: true
schema: { type: string }
description: Your Zernio profile ID (get from /v1/profiles)
- name: redirect_url
in: query
schema: { type: string, format: uri }
description: Your custom redirect URL after connection completes. Standard mode appends ?connected={platform}&profileId=X&accountId=Y&username=Z. Headless mode appends OAuth data params for platforms requiring selection (e.g. LinkedIn orgs, Facebook pages). If no selection is needed, the account is created directly and the redirect includes accountId.
- name: headless
in: query
schema: { type: boolean, default: false }
description: When true, the user is redirected to your redirect_url with raw OAuth data (code, state) instead of Zernio's default account selection UI. Use this to build a custom connect experience.
security:
- bearerAuth: []
responses:
'200':
description: OAuth authorization URL to redirect user to
content:
application/json:
schema:
type: object
properties:
authUrl:
type: string
format: uri
description: URL to redirect your user to for OAuth authorization
state:
type: string
description: State parameter for security (handled automatically)
example:
authUrl: "https://www.facebook.com/v21.0/dialog/oauth?client_id=..."
state: "user123-profile456-1234567890-https://yourdomain.com/callback"
'400':
description: "Missing/invalid parameters (e.g., invalid profileId format)"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: "No access to profile, or BYOK required for AppSumo Twitter"
'404':
description: Profile not found
post:
operationId: handleOAuthCallback
tags: [Connect]
summary: Complete OAuth callback
description: Exchange the OAuth authorization code for tokens and connect the account to the specified profile.
parameters:
- name: platform
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code, state, profileId]
properties:
code: { type: string }
state: { type: string }
profileId: { type: string }
responses:
'200': { description: Account connected }
'400': { description: Invalid params }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: BYOK required for AppSumo Twitter }
'500': { description: Failed to connect account }
/v1/connect/facebook/select-page:
get:
operationId: listFacebookPages
tags: [Connect]
summary: List Facebook pages
description: Returns the list of Facebook Pages the user can manage after OAuth. Extract tempToken and userProfile from the OAuth redirect params and pass them here. Use the X-Connect-Token header if connecting via API key.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
description: Profile ID from your connection flow
- name: tempToken
in: query
required: true
schema: { type: string }
description: Temporary Facebook access token from the OAuth callback redirect
security:
- bearerAuth: []
- connectToken: []
responses:
'200':
description: List of Facebook Pages available for connection
content:
application/json:
schema:
type: object
properties:
pages:
type: array
items:
type: object
properties:
id: { type: string, description: Facebook Page ID }
name: { type: string, description: Page name }
username: { type: string, description: Page username/handle (may be null) }
access_token: { type: string, description: Page-specific access token }
category: { type: string, description: Page category }
tasks: { type: array, items: { type: string }, description: User permissions for this page }
example:
pages:
- id: "123456789"
name: "My Brand Page"
username: "mybrand"
access_token: "EAAxxxxx..."
category: "Brand"
tasks: ["MANAGE", "CREATE_CONTENT"]
'400': { description: Missing required parameters (profileId or tempToken) }
'401': { $ref: '#/components/responses/Unauthorized' }
'500':
description: Failed to fetch pages (e.g., invalid token, insufficient permissions)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
post:
operationId: selectFacebookPage
tags: [Connect]
summary: Select Facebook page
description: Complete the headless flow by saving the user's selected Facebook page. Pass the userProfile from the OAuth redirect and use X-Connect-Token if connecting via API key.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, pageId, tempToken]
properties:
profileId:
type: string
description: Profile ID from your connection flow
pageId:
type: string
description: The Facebook Page ID selected by the user
tempToken:
type: string
description: Temporary Facebook access token from OAuth
userProfile:
type: object
description: Decoded user profile object from the OAuth callback
properties:
id: { type: string }
name: { type: string }
profilePicture: { type: string }
redirect_url:
type: string
format: uri
description: Optional custom redirect URL to return to after selection
example:
profileId: "507f1f77bcf86cd799439011"
pageId: "123456789"
tempToken: "EAAxxxxx..."
userProfile:
id: "987654321"
name: "John Doe"
profilePicture: "https://..."
redirect_url: "https://yourdomain.com/integrations/callback"
security:
- bearerAuth: []
- connectToken: []
responses:
'200':
description: Facebook Page connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
redirect_url:
type: string
description: Redirect URL if custom redirect_url was provided
account:
type: object
properties:
accountId:
type: string
description: ID of the created SocialAccount
platform: { type: string, enum: [facebook] }
username: { type: string }
displayName: { type: string }
profilePicture: { type: string }
isActive: { type: boolean }
selectedPageName: { type: string }
example:
message: "Facebook page connected successfully"
redirect_url: "https://yourdomain.com/integrations/callback?connected=facebook&profileId=507f1f77bcf86cd799439011&username=My+Brand+Page"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "facebook"
username: "mybrand"
displayName: "My Brand Page"
profilePicture: "https://..."
isActive: true
selectedPageName: "My Brand Page"
'400':
description: "Missing required fields (profileId, pageId, or tempToken)"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: User does not have access to the specified profile
'404':
description: Selected page not found in available pages
'500':
description: Failed to save Facebook connection
/v1/connect/googlebusiness/locations:
get:
operationId: listGoogleBusinessLocations
tags: [Connect]
summary: List GBP locations
description: For headless flows. Returns the list of GBP locations the user can manage. Use X-Connect-Token if connecting via API key.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
description: Profile ID from your connection flow
- name: tempToken
in: query
required: true
schema: { type: string }
description: Temporary Google access token from the OAuth callback redirect
security:
- bearerAuth: []
- connectToken: []
responses:
'200':
description: List of Google Business locations available for connection
content:
application/json:
schema:
type: object
properties:
locations:
type: array
items:
type: object
properties:
id: { type: string, description: Location ID }
name: { type: string, description: Business name }
accountId: { type: string, description: Google Business Account ID }
accountName: { type: string, description: Account name }
address: { type: string, description: Business address }
category: { type: string, description: Business category }
example:
locations:
- id: "9281089117903930794"
name: "My Coffee Shop"
accountId: "accounts/113303573364907650416"
accountName: "My Business Account"
address: "123 Main St, City, Country"
category: "Coffee shop"
'400': { description: Missing required parameters (profileId or tempToken) }
'401': { $ref: '#/components/responses/Unauthorized' }
'500':
description: Failed to fetch locations (e.g., invalid token, insufficient permissions)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
/v1/connect/googlebusiness/select-location:
post:
operationId: selectGoogleBusinessLocation
tags: [Connect]
summary: Select GBP location
description: Complete the headless flow by saving the user's selected GBP location. Include userProfile from the OAuth redirect (contains refresh token). Use X-Connect-Token if connecting via API key.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, locationId, tempToken]
properties:
profileId:
type: string
description: Profile ID from your connection flow
locationId:
type: string
description: The Google Business location ID selected by the user
tempToken:
type: string
description: Temporary Google access token from OAuth
userProfile:
type: object
description: Decoded user profile from the OAuth callback. Contains the refresh token. Always include this field.
properties:
id: { type: string }
name: { type: string }
refreshToken: { type: string, description: Google refresh token for long-lived access }
tokenExpiresIn: { type: integer, description: Token expiration time in seconds }
scope: { type: string, description: Granted OAuth scopes }
redirect_url:
type: string
format: uri
description: Optional custom redirect URL to return to after selection
example:
profileId: "507f1f77bcf86cd799439011"
locationId: "9281089117903930794"
tempToken: "ya29.xxxxx..."
userProfile:
id: "113303573364907650416"
name: "John Doe"
refreshToken: "1//0gxxxxx..."
tokenExpiresIn: 3599
scope: "https://www.googleapis.com/auth/business.manage"
redirect_url: "https://yourdomain.com/integrations/callback"
security:
- bearerAuth: []
- connectToken: []
responses:
'200':
description: Google Business location connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
redirect_url:
type: string
description: Redirect URL if custom redirect_url was provided
account:
type: object
properties:
accountId:
type: string
description: ID of the created SocialAccount
platform: { type: string, enum: [googlebusiness] }
username: { type: string }
displayName: { type: string }
isActive: { type: boolean }
selectedLocationName: { type: string }
selectedLocationId: { type: string }
example:
message: "Google Business location connected successfully"
redirect_url: "https://yourdomain.com/integrations/callback?connected=googlebusiness&profileId=507f1f77bcf86cd799439011&username=My+Coffee+Shop"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "googlebusiness"
username: "My Coffee Shop"
displayName: "My Coffee Shop"
isActive: true
selectedLocationName: "My Coffee Shop"
selectedLocationId: "9281089117903930794"
'400':
description: "Missing required fields (profileId, locationId, or tempToken)"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: User does not have access to the specified profile
'404':
description: Selected location not found in available locations
'500':
description: Failed to save Google Business connection
/v1/accounts/{accountId}/gmb-reviews:
get:
operationId: getGoogleBusinessReviews
tags: [GMB Reviews]
summary: Get reviews
description: Returns reviews for a GBP account including ratings, comments, and owner replies. Use nextPageToken for pagination.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The Zernio account ID (from /v1/accounts)
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: pageSize
in: query
schema: { type: integer, minimum: 1, maximum: 50, default: 50 }
description: Number of reviews to fetch per page (max 50)
- name: pageToken
in: query
schema: { type: string }
description: Pagination token from previous response
security:
- bearerAuth: []
responses:
'200':
description: Reviews fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
reviews:
type: array
items:
type: object
properties:
id: { type: string, description: Review ID }
name: { type: string, description: Full resource name }
reviewer:
type: object
properties:
displayName: { type: string }
profilePhotoUrl: { type: string, nullable: true }
isAnonymous: { type: boolean }
rating: { type: integer, minimum: 1, maximum: 5, description: Numeric star rating }
starRating: { type: string, enum: [ONE, TWO, THREE, FOUR, FIVE], description: Google's string rating }
comment: { type: string, description: Review text }
createTime: { type: string, format: date-time }
updateTime: { type: string, format: date-time }
reviewReply:
type: object
nullable: true
properties:
comment: { type: string, description: Business owner reply }
updateTime: { type: string, format: date-time }
averageRating: { type: number, description: Overall average rating }
totalReviewCount: { type: integer, description: Total number of reviews }
nextPageToken: { type: string, nullable: true, description: Token for next page }
example:
success: true
accountId: "64e1f0a9e2b5af0012ab34cd"
locationId: "9281089117903930794"
reviews:
- id: "AIe9_BGx1234567890"
name: "accounts/123456789/locations/9281089117903930794/reviews/AIe9_BGx1234567890"
reviewer:
displayName: "John Smith"
profilePhotoUrl: "https://lh3.googleusercontent.com/a/..."
isAnonymous: false
rating: 5
starRating: "FIVE"
comment: "Great service and friendly staff! Highly recommend."
createTime: "2024-01-15T10:30:00Z"
updateTime: "2024-01-15T10:30:00Z"
reviewReply:
comment: "Thank you for your kind words! We appreciate your support."
updateTime: "2024-01-16T08:00:00Z"
- id: "AIe9_BGx0987654321"
name: "accounts/123456789/locations/9281089117903930794/reviews/AIe9_BGx0987654321"
reviewer:
displayName: "Anonymous"
profilePhotoUrl: null
isAnonymous: true
rating: 4
starRating: "FOUR"
comment: "Good experience overall."
createTime: "2024-01-10T14:20:00Z"
updateTime: "2024-01-10T14:20:00Z"
reviewReply: null
averageRating: 4.5
totalReviewCount: 125
nextPageToken: "CiAKHAoUMTIzNDU2Nzg5"
'400':
description: Invalid request - not a Google Business account or missing location
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "This endpoint is only available for Google Business Profile accounts"
'401':
description: Unauthorized or token invalid
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "Access token invalid. Please reconnect your Google Business Profile account."
code: "token_invalid"
'403':
description: Permission denied for this location
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "You do not have permission to access reviews for this location."
'404': { $ref: '#/components/responses/NotFound' }
'500':
description: Failed to fetch reviews
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/accounts/{accountId}/gmb-food-menus:
get:
operationId: getGoogleBusinessFoodMenus
tags: [GMB Food Menus]
summary: Get food menus
description: Returns food menus for a GBP location including sections, items, pricing, and dietary info. Only for locations with food menu support.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The Zernio account ID (from /v1/accounts)
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
responses:
'200':
description: Food menus fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
name: { type: string, description: Resource name of the food menus }
menus:
type: array
items:
$ref: '#/components/schemas/FoodMenu'
example:
success: true
accountId: "64e1f0a9e2b5af0012ab34cd"
locationId: "9281089117903930794"
name: "accounts/123456789/locations/9281089117903930794/foodMenus"
menus:
- labels:
- displayName: "Lunch Menu"
description: "Available 11am-3pm"
languageCode: "en"
sections:
- labels:
- displayName: "Appetizers"
items:
- labels:
- displayName: "Caesar Salad"
description: "Romaine, parmesan, croutons"
attributes:
price:
currencyCode: "USD"
units: "12"
dietaryRestriction: ["VEGETARIAN"]
'400':
description: Invalid request - not a Google Business account or missing location
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "This endpoint is only available for Google Business Profile accounts"
'401':
description: Unauthorized or token invalid
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "Access token invalid. Please reconnect your Google Business Profile account."
code: "token_invalid"
'403':
description: Permission denied for this location
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "You do not have permission to access food menus for this location."
'404': { $ref: '#/components/responses/NotFound' }
'500':
description: Failed to fetch food menus
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
put:
operationId: updateGoogleBusinessFoodMenus
tags: [GMB Food Menus]
summary: Update food menus
description: Updates food menus for a GBP location. Send the full menus array. Use updateMask for partial updates.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The Zernio account ID (from /v1/accounts)
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [menus]
properties:
menus:
type: array
items:
$ref: '#/components/schemas/FoodMenu'
description: Array of food menus to set
updateMask:
type: string
description: Field mask for partial updates (e.g. "menus")
example:
menus:
- labels:
- displayName: "Dinner Menu"
languageCode: "en"
sections:
- labels:
- displayName: "Mains"
items:
- labels:
- displayName: "Grilled Salmon"
description: "With seasonal vegetables"
attributes:
price:
currencyCode: "USD"
units: "24"
allergen: ["FISH"]
updateMask: "menus"
responses:
'200':
description: Food menus updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
name: { type: string }
menus:
type: array
items:
$ref: '#/components/schemas/FoodMenu'
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
error: "Request body must include a \"menus\" array"
'401':
description: Unauthorized or token expired
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'403':
description: Permission denied for this location
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'404': { $ref: '#/components/responses/NotFound' }
'500':
description: Failed to update food menus
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/accounts/{accountId}/gmb-location-details:
get:
operationId: getGoogleBusinessLocationDetails
tags: [GMB Location Details]
summary: Get location details
description: Returns detailed GBP location info (hours, description, phone, website, categories, services). Use readMask to request specific fields.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The Zernio account ID (from /v1/accounts)
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: readMask
in: query
required: false
schema: { type: string }
description: "Comma-separated fields to return. Available: name, title, phoneNumbers, categories, storefrontAddress, websiteUri, regularHours, specialHours, serviceArea, serviceItems, profile, openInfo, metadata, moreHours."
security:
- bearerAuth: []
responses:
'200':
description: Location details fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
title: { type: string, description: Business name }
regularHours:
type: object
properties:
periods:
type: array
items:
type: object
properties:
openDay: { type: string, enum: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] }
openTime: { type: string, description: "Opening time in HH:MM format" }
closeDay: { type: string }
closeTime: { type: string }
specialHours:
type: object
properties:
specialHourPeriods:
type: array
items:
type: object
properties:
startDate: { type: object, properties: { year: { type: integer }, month: { type: integer }, day: { type: integer } } }
endDate: { type: object, properties: { year: { type: integer }, month: { type: integer }, day: { type: integer } } }
openTime: { type: string }
closeTime: { type: string }
closed: { type: boolean }
profile:
type: object
properties:
description: { type: string, description: Business description }
websiteUri: { type: string }
phoneNumbers:
type: object
properties:
primaryPhone: { type: string }
additionalPhones: { type: array, items: { type: string } }
categories:
type: object
description: "Business categories (returned when readMask includes 'categories')"
properties:
primaryCategory:
type: object
properties:
name: { type: string, description: "Category resource name" }
displayName: { type: string, description: "Human-readable category name" }
additionalCategories:
type: array
items:
type: object
properties:
name: { type: string }
displayName: { type: string }
serviceItems:
type: array
description: "Services offered (returned when readMask includes 'serviceItems')"
items:
type: object
properties:
structuredServiceItem:
type: object
properties:
serviceTypeId: { type: string }
description: { type: string }
freeFormServiceItem:
type: object
properties:
category: { type: string }
label:
type: object
properties:
displayName: { type: string }
languageCode: { type: string }
price:
type: object
properties:
currencyCode: { type: string }
units: { type: string }
nanos: { type: integer }
example:
success: true
accountId: "64e1f0a9e2b5af0012ab34cd"
locationId: "9281089117903930794"
title: "Joe's Pizza"
regularHours:
periods:
- openDay: "MONDAY"
openTime: "11:00"
closeDay: "MONDAY"
closeTime: "22:00"
- openDay: "TUESDAY"
openTime: "11:00"
closeDay: "TUESDAY"
closeTime: "22:00"
specialHours:
specialHourPeriods:
- startDate: { year: 2026, month: 12, day: 25 }
closed: true
profile:
description: "Authentic New York style pizza since 1985"
websiteUri: "https://joespizza.com"
categories:
primaryCategory:
name: "categories/gcid:pizza_restaurant"
displayName: "Pizza restaurant"
additionalCategories:
- name: "categories/gcid:italian_restaurant"
displayName: "Italian restaurant"
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized or token expired
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: updateGoogleBusinessLocationDetails
tags: [GMB Location Details]
summary: Update location details
description: |
Updates GBP location details. The updateMask field is required and specifies which fields to update.
This endpoint proxies Google's Business Information API locations.patch, so any valid updateMask field is supported.
Common fields: regularHours, specialHours, profile.description, websiteUri, phoneNumbers, categories, serviceItems.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
description: The Zernio account ID (from /v1/accounts)
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [updateMask]
properties:
updateMask:
type: string
description: "Required. Comma-separated fields to update (e.g. 'regularHours', 'specialHours', 'profile.description', 'categories', 'serviceItems'). Any valid Google Business Information API updateMask field is supported."
regularHours:
type: object
properties:
periods:
type: array
items:
type: object
properties:
openDay: { type: string }
openTime: { type: string }
closeDay: { type: string }
closeTime: { type: string }
specialHours:
type: object
properties:
specialHourPeriods:
type: array
items:
type: object
properties:
startDate: { type: object, properties: { year: { type: integer }, month: { type: integer }, day: { type: integer } } }
endDate: { type: object, properties: { year: { type: integer }, month: { type: integer }, day: { type: integer } } }
openTime: { type: string }
closeTime: { type: string }
closed: { type: boolean }
profile:
type: object
properties:
description: { type: string }
websiteUri: { type: string }
phoneNumbers:
type: object
properties:
primaryPhone: { type: string }
additionalPhones: { type: array, items: { type: string } }
categories:
type: object
description: "Primary and additional business categories. Use updateMask='categories' to update."
properties:
primaryCategory:
type: object
properties:
name:
type: string
description: "Category resource name (e.g. 'categories/gcid:laundromat'). Use Google's Categories API to look up valid IDs."
additionalCategories:
type: array
items:
type: object
properties:
name:
type: string
description: "Category resource name (e.g. 'categories/gcid:dry_cleaner')"
serviceItems:
type: array
description: "Services offered by the business. Use updateMask='serviceItems' to update."
items:
type: object
properties:
structuredServiceItem:
type: object
description: "A predefined service from Google's service type catalog"
properties:
serviceTypeId:
type: string
description: "Service type ID from Google's catalog (e.g. 'job_type_id:plumbing_drain_repair')"
description:
type: string
description: "Optional description of the service"
freeFormServiceItem:
type: object
description: "A custom service not in Google's catalog"
properties:
category:
type: string
description: "Category resource name this service belongs to (e.g. 'categories/gcid:laundromat')"
label:
type: object
properties:
displayName:
type: string
description: "Service name as displayed to users"
languageCode:
type: string
description: "Language code (e.g. 'en')"
price:
type: object
description: "Optional price for the service"
properties:
currencyCode: { type: string, description: "ISO 4217 currency code (e.g. 'USD')" }
units: { type: string, description: "Whole units of the amount" }
nanos: { type: integer, description: "Nano units (10^-9) of the amount" }
examples:
updateHours:
summary: Update business hours
value:
updateMask: "regularHours,specialHours"
regularHours:
periods:
- openDay: "MONDAY"
openTime: "09:00"
closeDay: "MONDAY"
closeTime: "17:00"
- openDay: "SATURDAY"
openTime: "10:00"
closeDay: "SATURDAY"
closeTime: "14:00"
specialHours:
specialHourPeriods:
- startDate: { year: 2026, month: 12, day: 25 }
closed: true
- startDate: { year: 2026, month: 12, day: 31 }
openTime: "09:00"
closeTime: "15:00"
updateCategories:
summary: Update business categories
value:
updateMask: "categories"
categories:
primaryCategory:
name: "categories/gcid:laundromat"
additionalCategories:
- name: "categories/gcid:dry_cleaner"
- name: "categories/gcid:laundry_service"
updateServices:
summary: Update service items
value:
updateMask: "serviceItems"
serviceItems:
- structuredServiceItem:
serviceTypeId: "job_type_id:plumbing_drain_repair"
description: "Full drain cleaning and repair service"
- freeFormServiceItem:
category: "categories/gcid:laundromat"
label:
displayName: "Wash & Fold Service"
languageCode: "en"
price:
currencyCode: "USD"
units: "25"
responses:
'200':
description: Location updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
'400':
description: Invalid request or missing updateMask
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized or token expired
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/accounts/{accountId}/gmb-media:
get:
operationId: listGoogleBusinessMedia
tags: [GMB Media]
summary: List media
description: |
Lists media items (photos) for a Google Business Profile location.
Returns photo URLs, descriptions, categories, and metadata.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: pageSize
in: query
schema: { type: integer, maximum: 100, default: 100 }
description: Number of items to return (max 100)
- name: pageToken
in: query
schema: { type: string }
description: Pagination token from previous response
security:
- bearerAuth: []
responses:
'200':
description: Media items fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
mediaItems:
type: array
items:
type: object
properties:
name: { type: string, description: Resource name }
mediaFormat: { type: string, enum: [PHOTO, VIDEO] }
sourceUrl: { type: string }
googleUrl: { type: string, description: Google-hosted URL }
thumbnailUrl: { type: string }
description: { type: string }
createTime: { type: string, format: date-time }
locationAssociation:
type: object
properties:
category: { type: string }
nextPageToken: { type: string }
totalMediaItemsCount: { type: integer }
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
post:
operationId: createGoogleBusinessMedia
tags: [GMB Media]
summary: Upload photo
description: |
Creates a media item (photo) for a location from a publicly accessible URL.
Categories determine where the photo appears: COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, FOOD_AND_DRINK, MENU, PRODUCT, TEAMS, ADDITIONAL.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [sourceUrl]
properties:
sourceUrl: { type: string, description: Publicly accessible image URL }
mediaFormat: { type: string, enum: [PHOTO, VIDEO], default: PHOTO }
description: { type: string, description: Photo description }
category:
type: string
enum: [COVER, PROFILE, LOGO, EXTERIOR, INTERIOR, FOOD_AND_DRINK, MENU, PRODUCT, TEAMS, ADDITIONAL]
description: Where the photo appears on the listing
example:
sourceUrl: "https://example.com/photos/restaurant-interior.jpg"
description: "Dining area with outdoor seating"
category: "INTERIOR"
responses:
'200':
description: Media created successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
name: { type: string }
mediaFormat: { type: string }
googleUrl: { type: string }
'400':
description: Invalid request or unsupported media format
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
delete:
operationId: deleteGoogleBusinessMedia
tags: [GMB Media]
summary: Delete photo
description: Deletes a photo or media item from a GBP location.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: mediaId
in: query
required: true
schema: { type: string }
description: The media item ID to delete
security:
- bearerAuth: []
responses:
'200':
description: Media deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
deleted: { type: boolean }
mediaId: { type: string }
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/accounts/{accountId}/gmb-attributes:
get:
operationId: getGoogleBusinessAttributes
tags: [GMB Attributes]
summary: Get attributes
description: Returns GBP location attributes (amenities, services, accessibility, payment types). Available attributes vary by business category.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
responses:
'200':
description: Attributes fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
attributes:
type: array
items:
type: object
properties:
name: { type: string, description: "Attribute identifier (e.g. has_delivery)" }
valueType: { type: string, description: "Value type (BOOL, ENUM, URL, REPEATED_ENUM)" }
values: { type: array, items: {} }
repeatedEnumValue:
type: object
properties:
setValues: { type: array, items: { type: string } }
unsetValues: { type: array, items: { type: string } }
example:
success: true
attributes:
- name: "has_delivery"
valueType: "BOOL"
values: [true]
- name: "has_takeout"
valueType: "BOOL"
values: [true]
- name: "has_outdoor_seating"
valueType: "BOOL"
values: [true]
- name: "pay_credit_card_types_accepted"
valueType: "REPEATED_ENUM"
repeatedEnumValue:
setValues: ["visa", "mastercard", "amex"]
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
put:
operationId: updateGoogleBusinessAttributes
tags: [GMB Attributes]
summary: Update attributes
description: |
Updates location attributes (amenities, services, etc.).
The attributeMask specifies which attributes to update (comma-separated).
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [attributes, attributeMask]
properties:
attributes:
type: array
items:
type: object
properties:
name: { type: string }
values: { type: array, items: {} }
repeatedEnumValue:
type: object
properties:
setValues: { type: array, items: { type: string } }
unsetValues: { type: array, items: { type: string } }
attributeMask:
type: string
description: "Comma-separated attribute names to update (e.g. 'has_delivery,has_takeout')"
example:
attributes:
- name: "has_delivery"
values: [true]
- name: "has_takeout"
values: [true]
- name: "has_outdoor_seating"
values: [false]
attributeMask: "has_delivery,has_takeout,has_outdoor_seating"
responses:
'200':
description: Attributes updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
attributes: { type: array, items: { type: object } }
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/accounts/{accountId}/gmb-place-actions:
get:
operationId: listGoogleBusinessPlaceActions
tags: [GMB Place Actions]
summary: List action links
description: |
Lists place action links for a Google Business Profile location.
Place actions are the booking, ordering, and reservation buttons that appear on your listing.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: pageSize
in: query
schema: { type: integer, maximum: 100, default: 100 }
- name: pageToken
in: query
schema: { type: string }
security:
- bearerAuth: []
responses:
'200':
description: Place actions fetched successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
accountId: { type: string }
locationId: { type: string }
placeActionLinks:
type: array
items:
type: object
properties:
name: { type: string, description: Resource name }
uri: { type: string, description: Action URL }
placeActionType: { type: string }
createTime: { type: string, format: date-time }
updateTime: { type: string, format: date-time }
nextPageToken: { type: string }
example:
success: true
placeActionLinks:
- name: "locations/123/placeActionLinks/456"
uri: "https://order.ubereats.com/joespizza"
placeActionType: "FOOD_ORDERING"
- name: "locations/123/placeActionLinks/789"
uri: "https://www.opentable.com/joespizza"
placeActionType: "DINING_RESERVATION"
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
post:
operationId: createGoogleBusinessPlaceAction
tags: [GMB Place Actions]
summary: Create action link
description: |
Creates a place action link for a location.
Available action types: APPOINTMENT, ONLINE_APPOINTMENT, DINING_RESERVATION, FOOD_ORDERING, FOOD_DELIVERY, FOOD_TAKEOUT, SHOP_ONLINE.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [uri, placeActionType]
properties:
uri: { type: string, description: The action URL }
placeActionType:
type: string
enum: [APPOINTMENT, ONLINE_APPOINTMENT, DINING_RESERVATION, FOOD_ORDERING, FOOD_DELIVERY, FOOD_TAKEOUT, SHOP_ONLINE]
description: Type of action
example:
uri: "https://order.ubereats.com/joespizza"
placeActionType: "FOOD_ORDERING"
responses:
'200':
description: Place action created successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
name: { type: string, description: Resource name of the created link }
uri: { type: string }
placeActionType: { type: string }
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
delete:
operationId: deleteGoogleBusinessPlaceAction
tags: [GMB Place Actions]
summary: Delete action link
description: Deletes a place action link (e.g. booking or ordering URL) from a GBP location.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: locationId
in: query
schema: { type: string }
description: Override which location to target. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.
- name: name
in: query
required: true
schema: { type: string }
description: "The resource name of the place action link (e.g. locations/123/placeActionLinks/456)"
security:
- bearerAuth: []
responses:
'200':
description: Place action deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
deleted: { type: boolean }
name: { type: string }
'400':
description: Invalid request
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/connect/pending-data:
get:
operationId: getPendingOAuthData
tags: [Connect]
summary: Get pending OAuth data
description: Fetch pending OAuth data for headless mode using the pendingDataToken from the redirect URL. One-time use, expires after 10 minutes. No authentication required.
parameters:
- name: token
in: query
required: true
schema: { type: string }
description: The pending data token from the OAuth redirect URL (pendingDataToken parameter)
responses:
'200':
description: OAuth data fetched successfully
content:
application/json:
schema:
type: object
properties:
platform:
type: string
description: The platform (e.g., "linkedin")
profileId:
type: string
description: The Zernio profile ID
tempToken:
type: string
description: Temporary access token for the platform
refreshToken:
type: string
description: Refresh token (if available)
expiresIn:
type: number
description: Token expiry in seconds
userProfile:
type: object
description: User profile data (id, username, displayName, profilePicture)
selectionType:
type: string
enum: [organizations, pages, boards, locations, profiles]
description: Type of selection data
organizations:
type: array
description: LinkedIn organizations (when selectionType is "organizations")
items:
type: object
properties:
id: { type: string }
urn: { type: string }
name: { type: string }
vanityName: { type: string }
example:
platform: "linkedin"
profileId: "abc123"
tempToken: "AQV..."
refreshToken: "AQW..."
expiresIn: 5183999
userProfile:
id: "ABC123"
username: "John Doe"
displayName: "John Doe"
profilePicture: "https://..."
selectionType: "organizations"
organizations:
- id: "12345"
urn: "urn:li:organization:12345"
name: "Acme Corp"
vanityName: "acme-corp"
- id: "67890"
urn: "urn:li:organization:67890"
name: "Example Inc"
vanityName: "example-inc"
'400':
description: Missing token parameter
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
'404':
description: Token not found or expired
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/v1/connect/linkedin/organizations:
get:
operationId: listLinkedInOrganizations
tags: [Connect]
summary: List LinkedIn orgs
description: Fetch full LinkedIn organization details (logos, vanity names, websites) for custom UI. No authentication required, just the tempToken from OAuth.
parameters:
- name: tempToken
in: query
required: true
schema: { type: string }
description: The temporary LinkedIn access token from the OAuth redirect
- name: orgIds
in: query
required: true
schema: { type: string }
description: Comma-separated list of organization IDs to fetch details for (max 100)
example: "12345678,87654321,11111111"
responses:
'200':
description: Organization details fetched successfully
content:
application/json:
schema:
type: object
properties:
organizations:
type: array
items:
type: object
properties:
id: { type: string, description: Organization ID }
logoUrl: { type: string, format: uri, description: Logo URL (may be absent if no logo) }
vanityName: { type: string, description: Organization's vanity name/slug }
website: { type: string, format: uri, description: Organization's website URL }
industry: { type: string, description: Organization's primary industry }
description: { type: string, description: Organization's description }
example:
organizations:
- id: "12345678"
logoUrl: "https://media.licdn.com/dms/image/v2/..."
vanityName: "acme-corp"
website: "https://acme.com"
industry: "Technology"
description: "Leading provider of innovative solutions"
- id: "87654321"
logoUrl: "https://media.licdn.com/dms/image/v2/..."
vanityName: "example-inc"
website: "https://example.com"
- id: "11111111"
'400':
description: Missing required parameters or too many organization IDs
content:
application/json:
schema:
type: object
properties:
error: { type: string }
example:
error: "Missing tempToken parameter"
'500':
description: Failed to fetch organization details
/v1/connect/linkedin/select-organization:
post:
operationId: selectLinkedInOrganization
tags: [Connect]
summary: Select LinkedIn org
description: Complete the LinkedIn connection flow. Set accountType to "personal" or "organization" to connect as a company page. Use X-Connect-Token if connecting via API key.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, tempToken, userProfile, accountType]
properties:
profileId: { type: string }
tempToken: { type: string }
userProfile: { type: object }
accountType: { type: string, enum: [personal, organization] }
selectedOrganization: { type: object }
redirect_url: { type: string, format: uri }
examples:
personalAccount:
summary: Connect as personal LinkedIn profile
description: For personal accounts, set accountType to "personal" and omit selectedOrganization
value:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
tempToken: "AQX..."
userProfile:
id: "abc123"
username: "johndoe"
displayName: "John Doe"
profilePicture: "https://media.licdn.com/dms/image/v2/..."
accountType: "personal"
organizationAccount:
summary: Connect as org/company page
description: For organization pages, include the selectedOrganization object
value:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
tempToken: "AQX..."
userProfile:
id: "abc123"
username: "johndoe"
displayName: "John Doe"
profilePicture: "https://media.licdn.com/dms/image/v2/..."
accountType: "organization"
selectedOrganization:
id: "12345678"
urn: "urn:li:organization:12345678"
name: "Acme Corporation"
redirect_url: "https://yourapp.com/callback"
responses:
'200':
description: LinkedIn account connected
content:
application/json:
schema:
type: object
properties:
message: { type: string }
redirect_url:
type: string
description: The redirect URL with connection params appended (only if redirect_url was provided in request)
account:
type: object
properties:
accountId:
type: string
description: ID of the created SocialAccount
platform: { type: string, enum: [linkedin] }
username: { type: string }
displayName: { type: string }
profilePicture: { type: string }
isActive: { type: boolean }
accountType: { type: string, enum: [personal, organization] }
bulkRefresh:
type: object
properties:
updatedCount: { type: integer }
errors: { type: integer }
examples:
personalAccountResponse:
summary: Personal account connected
value:
message: "LinkedIn account connected successfully"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "linkedin"
username: "johndoe"
displayName: "John Doe"
profilePicture: "https://media.licdn.com/..."
isActive: true
accountType: "personal"
organizationWithRedirect:
summary: Org account with redirect URL
value:
message: "LinkedIn account connected successfully"
redirect_url: "https://yourapp.com/callback?connected=linkedin&profileId=507f1f77bcf86cd799439011&accountId=64e1f0a9e2b5af0012ab34cd&username=Acme+Corporation"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "linkedin"
username: "acme-corp"
displayName: "Acme Corporation"
profilePicture: "https://media.licdn.com/..."
isActive: true
accountType: "organization"
bulkRefresh:
updatedCount: 5
errors: 0
'400': { description: Missing required fields }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { description: Failed to connect LinkedIn account }
/v1/connect/pinterest/select-board:
get:
operationId: listPinterestBoardsForSelection
tags: [Connect]
summary: List Pinterest boards
description: For headless flows. Returns Pinterest boards the user can post to. Use X-Connect-Token from the redirect URL.
parameters:
- name: X-Connect-Token
in: header
required: true
schema: { type: string }
description: Short-lived connect token from the OAuth redirect
- name: profileId
in: query
required: true
schema: { type: string }
description: Your Zernio profile ID
- name: tempToken
in: query
required: true
schema: { type: string }
description: Temporary Pinterest access token from the OAuth callback redirect
responses:
'200':
description: List of Pinterest Boards available for connection
content:
application/json:
schema:
type: object
properties:
boards:
type: array
items:
type: object
properties:
id: { type: string, description: Pinterest Board ID }
name: { type: string, description: Board name }
description: { type: string, description: Board description }
privacy: { type: string, description: Board privacy setting }
example:
boards:
- id: "123456789012345678"
name: "Marketing Ideas"
description: "Collection of marketing inspiration"
privacy: "PUBLIC"
- id: "234567890123456789"
name: "Product Photos"
description: "Product photography"
privacy: "PUBLIC"
'400': { description: Missing required parameters }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: No access to profile }
'500': { description: Failed to fetch boards }
post:
operationId: selectPinterestBoard
tags: [Connect]
summary: Select Pinterest board
description: |
Complete the Pinterest connection flow. After OAuth, use this endpoint to save the selected board and complete the account connection. Use the X-Connect-Token header if you initiated the connection via API key.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, boardId, tempToken]
properties:
profileId:
type: string
description: Your Zernio profile ID
boardId:
type: string
description: The Pinterest Board ID selected by the user
boardName:
type: string
description: The board name (for display purposes)
tempToken:
type: string
description: Temporary Pinterest access token from OAuth
userProfile:
type: object
description: User profile data from OAuth redirect
refreshToken:
type: string
description: Pinterest refresh token (if available)
expiresIn:
type: integer
description: Token expiration time in seconds
redirect_url:
type: string
format: uri
description: Custom redirect URL after connection completes
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
boardId: "123456789012345678"
boardName: "Marketing Ideas"
tempToken: "pina_..."
userProfile:
id: "user123"
username: "mybrand"
displayName: "My Brand"
profilePicture: "https://i.pinimg.com/..."
redirect_url: "https://yourapp.com/callback"
responses:
'200':
description: Pinterest Board connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
redirect_url: { type: string, description: Redirect URL with connection params (if provided) }
account:
type: object
properties:
accountId:
type: string
description: ID of the created SocialAccount
platform: { type: string, enum: [pinterest] }
username: { type: string }
displayName: { type: string }
profilePicture: { type: string }
isActive: { type: boolean }
defaultBoardName: { type: string }
example:
message: "Pinterest connected successfully with default board"
redirect_url: "https://yourdomain.com/integrations/callback?connected=pinterest&profileId=507f1f77bcf86cd799439011&board=Marketing+Ideas"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "pinterest"
username: "mybrand"
displayName: "My Brand"
profilePicture: "https://i.pinimg.com/..."
isActive: true
defaultBoardName: "Marketing Ideas"
'400':
description: Missing required fields
content:
application/json:
example:
error: "Missing required fields"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: No access to profile or profile limit exceeded
content:
application/json:
examples:
forbidden:
value: { error: "Forbidden" }
limitExceeded:
value:
error: "Cannot connect to this profile. It exceeds your Pro plan limit of 5 profiles."
code: "PROFILE_LIMIT_EXCEEDED"
'500':
description: Failed to save Pinterest connection
/v1/connect/snapchat/select-profile:
get:
operationId: listSnapchatProfiles
tags: [Connect]
summary: List Snapchat profiles
description: For headless flows. Returns Snapchat Public Profiles the user can post to. Use X-Connect-Token from the redirect URL.
parameters:
- name: X-Connect-Token
in: header
required: true
schema: { type: string }
description: Short-lived connect token from the OAuth redirect
- name: profileId
in: query
required: true
schema: { type: string }
description: Your Zernio profile ID
- name: tempToken
in: query
required: true
schema: { type: string }
description: Temporary Snapchat access token from the OAuth callback redirect
responses:
'200':
description: List of Snapchat Public Profiles available for connection
content:
application/json:
schema:
type: object
properties:
publicProfiles:
type: array
items:
type: object
properties:
id: { type: string, description: Snapchat Public Profile ID }
display_name: { type: string, description: Public profile display name }
username: { type: string, description: Public profile username/handle }
profile_image_url: { type: string, description: Profile image URL }
subscriber_count: { type: integer, description: Number of subscribers }
example:
publicProfiles:
- id: "abc123-def456"
display_name: "My Brand"
username: "mybrand"
profile_image_url: "https://cf-st.sc-cdn.net/..."
subscriber_count: 15000
- id: "xyz789-uvw012"
display_name: "Side Project"
username: "sideproject"
profile_image_url: "https://cf-st.sc-cdn.net/..."
subscriber_count: 5000
'400': { description: Missing required parameters (profileId or tempToken) }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: No access to profile }
'500': { description: Failed to fetch public profiles }
post:
operationId: selectSnapchatProfile
tags: [Connect]
summary: Select Snapchat profile
description: Complete the Snapchat connection flow by saving the selected Public Profile. Snapchat requires a Public Profile to publish content. Use X-Connect-Token if connecting via API key.
parameters:
- name: X-Connect-Token
in: header
required: false
schema: { type: string }
description: Short-lived connect token from the OAuth redirect (for API users)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, selectedPublicProfile, tempToken, userProfile]
properties:
profileId:
type: string
description: Your Zernio profile ID
selectedPublicProfile:
type: object
description: The selected Snapchat Public Profile
required: [id, display_name]
properties:
id:
type: string
description: Snapchat Public Profile ID
display_name:
type: string
description: Display name of the public profile
username:
type: string
description: Username/handle
profile_image_url:
type: string
description: Profile image URL
subscriber_count:
type: integer
description: Number of subscribers
tempToken:
type: string
description: Temporary Snapchat access token from OAuth
userProfile:
type: object
description: User profile data from OAuth redirect
refreshToken:
type: string
description: Snapchat refresh token (if available)
expiresIn:
type: integer
description: Token expiration time in seconds
redirect_url:
type: string
format: uri
description: Custom redirect URL after connection completes
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
selectedPublicProfile:
id: "abc123-def456"
display_name: "My Brand"
username: "mybrand"
profile_image_url: "https://cf-st.sc-cdn.net/..."
subscriber_count: 15000
tempToken: "eyJ..."
userProfile:
id: "user123"
username: "mybrand"
displayName: "My Brand"
profilePicture: "https://cf-st.sc-cdn.net/..."
redirect_url: "https://yourapp.com/callback"
responses:
'200':
description: Snapchat Public Profile connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
redirect_url: { type: string, description: Redirect URL with connection params (if provided in request) }
account:
type: object
properties:
accountId:
type: string
description: ID of the created SocialAccount
platform: { type: string, enum: [snapchat] }
username: { type: string }
displayName: { type: string }
profilePicture: { type: string }
isActive: { type: boolean }
publicProfileName: { type: string }
example:
message: "Snapchat connected successfully with public profile"
redirect_url: "https://yourdomain.com/integrations/callback?connected=snapchat&profileId=507f1f77bcf86cd799439011&publicProfile=My+Brand"
account:
accountId: "64e1f0a9e2b5af0012ab34cd"
platform: "snapchat"
username: "mybrand"
displayName: "My Brand"
profilePicture: "https://cf-st.sc-cdn.net/..."
isActive: true
publicProfileName: "My Brand"
'400':
description: Missing required fields
content:
application/json:
example:
error: "Missing required fields"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: No access to profile or profile limit exceeded
content:
application/json:
examples:
forbidden:
value: { error: "Forbidden" }
limitExceeded:
value:
error: "Cannot connect to this profile. It exceeds your Pro plan limit of 5 profiles."
code: "PROFILE_LIMIT_EXCEEDED"
'500':
description: Failed to connect Snapchat account
/v1/connect/bluesky/credentials:
post:
operationId: connectBlueskyCredentials
tags: [Connect]
summary: Connect Bluesky account
description: |
Connect a Bluesky account using identifier (handle or email) and an app password.
To get your userId for the state parameter, call GET /v1/users which includes a currentUserId field.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [identifier, appPassword, state]
properties:
identifier:
type: string
description: Your Bluesky handle (e.g. user.bsky.social) or email address
appPassword:
type: string
description: App password generated from Bluesky Settings > App Passwords
state:
type: string
description: Required state formatted as {userId}-{profileId}. Get userId from GET /v1/users and profileId from GET /v1/profiles.
example: "6507a1b2c3d4e5f6a7b8c9d0-6507a1b2c3d4e5f6a7b8c9d1"
redirectUri:
type: string
format: uri
description: Optional URL to redirect to after successful connection
example:
identifier: "yourhandle.bsky.social"
appPassword: "xxxx-xxxx-xxxx-xxxx"
state: "6507a1b2c3d4e5f6a7b8c9d0-6507a1b2c3d4e5f6a7b8c9d1"
redirectUri: "https://yourapp.com/connected"
responses:
'200':
description: Bluesky connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
account: { $ref: '#/components/schemas/SocialAccount' }
example:
message: "Bluesky connected successfully"
account:
platform: "bluesky"
username: "yourhandle.bsky.social"
displayName: "Your Name"
isActive: true
redirectUrl: "https://zernio.com/dashboard/profiles/64f0.../accounts"
'400': { description: Invalid request - missing fields or invalid state format }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { description: Internal error }
/v1/connect/whatsapp/credentials:
post:
operationId: connectWhatsAppCredentials
tags: [Connect]
summary: Connect WhatsApp via credentials
description: |
Connect a WhatsApp Business Account by providing Meta credentials directly.
This is the headless alternative to the Embedded Signup browser flow.
To get the required credentials:
1. Go to Meta Business Suite (business.facebook.com)
2. Create or select a WhatsApp Business Account
3. In Business Settings > System Users, create a System User
4. Assign it the `whatsapp_business_management` and `whatsapp_business_messaging` permissions
5. Generate a permanent access token
6. Get the WABA ID from WhatsApp Manager > Account Tools > Phone Numbers
7. Get the Phone Number ID from the same page (click on the number)
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, accessToken, wabaId, phoneNumberId]
properties:
profileId:
type: string
description: Your Late profile ID
accessToken:
type: string
description: Permanent System User access token from Meta Business Suite
wabaId:
type: string
description: WhatsApp Business Account ID from Meta
phoneNumberId:
type: string
description: Phone Number ID from Meta WhatsApp Manager
example:
profileId: "6507a1b2c3d4e5f6a7b8c9d0"
accessToken: "EAABsbCS...your-system-user-token"
wabaId: "123456789012345"
phoneNumberId: "987654321098765"
responses:
'200':
description: WhatsApp connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
account:
type: object
properties:
accountId: { type: string }
platform: { type: string, enum: [whatsapp] }
username: { type: string, description: Display phone number }
displayName: { type: string, description: Meta-verified business name }
isActive: { type: boolean }
phoneNumber: { type: string }
verifiedName: { type: string }
qualityRating: { type: string, description: "GREEN, YELLOW, or RED" }
example:
message: "WhatsApp connected successfully"
account:
accountId: "6507a1b2c3d4e5f6a7b8c9d0"
platform: "whatsapp"
username: "+1 555-123-4567"
displayName: "Acme Corp"
isActive: true
phoneNumber: "+1 555-123-4567"
verifiedName: "Acme Corp"
qualityRating: "GREEN"
'400':
description: |
Invalid request. Either missing fields or the phoneNumberId was not found
in the specified WABA. If the phone was not found, the response includes
`availablePhoneNumbers` to help identify the correct ID.
'401':
description: Invalid or expired access token
'403':
description: Profile limit exceeded for this plan
/v1/connect/telegram:
get:
operationId: getTelegramConnectStatus
tags: [Connect]
summary: Generate Telegram code
description: Generate an access code (valid 15 minutes) for connecting a Telegram channel or group. Add the bot as admin, then send the code + @yourchannel to the bot. Poll PATCH /v1/connect/telegram to check status.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
description: The profile ID to connect the Telegram account to
responses:
'200':
description: Access code generated
content:
application/json:
schema:
type: object
properties:
code:
type: string
description: The access code to send to the Telegram bot
example: "ZRN-ABC123"
expiresAt:
type: string
format: date-time
description: When the code expires
expiresIn:
type: integer
description: Seconds until expiration
example: 900
botUsername:
type: string
description: The Telegram bot username to message
example: "LateScheduleBot"
instructions:
type: array
items: { type: string }
description: Step-by-step connection instructions
example:
code: "ZRN-ABC123"
expiresAt: "2024-01-15T12:30:00.000Z"
expiresIn: 900
botUsername: "LateScheduleBot"
instructions:
- "1. Add @ZernioScheduleBot as an administrator in your channel/group"
- "2. Open a private chat with @ZernioScheduleBot"
- "3. Send: ZRN-ABC123 @yourchannel (replace @yourchannel with your channel username)"
- "4. Wait for confirmation - the connection will appear in your dashboard"
- "Tip: If your channel has no public username, forward a message from it along with the code"
'400': { description: Profile ID required or invalid format }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: No access to this profile }
'404': { description: Profile not found }
'500': { description: Internal error }
post:
operationId: initiateTelegramConnect
tags: [Connect]
summary: Connect Telegram directly
description: Connect a Telegram channel/group directly using the chat ID. Alternative to the access code flow. The bot must already be an admin in the channel/group.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [chatId, profileId]
properties:
chatId:
type: string
description: The Telegram chat ID. Numeric ID (e.g. "-1001234567890") or username with @ prefix (e.g. "@mychannel").
profileId:
type: string
description: The profile ID to connect the account to
example:
chatId: "-1001234567890"
profileId: "6507a1b2c3d4e5f6a7b8c9d0"
responses:
'200':
description: Telegram channel connected successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
account:
type: object
properties:
_id: { type: string }
platform: { type: string, enum: [telegram] }
username: { type: string }
displayName: { type: string }
isActive: { type: boolean }
chatType: { type: string, enum: [channel, group, supergroup, private] }
example:
message: "Telegram channel connected successfully"
account:
_id: "64e1f0a9e2b5af0012ab34cd"
platform: "telegram"
username: "mychannel"
displayName: "My Channel"
isActive: true
chatType: "channel"
'400': { description: Chat ID required, bot not admin, or cannot access chat }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: No access to this profile }
'404': { description: Profile not found }
'500': { description: Internal error }
patch:
operationId: completeTelegramConnect
tags: [Connect]
summary: Check Telegram status
description: |
Poll this endpoint to check if a Telegram access code has been used to connect a channel/group. Recommended polling interval: 3 seconds.
Status values: pending (waiting for user), connected (channel/group linked), expired (generate a new code).
parameters:
- name: code
in: query
required: true
schema: { type: string }
description: The access code to check status for
example: "ZRN-ABC123"
responses:
'200':
description: Connection status
content:
application/json:
schema:
oneOf:
- type: object
title: Pending
properties:
status: { type: string, enum: [pending] }
expiresAt: { type: string, format: date-time }
expiresIn: { type: integer, description: Seconds until expiration }
- type: object
title: Connected
properties:
status: { type: string, enum: [connected] }
chatId: { type: string }
chatTitle: { type: string }
chatType: { type: string, enum: [channel, group, supergroup] }
account:
type: object
properties:
_id: { type: string }
platform: { type: string }
username: { type: string }
displayName: { type: string }
- type: object
title: Expired
properties:
status: { type: string, enum: [expired] }
message: { type: string }
examples:
pending:
summary: Waiting for connection
value:
status: "pending"
expiresAt: "2024-01-15T12:30:00.000Z"
expiresIn: 542
connected:
summary: Successfully connected
value:
status: "connected"
chatId: "-1001234567890"
chatTitle: "My Channel"
chatType: "channel"
account:
_id: "64e1f0a9e2b5af0012ab34cd"
platform: "telegram"
username: "mychannel"
displayName: "My Channel"
expired:
summary: Code expired
value:
status: "expired"
message: "Access code has expired. Please generate a new one."
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Code not found }
'500': { description: Internal error }
/v1/accounts/{accountId}/facebook-page:
get:
operationId: getFacebookPages
tags: [Connect]
summary: List Facebook pages
description: Returns all Facebook pages the connected account has access to, including the currently selected page.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Pages list
content:
application/json:
schema:
type: object
properties:
pages:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
username: { type: string }
category: { type: string }
fan_count: { type: integer }
selectedPageId: { type: string }
cached: { type: boolean }
example:
pages:
- id: "123456789012345"
name: "My Brand Page"
username: "mybrand"
category: "Brand"
fan_count: 5000
- id: "234567890123456"
name: "My Other Page"
username: "myotherpage"
category: "Business"
fan_count: 1200
selectedPageId: "123456789012345"
cached: true
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
put:
operationId: updateFacebookPage
tags: [Connect]
summary: Update Facebook page
description: Switch which Facebook Page is active for a connected account.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [selectedPageId]
properties:
selectedPageId: { type: string }
example:
selectedPageId: "123456789012345"
responses:
'200':
description: Page updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
selectedPage:
type: object
properties:
id: { type: string }
name: { type: string }
example:
message: "Facebook page updated successfully"
selectedPage:
id: "123456789012345"
name: "My Brand Page"
'400': { description: Page not in available pages }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/linkedin-organizations:
get:
operationId: getLinkedInOrganizations
tags: [Connect]
summary: List LinkedIn orgs
description: Returns LinkedIn organizations (company pages) the connected account has admin access to.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Organizations list
content:
application/json:
schema:
type: object
properties:
organizations:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
vanityName: { type: string }
localizedName: { type: string }
example:
organizations:
- id: "12345678"
name: "Acme Corporation"
vanityName: "acme-corp"
localizedName: "Acme Corporation"
- id: "87654321"
name: "Acme Subsidiary"
vanityName: "acme-sub"
localizedName: "Acme Subsidiary"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/linkedin-aggregate-analytics:
get:
operationId: getLinkedInAggregateAnalytics
tags: [Analytics]
summary: Get LinkedIn aggregate stats
description: Returns aggregate analytics across all posts for a LinkedIn personal account. Org accounts should use /v1/analytics instead. Requires r_member_postAnalytics scope.
parameters:
- name: accountId
in: path
required: true
description: The ID of the LinkedIn personal account
schema: { type: string }
- name: aggregation
in: query
required: false
description: TOTAL (default, lifetime totals) or DAILY (time series). MEMBERS_REACHED not available with DAILY.
schema:
type: string
enum: [TOTAL, DAILY]
default: TOTAL
- name: startDate
in: query
required: false
description: Start date (YYYY-MM-DD). If omitted, returns lifetime analytics.
schema:
type: string
format: date
example: "2024-01-01"
- name: endDate
in: query
required: false
description: End date (YYYY-MM-DD, exclusive). Defaults to today if omitted.
schema:
type: string
format: date
example: "2024-01-31"
- name: metrics
in: query
required: false
description: "Comma-separated metrics: IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE. Omit for all."
schema:
type: string
example: "IMPRESSION,REACTION,COMMENT"
responses:
'200':
description: Aggregate analytics data
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/LinkedInAggregateAnalyticsTotalResponse'
- $ref: '#/components/schemas/LinkedInAggregateAnalyticsDailyResponse'
examples:
totalAggregation:
summary: TOTAL aggregation (lifetime totals)
value:
accountId: "64abc123def456"
platform: "linkedin"
accountType: "personal"
username: "John Doe"
aggregation: "TOTAL"
dateRange: null
analytics:
impressions: 1250000
reach: 450000
reactions: 7500
comments: 2500
shares: 1200
engagementRate: 0.90
note: "Aggregate analytics across all posts on this LinkedIn personal account (lifetime totals)."
lastUpdated: "2025-01-15T10:30:00.000Z"
totalWithDateRange:
summary: TOTAL aggregation with date range
value:
accountId: "64abc123def456"
platform: "linkedin"
accountType: "personal"
username: "John Doe"
aggregation: "TOTAL"
dateRange:
startDate: "2024-01-01"
endDate: "2024-01-31"
analytics:
impressions: 125000
reach: 45000
reactions: 750
comments: 250
shares: 120
engagementRate: 0.90
note: "Aggregate analytics for the specified date range."
lastUpdated: "2025-01-15T10:30:00.000Z"
dailyAggregation:
summary: DAILY aggregation (time series)
value:
accountId: "64abc123def456"
platform: "linkedin"
accountType: "personal"
username: "John Doe"
aggregation: "DAILY"
dateRange:
startDate: "2024-05-04"
endDate: "2024-05-06"
analytics:
impressions:
- date: "2024-05-04"
count: 1500
- date: "2024-05-05"
count: 2300
reactions:
- date: "2024-05-04"
count: 10
- date: "2024-05-05"
count: 20
comments:
- date: "2024-05-04"
count: 3
- date: "2024-05-05"
count: 5
shares:
- date: "2024-05-04"
count: 2
- date: "2024-05-05"
count: 4
skippedMetrics:
- "MEMBERS_REACHED (not supported with DAILY aggregation)"
note: "Daily breakdown of analytics across all posts. MEMBERS_REACHED is not available with DAILY aggregation per LinkedIn API limitations."
lastUpdated: "2025-01-15T10:30:00.000Z"
'400':
description: Invalid request
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string }
validOptions: { type: array, items: { type: string } }
examples:
not_linkedin:
summary: Not a LinkedIn account
value:
error: "This endpoint only supports LinkedIn accounts"
code: "invalid_platform"
organization:
summary: Org account not supported
value:
error: "Aggregate analytics only available for LinkedIn personal accounts. Organization accounts can use per-post analytics via /v1/analytics."
code: "organization_not_supported"
invalid_aggregation:
summary: Invalid aggregation type
value:
error: "Invalid aggregation type. Must be one of: TOTAL, DAILY"
code: "invalid_aggregation"
validOptions: ["TOTAL", "DAILY"]
invalid_date:
summary: Invalid date format
value:
error: "Invalid date format. Use YYYY-MM-DD format."
code: "invalid_date_format"
example:
startDate: "2024-01-01"
endDate: "2024-01-31"
invalid_metrics:
summary: Invalid metrics requested
value:
error: "Invalid metrics: INVALID_METRIC. Valid options: IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE"
code: "invalid_metrics"
validOptions: ["IMPRESSION", "MEMBERS_REACHED", "REACTION", "COMMENT", "RESHARE"]
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string }
'403':
description: Missing required LinkedIn scope
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string, example: missing_scope }
requiredScope: { type: string, example: r_member_postAnalytics }
action: { type: string, example: reconnect }
example:
error: "Missing r_member_postAnalytics scope. Please reconnect your LinkedIn account to grant analytics permissions."
code: "missing_scope"
requiredScope: "r_member_postAnalytics"
action: "reconnect"
'404': { description: Account not found }
/v1/accounts/{accountId}/linkedin-post-analytics:
get:
operationId: getLinkedInPostAnalytics
tags: [Analytics]
summary: Get LinkedIn post stats
description: Returns analytics for a specific LinkedIn post by URN. Works for both personal and organization accounts.
parameters:
- name: accountId
in: path
required: true
description: The ID of the LinkedIn account
schema: { type: string }
- name: urn
in: query
required: true
description: The LinkedIn post URN
schema: { type: string }
example: "urn:li:share:7123456789012345678"
responses:
'200':
description: Post analytics data
content:
application/json:
schema:
type: object
properties:
accountId: { type: string }
platform: { type: string, example: linkedin }
accountType: { type: string, enum: [personal, organization] }
username: { type: string }
postUrn: { type: string }
analytics:
type: object
properties:
impressions: { type: integer, description: Times the post was shown }
reach: { type: integer, description: Unique members who saw the post }
likes: { type: integer, description: Reactions on the post }
comments: { type: integer, description: Comments on the post }
shares: { type: integer, description: Reshares of the post }
clicks: { type: integer, description: Clicks on the post (organization accounts only) }
views: { type: integer, description: Video views (video posts only) }
engagementRate: { type: number, description: Engagement rate as percentage }
lastUpdated: { type: string, format: date-time }
example:
accountId: "64abc123def456"
platform: "linkedin"
accountType: "personal"
username: "John Doe"
postUrn: "urn:li:share:7123456789012345678"
analytics:
impressions: 5420
reach: 3200
likes: 156
comments: 23
shares: 12
clicks: 0
views: 1250
engagementRate: 5.17
lastUpdated: "2025-01-15T10:30:00.000Z"
'400':
description: Invalid request
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string, enum: [missing_urn, invalid_urn, invalid_platform] }
examples:
missing_urn:
value:
error: "Missing required parameter: urn"
code: "missing_urn"
example: "urn:li:share:7123456789012345678 or urn:li:ugcPost:7123456789012345678"
invalid_urn:
value:
error: "Invalid URN format. Must be urn:li:share:ID or urn:li:ugcPost:ID"
code: "invalid_urn"
providedUrn: "invalid-urn"
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
'403':
description: Missing required LinkedIn scope
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string, example: missing_scope }
requiredScope: { type: string }
action: { type: string, example: reconnect }
'404':
description: Account or post not found
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string }
examples:
account_not_found:
value:
error: "Account not found"
post_not_found:
value:
error: "Post not found. The URN may be invalid or the post may have been deleted."
code: "post_not_found"
postUrn: "urn:li:share:123"
/v1/accounts/{accountId}/linkedin-post-reactions:
get:
operationId: getLinkedInPostReactions
tags: [Analytics]
summary: Get LinkedIn post reactions
description: |
Returns individual reactions for a specific LinkedIn post, including reactor profiles
(name, headline/job title, profile picture, profile URL, reaction type).
Only works for **organization/company page** accounts. LinkedIn restricts reaction
data for personal profiles (r_member_social_feed is a closed permission).
parameters:
- name: accountId
in: path
required: true
description: The ID of the LinkedIn organization account
schema: { type: string }
- name: urn
in: query
required: true
description: The LinkedIn post URN
schema: { type: string }
example: "urn:li:share:7123456789012345678"
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
description: Maximum number of reactions to return per page
- name: cursor
in: query
schema: { type: string }
description: Offset-based pagination start index
responses:
'200':
description: Reactions with reactor profiles
content:
application/json:
schema:
type: object
properties:
accountId: { type: string }
platform: { type: string, example: linkedin }
accountType: { type: string, example: organization }
username: { type: string }
postUrn: { type: string }
reactions:
type: array
items:
type: object
properties:
reactionType:
type: string
description: LinkedIn reaction enum (LIKE, PRAISE, EMPATHY, INTEREST, APPRECIATION, ENTERTAINMENT)
reactionLabel:
type: string
description: User-friendly label (Like, Celebrate, Love, Insightful, Support, Funny)
reactedAt: { type: string, format: date-time }
from:
type: object
properties:
urn: { type: string, description: "LinkedIn person or organization URN" }
name: { type: string, description: "Reactor's display name" }
headline: { type: string, description: "Reactor's headline/job title" }
username: { type: string, description: "LinkedIn vanity name" }
profilePicture: { type: string, description: "Profile picture URL" }
profileUrl: { type: string, description: "Direct link to LinkedIn profile" }
pagination:
type: object
properties:
hasMore: { type: boolean }
cursor: { type: string, description: "Offset for next page" }
total: { type: integer, description: "Total number of reactions (when available)" }
lastUpdated: { type: string, format: date-time }
example:
accountId: "64abc123def456"
platform: "linkedin"
accountType: "organization"
username: "Acme Corp"
postUrn: "urn:li:share:7123456789012345678"
reactions:
- reactionType: "LIKE"
reactionLabel: "Like"
reactedAt: "2026-03-08T12:00:00.000Z"
from:
urn: "urn:li:person:abc123"
name: "Jane Smith"
headline: "Product Manager at Acme Corp"
username: "janesmith"
profilePicture: "https://media.licdn.com/..."
profileUrl: "https://www.linkedin.com/in/janesmith"
pagination:
hasMore: true
cursor: "25"
total: 156
lastUpdated: "2026-03-08T12:00:00.000Z"
'400':
description: Invalid request or platform limitation
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code: { type: string, enum: [missing_urn, invalid_urn, invalid_platform, PLATFORM_LIMITATION] }
'401': { $ref: '#/components/responses/Unauthorized' }
'402':
description: Analytics add-on required
'403':
description: Missing required LinkedIn scope
'404':
description: Account or post not found
/v1/accounts/{accountId}/linkedin-organization:
put:
operationId: updateLinkedInOrganization
tags: [Connect]
summary: Switch LinkedIn account type
description: Switch a LinkedIn account between personal profile and organization (company page) posting.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountType]
properties:
accountType: { type: string, enum: [personal, organization] }
selectedOrganization: { type: object }
example:
accountType: "organization"
selectedOrganization:
id: "12345678"
name: "Acme Corporation"
vanityName: "acme-corp"
responses:
'200':
description: Account updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
account: { $ref: '#/components/schemas/SocialAccount' }
example:
message: "LinkedIn account type updated successfully"
account:
_id: "64e1f0a9e2b5af0012ab34cd"
platform: "linkedin"
username: "acme-corp"
displayName: "Acme Corporation"
isActive: true
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/linkedin-mentions:
get:
operationId: getLinkedInMentions
tags: [LinkedIn Mentions]
summary: Resolve LinkedIn mention
description: Converts a LinkedIn profile or company URL to a URN for @mentions in posts. Person mentions require org admin access. Use the returned mentionFormat in post content.
parameters:
- name: accountId
in: path
required: true
description: The LinkedIn account ID
schema: { type: string }
- name: url
in: query
required: true
description: LinkedIn profile URL, company URL, or vanity name.
schema: { type: string }
examples:
personVanityName:
value: "miquelpalet"
summary: Person - just the vanity name
personFullUrl:
value: "https://www.linkedin.com/in/miquelpalet"
summary: Person - full LinkedIn URL
orgShortUrl:
value: "company/microsoft"
summary: Org - short format
orgFullUrl:
value: "https://www.linkedin.com/company/microsoft"
summary: Org - full LinkedIn URL
- name: displayName
in: query
required: false
description: Exact display name as shown on LinkedIn. Required for person mentions to be clickable. Optional for org mentions.
schema: { type: string }
examples:
personName:
value: "Miquel Palet"
summary: Exact name as shown on LinkedIn profile
orgName:
value: "Microsoft"
summary: Company name (optional for orgs)
responses:
'200':
description: URN resolved successfully
content:
application/json:
schema:
type: object
properties:
urn:
type: string
description: The LinkedIn URN (person or organization)
example: "urn:li:person:4qj5ox-agD"
type:
type: string
enum: [person, organization]
description: The type of entity (person or organization)
example: "person"
displayName:
type: string
description: Display name (provided, from API, or derived from vanity URL)
example: "Miquel Palet"
mentionFormat:
type: string
description: Ready-to-use mention format for post content
example: "@[Miquel Palet](urn:li:person:4qj5ox-agD)"
vanityName:
type: string
description: The vanity name/slug (only for organization mentions)
example: "microsoft"
warning:
type: string
description: Warning about clickable mentions (only present for person mentions if displayName was not provided)
example: "For clickable person mentions, provide the displayName parameter with the exact name as shown on their LinkedIn profile."
examples:
personWithDisplayName:
summary: Person mention with displayName (recommended)
value:
urn: "urn:li:person:4qj5ox-agD"
type: "person"
displayName: "Miquel Palet"
mentionFormat: "@[Miquel Palet](urn:li:person:4qj5ox-agD)"
personWithoutDisplayName:
summary: Person mention without displayName (may not be clickable)
value:
urn: "urn:li:person:4qj5ox-agD"
type: "person"
displayName: "Miquelpalet"
mentionFormat: "@[Miquelpalet](urn:li:person:4qj5ox-agD)"
warning: "For clickable person mentions, provide the displayName parameter with the exact name as shown on their LinkedIn profile."
organization:
summary: Org mention
value:
urn: "urn:li:organization:1035"
type: "organization"
displayName: "Microsoft"
mentionFormat: "@[Microsoft](urn:li:organization:1035)"
vanityName: "microsoft"
'400':
description: Invalid request or no organization found (for person mentions)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
examples:
missingUrl:
value: { error: "url parameter is required" }
noOrgForPersonMention:
value: { error: "No organization found. You need to be an admin of a LinkedIn organization to use person mentions. Organization mentions work without this requirement." }
'401': { $ref: '#/components/responses/Unauthorized' }
'404':
description: Person or organization not found
content:
application/json:
schema:
type: object
properties:
error: { type: string }
examples:
memberNotFound:
value: { error: "Member not found. Check the LinkedIn URL is correct." }
orgNotFound:
value: { error: "Organization not found. Check the LinkedIn company URL is correct." }
/v1/accounts/{accountId}/pinterest-boards:
get:
operationId: getPinterestBoards
tags: [Connect]
summary: List Pinterest boards
description: Returns the boards available for a connected Pinterest account. Use this to get a board ID when creating a Pinterest post.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Boards list
content:
application/json:
schema:
type: object
properties:
boards:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
description: { type: string }
privacy: { type: string }
example:
boards:
- id: "123456789012345678"
name: "Marketing Ideas"
description: "Collection of marketing inspiration"
privacy: "PUBLIC"
- id: "234567890123456789"
name: "Product Photos"
description: "Product photography"
privacy: "PUBLIC"
'400': { description: Not a Pinterest account }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
put:
operationId: updatePinterestBoards
tags: [Connect]
summary: Set default Pinterest board
description: Sets the default board used when publishing pins for this account.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [defaultBoardId]
properties:
defaultBoardId: { type: string }
defaultBoardName: { type: string }
example:
defaultBoardId: "123456789012345678"
defaultBoardName: "Marketing Ideas"
responses:
'200':
description: Default board set
content:
application/json:
schema:
type: object
properties:
message: { type: string }
account: { $ref: '#/components/schemas/SocialAccount' }
example:
message: "Default Pinterest board updated successfully"
account:
_id: "64e1f0a9e2b5af0012ab34cd"
platform: "pinterest"
username: "mybrand"
displayName: "My Brand"
isActive: true
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/gmb-locations:
get:
operationId: getGmbLocations
tags: [Connect]
summary: List GBP locations
description: Returns all Google Business Profile locations the connected account has access to, including the currently selected location.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Locations list
content:
application/json:
schema:
type: object
properties:
locations:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
accountId: { type: string }
accountName: { type: string }
address: { type: string }
category: { type: string }
websiteUrl: { type: string }
selectedLocationId: { type: string }
cached: { type: boolean }
example:
locations:
- id: "12345678901234567890"
name: "My Business Location"
accountId: "accounts/123456789"
accountName: "My Business Account"
address: "123 Main St, San Francisco, CA"
category: "Restaurant"
websiteUrl: "https://mybusiness.com"
selectedLocationId: "12345678901234567890"
cached: true
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
put:
operationId: updateGmbLocation
tags: [Connect]
summary: Update GBP location
description: Switch which GBP location is active for a connected account.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [selectedLocationId]
properties:
selectedLocationId: { type: string }
example:
selectedLocationId: "12345678901234567890"
responses:
'200':
description: Location updated
content:
application/json:
schema:
type: object
properties:
message: { type: string }
selectedLocation:
type: object
properties:
id: { type: string }
name: { type: string }
example:
message: "Google Business location updated successfully"
selectedLocation:
id: "12345678901234567890"
name: "My Business Location"
'400': { description: Location not in available locations }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/reddit-subreddits:
get:
operationId: getRedditSubreddits
tags: [Connect]
summary: List Reddit subreddits
description: Returns the subreddits the connected Reddit account can post to. Use this to get a subreddit name when creating a Reddit post.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Subreddits list
content:
application/json:
schema:
type: object
properties:
subreddits:
type: array
items:
type: object
properties:
id: { type: string, description: Reddit subreddit ID }
name: { type: string, description: Subreddit name without r/ prefix }
title: { type: string, description: Subreddit title }
url: { type: string, description: Subreddit URL path }
over18: { type: boolean, description: Whether the subreddit is NSFW }
defaultSubreddit:
type: string
description: Currently set default subreddit for posting
example:
subreddits:
- id: "2qh1i"
name: "marketing"
title: "Marketing"
url: "/r/marketing/"
over18: false
- id: "2qh3l"
name: "socialmedia"
title: "Social Media"
url: "/r/socialmedia/"
over18: false
defaultSubreddit: "marketing"
'400': { description: Not a Reddit account }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
put:
operationId: updateRedditSubreddits
tags: [Connect]
summary: Set default subreddit
description: Sets the default subreddit used when publishing posts for this Reddit account.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [defaultSubreddit]
properties:
defaultSubreddit: { type: string }
example:
defaultSubreddit: "marketing"
responses:
'200':
description: Default subreddit set
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
example:
success: true
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/accounts/{accountId}/reddit-flairs:
get:
operationId: getRedditFlairs
tags: [Connect]
summary: List subreddit flairs
description: Returns available post flairs for a subreddit. Some subreddits require a flair when posting.
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
- name: subreddit
in: query
required: true
schema: { type: string }
description: Subreddit name (without "r/" prefix) to fetch flairs for
responses:
'200':
description: Flairs list
content:
application/json:
schema:
type: object
properties:
flairs:
type: array
items:
type: object
properties:
id: { type: string, description: Flair ID to pass as flairId in platformSpecificData }
text: { type: string, description: Flair display text }
textColor: { type: string, description: "Text color: 'dark' or 'light'" }
backgroundColor: { type: string, description: "Background hex color (e.g. '#ff4500')" }
example:
flairs:
- id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
text: "Discussion"
textColor: "dark"
backgroundColor: "#edeff1"
- id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
text: "News"
textColor: "light"
backgroundColor: "#ff4500"
'400': { description: Not a Reddit account or missing subreddit parameter }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/queue/slots:
get:
operationId: listQueueSlots
tags: [Queue]
summary: List schedules
description: Returns queue schedules for a profile. Use all=true for all queues, or queueId for a specific one. Defaults to the default queue.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
description: Profile ID to get queues for
- name: queueId
in: query
required: false
schema: { type: string }
description: Specific queue ID to retrieve (optional)
- name: all
in: query
required: false
schema: { type: string, enum: ['true'] }
description: Set to 'true' to list all queues for the profile
responses:
'200':
description: Queue schedule(s) retrieved
content:
application/json:
schema:
oneOf:
- type: object
description: Single queue response (default behavior)
properties:
exists: { type: boolean }
schedule:
$ref: '#/components/schemas/QueueSchedule'
nextSlots:
type: array
items: { type: string, format: date-time }
- type: object
description: All queues response (when all=true)
properties:
queues:
type: array
items:
$ref: '#/components/schemas/QueueSchedule'
count: { type: integer }
examples:
singleQueue:
summary: Single queue response
value:
exists: true
schedule:
_id: "64f0a1b2c3d4e5f6a7b8c9d1"
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
name: "Morning Posts"
timezone: "America/New_York"
slots:
- dayOfWeek: 1
time: "09:00"
- dayOfWeek: 3
time: "09:00"
- dayOfWeek: 5
time: "10:00"
active: true
isDefault: true
nextSlots:
- "2024-11-04T09:00:00-05:00"
- "2024-11-06T09:00:00-05:00"
allQueues:
summary: All queues response (all=true)
value:
queues:
- _id: "64f0a1b2c3d4e5f6a7b8c9d1"
name: "Morning Posts"
isDefault: true
timezone: "America/New_York"
slots: [{ dayOfWeek: 1, time: "09:00" }]
active: true
- _id: "64f0a1b2c3d4e5f6a7b8c9d2"
name: "Evening Content"
isDefault: false
timezone: "America/New_York"
slots: [{ dayOfWeek: 1, time: "18:00" }]
active: true
count: 2
'400': { description: Missing profileId }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Profile not found }
post:
operationId: createQueueSlot
tags: [Queue]
summary: Create schedule
description: |
Create an additional queue for a profile. The first queue created becomes the default.
Subsequent queues are non-default unless explicitly set.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, name, timezone, slots]
properties:
profileId: { type: string, description: Profile ID }
name: { type: string, description: "Queue name (e.g., Evening Posts)" }
timezone: { type: string, description: IANA timezone }
slots:
type: array
items:
$ref: '#/components/schemas/QueueSlot'
active: { type: boolean, default: true }
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
name: "Evening Posts"
timezone: "America/New_York"
slots:
- dayOfWeek: 1
time: "18:00"
- dayOfWeek: 3
time: "18:00"
- dayOfWeek: 5
time: "18:00"
active: true
responses:
'201':
description: Queue created
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
schedule:
$ref: '#/components/schemas/QueueSchedule'
nextSlots:
type: array
items: { type: string, format: date-time }
'400': { description: Invalid request or validation error }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Profile not found }
put:
operationId: updateQueueSlot
tags: [Queue]
summary: Update schedule
description: |
Create a new queue or update an existing one. Without queueId, creates/updates the default queue. With queueId, updates a specific queue. With setAsDefault=true, makes this queue the default for the profile.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [profileId, timezone, slots]
properties:
profileId: { type: string }
queueId: { type: string, description: Queue ID to update (optional) }
name: { type: string, description: Queue name }
timezone: { type: string }
slots:
type: array
items:
$ref: '#/components/schemas/QueueSlot'
active: { type: boolean, default: true }
setAsDefault: { type: boolean, description: Make this queue the default }
reshuffleExisting:
type: boolean
default: false
description: Whether to reschedule existing queued posts to match new slots
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
queueId: "64f0a1b2c3d4e5f6a7b8c9d1"
name: "Morning Posts"
timezone: "America/New_York"
slots:
- dayOfWeek: 1
time: "09:00"
- dayOfWeek: 3
time: "09:00"
- dayOfWeek: 5
time: "10:00"
active: true
setAsDefault: false
responses:
'200':
description: Queue schedule updated
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
schedule:
$ref: '#/components/schemas/QueueSchedule'
nextSlots:
type: array
items: { type: string, format: date-time }
reshuffledCount: { type: integer }
example:
success: true
schedule:
_id: "64f0a1b2c3d4e5f6a7b8c9d1"
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
name: "Morning Posts"
timezone: "America/New_York"
slots:
- dayOfWeek: 1
time: "09:00"
- dayOfWeek: 3
time: "09:00"
- dayOfWeek: 5
time: "10:00"
active: true
isDefault: true
nextSlots:
- "2024-11-04T09:00:00-05:00"
- "2024-11-06T09:00:00-05:00"
reshuffledCount: 0
'400': { description: Invalid request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Profile not found }
delete:
operationId: deleteQueueSlot
tags: [Queue]
summary: Delete schedule
description: |
Delete a queue from a profile. Requires queueId to specify which queue to delete.
If deleting the default queue, another queue will be promoted to default.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
- name: queueId
in: query
required: true
schema: { type: string }
description: Queue ID to delete
responses:
'200':
description: Queue schedule deleted
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
deleted: { type: boolean }
example:
success: true
deleted: true
'400': { description: Missing profileId or queueId }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/queue/preview:
get:
operationId: previewQueue
tags: [Queue]
summary: Preview upcoming slots
description: Returns the next N upcoming queue slot times for a profile as ISO datetime strings.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
- name: queueId
in: query
schema: { type: string }
description: Filter by specific queue ID. Omit to use the default queue.
- name: count
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
'200':
description: Queue slots preview
content:
application/json:
schema:
type: object
properties:
profileId: { type: string }
count: { type: integer }
slots:
type: array
items: { type: string, format: date-time }
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
count: 10
slots:
- "2024-11-04T09:00:00-05:00"
- "2024-11-04T14:00:00-05:00"
- "2024-11-06T09:00:00-05:00"
- "2024-11-08T10:00:00-05:00"
- "2024-11-11T09:00:00-05:00"
- "2024-11-11T14:00:00-05:00"
- "2024-11-13T09:00:00-05:00"
- "2024-11-15T10:00:00-05:00"
- "2024-11-18T09:00:00-05:00"
- "2024-11-18T14:00:00-05:00"
'400': { description: Invalid parameters }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Profile or queue schedule not found }
/v1/queue/next-slot:
get:
operationId: getNextQueueSlot
tags: [Queue]
summary: Get next available slot
description: Returns the next available queue slot for preview purposes. To create a queue post, use POST /v1/posts with queuedFromProfile instead of scheduledFor.
parameters:
- name: profileId
in: query
required: true
schema: { type: string }
- name: queueId
in: query
required: false
schema: { type: string }
description: Specific queue ID (optional, defaults to profile's default queue)
responses:
'200':
description: Next available slot
content:
application/json:
schema:
type: object
properties:
profileId: { type: string }
nextSlot: { type: string, format: date-time }
timezone: { type: string }
queueId: { type: string, description: Queue ID this slot belongs to }
queueName: { type: string, description: Queue name }
example:
profileId: "64f0a1b2c3d4e5f6a7b8c9d0"
nextSlot: "2024-11-04T09:00:00-05:00"
timezone: "America/New_York"
queueId: "64f0a1b2c3d4e5f6a7b8c9d1"
queueName: "Morning Posts"
'400':
description: Invalid parameters or inactive queue
'401': { $ref: '#/components/responses/Unauthorized' }
'404':
description: "Profile or queue schedule not found, or no available slots"
/v1/webhooks/settings:
get:
operationId: getWebhookSettings
tags: [Webhooks]
summary: List webhooks
description: Retrieve all configured webhooks for the authenticated user. Supports up to 10 webhooks per user.
security:
- bearerAuth: []
responses:
'200':
description: Webhooks retrieved successfully
content:
application/json:
schema:
type: object
properties:
webhooks:
type: array
items:
$ref: '#/components/schemas/Webhook'
example:
webhooks:
- _id: "507f1f77bcf86cd799439011"
name: "My Production Webhook"
url: "https://example.com/webhook"
events: ["post.published", "post.failed"]
isActive: true
lastFiredAt: "2024-01-15T10:30:00Z"
failureCount: 0
- _id: "507f1f77bcf86cd799439012"
name: "Slack Notifications"
url: "https://hooks.slack.com/services/xxx"
events: ["post.failed", "account.disconnected"]
isActive: true
failureCount: 0
'401': { $ref: '#/components/responses/Unauthorized' }
post:
operationId: createWebhookSettings
tags: [Webhooks]
summary: Create webhook
description: |
Create a new webhook configuration. Maximum 10 webhooks per user.
Webhooks are automatically disabled after 10 consecutive delivery failures.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Webhook name (max 50 characters)
maxLength: 50
url:
type: string
format: uri
description: Webhook endpoint URL (must be HTTPS in production)
secret:
type: string
description: Secret key for HMAC-SHA256 signature verification
events:
type: array
items:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received]
description: Events to subscribe to
isActive:
type: boolean
description: Enable or disable webhook delivery
customHeaders:
type: object
additionalProperties:
type: string
description: Custom headers to include in webhook requests
examples:
createWebhook:
summary: Create webhook with all events
value:
name: "My Production Webhook"
url: "https://example.com/webhook"
secret: "your-secret-key"
events: ["post.scheduled", "post.published", "post.failed", "post.partial", "account.connected", "account.disconnected", "message.received", "comment.received"]
isActive: true
responses:
'200':
description: Webhook created successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
webhook:
$ref: '#/components/schemas/Webhook'
'400': { description: Validation error or maximum webhooks reached }
'401': { $ref: '#/components/responses/Unauthorized' }
put:
operationId: updateWebhookSettings
tags: [Webhooks]
summary: Update webhook
description: |
Update an existing webhook configuration. All fields except _id are optional; only provided fields will be updated.
Webhooks are automatically disabled after 10 consecutive delivery failures.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- _id
properties:
_id:
type: string
description: Webhook ID to update (required)
name:
type: string
description: Webhook name (max 50 characters)
maxLength: 50
url:
type: string
format: uri
description: Webhook endpoint URL (must be HTTPS in production)
secret:
type: string
description: Secret key for HMAC-SHA256 signature verification
events:
type: array
items:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received]
description: Events to subscribe to
isActive:
type: boolean
description: Enable or disable webhook delivery
customHeaders:
type: object
additionalProperties:
type: string
description: Custom headers to include in webhook requests
examples:
updateWebhook:
summary: Update webhook URL and events
value:
_id: "507f1f77bcf86cd799439011"
url: "https://new-example.com/webhook"
events: ["post.published", "post.failed"]
toggleWebhook:
summary: Enable/disable webhook
value:
_id: "507f1f77bcf86cd799439011"
isActive: false
responses:
'200':
description: Webhook updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
webhook:
$ref: '#/components/schemas/Webhook'
'400': { description: Validation error or missing webhook ID }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Webhook not found }
delete:
operationId: deleteWebhookSettings
tags: [Webhooks]
summary: Delete webhook
description: Permanently delete a webhook configuration.
security:
- bearerAuth: []
parameters:
- name: id
in: query
required: true
description: Webhook ID to delete
schema:
type: string
responses:
'200':
description: Webhook deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
'400': { description: Webhook ID required }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/webhooks/test:
post:
operationId: testWebhook
tags: [Webhooks]
summary: Send test webhook
description: |
Send a test webhook to verify your endpoint is configured correctly. The test payload includes event: "webhook.test" to distinguish it from real events.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- webhookId
properties:
webhookId:
type: string
description: ID of the webhook to test
example:
webhookId: "507f1f77bcf86cd799439011"
responses:
'200':
description: Test webhook sent successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Test webhook sent successfully"
'400': { description: Webhook ID required }
'401': { $ref: '#/components/responses/Unauthorized' }
'500':
description: Test webhook failed to deliver
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: false
message: "Test webhook failed"
/v1/webhooks/logs:
get:
operationId: getWebhookLogs
tags: [Webhooks]
summary: Get delivery logs
description: |
Retrieve webhook delivery history. Logs are automatically deleted after 7 days.
security:
- bearerAuth: []
parameters:
- name: limit
in: query
description: Maximum number of logs to return (max 100)
schema:
type: integer
minimum: 1
maximum: 100
default: 50
- name: status
in: query
description: Filter by delivery status
schema:
type: string
enum: [success, failed]
- name: event
in: query
description: Filter by event type
schema:
type: string
enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test]
- name: webhookId
in: query
description: Filter by webhook ID
schema:
type: string
responses:
'200':
description: Webhook logs retrieved successfully
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
$ref: '#/components/schemas/WebhookLog'
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/posts/logs:
get:
operationId: listPostsLogs
tags: [Logs]
summary: List publishing logs
description: |
Retrieve publishing logs for all posts with detailed information about each publishing attempt. Filter by status, platform, or action. Logs are automatically deleted after 7 days.
security:
- bearerAuth: []
parameters:
- name: status
in: query
description: Filter by log status
schema:
type: string
enum: [success, failed, pending, skipped, all]
- name: platform
in: query
description: Filter by platform
schema:
type: string
enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, all]
- name: action
in: query
description: Filter by action type
schema:
type: string
enum: [publish, retry, media_upload, rate_limit_pause, token_refresh, cancelled, all]
- name: days
in: query
description: Number of days to look back (max 7)
schema:
type: integer
minimum: 1
maximum: 7
default: 7
- name: limit
in: query
description: Maximum number of logs to return (max 100)
schema:
type: integer
minimum: 1
maximum: 100
default: 50
- name: skip
in: query
description: Number of logs to skip (for pagination)
schema:
type: integer
minimum: 0
default: 0
- name: search
in: query
description: Search through log entries by text content.
schema:
type: string
responses:
'200':
description: Publishing logs retrieved successfully
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
$ref: '#/components/schemas/PostLog'
pagination:
type: object
properties:
total:
type: integer
description: Total number of logs matching the query
limit:
type: integer
skip:
type: integer
pages:
type: integer
description: Total number of pages
hasMore:
type: boolean
example:
logs:
- _id: "675f1c0a9e2b5af0012ab34cd"
postId:
_id: "65f1c0a9e2b5af0012ab34cd"
content: "Check out our new feature!"
status: "published"
userId: "64e1f0a9e2b5af0012ab34de"
platform: "instagram"
accountId: "64e1f0a9e2b5af0012ab34ef"
accountUsername: "@acmecorp"
action: "publish"
status: "success"
statusCode: 200
endpoint: "graph.facebook.com/me/media_publish"
request:
contentPreview: "Check out our new feature!"
mediaCount: 1
mediaTypes: ["image"]
mediaUrls: ["https://storage.zernio.com/abc123.jpg"]
response:
platformPostId: "17895695668004550"
platformPostUrl: "https://www.instagram.com/p/ABC123/"
durationMs: 2340
attemptNumber: 1
createdAt: "2024-11-01T10:00:05Z"
pagination:
total: 150
limit: 50
skip: 0
pages: 3
hasMore: true
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/connections/logs:
get:
operationId: listConnectionLogs
tags: [Logs]
summary: List connection logs
description: |
Retrieve connection event logs showing account connection and disconnection history. Event types: connect_success, connect_failed, disconnect, reconnect_success, reconnect_failed.
Logs are automatically deleted after 7 days.
security:
- bearerAuth: []
parameters:
- name: platform
in: query
description: Filter by platform
schema:
type: string
enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, all]
- name: eventType
in: query
description: Filter by event type
schema:
type: string
enum: [connect_success, connect_failed, disconnect, reconnect_success, reconnect_failed, all]
- name: status
in: query
description: Filter by status (shorthand for event types)
schema:
type: string
enum: [success, failed, all]
description: success = connect_success + reconnect_success, failed = connect_failed + reconnect_failed
- name: days
in: query
description: Number of days to look back (max 7)
schema:
type: integer
minimum: 1
maximum: 7
default: 7
- name: limit
in: query
description: Maximum number of logs to return (max 100)
schema:
type: integer
minimum: 1
maximum: 100
default: 50
- name: skip
in: query
description: Number of logs to skip (for pagination)
schema:
type: integer
minimum: 0
default: 0
responses:
'200':
description: Connection logs retrieved successfully
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
$ref: '#/components/schemas/ConnectionLog'
pagination:
type: object
properties:
total:
type: integer
description: Total number of logs matching the query
limit:
type: integer
skip:
type: integer
pages:
type: integer
description: Total number of pages
hasMore:
type: boolean
example:
logs:
- _id: "675f1c0a9e2b5af0012ab34cd"
userId: "64e1f0a9e2b5af0012ab34de"
profileId: "64e1f0a9e2b5af0012ab34ef"
accountId: "64e1f0a9e2b5af0012ab3500"
platform: "instagram"
eventType: "connect_success"
connectionMethod: "oauth"
success:
displayName: "Acme Corp"
username: "acmecorp"
profilePicture: "https://..."
permissions: ["instagram_basic", "instagram_content_publish"]
tokenExpiresAt: "2024-12-01T10:00:00Z"
accountType: "business"
context:
hasCustomRedirectUrl: false
createdAt: "2024-11-01T10:00:00Z"
- _id: "675f1c0a9e2b5af0012ab34ce"
userId: "64e1f0a9e2b5af0012ab34de"
profileId: "64e1f0a9e2b5af0012ab34ef"
platform: "twitter"
eventType: "connect_failed"
connectionMethod: "oauth"
error:
code: "oauth_denied"
message: "OAuth error: access_denied"
context:
hasCustomRedirectUrl: true
createdAt: "2024-11-01T09:00:00Z"
pagination:
total: 25
limit: 50
skip: 0
pages: 1
hasMore: false
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/posts/{postId}/logs:
get:
operationId: getPostLogs
tags: [Logs]
summary: Get post logs
description: |
Retrieve all publishing logs for a specific post. Shows the complete history
of publishing attempts for that post across all platforms.
security:
- bearerAuth: []
parameters:
- name: postId
in: path
required: true
description: The post ID
schema:
type: string
- name: limit
in: query
description: Maximum number of logs to return (max 100)
schema:
type: integer
minimum: 1
maximum: 100
default: 50
responses:
'200':
description: Post logs retrieved successfully
content:
application/json:
schema:
type: object
properties:
logs:
type: array
items:
$ref: '#/components/schemas/PostLog'
count:
type: integer
description: Number of logs returned
postId:
type: string
example:
logs:
- _id: "675f1c0a9e2b5af0012ab34cd"
postId: "65f1c0a9e2b5af0012ab34cd"
userId: "64e1f0a9e2b5af0012ab34de"
platform: "instagram"
accountUsername: "@acmecorp"
action: "publish"
status: "success"
statusCode: 200
durationMs: 2340
createdAt: "2024-11-01T10:00:05Z"
- _id: "675f1c0a9e2b5af0012ab34ce"
postId: "65f1c0a9e2b5af0012ab34cd"
userId: "64e1f0a9e2b5af0012ab34de"
platform: "twitter"
accountUsername: "@acme"
action: "publish"
status: "failed"
statusCode: 429
response:
errorMessage: "Rate limit exceeded"
errorCode: "RATE_LIMITED"
durationMs: 150
attemptNumber: 1
createdAt: "2024-11-01T10:00:03Z"
count: 2
postId: "65f1c0a9e2b5af0012ab34cd"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Forbidden - not authorized to view this post
'404': { $ref: '#/components/responses/NotFound' }
/v1/inbox/conversations:
get:
operationId: listInboxConversations
summary: List conversations
description: |
Fetch conversations (DMs) from all connected messaging accounts in a single API call. Supports filtering by profile and platform. Results are aggregated and deduplicated.
Supported platforms: Facebook, Instagram, Twitter/X, Bluesky, Reddit, Telegram.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID
- name: platform
in: query
schema: { type: string, enum: [facebook, instagram, twitter, bluesky, reddit, telegram] }
description: Filter by platform
- name: status
in: query
schema: { type: string, enum: [active, archived] }
description: Filter by conversation status
- name: sortOrder
in: query
schema: { type: string, enum: [asc, desc], default: desc }
description: Sort order by updated time
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
description: Maximum number of conversations to return
- name: cursor
in: query
schema: { type: string }
description: Pagination cursor for next page
- name: accountId
in: query
schema: { type: string }
description: Filter by specific social account ID
responses:
'200':
description: Aggregated conversations
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id: { type: string }
platform: { type: string }
accountId: { type: string }
accountUsername: { type: string }
participantId: { type: string }
participantName: { type: string }
participantPicture: { type: string, nullable: true }
lastMessage: { type: string }
updatedTime: { type: string, format: date-time }
status: { type: string, enum: [active, archived] }
unreadCount: { type: integer, nullable: true, description: Number of unread messages }
url:
type: string
nullable: true
description: Direct link to open the conversation on the platform (if available)
instagramProfile:
type: object
nullable: true
description: Instagram profile data for the participant. Only present for Instagram conversations.
properties:
isFollower:
type: boolean
nullable: true
description: Whether the participant follows your Instagram business account
isFollowing:
type: boolean
nullable: true
description: Whether your Instagram business account follows the participant
followerCount:
type: integer
nullable: true
description: The participant's follower count on Instagram
isVerified:
type: boolean
nullable: true
description: Whether the participant is a verified Instagram user
fetchedAt:
type: string
format: date-time
nullable: true
description: When this profile data was last fetched from Instagram
pagination:
type: object
properties:
hasMore: { type: boolean }
nextCursor: { type: string, nullable: true }
meta:
type: object
properties:
accountsQueried: { type: integer }
accountsFailed: { type: integer }
failedAccounts:
type: array
items:
type: object
properties:
accountId: { type: string }
accountUsername: { type: string, nullable: true }
platform: { type: string }
error: { type: string }
code: { type: string, nullable: true, description: Error code if available }
retryAfter: { type: integer, nullable: true, description: Seconds to wait before retry (rate limits) }
lastUpdated: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/conversations/{conversationId}:
get:
operationId: getInboxConversation
summary: Get conversation
description: Retrieve details and metadata for a specific conversation. Requires accountId query parameter.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: conversationId
in: path
required: true
schema: { type: string }
description: The conversation ID (id field from list conversations endpoint). This is the platform-specific conversation identifier, not an internal database ID.
- name: accountId
in: query
required: true
schema: { type: string }
description: The social account ID
responses:
'200':
description: Conversation details
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id: { type: string }
accountId: { type: string }
accountUsername: { type: string }
platform: { type: string }
status: { type: string, enum: [active, archived] }
participantName: { type: string }
participantId: { type: string }
lastMessage: { type: string }
lastMessageAt: { type: string, format: date-time }
updatedTime: { type: string, format: date-time }
participants:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
instagramProfile:
type: object
nullable: true
description: Instagram profile data for the participant. Only present for Instagram conversations.
properties:
isFollower:
type: boolean
nullable: true
description: Whether the participant follows your Instagram business account
isFollowing:
type: boolean
nullable: true
description: Whether your Instagram business account follows the participant
followerCount:
type: integer
nullable: true
description: The participant's follower count on Instagram
isVerified:
type: boolean
nullable: true
description: Whether the participant is a verified Instagram user
fetchedAt:
type: string
format: date-time
nullable: true
description: When this profile data was last fetched from Instagram
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
'404':
description: Conversation not found
put:
operationId: updateInboxConversation
summary: Update conversation status
description: Archive or activate a conversation. Requires accountId in request body.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: conversationId
in: path
required: true
schema: { type: string }
description: The conversation ID (id field from list conversations endpoint). This is the platform-specific conversation identifier, not an internal database ID.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, status]
properties:
accountId: { type: string, description: Social account ID }
status: { type: string, enum: [active, archived] }
responses:
'200':
description: Conversation updated
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
data:
type: object
properties:
id: { type: string }
accountId: { type: string }
status: { type: string, enum: [active, archived] }
platform: { type: string }
updatedAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/conversations/{conversationId}/messages:
get:
operationId: getInboxConversationMessages
summary: List messages
description: Fetch messages for a specific conversation. Requires accountId query parameter.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: conversationId
in: path
required: true
schema: { type: string }
description: The conversation ID (id field from list conversations endpoint). This is the platform-specific conversation identifier, not an internal database ID.
- name: accountId
in: query
required: true
schema: { type: string }
description: Social account ID
responses:
'200':
description: Messages in conversation
content:
application/json:
schema:
type: object
properties:
status: { type: string }
messages:
type: array
items:
type: object
properties:
id: { type: string }
conversationId: { type: string }
accountId: { type: string }
platform: { type: string }
message: { type: string }
senderId: { type: string }
senderName: { type: string, nullable: true }
direction: { type: string, enum: [incoming, outgoing] }
createdAt: { type: string, format: date-time }
attachments:
type: array
items:
type: object
properties:
id: { type: string }
type: { type: string, enum: [image, video, audio, file, sticker, share] }
url: { type: string }
filename: { type: string, nullable: true }
previewUrl: { type: string, nullable: true }
subject: { type: string, nullable: true, description: Reddit message subject }
storyReply: { type: boolean, nullable: true, description: Instagram story reply }
isStoryMention: { type: boolean, nullable: true, description: Instagram story mention }
lastUpdated: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
post:
operationId: sendInboxMessage
summary: Send message
description: Send a message in a conversation. Supports text, attachments, quick replies, buttons, and message tags. Attachment and interactive message support varies by platform.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: conversationId
in: path
required: true
schema: { type: string }
description: The conversation ID (id field from list conversations endpoint). This is the platform-specific conversation identifier, not an internal database ID.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string, description: Social account ID }
message: { type: string, description: Message text }
quickReplies:
type: array
maxItems: 13
description: Quick reply buttons. Mutually exclusive with buttons. Max 13 items.
items:
type: object
required: [title, payload]
properties:
title: { type: string, maxLength: 20, description: Button label (max 20 chars) }
payload: { type: string, description: Payload sent back on tap }
imageUrl: { type: string, description: Optional icon URL (Meta only) }
buttons:
type: array
maxItems: 3
description: Action buttons. Mutually exclusive with quickReplies. Max 3 items.
items:
type: object
required: [type, title]
properties:
type: { type: string, enum: [url, postback, phone], description: Button type. phone is Facebook only. }
title: { type: string, maxLength: 20, description: Button label (max 20 chars) }
url: { type: string, description: URL for url-type buttons }
payload: { type: string, description: Payload for postback-type buttons }
phone: { type: string, description: Phone number for phone-type buttons (Facebook only) }
template:
type: object
description: Generic template for carousels (Instagram/Facebook only, ignored on Telegram).
properties:
type: { type: string, enum: [generic], description: Template type }
elements:
type: array
maxItems: 10
items:
type: object
required: [title]
properties:
title: { type: string, maxLength: 80, description: Element title (max 80 chars) }
subtitle: { type: string, description: Element subtitle }
imageUrl: { type: string, description: Element image URL }
buttons:
type: array
maxItems: 3
items:
type: object
properties:
type: { type: string, enum: [url, postback] }
title: { type: string, maxLength: 20 }
url: { type: string }
payload: { type: string }
replyMarkup:
type: object
description: Telegram-native keyboard markup. Ignored on other platforms.
properties:
type: { type: string, enum: [inline_keyboard, reply_keyboard], description: Keyboard type }
keyboard:
type: array
description: Array of rows, each row is an array of buttons
items:
type: array
items:
type: object
properties:
text: { type: string, description: Button text }
callbackData: { type: string, maxLength: 64, description: Callback data (inline_keyboard only, max 64 bytes) }
url: { type: string, description: URL to open (inline_keyboard only) }
oneTime: { type: boolean, default: true, description: Hide keyboard after use (reply_keyboard only) }
messagingType:
type: string
enum: [RESPONSE, UPDATE, MESSAGE_TAG]
description: Facebook messaging type. Required when using messageTag.
messageTag:
type: string
enum: [CONFIRMED_EVENT_UPDATE, POST_PURCHASE_UPDATE, ACCOUNT_UPDATE, HUMAN_AGENT]
description: Facebook message tag for messaging outside 24h window. Requires messagingType MESSAGE_TAG. Instagram only supports HUMAN_AGENT.
replyTo:
type: string
description: Platform message ID to reply to (Telegram only).
multipart/form-data:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string, description: Social account ID }
message: { type: string, description: Message text (optional when sending attachment) }
attachment:
type: string
format: binary
description: "File attachment (images, videos, documents). Supported formats: JPEG, PNG, GIF, MP4, AAC, WAV. Max 25MB."
quickReplies:
type: string
description: JSON string of quick replies array (same schema as application/json body)
buttons:
type: string
description: JSON string of buttons array (same schema as application/json body)
template:
type: string
description: JSON string of template object (same schema as application/json body)
replyMarkup:
type: string
description: JSON string of replyMarkup object (same schema as application/json body)
messagingType:
type: string
description: Messaging type (Facebook only). RESPONSE, UPDATE, or MESSAGE_TAG.
messageTag:
type: string
description: Message tag (requires messagingType MESSAGE_TAG)
replyTo:
type: string
description: Platform message ID to reply to (Telegram only)
responses:
'200':
description: Message sent
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
data:
type: object
properties:
messageId: { type: string, description: ID of the sent message (not returned for Reddit) }
conversationId: { type: string, nullable: true, description: Twitter conversation ID }
sentAt: { type: string, format: date-time, nullable: true, description: Bluesky sent timestamp }
message: { type: string, nullable: true, description: Success message (Reddit only) }
'400':
description: Bad request (e.g., attachment not supported for platform, validation error)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
code:
type: string
enum: [PLATFORM_LIMITATION]
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/conversations/{conversationId}/messages/{messageId}:
patch:
operationId: editInboxMessage
summary: Edit message
description: |
Edit the text and/or reply markup of a previously sent Telegram message.
Only supported for Telegram. Returns 400 for other platforms.
tags: [Messages]
security: [{ bearerAuth: [] }]
parameters:
- name: conversationId
in: path
required: true
schema: { type: string }
description: The conversation ID
- name: messageId
in: path
required: true
schema: { type: string }
description: The Telegram message ID to edit
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string, description: Social account ID }
text: { type: string, description: New message text }
replyMarkup:
type: object
description: New inline keyboard markup
properties:
type: { type: string, enum: [inline_keyboard] }
keyboard:
type: array
items:
type: array
items:
type: object
properties:
text: { type: string }
callbackData: { type: string }
url: { type: string }
responses:
'200':
description: Message edited
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
data:
type: object
properties:
messageId: { type: integer }
'400':
description: Not supported or invalid request
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/accounts/{accountId}/messenger-menu:
get:
operationId: getMessengerMenu
summary: Get FB persistent menu
description: Get the persistent menu configuration for a Facebook Messenger account.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Persistent menu configuration
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { type: object } }
'400':
description: Not a Facebook account
'401': { $ref: '#/components/responses/Unauthorized' }
put:
operationId: setMessengerMenu
summary: Set FB persistent menu
description: Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [persistent_menu]
properties:
persistent_menu:
type: array
description: Persistent menu configuration array (Meta format)
items: { type: object }
responses:
'200':
description: Menu set successfully
'400':
description: Invalid request
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
operationId: deleteMessengerMenu
description: Removes the persistent menu from Facebook Messenger conversations for this account.
summary: Delete FB persistent menu
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Menu deleted
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/accounts/{accountId}/instagram-ice-breakers:
get:
operationId: getInstagramIceBreakers
summary: Get IG ice breakers
description: Get the ice breaker configuration for an Instagram account.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Ice breaker configuration
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { type: object } }
'400':
description: Not an Instagram account
'401': { $ref: '#/components/responses/Unauthorized' }
put:
operationId: setInstagramIceBreakers
summary: Set IG ice breakers
description: Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [ice_breakers]
properties:
ice_breakers:
type: array
maxItems: 4
items:
type: object
required: [question, payload]
properties:
question: { type: string, maxLength: 80 }
payload: { type: string }
responses:
'200':
description: Ice breakers set successfully
'400':
description: Invalid request
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
operationId: deleteInstagramIceBreakers
description: Removes the ice breaker questions from an Instagram account's Messenger experience.
summary: Delete IG ice breakers
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Ice breakers deleted
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/accounts/{accountId}/telegram-commands:
get:
operationId: getTelegramCommands
summary: Get TG bot commands
description: Get the bot commands configuration for a Telegram account.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Bot commands list
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
command: { type: string }
description: { type: string }
'400':
description: Not a Telegram account
'401': { $ref: '#/components/responses/Unauthorized' }
put:
operationId: setTelegramCommands
summary: Set TG bot commands
description: Set bot commands for a Telegram account.
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [commands]
properties:
commands:
type: array
items:
type: object
required: [command, description]
properties:
command: { type: string, description: Bot command without leading slash }
description: { type: string, description: Command description }
responses:
'200':
description: Commands set successfully
'400':
description: Invalid request
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
operationId: deleteTelegramCommands
description: Clears all bot commands configured for a Telegram bot account.
summary: Delete TG bot commands
tags: [Account Settings]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Commands deleted
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/inbox/comments:
get:
operationId: listInboxComments
summary: List commented posts
description: Returns posts with comment counts from all connected accounts. Aggregates data across multiple accounts.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: profileId
in: query
schema: { type: string }
description: Filter by profile ID
- name: platform
in: query
schema: { type: string, enum: [facebook, instagram, twitter, bluesky, threads, youtube, linkedin, reddit] }
description: Filter by platform
- name: minComments
in: query
schema: { type: integer, minimum: 0 }
description: Minimum comment count
- name: since
in: query
schema: { type: string, format: date-time }
description: Posts created after this date
- name: sortBy
in: query
schema: { type: string, enum: [date, comments], default: date }
description: Sort field
- name: sortOrder
in: query
schema: { type: string, enum: [asc, desc], default: desc }
description: Sort order
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
- name: cursor
in: query
schema: { type: string }
- name: accountId
in: query
schema: { type: string }
description: Filter by specific social account ID
responses:
'200':
description: Aggregated posts with comments
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id: { type: string }
platform: { type: string }
accountId: { type: string }
accountUsername: { type: string }
content: { type: string }
picture: { type: string, nullable: true }
permalink: { type: string, nullable: true }
createdTime: { type: string, format: date-time }
commentCount: { type: integer }
likeCount: { type: integer }
cid: { type: string, nullable: true, description: Bluesky content identifier }
subreddit: { type: string, nullable: true, description: Reddit subreddit name }
pagination:
type: object
properties:
hasMore: { type: boolean }
nextCursor: { type: string, nullable: true }
meta:
type: object
properties:
accountsQueried: { type: integer }
accountsFailed: { type: integer }
failedAccounts:
type: array
items:
type: object
properties:
accountId: { type: string }
accountUsername: { type: string, nullable: true }
platform: { type: string }
error: { type: string }
code: { type: string, nullable: true, description: Error code if available }
retryAfter: { type: integer, nullable: true, description: Seconds to wait before retry (rate limits) }
lastUpdated: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/comments/{postId}:
get:
operationId: getInboxPostComments
summary: Get post comments
description: Fetch comments for a specific post. Requires accountId query parameter.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
description: Zernio post ID or platform-specific post ID. Zernio IDs are auto-resolved. LinkedIn third-party posts accept full activity URN or numeric ID.
schema: { type: string }
- name: accountId
in: query
required: true
schema: { type: string }
- name: subreddit
in: query
schema: { type: string }
description: (Reddit only) Subreddit name
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
description: Maximum number of comments to return
- name: cursor
in: query
schema: { type: string }
description: Pagination cursor
- name: commentId
in: query
schema: { type: string }
description: (Reddit only) Get replies to a specific comment
responses:
'200':
description: Comments for the post
content:
application/json:
schema:
type: object
properties:
status: { type: string }
comments:
type: array
items:
type: object
properties:
id: { type: string }
message: { type: string }
createdTime: { type: string, format: date-time }
from:
type: object
properties:
id: { type: string }
name: { type: string }
username: { type: string }
picture: { type: string, nullable: true }
isOwner: { type: boolean }
likeCount: { type: integer }
replyCount: { type: integer }
platform: { type: string, description: The platform this comment is from }
url:
type: string
nullable: true
description: Direct link to the comment on the platform (if available)
replies:
type: array
items: { type: object }
canReply: { type: boolean }
canDelete: { type: boolean }
canHide: { type: boolean, description: Whether this comment can be hidden (Facebook, Instagram, Threads) }
canLike: { type: boolean, description: Whether this comment can be liked (Facebook, Twitter/X, Bluesky, Reddit) }
isHidden: { type: boolean, description: Whether the comment is currently hidden }
isLiked: { type: boolean, description: Whether the current user has liked this comment }
likeUri: { type: string, nullable: true, description: Bluesky like URI for unliking }
cid: { type: string, nullable: true, description: Bluesky content identifier }
parentId: { type: string, nullable: true, description: Parent comment ID for nested replies }
rootUri: { type: string, nullable: true, description: Bluesky root post URI }
rootCid: { type: string, nullable: true, description: Bluesky root post CID }
pagination:
type: object
properties:
hasMore: { type: boolean }
cursor: { type: string, nullable: true }
meta:
type: object
properties:
platform: { type: string }
postId: { type: string }
accountId: { type: string }
subreddit: { type: string, nullable: true, description: (Reddit only) Subreddit name }
lastUpdated: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
post:
operationId: replyToInboxPost
summary: Reply to comment
description: Post a reply to a post or specific comment. Requires accountId in request body.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
description: Zernio post ID or platform-specific post ID. LinkedIn third-party posts accept full activity URN or numeric ID.
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, message]
properties:
accountId: { type: string }
message: { type: string }
commentId: { type: string, description: Reply to specific comment (optional) }
parentCid: { type: string, description: (Bluesky only) Parent content identifier }
rootUri: { type: string, description: (Bluesky only) Root post URI }
rootCid: { type: string, description: (Bluesky only) Root post CID }
responses:
'200':
description: Reply posted
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
data:
type: object
properties:
commentId: { type: string }
isReply: { type: boolean }
cid: { type: string, nullable: true, description: Bluesky CID }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
delete:
operationId: deleteInboxComment
summary: Delete comment
description: |
Delete a comment on a post. Supported by Facebook, Instagram, Bluesky, Reddit, YouTube, and LinkedIn.
Requires accountId and commentId query parameters.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
description: Zernio post ID or platform-specific post ID. LinkedIn third-party posts accept full activity URN or numeric ID.
schema: { type: string }
- name: accountId
in: query
required: true
schema: { type: string }
- name: commentId
in: query
required: true
schema: { type: string }
responses:
'200':
description: Comment deleted
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
data:
type: object
properties:
message: { type: string }
'400':
description: Platform rejected the operation (e.g., comment already deleted, insufficient permissions on the video)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/comments/{postId}/{commentId}/hide:
post:
operationId: hideInboxComment
summary: Hide comment
description: |
Hide a comment on a post. Supported by Facebook, Instagram, Threads, and X/Twitter.
Hidden comments are only visible to the commenter and page admin.
For X/Twitter, the reply must belong to a conversation started by the authenticated user.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
schema: { type: string }
- name: commentId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string, description: The social account ID }
responses:
'200':
description: Comment hidden
content:
application/json:
schema:
type: object
properties:
status: { type: string }
commentId: { type: string }
hidden: { type: boolean }
platform: { type: string }
'400':
description: Platform does not support hiding comments
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
delete:
operationId: unhideInboxComment
summary: Unhide comment
description: |
Unhide a previously hidden comment. Supported by Facebook, Instagram, Threads, and X/Twitter.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
schema: { type: string }
- name: commentId
in: path
required: true
schema: { type: string }
- name: accountId
in: query
required: true
schema: { type: string }
responses:
'200':
description: Comment unhidden
content:
application/json:
schema:
type: object
properties:
status: { type: string }
commentId: { type: string }
hidden: { type: boolean }
platform: { type: string }
'400':
description: Platform does not support unhiding comments
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/comments/{postId}/{commentId}/like:
post:
operationId: likeInboxComment
summary: Like comment
description: |
Like or upvote a comment on a post. Supported platforms: Facebook, Twitter/X, Bluesky, Reddit.
For Bluesky, the cid (content identifier) is required in the request body.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
schema: { type: string }
- name: commentId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string, description: The social account ID }
cid: { type: string, description: (Bluesky only) Content identifier for the comment }
responses:
'200':
description: Comment liked
content:
application/json:
schema:
type: object
properties:
status: { type: string }
commentId: { type: string }
liked: { type: boolean }
likeUri: { type: string, description: (Bluesky only) URI to use for unliking }
platform: { type: string }
'400':
description: Platform does not support liking comments
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
delete:
operationId: unlikeInboxComment
summary: Unlike comment
description: |
Remove a like from a comment. Supported platforms: Facebook, Twitter/X, Bluesky, Reddit.
For Bluesky, the likeUri query parameter is required.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
schema: { type: string }
- name: commentId
in: path
required: true
schema: { type: string }
- name: accountId
in: query
required: true
schema: { type: string }
- name: likeUri
in: query
schema: { type: string }
description: (Bluesky only) The like URI returned when liking
responses:
'200':
description: Comment unliked
content:
application/json:
schema:
type: object
properties:
status: { type: string }
commentId: { type: string }
liked: { type: boolean }
platform: { type: string }
'400':
description: Platform does not support unliking comments
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/comments/{postId}/{commentId}/private-reply:
post:
operationId: sendPrivateReplyToComment
summary: Send private reply
description: Send a private message to the author of a comment. Supported on Instagram and Facebook only. One reply per comment, must be sent within 7 days, text only.
tags: [Comments]
security: [{ bearerAuth: [] }]
parameters:
- name: postId
in: path
required: true
schema: { type: string }
description: The media/post ID (Instagram media ID or Facebook post ID)
- name: commentId
in: path
required: true
schema: { type: string }
description: The comment ID to send a private reply to
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, message]
properties:
accountId:
type: string
description: The social account ID (Instagram or Facebook)
message:
type: string
description: The message text to send as a private DM
example:
accountId: "507f1f77bcf86cd799439011"
message: "Hi! Thanks for your comment. I wanted to reach out privately to help with your question."
responses:
'200':
description: Private reply sent successfully
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: success
messageId:
type: string
description: The ID of the sent message
commentId:
type: string
description: The comment ID that was replied to
platform:
type: string
enum: [instagram, facebook]
example: instagram
'400':
description: Bad request
content:
application/json:
schema:
type: object
properties:
error:
type: string
code:
type: string
enum: [PLATFORM_LIMITATION]
examples:
platformNotSupported:
summary: Platform not supported
value:
error: "Private replies to comments are only supported on Instagram and Facebook."
code: "PLATFORM_LIMITATION"
alreadyReplied:
summary: Already sent a private reply
value:
error: "A private reply has already been sent to this comment, or the 7-day reply window has expired. Only one private reply per comment is allowed within 7 days."
commentTooOld:
summary: Comment older than 7 days
value:
error: "The comment is older than 7 days. Private replies can only be sent within 7 days of the comment being posted."
missingMessage:
summary: Missing message
value:
error: "message is required and must be a non-empty string"
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
'404':
description: Account not found
/v1/twitter/retweet:
post:
operationId: retweetPost
summary: Retweet a post
description: |
Retweet (repost) a tweet by ID.
Rate limit: 50 requests per 15-min window. Shares the 300/3hr creation limit with tweet creation.
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, tweetId]
properties:
accountId: { type: string, description: The social account ID }
tweetId: { type: string, description: The ID of the tweet to retweet }
responses:
'200':
description: Tweet retweeted
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
tweetId: { type: string }
retweeted: { type: boolean }
platform: { type: string, example: twitter }
'400': { description: Bad request or platform limitation }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
delete:
operationId: undoRetweet
summary: Undo retweet
description: |
Undo a retweet (un-repost a tweet).
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: query
required: true
schema: { type: string }
- name: tweetId
in: query
required: true
schema: { type: string }
description: The ID of the original tweet to un-retweet
responses:
'200':
description: Retweet undone
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
tweetId: { type: string }
retweeted: { type: boolean, example: false }
platform: { type: string, example: twitter }
'400': { description: Bad request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/twitter/bookmark:
post:
operationId: bookmarkPost
summary: Bookmark a tweet
description: |
Bookmark a tweet by ID.
Requires the bookmark.write OAuth scope.
Rate limit: 50 requests per 15-min window.
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, tweetId]
properties:
accountId: { type: string, description: The social account ID }
tweetId: { type: string, description: The ID of the tweet to bookmark }
responses:
'200':
description: Tweet bookmarked
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
tweetId: { type: string }
bookmarked: { type: boolean }
platform: { type: string, example: twitter }
'400': { description: Bad request or platform limitation }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
delete:
operationId: removeBookmark
summary: Remove bookmark
description: |
Remove a bookmark from a tweet.
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: query
required: true
schema: { type: string }
- name: tweetId
in: query
required: true
schema: { type: string }
description: The ID of the tweet to unbookmark
responses:
'200':
description: Bookmark removed
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
tweetId: { type: string }
bookmarked: { type: boolean, example: false }
platform: { type: string, example: twitter }
'400': { description: Bad request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/twitter/follow:
post:
operationId: followUser
summary: Follow a user
description: |
Follow a user on X/Twitter.
Requires the follows.write OAuth scope.
For protected accounts, a follow request is sent instead (pending_follow will be true).
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, targetUserId]
properties:
accountId: { type: string, description: The social account ID }
targetUserId: { type: string, description: The Twitter ID of the user to follow }
responses:
'200':
description: User followed or follow request sent
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
targetUserId: { type: string }
following: { type: boolean }
pending_follow: { type: boolean, description: True if the target account is protected and a follow request was sent }
platform: { type: string, example: twitter }
'400': { description: Bad request or platform limitation }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
delete:
operationId: unfollowUser
summary: Unfollow a user
description: |
Unfollow a user on X/Twitter.
tags: [Twitter Engagement]
security: [{ bearerAuth: [] }]
parameters:
- name: accountId
in: query
required: true
schema: { type: string }
- name: targetUserId
in: query
required: true
schema: { type: string }
description: The Twitter ID of the user to unfollow
responses:
'200':
description: User unfollowed
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: success }
targetUserId: { type: string }
following: { type: boolean, example: false }
platform: { type: string, example: twitter }
'400': { description: Bad request }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Account not found }
/v1/inbox/reviews:
get:
operationId: listInboxReviews
summary: List reviews
description: |
Fetch reviews from all connected Facebook Pages and Google Business accounts. Aggregates data with filtering and sorting options.
Supported platforms: Facebook, Google Business.
tags: [Reviews]
security: [{ bearerAuth: [] }]
parameters:
- name: profileId
in: query
schema: { type: string }
- name: platform
in: query
schema: { type: string, enum: [facebook, googlebusiness] }
- name: minRating
in: query
schema: { type: integer, minimum: 1, maximum: 5 }
- name: maxRating
in: query
schema: { type: integer, minimum: 1, maximum: 5 }
- name: hasReply
in: query
schema: { type: boolean }
description: Filter by reply status
- name: sortBy
in: query
schema: { type: string, enum: [date, rating], default: date }
- name: sortOrder
in: query
schema: { type: string, enum: [asc, desc], default: desc }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 50, default: 25 }
- name: cursor
in: query
schema: { type: string }
- name: accountId
in: query
schema: { type: string }
description: Filter by specific social account ID
responses:
'200':
description: Aggregated reviews
content:
application/json:
schema:
type: object
properties:
status: { type: string }
data:
type: array
items:
type: object
properties:
id: { type: string }
platform: { type: string }
accountId: { type: string }
accountUsername: { type: string }
reviewer:
type: object
properties:
id: { type: string, nullable: true }
name: { type: string }
profileImage: { type: string, nullable: true }
rating: { type: integer }
text: { type: string }
created: { type: string, format: date-time }
hasReply: { type: boolean }
reply:
type: object
nullable: true
properties:
id: { type: string }
text: { type: string }
created: { type: string, format: date-time }
reviewUrl: { type: string, nullable: true }
pagination:
type: object
properties:
hasMore: { type: boolean }
nextCursor: { type: string, nullable: true }
meta:
type: object
properties:
accountsQueried: { type: integer }
accountsFailed: { type: integer }
failedAccounts:
type: array
items:
type: object
properties:
accountId: { type: string }
accountUsername: { type: string, nullable: true }
platform: { type: string }
error: { type: string }
code: { type: string, nullable: true, description: Error code if available }
retryAfter: { type: integer, nullable: true, description: Seconds to wait before retry (rate limits) }
lastUpdated: { type: string, format: date-time }
summary:
type: object
properties:
totalReviews: { type: integer }
averageRating: { type: number, nullable: true }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/inbox/reviews/{reviewId}/reply:
post:
operationId: replyToInboxReview
summary: Reply to review
description: Post a reply to a review. Requires accountId in request body.
tags: [Reviews]
security: [{ bearerAuth: [] }]
parameters:
- name: reviewId
in: path
required: true
schema: { type: string }
description: Review ID (URL-encoded for Google Business)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, message]
properties:
accountId: { type: string }
message: { type: string }
responses:
'200':
description: Reply posted
content:
application/json:
schema:
type: object
properties:
status: { type: string }
reply:
type: object
properties:
id: { type: string }
text: { type: string }
created: { type: string, format: date-time }
platform: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
delete:
operationId: deleteInboxReviewReply
summary: Delete review reply
description: Delete a reply to a review (Google Business only). Requires accountId in request body.
tags: [Reviews]
security: [{ bearerAuth: [] }]
parameters:
- name: reviewId
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId]
properties:
accountId: { type: string }
responses:
'200':
description: Reply deleted
content:
application/json:
schema:
type: object
properties:
status: { type: string }
message: { type: string }
platform: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'403':
description: Inbox addon required
/v1/whatsapp/bulk:
post:
operationId: sendWhatsAppBulk
tags: [WhatsApp]
summary: Bulk send template messages
description: |
Send a template message to multiple recipients in a single request. Maximum 100 recipients per request.
Only template messages are supported for bulk sending (not free-form text).
Each recipient can have optional per-recipient template variables for personalization.
Returns detailed results for each recipient.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- recipients
- template
properties:
accountId:
type: string
description: WhatsApp social account ID
recipients:
type: array
maxItems: 100
description: List of recipients (max 100)
items:
type: object
required:
- phone
properties:
phone:
type: string
description: Recipient phone number in E.164 format
variables:
type: object
additionalProperties: { type: string }
description: Per-recipient template variables keyed by index (e.g., "1", "2")
template:
type: object
required:
- name
- language
properties:
name:
type: string
description: Template name
language:
type: string
description: Template language code
components:
type: array
description: Base template components
items:
type: object
example:
accountId: "507f1f77bcf86cd799439011"
recipients:
- phone: "+1234567890"
variables: { "1": "John" }
- phone: "+0987654321"
variables: { "1": "Jane" }
template:
name: "welcome_message"
language: "en_US"
responses:
'200':
description: Bulk send completed
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
summary:
type: object
properties:
total: { type: integer }
sent: { type: integer }
failed: { type: integer }
results:
type: array
items:
type: object
properties:
phone: { type: string }
success: { type: boolean }
messageId: { type: string }
error: { type: string }
'400': { description: Validation error (missing fields, too many recipients, etc.) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/contacts:
get:
operationId: getWhatsAppContacts
tags: [WhatsApp]
summary: List contacts
description: |
List WhatsApp contacts for an account. Supports filtering by tags, groups, opt-in status,
and text search. Returns contacts sorted by name with available filter options.
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
- name: search
in: query
required: false
description: Search contacts by name, phone, email, or company
schema:
type: string
- name: tag
in: query
required: false
description: Filter by tag
schema:
type: string
- name: group
in: query
required: false
description: Filter by group
schema:
type: string
- name: optedIn
in: query
required: false
description: Filter by opt-in status
schema:
type: string
enum: ["true", "false"]
- name: limit
in: query
required: false
description: Maximum results (default 50)
schema:
type: integer
default: 50
- name: skip
in: query
required: false
description: Offset for pagination
schema:
type: integer
default: 0
responses:
'200':
description: Contacts retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
contacts:
type: array
items:
type: object
properties:
id: { type: string }
phone: { type: string }
waId: { type: string }
name: { type: string }
email: { type: string }
company: { type: string }
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
isOptedIn: { type: boolean }
lastMessageSentAt: { type: string, format: date-time }
lastMessageReceivedAt: { type: string, format: date-time }
messagesSentCount: { type: integer }
messagesReceivedCount: { type: integer }
customFields: { type: object }
notes: { type: string }
createdAt: { type: string, format: date-time }
filters:
type: object
properties:
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
pagination:
type: object
properties:
total: { type: integer }
limit: { type: integer }
skip: { type: integer }
hasMore: { type: boolean }
'400': { description: accountId is required }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: createWhatsAppContact
tags: [WhatsApp]
summary: Create contact
description: |
Create a new WhatsApp contact. Phone number must be unique per account
and in E.164 format (e.g., +1234567890).
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- phone
- name
properties:
accountId:
type: string
description: WhatsApp social account ID
phone:
type: string
description: Phone number in E.164 format
name:
type: string
description: Contact name
email:
type: string
description: Contact email
company:
type: string
description: Company name
tags:
type: array
items: { type: string }
description: Tags for categorization
groups:
type: array
items: { type: string }
description: Groups the contact belongs to
isOptedIn:
type: boolean
default: true
description: Whether the contact has opted in to receive messages
customFields:
type: object
additionalProperties: { type: string }
description: Custom key-value fields
notes:
type: string
description: Notes about the contact
example:
accountId: "507f1f77bcf86cd799439011"
phone: "+1234567890"
name: "John Doe"
email: "john@example.com"
tags: ["vip", "newsletter"]
groups: ["customers"]
responses:
'200':
description: Contact created successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
contact:
type: object
properties:
id: { type: string }
phone: { type: string }
name: { type: string }
email: { type: string }
company: { type: string }
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
isOptedIn: { type: boolean }
createdAt: { type: string, format: date-time }
'400': { description: Validation error (missing fields, invalid phone number) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
'409': { description: Contact with this phone number already exists }
/v1/whatsapp/contacts/{contactId}:
get:
operationId: getWhatsAppContact
tags: [WhatsApp]
summary: Get contact
description: Retrieve a single WhatsApp contact by ID with full details.
security:
- bearerAuth: []
parameters:
- name: contactId
in: path
required: true
description: Contact ID
schema:
type: string
responses:
'200':
description: Contact retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
contact:
type: object
properties:
id: { type: string }
phone: { type: string }
waId: { type: string }
name: { type: string }
email: { type: string }
company: { type: string }
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
isOptedIn: { type: boolean }
optInDate: { type: string, format: date-time }
optOutDate: { type: string, format: date-time }
isBlocked: { type: boolean }
lastMessageSentAt: { type: string, format: date-time }
lastMessageReceivedAt: { type: string, format: date-time }
messagesSentCount: { type: integer }
messagesReceivedCount: { type: integer }
customFields: { type: object }
notes: { type: string }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: updateWhatsAppContact
tags: [WhatsApp]
summary: Update contact
description: |
Update an existing WhatsApp contact. All fields are optional; only provided fields will be updated.
Custom fields are merged with existing values. Set a custom field to null to remove it.
security:
- bearerAuth: []
parameters:
- name: contactId
in: path
required: true
description: Contact ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Contact name
email:
type: string
description: Contact email
company:
type: string
description: Company name
tags:
type: array
items: { type: string }
description: Tags (replaces existing)
groups:
type: array
items: { type: string }
description: Groups (replaces existing)
isOptedIn:
type: boolean
description: Opt-in status (changes are timestamped)
isBlocked:
type: boolean
description: Block status
customFields:
type: object
additionalProperties: { type: string, nullable: true }
description: Custom fields to merge (set value to null to remove a field)
notes:
type: string
description: Notes about the contact
example:
name: "John Doe Updated"
tags: ["vip", "premium"]
customFields: { "plan": "enterprise", "remove_me": null }
responses:
'200':
description: Contact updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
contact:
type: object
properties:
id: { type: string }
phone: { type: string }
name: { type: string }
email: { type: string }
company: { type: string }
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
isOptedIn: { type: boolean }
isBlocked: { type: boolean }
customFields: { type: object }
notes: { type: string }
updatedAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteWhatsAppContact
tags: [WhatsApp]
summary: Delete contact
description: Permanently delete a WhatsApp contact.
security:
- bearerAuth: []
parameters:
- name: contactId
in: path
required: true
description: Contact ID
schema:
type: string
responses:
'200':
description: Contact deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Contact deleted successfully"
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/contacts/import:
post:
operationId: importWhatsAppContacts
tags: [WhatsApp]
summary: Bulk import contacts
description: |
Import up to 1000 contacts at once. Each contact requires a phone number and name.
Duplicates are skipped by default. Supports default tags and groups applied to all imported contacts.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- contacts
properties:
accountId:
type: string
description: WhatsApp social account ID
contacts:
type: array
maxItems: 1000
description: Contacts to import (max 1000)
items:
type: object
required:
- phone
- name
properties:
phone:
type: string
description: Phone number in E.164 format
name:
type: string
description: Contact name
email:
type: string
company:
type: string
tags:
type: array
items: { type: string }
groups:
type: array
items: { type: string }
customFields:
type: object
additionalProperties: { type: string }
notes:
type: string
defaultTags:
type: array
items: { type: string }
description: Tags applied to all imported contacts
defaultGroups:
type: array
items: { type: string }
description: Groups applied to all imported contacts
skipDuplicates:
type: boolean
default: true
description: Skip contacts with existing phone numbers
example:
accountId: "507f1f77bcf86cd799439011"
contacts:
- phone: "+1234567890"
name: "John Doe"
email: "john@example.com"
- phone: "+0987654321"
name: "Jane Smith"
defaultTags: ["imported"]
defaultGroups: ["new-leads"]
responses:
'200':
description: Import completed
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
summary:
type: object
properties:
total: { type: integer }
created: { type: integer }
skipped: { type: integer }
failed: { type: integer }
results:
type: array
items:
type: object
properties:
phone: { type: string }
name: { type: string }
success: { type: boolean }
contactId: { type: string }
error: { type: string }
'400': { description: Validation error (missing fields, too many contacts) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/contacts/bulk:
post:
operationId: bulkUpdateWhatsAppContacts
tags: [WhatsApp]
summary: Bulk update contacts
description: |
Perform bulk operations on multiple contacts (max 500 per request). Supported actions:
addTags, removeTags, addGroups, removeGroups, optIn, optOut, block, unblock.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- action
- contactIds
properties:
action:
type: string
enum: [addTags, removeTags, addGroups, removeGroups, optIn, optOut, block, unblock]
description: Bulk action to perform
contactIds:
type: array
maxItems: 500
items: { type: string }
description: Contact IDs to update (max 500)
tags:
type: array
items: { type: string }
description: Tags to add or remove (required for addTags/removeTags)
groups:
type: array
items: { type: string }
description: Groups to add or remove (required for addGroups/removeGroups)
examples:
addTags:
summary: Add tags to contacts
value:
action: "addTags"
contactIds: ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
tags: ["vip", "priority"]
optOut:
summary: Opt out contacts
value:
action: "optOut"
contactIds: ["507f1f77bcf86cd799439011"]
responses:
'200':
description: Bulk update completed
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
action: { type: string }
modified: { type: integer, description: Number of contacts modified }
matched: { type: integer, description: Number of contacts matched }
'400': { description: Validation error (invalid action, missing required fields) }
'401': { $ref: '#/components/responses/Unauthorized' }
delete:
operationId: bulkDeleteWhatsAppContacts
tags: [WhatsApp]
summary: Bulk delete contacts
description: Permanently delete multiple contacts at once (max 500 per request).
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- contactIds
properties:
contactIds:
type: array
maxItems: 500
items: { type: string }
description: Contact IDs to delete (max 500)
example:
contactIds: ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
responses:
'200':
description: Bulk delete completed
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
deleted: { type: integer, description: Number of contacts deleted }
'400': { description: contactIds array is required }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/whatsapp/groups:
get:
operationId: getWhatsAppGroups
tags: [WhatsApp]
summary: List contact groups
description: |
List all contact groups for a WhatsApp account with contact counts.
Groups are derived from the groups field on contacts, not stored as separate documents.
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Groups retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
groups:
type: array
items:
type: object
properties:
name: { type: string }
totalCount: { type: integer, description: Total contacts in this group }
optedInCount: { type: integer, description: Opted-in contacts in this group }
summary:
type: object
properties:
totalContacts: { type: integer }
optedInContacts: { type: integer }
groupCount: { type: integer }
'400': { description: accountId is required }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: renameWhatsAppGroup
tags: [WhatsApp]
summary: Rename group
description: Rename a contact group. This updates the group name on all contacts that belong to the group.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- oldName
- newName
properties:
accountId:
type: string
description: WhatsApp social account ID
oldName:
type: string
description: Current group name
newName:
type: string
description: New group name
example:
accountId: "507f1f77bcf86cd799439011"
oldName: "customers"
newName: "active-customers"
responses:
'200':
description: Group renamed successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
modified: { type: integer, description: Number of contacts updated }
'400': { description: Validation error (missing fields, same name) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
delete:
operationId: deleteWhatsAppGroup
tags: [WhatsApp]
summary: Delete group
description: Delete a contact group. This removes the group from all contacts but does not delete the contacts themselves.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- groupName
properties:
accountId:
type: string
description: WhatsApp social account ID
groupName:
type: string
description: Group name to delete
example:
accountId: "507f1f77bcf86cd799439011"
groupName: "old-leads"
responses:
'200':
description: Group deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
modified: { type: integer, description: Number of contacts updated }
'400': { description: Validation error (missing fields) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/templates:
get:
operationId: getWhatsAppTemplates
tags: [WhatsApp]
summary: List templates
description: |
List all message templates for the WhatsApp Business Account (WABA) associated with the given account.
Templates are fetched directly from the WhatsApp Cloud API.
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Templates retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
templates:
type: array
items:
type: object
properties:
id: { type: string, description: WhatsApp template ID }
name: { type: string }
status: { type: string, enum: [APPROVED, PENDING, REJECTED] }
category: { type: string, enum: [AUTHENTICATION, MARKETING, UTILITY] }
language: { type: string }
components:
type: array
items:
type: object
'400': { description: accountId is required or WABA ID not found }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: createWhatsAppTemplate
tags: [WhatsApp]
summary: Create template
description: |
Create a new message template. Supports two modes:
**Custom template:** Provide `components` with your own content. Submitted to Meta for review (can take up to 24h).
**Library template:** Provide `library_template_name` instead of `components` to use a pre-built template
from Meta's template library. Library templates are **pre-approved** (no review wait). You can optionally
customize parameters and buttons via `library_template_body_inputs` and `library_template_button_inputs`.
Browse available library templates at: https://business.facebook.com/wa/manage/message-templates/
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- name
- category
- language
properties:
accountId:
type: string
description: WhatsApp social account ID
name:
type: string
pattern: "^[a-z][a-z0-9_]*$"
description: Template name (lowercase, letters/numbers/underscores, must start with a letter)
category:
type: string
enum: [AUTHENTICATION, MARKETING, UTILITY]
description: Template category
language:
type: string
description: Template language code (e.g., en_US)
components:
type: array
description: "Template components (header, body, footer, buttons). Required for custom templates, omit when using library_template_name."
items:
type: object
library_template_name:
type: string
description: |
Name of a pre-built template from Meta's template library (e.g., "appointment_reminder",
"auto_pay_reminder_1", "address_update"). When provided, the template is pre-approved
by Meta with no review wait. Omit `components` when using this field.
library_template_body_inputs:
type: object
description: |
Optional body customizations for library templates. Available options depend on the
template (e.g., add_contact_number, add_learn_more_link, add_security_recommendation,
add_track_package_link, code_expiration_minutes).
library_template_button_inputs:
type: array
description: |
Optional button customizations for library templates. Each item specifies button type
and configuration (e.g., URL, phone number, quick reply).
items:
type: object
properties:
type:
type: string
enum: [QUICK_REPLY, URL, PHONE_NUMBER]
url:
type: object
properties:
base_url: { type: string }
phone_number:
type: string
examples:
custom:
summary: Custom template (requires review)
value:
accountId: "507f1f77bcf86cd799439011"
name: "order_confirmation"
category: "UTILITY"
language: "en_US"
components:
- type: "body"
text: "Your order {{1}} has been confirmed. Expected delivery: {{2}}"
library:
summary: Library template (pre-approved, no review)
value:
accountId: "507f1f77bcf86cd799439011"
name: "my_appointment_reminder"
category: "UTILITY"
language: "en_US"
library_template_name: "appointment_reminder"
library_template_button_inputs:
- type: "URL"
url:
base_url: "https://myapp.com/appointments/{{1}}"
responses:
'200':
description: Template created (pre-approved for library templates, pending review for custom)
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
template:
type: object
properties:
id: { type: string }
name: { type: string }
status: { type: string, description: "APPROVED for library templates, PENDING for custom" }
category: { type: string }
language: { type: string }
'400': { description: Validation error (invalid name format, missing fields, invalid category) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/templates/{templateName}:
get:
operationId: getWhatsAppTemplate
tags: [WhatsApp]
summary: Get template
description: Retrieve a single message template by name.
security:
- bearerAuth: []
parameters:
- name: templateName
in: path
required: true
description: Template name
schema:
type: string
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Template retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
template:
type: object
properties:
id: { type: string }
name: { type: string }
status: { type: string }
category: { type: string }
language: { type: string }
components:
type: array
items:
type: object
'400': { description: accountId is required }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
operationId: updateWhatsAppTemplate
tags: [WhatsApp]
summary: Update template
description: |
Update a message template's components. Only certain fields can be updated depending on
the template's current approval state. Approved templates can only have components updated.
security:
- bearerAuth: []
parameters:
- name: templateName
in: path
required: true
description: Template name
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- components
properties:
accountId:
type: string
description: WhatsApp social account ID
components:
type: array
description: Updated template components
items:
type: object
example:
accountId: "507f1f77bcf86cd799439011"
components:
- type: "body"
text: "Updated: Your order {{1}} is confirmed. Delivery by {{2}}"
responses:
'200':
description: Template updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
template:
type: object
properties:
id: { type: string }
name: { type: string }
status: { type: string }
'400': { description: Validation error (missing fields) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteWhatsAppTemplate
tags: [WhatsApp]
summary: Delete template
description: Permanently delete a message template by name.
security:
- bearerAuth: []
parameters:
- name: templateName
in: path
required: true
description: Template name
schema:
type: string
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Template deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Template \"order_confirmation\" deleted successfully"
'400': { description: accountId or template name is required }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/broadcasts:
get:
operationId: getWhatsAppBroadcasts
tags: [WhatsApp]
summary: List broadcasts
description: |
List all WhatsApp broadcasts for an account. Returns broadcasts sorted by creation date
(newest first) without the full recipients list for performance.
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
- name: status
in: query
required: false
description: Filter by broadcast status
schema:
type: string
enum: [draft, scheduled, sending, completed, failed, cancelled]
- name: limit
in: query
required: false
description: Maximum results (default 50)
schema:
type: integer
default: 50
- name: skip
in: query
required: false
description: Offset for pagination
schema:
type: integer
default: 0
responses:
'200':
description: Broadcasts retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
broadcasts:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
description: { type: string }
template:
type: object
properties:
name: { type: string }
language: { type: string }
status: { type: string, enum: [draft, scheduled, sending, completed, failed, cancelled] }
recipientCount: { type: integer }
scheduledAt: { type: string, format: date-time }
startedAt: { type: string, format: date-time }
completedAt: { type: string, format: date-time }
sentCount: { type: integer }
deliveredCount: { type: integer }
readCount: { type: integer }
failedCount: { type: integer }
createdAt: { type: string, format: date-time }
pagination:
type: object
properties:
total: { type: integer }
limit: { type: integer }
skip: { type: integer }
hasMore: { type: boolean }
'400': { description: accountId is required }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: createWhatsAppBroadcast
tags: [WhatsApp]
summary: Create broadcast
description: |
Create a new draft broadcast. Optionally include initial recipients.
After creation, add recipients and then send or schedule the broadcast.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
- name
- template
properties:
accountId:
type: string
description: WhatsApp social account ID
name:
type: string
description: Broadcast name
description:
type: string
description: Broadcast description
template:
type: object
required:
- name
- language
properties:
name:
type: string
description: Template name
language:
type: string
description: Template language code
components:
type: array
description: Base template components
items:
type: object
recipients:
type: array
description: Initial recipients (optional)
items:
type: object
required:
- phone
properties:
phone:
type: string
description: Phone number in E.164 format
name:
type: string
variables:
type: object
additionalProperties: { type: string }
description: Per-recipient template variables
example:
accountId: "507f1f77bcf86cd799439011"
name: "Weekly Newsletter"
description: "Weekly product updates"
template:
name: "weekly_update"
language: "en_US"
recipients:
- phone: "+1234567890"
name: "John"
variables: { "1": "John" }
responses:
'200':
description: Broadcast created as draft
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
broadcast:
type: object
properties:
id: { type: string }
name: { type: string }
description: { type: string }
template: { type: object }
status: { type: string, description: Always "draft" for new broadcasts }
recipientCount: { type: integer }
createdAt: { type: string, format: date-time }
'400': { description: Validation error (missing name, template, etc.) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/broadcasts/{broadcastId}:
get:
operationId: getWhatsAppBroadcast
tags: [WhatsApp]
summary: Get broadcast
description: Retrieve detailed information about a single broadcast including delivery statistics.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
responses:
'200':
description: Broadcast retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
broadcast:
type: object
properties:
id: { type: string }
name: { type: string }
description: { type: string }
template: { type: object }
status: { type: string, enum: [draft, scheduled, sending, completed, failed, cancelled] }
recipientCount: { type: integer }
scheduledAt: { type: string, format: date-time }
startedAt: { type: string, format: date-time }
completedAt: { type: string, format: date-time }
sentCount: { type: integer }
deliveredCount: { type: integer }
readCount: { type: integer }
failedCount: { type: integer }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: deleteWhatsAppBroadcast
tags: [WhatsApp]
summary: Delete broadcast
description: Delete a broadcast. Only draft or cancelled broadcasts can be deleted.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
responses:
'200':
description: Broadcast deleted successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Broadcast deleted successfully"
'400': { description: Can only delete draft or cancelled broadcasts }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/broadcasts/{broadcastId}/send:
post:
operationId: sendWhatsAppBroadcast
tags: [WhatsApp]
summary: Send broadcast
description: |
Start sending a broadcast immediately. The broadcast must be in draft or scheduled status
and have at least one recipient. Messages are sent sequentially with rate limiting.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
responses:
'200':
description: Broadcast send completed
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
status: { type: string, enum: [completed, failed], description: Final broadcast status }
sent: { type: integer, description: Number of messages sent successfully }
failed: { type: integer, description: Number of messages that failed }
total: { type: integer, description: Total recipient count }
example:
success: true
status: "completed"
sent: 95
failed: 5
total: 100
'400': { description: Invalid broadcast status or no recipients }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/broadcasts/{broadcastId}/schedule:
post:
operationId: scheduleWhatsAppBroadcast
tags: [WhatsApp]
summary: Schedule broadcast
description: |
Schedule a draft broadcast for future sending. The scheduled time must be in the future
and no more than 30 days in advance. The broadcast must be in draft status and have recipients.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- scheduledAt
properties:
scheduledAt:
type: string
format: date-time
description: ISO 8601 date-time for sending (must be in the future, max 30 days)
example:
scheduledAt: "2026-03-15T10:00:00Z"
responses:
'200':
description: Broadcast scheduled successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
broadcast:
type: object
properties:
id: { type: string }
status: { type: string, description: "\"scheduled\"" }
scheduledAt: { type: string, format: date-time }
'400': { description: Invalid schedule time or broadcast not in draft status }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: cancelWhatsAppBroadcastSchedule
tags: [WhatsApp]
summary: Cancel scheduled broadcast
description: |
Cancel a scheduled broadcast and return it to draft status. Only broadcasts in
scheduled status can be cancelled.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
responses:
'200':
description: Schedule cancelled, broadcast returned to draft
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
broadcast:
type: object
properties:
id: { type: string }
status: { type: string, description: "\"draft\"" }
message: { type: string }
example:
success: true
broadcast:
id: "507f1f77bcf86cd799439011"
status: "draft"
message: "Broadcast returned to draft status"
'400': { description: Broadcast is not scheduled }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/broadcasts/{broadcastId}/recipients:
get:
operationId: getWhatsAppBroadcastRecipients
tags: [WhatsApp]
summary: List recipients
description: |
List recipients of a broadcast with their delivery status. Supports filtering
by delivery status and pagination.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
- name: status
in: query
required: false
description: Filter by recipient delivery status
schema:
type: string
enum: [pending, sent, delivered, read, failed]
- name: limit
in: query
required: false
description: Maximum results (default 100)
schema:
type: integer
default: 100
- name: skip
in: query
required: false
description: Offset for pagination
schema:
type: integer
default: 0
responses:
'200':
description: Recipients retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
recipients:
type: array
items:
type: object
properties:
phone: { type: string }
name: { type: string }
variables: { type: object }
status: { type: string, enum: [pending, sent, delivered, read, failed] }
messageId: { type: string }
error: { type: string }
sentAt: { type: string, format: date-time }
deliveredAt: { type: string, format: date-time }
readAt: { type: string, format: date-time }
pagination:
type: object
properties:
total: { type: integer }
limit: { type: integer }
skip: { type: integer }
hasMore: { type: boolean }
summary:
type: object
properties:
total: { type: integer }
pending: { type: integer }
sent: { type: integer }
delivered: { type: integer }
read: { type: integer }
failed: { type: integer }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
operationId: addWhatsAppBroadcastRecipients
tags: [WhatsApp]
summary: Add recipients
description: |
Add recipients to a draft broadcast. Maximum 1000 recipients per request.
Duplicate phone numbers are automatically skipped.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- recipients
properties:
recipients:
type: array
maxItems: 1000
description: Recipients to add (max 1000)
items:
type: object
required:
- phone
properties:
phone:
type: string
description: Phone number in E.164 format
name:
type: string
variables:
type: object
additionalProperties: { type: string }
example:
recipients:
- phone: "+1234567890"
name: "John"
variables: { "1": "John" }
- phone: "+0987654321"
name: "Jane"
responses:
'200':
description: Recipients added successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
added: { type: integer, description: Number of new recipients added }
duplicates: { type: integer, description: Number of duplicate phone numbers skipped }
totalRecipients: { type: integer, description: Total recipient count after addition }
'400': { description: Validation error or broadcast not in draft status }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: removeWhatsAppBroadcastRecipients
tags: [WhatsApp]
summary: Remove recipients
description: Remove recipients from a draft broadcast by phone number.
security:
- bearerAuth: []
parameters:
- name: broadcastId
in: path
required: true
description: Broadcast ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- phones
properties:
phones:
type: array
items: { type: string }
description: Phone numbers to remove
example:
phones: ["+1234567890", "+0987654321"]
responses:
'200':
description: Recipients removed successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
removed: { type: integer, description: Number of recipients removed }
totalRecipients: { type: integer, description: Remaining recipient count }
'400': { description: Validation error or broadcast not in draft status }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/whatsapp/business-profile:
get:
operationId: getWhatsAppBusinessProfile
tags: [WhatsApp]
summary: Get business profile
description: Retrieve the WhatsApp Business profile for the account (about, address, description, email, websites, etc.).
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Business profile retrieved successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
businessProfile:
type: object
properties:
about: { type: string, description: Short description (max 139 chars) }
address: { type: string }
description: { type: string, description: Full description (max 512 chars) }
email: { type: string }
profilePictureUrl: { type: string, format: uri }
websites:
type: array
items: { type: string }
maxItems: 2
vertical: { type: string, description: Business category }
'400': { description: accountId is required or phone number ID not found }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: updateWhatsAppBusinessProfile
tags: [WhatsApp]
summary: Update business profile
description: |
Update the WhatsApp Business profile. All fields are optional; only provided fields will be updated.
Constraints: about max 139 chars, description max 512 chars, max 2 websites.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- accountId
properties:
accountId:
type: string
description: WhatsApp social account ID
about:
type: string
maxLength: 139
description: Short business description (max 139 characters)
address:
type: string
description: Business address
description:
type: string
maxLength: 512
description: Full business description (max 512 characters)
email:
type: string
format: email
description: Business email
websites:
type: array
maxItems: 2
items: { type: string, format: uri }
description: Business websites (max 2)
vertical:
type: string
description: Business category (e.g., RETAIL, ENTERTAINMENT, etc.)
profilePictureHandle:
type: string
description: Handle from resumable upload for profile picture
example:
accountId: "507f1f77bcf86cd799439011"
about: "We help businesses grow"
description: "Premium business solutions for startups and enterprises"
email: "hello@example.com"
websites: ["https://example.com"]
responses:
'200':
description: Business profile updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
example:
success: true
message: "Business profile updated successfully"
'400': { description: Validation error (field too long, too many websites, etc.) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/business-profile/photo:
post:
operationId: uploadWhatsAppProfilePhoto
tags: [WhatsApp]
summary: Upload profile picture
description: |
Upload a new profile picture for the WhatsApp Business Profile.
Uses Meta's resumable upload API under the hood: creates an upload session,
uploads the image bytes, then updates the business profile with the resulting handle.
security:
- bearerAuth: []
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [accountId, file]
properties:
accountId:
type: string
description: WhatsApp social account ID
file:
type: string
format: binary
description: Image file (JPEG or PNG, max 5MB, recommended 640x640)
responses:
'200':
description: Profile picture updated successfully
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
'400': { description: Invalid file type, file too large, or missing parameters }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/business-profile/display-name:
get:
operationId: getWhatsAppDisplayName
tags: [WhatsApp]
summary: Get display name and review status
description: |
Fetch the current display name and its Meta review status for a WhatsApp Business account.
Display name changes require Meta approval and can take 1-3 business days.
security:
- bearerAuth: []
parameters:
- name: accountId
in: query
required: true
description: WhatsApp social account ID
schema:
type: string
responses:
'200':
description: Display name info retrieved
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
displayName:
type: object
properties:
name: { type: string, description: Current verified display name }
status:
type: string
enum: [APPROVED, PENDING_REVIEW, DECLINED, NONE]
description: Meta review status for the display name
phoneNumber: { type: string, description: Display phone number }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
post:
operationId: updateWhatsAppDisplayName
tags: [WhatsApp]
summary: Request display name change
description: |
Submit a display name change request for the WhatsApp Business account.
The new name must follow WhatsApp naming guidelines (3-512 characters, must represent your business).
Changes require Meta review and approval, which typically takes 1-3 business days.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [accountId, displayName]
properties:
accountId:
type: string
description: WhatsApp social account ID
displayName:
type: string
minLength: 3
maxLength: 512
description: New display name (must follow WhatsApp naming guidelines)
example:
accountId: "507f1f77bcf86cd799439011"
displayName: "My Business Name"
responses:
'200':
description: Display name change submitted for review
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
displayName:
type: object
properties:
name: { type: string }
status: { type: string, enum: [PENDING_REVIEW] }
'400': { description: Invalid display name (too short, too long, or missing) }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: WhatsApp account not found }
/v1/whatsapp/phone-numbers:
get:
operationId: getWhatsAppPhoneNumbers
tags: [WhatsApp Phone Numbers]
summary: List phone numbers
description: |
List all WhatsApp phone numbers purchased by the authenticated user.
By default, released numbers are excluded.
security:
- bearerAuth: []
parameters:
- name: status
in: query
required: false
description: Filter by status (by default excludes released numbers)
schema:
type: string
enum: [provisioning, active, suspended, releasing, released]
- name: profileId
in: query
required: false
description: Filter by profile
schema:
type: string
responses:
'200':
description: Phone numbers retrieved successfully
content:
application/json:
schema:
type: object
properties:
numbers:
type: array
items:
type: object
properties:
_id: { type: string }
phoneNumber: { type: string }
country: { type: string }
status: { type: string, enum: [pending_payment, provisioning, active, suspended, releasing, released] }
profileId: { type: object }
provisionedAt: { type: string, format: date-time }
metaPreverifiedId: { type: string }
metaVerificationStatus: { type: string }
createdAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/whatsapp/phone-numbers/purchase:
post:
operationId: purchaseWhatsAppPhoneNumber
tags: [WhatsApp Phone Numbers]
summary: Purchase phone number
description: |
Initiate purchasing a WhatsApp phone number. Payment-first flow: the user does not pick
a specific number. The system either creates a Stripe Checkout Session (first number)
or increments the existing subscription quantity and provisions inline (subsequent numbers).
Requires a paid plan. The maximum number of phone numbers is determined by the user's plan.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- profileId
properties:
profileId:
type: string
description: Profile to associate the number with
example:
profileId: "507f1f77bcf86cd799439011"
responses:
'200':
description: |
Either a checkout URL (first number) or the provisioned phone number (subsequent numbers).
content:
application/json:
schema:
oneOf:
- type: object
description: Checkout session created (first number)
properties:
message: { type: string }
checkoutUrl: { type: string, format: uri }
- type: object
description: Phone number provisioned inline (subsequent numbers)
properties:
message: { type: string }
phoneNumber:
type: object
properties:
id: { type: string }
phoneNumber: { type: string }
status: { type: string }
country: { type: string }
provisionedAt: { type: string, format: date-time }
metaPreverifiedId: { type: string }
metaVerificationStatus: { type: string }
'400': { description: Plan limit reached or profileId required }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { description: A paid plan is required }
/v1/whatsapp/phone-numbers/{phoneNumberId}:
get:
operationId: getWhatsAppPhoneNumber
tags: [WhatsApp Phone Numbers]
summary: Get phone number
description: |
Retrieve the current status of a purchased phone number. Used to poll for
Meta pre-verification completion after purchase.
security:
- bearerAuth: []
parameters:
- name: phoneNumberId
in: path
required: true
description: Phone number record ID
schema:
type: string
responses:
'200':
description: Phone number retrieved successfully
content:
application/json:
schema:
type: object
properties:
phoneNumber:
type: object
properties:
id: { type: string }
phoneNumber: { type: string }
status: { type: string, enum: [pending_payment, provisioning, active, suspended, releasing, released] }
country: { type: string }
metaPreverifiedId: { type: string }
metaVerificationStatus: { type: string }
provisionedAt: { type: string, format: date-time }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
operationId: releaseWhatsAppPhoneNumber
tags: [WhatsApp Phone Numbers]
summary: Release phone number
description: |
Release a purchased phone number. This will:
1. Disconnect any linked WhatsApp social account
2. Decrement the Stripe subscription quantity (or cancel if last number)
3. Release the number from Telnyx
4. Mark the number as released
security:
- bearerAuth: []
parameters:
- name: phoneNumberId
in: path
required: true
description: Phone number record ID
schema:
type: string
responses:
'200':
description: Phone number released successfully
content:
application/json:
schema:
type: object
properties:
message: { type: string }
phoneNumber:
type: object
properties:
id: { type: string }
phoneNumber: { type: string }
status: { type: string, description: "\"released\"" }
releasedAt: { type: string, format: date-time }
'400': { description: Phone number is already released or being released }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }