API Reference
OneSocial API
One REST API (and a parity MCP server at /api/mcp) to connect social accounts, schedule/publish posts, track analytics, manage a unified inbox (comments/mentions/reviews/DMs), maintain a contacts CRM with custom fields, run broadcasts/sequences/comment-to-DM automations, and run a paid Ads Manager (ad accounts, campaigns, ads, audiences, tracking tags) across Meta, Google, TikTok, LinkedIn, Pinterest, and X. All endpoints are versioned under /api/v1 (except the D1 media file server at /api/v1/media/file/{tenantId}/{filename}, which is a plain unauthenticated static-file GET), authenticate via an `sk_`-prefixed API key as an HTTP Bearer token, and respond with the envelope `{ data }` for single resources or `{ data, page, limit, total }` for lists. Errors are `{ error: { code, message } }` with a matching HTTP status. Scopes are a closed allowlist (see `@/lib/scopes`): a key needs the exact scope string for an action (or `*`/`admin` for full access) — an empty scopes array grants nothing. A handful of read endpoints (comments, mentions, reviews, broadcasts, sequences, automations lists) do not yet enforce their `:read` scope — any authenticated key can call them; this is called out per-operation below. Ad-network credentials are entirely optional server-side config (`<NETWORK>_ADS_*` env vars); every Ads Manager endpoint is fully exercisable with zero ad-network credentials configured — unconfigured live calls fail closed and the local DB state is persisted regardless (see the AdAccounts/Ads/AdCampaigns/AdAudiences/TrackingTags operations below for specifics).
Authentication
Every request carries your API key as an HTTP Bearer token. Mint one from the dashboard or POST /api/v1/api-keys — the raw sk_ value is shown exactly once. Responses are always { data } (or { data, page, limit, total } for lists); errors are { error: { code, message } }.
curl /api/v1/user \
-H "Authorization: Bearer sk_live_..."Prefer an AI agent? The same operations are exposed as MCP tools at /api/mcp (JSON-RPC over HTTP, same Bearer key) — REST and MCP call identical server-side logic, so behavior never diverges.
Profiles
5 endpointsPer-tenant containers for accounts and queue slots.
- get
/api/v1/profilesList profiles
- post
/api/v1/profilesCreate a profile
Requires scope `profiles:write`.
- get
/api/v1/profiles/{id}Get a profile
- patch
/api/v1/profiles/{id}Update a profile
Requires scope `profiles:write`.
- delete
/api/v1/profiles/{id}Delete a profile
Requires scope `profiles:write`. Cascades to the profile's accounts and queue slots.
Accounts
10 endpointsConnected social accounts and account groups.
- get
/api/v1/accountsList accounts
Accounts are created via the connect flow, not directly.
- get
/api/v1/accounts/{id}Get an account
- patch
/api/v1/accounts/{id}Update an account
Requires scope `accounts:write`.
- delete
/api/v1/accounts/{id}Disconnect an account
Requires scope `accounts:write`. Soft-disconnect: clears stored credentials and flips status to disconnected; PostTarget history is preserved.
- get
/api/v1/accounts/{id}/healthGet account health
DB-derived health snapshot (does not make a live call to the platform): connection status, whether we still hold encrypted credentials, and the last recorded error.
- get
/api/v1/account-groupsList account groups
- post
/api/v1/account-groupsCreate an account group
Requires scope `account-groups:write`. Bundles accounts under a profile for bulk targeting.
- get
/api/v1/account-groups/{id}Get an account group
- patch
/api/v1/account-groups/{id}Update an account group
Requires scope `account-groups:write`.
- delete
/api/v1/account-groups/{id}Delete an account group
Requires scope `account-groups:write`.
Connect
3 endpointsOAuth and direct-credential flows for connecting an account to a platform.
- post
/api/v1/connect/authorizeStart connecting an account
Requires scope `accounts:connect`. For platform=mastodon this dynamically registers an OAuth app with the given instance and returns a real authUrl; for every other platform it returns mode='manual' with the fields to collect and POST to /connect/manual.
- post
/api/v1/connect/manualConnect an account with direct credentials
Requires scope `accounts:connect`. Bypasses OAuth: validates the given credentials via the platform's PublisherAdapter.connectAccount() and persists them AES-256-GCM encrypted. Upserts on (tenantId, profileId, platform, platformAccountId).
- get
/api/v1/connect/callbackno api keyMastodon OAuth callback
Redirect target for the Mastodon OAuth flow started by POST /connect/authorize. Not called with a Bearer sk_ key — tenant/profile authorization is instead proven by successfully decrypting the `state` query param, which was minted server-side after the original caller passed authenticate()+requireScope('accounts:connect').
Posts
7 endpointsCreate, schedule, publish, and retry posts across platforms.
- get
/api/v1/postsList posts
- post
/api/v1/postsCreate (and optionally schedule/publish) a post
Requires scope `posts:write`. One PostTarget is created per accountId. If `publishNow` is true the post is claimed by the next cron tick immediately; if `scheduledFor` is set it publishes at that time; otherwise it is saved as a draft. Publishing always goes through the scheduler (claimDuePosts/publishPost) — never directly through adapters.
- get
/api/v1/posts/{id}Get a post
- patch
/api/v1/posts/{id}Update a post
Requires scope `posts:write`. Only posts in status draft or scheduled may be updated.
- delete
/api/v1/posts/{id}Delete a post
Requires scope `posts:write`. Cascades to its PostTarget and PostMedia rows.
- post
/api/v1/posts/{id}/retryRetry a post's failed targets
Requires scope `posts:write`. Calls retryPost() from the scheduler — only re-attempts PostTarget rows currently in status failed; published targets are never re-sent.
- post
/api/v1/posts/validatePre-flight validate content against target platforms
Any authenticated API key (no extra scope). Read-only: reports whether `content` fits each target platform's character limit and whether the referenced `accountId`s are valid/connected, without creating anything.
Queue
6 endpointsRecurring auto-schedule slots.
- get
/api/v1/queueList queue slots
- post
/api/v1/queueCreate a queue slot
Requires scope `queue:write`.
- get
/api/v1/queue/previewPreview upcoming queue slot occurrences
Any authenticated API key (no extra scope). Read-only projection of a profile's enabled QueueSlots into their next N upcoming UTC occurrences; does not read or write any Post.
- get
/api/v1/queue/{id}Get a queue slot
- patch
/api/v1/queue/{id}Update a queue slot
Requires scope `queue:write`.
- delete
/api/v1/queue/{id}Delete a queue slot
Requires scope `queue:write`.
Media
4 endpointsPass-through media storage (no transcoding).
- post
/api/v1/media/uploadUpload media
Requires scope `media:write`. Accepts multipart/form-data (field `file`) or a JSON body with `url` (fetched pass-through and re-hosted) or `dataBase64`. Bytes are stored as-is — no transcoding. 25MB max.
- post
/api/v1/media/presignPre-negotiate an upload slot
Requires scope `media:write`. Returns a short-lived, tenant-bound token plus the URL/method/field name to POST bytes to (currently /v1/media/upload — swappable for a real object-storage presigned URL later without changing callers).
- post
/api/v1/validate/media-urlValidate a remote media URL
Read-only pre-flight check (any valid API key, no extra scope): confirms the URL is reachable, resolves its mime type/size, without downloading or storing it.
- get
/api/v1/media/file/{tenantId}/{filename}no api keyServe an uploaded media file from disk
Not bearer-authenticated on purpose — URLs returned by POST /v1/media/upload are embedded directly in outbound posts/DMs and rendered in <img>/<video> tags, so they must be fetchable by browsers and third-party platforms that can't attach an Authorization header. Access control relies on `filename` being an unguessable random UUID. Storage lives outside `public/` (fixes uploaded media 404ing under `next start`); both path segments are validated against a strict filesystem-safe pattern and resolved against the uploads root, rejecting any traversal attempt. Content-Type is derived from a small extension allowlist (falls back to application/octet-stream); responses always carry X-Content-Type-Options: nosniff and a restrictive Content-Security-Policy. Returns 404 (never 403) on a miss or a rejected/malformed path, so the endpoint never reveals filesystem shape.
Webhooks
7 endpointsOutbound event subscriptions and delivery history.
- get
/api/v1/webhooksList webhooks
- post
/api/v1/webhooksCreate a webhook
Requires scope `webhooks:write`. The response is the ONLY place `secret` (used to compute X-OneSocial-Signature) is ever returned.
- get
/api/v1/webhooks/{id}Get a webhook
- patch
/api/v1/webhooks/{id}Update a webhook
Requires scope `webhooks:write`.
- delete
/api/v1/webhooks/{id}Delete a webhook
Requires scope `webhooks:write`.
- get
/api/v1/webhooks/{id}/logsList a webhook's delivery history
- post
/api/v1/webhooks/testSend a test delivery
Requires scope `webhooks:write`. Sends a one-off `webhook.test` delivery to the given webhook regardless of its subscribed events list.
ApiKeys
3 endpointssk_ API key lifecycle.
- get
/api/v1/api-keysList API keys
- post
/api/v1/api-keysCreate an API key
Requires scope `api-keys:write`. The raw sk_ value is returned ONCE in this response and is never retrievable again.
- delete
/api/v1/api-keys/{id}Revoke an API key
Requires scope `api-keys:write`. Sets revokedAt; the key immediately stops authenticating.
User
1 endpointsThe currently authenticated actor.
- get
/api/v1/userGet the current actor
Returns the tenant and API key identity behind the bearer sk_ token used on this request.
Usage
1 endpointsPer-tenant usage counters.
- get
/api/v1/usageGet usage summary
Account and post counters for the authenticated tenant. `from`/`to` default to the current UTC calendar month.
Analytics
3 endpointsPer-day post/account metric rollups, daily series, and best-time-to-post suggestions.
- get
/api/v1/analyticsGet an analytics rollup
Requires scope `analytics:read`. Sums the typed PostMetric counters across `since`/`until` (default: trailing 30 days).
- get
/api/v1/analytics/dailyGet analytics grouped by day
Requires scope `analytics:read`. Same filters as GET /analytics, grouped by PostMetric.date for charting.
- get
/api/v1/analytics/best-timeGet the best time to post
Requires scope `analytics:read`. Buckets PostMetric rows by weekday x hour and ranks by engagement, to suggest optimal posting slots.
Mentions
1 endpointsInbound @-mentions across connected accounts.
- get
/api/v1/mentionsList mentions
Any authenticated API key (read-scope enforcement not yet applied to this endpoint). Read-only — mentions have no reply concept.
Reviews
3 endpointsPlatform reviews (e.g. Google Business style) and replies to them.
- get
/api/v1/reviewsList reviews
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- get
/api/v1/reviews/{id}Get a review
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- post
/api/v1/reviews/{id}/replyReply to a review
Requires scope `reviews:write`. Sets reply/repliedAt and status=replied, and dispatches `review.replied`. Re-posting to an already-replied review overwrites the stored reply (edit-in-place; there is a single `reply` field, not a thread).
Messages
6 endpointsDM conversations and messages across connected accounts.
- get
/api/v1/messagesList conversations
Requires scope `messages:read`. Despite the path, this lists Conversation rows (message threads), sorted by lastMessageAt.
- get
/api/v1/conversations/{id}Get a conversation
Requires scope `messages:read`. Includes its messages, oldest first.
- post
/api/v1/conversations/{id}/mark-readMark a conversation read
Requires scope `messages:write`. Sets unreadCount=0 and isRead=true on its inbound messages.
- post
/api/v1/messages/sendSend a DM
Requires scope `messages:write`. Sends via the platform adapter and records an outbound Message; pass `conversationId` to append to an existing thread, or `accountId`+`contactId` to start a new one.
- patch
/api/v1/messages/{id}Edit a message
Requires scope `messages:write`. Only own outbound messages may be edited; sets editedAt.
- delete
/api/v1/messages/{id}Delete a message
Requires scope `messages:write`. Soft-delete: sets deletedAt and status=deleted; thread ordering is preserved.
Contacts
7 endpointsCRM contact records, their channels, and bulk import.
- get
/api/v1/contactsList contacts
Requires scope `contacts:read`.
- post
/api/v1/contactsCreate a contact
Requires scope `contacts:write`. `customFields` is validated against this tenant's CustomFieldDef rows via validateCustomFields() before persistence.
- post
/api/v1/contacts/bulk-createBulk-create contacts
Requires scope `contacts:write`. Up to 1000 contacts per call; each is validated (including customFields) before a single createMany.
- get
/api/v1/contacts/{id}Get a contact
Requires scope `contacts:read`. Includes channels.
- patch
/api/v1/contacts/{id}Update a contact
Requires scope `contacts:write`. `customFields`, if present, is re-validated in full against this tenant's CustomFieldDef rows.
- delete
/api/v1/contacts/{id}Delete a contact
Requires scope `contacts:write`. Cascades to its channels, broadcast recipient rows, and sequence enrollments.
- post
/api/v1/contacts/{id}/channelsAdd a channel to a contact
Requires scope `contacts:write`. Rejects with 409 if this exact (platform, externalId) is already attached to the contact — unique per (contactId, platform, externalId).
CustomFields
5 endpointsTenant-defined custom field schemas for Contact.customFields.
- get
/api/v1/custom-fieldsList custom field definitions
Requires scope `custom-fields:read`.
- post
/api/v1/custom-fieldsCreate a custom field definition
Requires scope `custom-fields:write`. Unique per tenant on `key`; 409 if that key already exists.
- get
/api/v1/custom-fields/{id}Get a custom field definition
Requires scope `custom-fields:read`.
- patch
/api/v1/custom-fields/{id}Update a custom field definition
Requires scope `custom-fields:write`.
- delete
/api/v1/custom-fields/{id}Delete a custom field definition
Requires scope `custom-fields:write`. Does not retroactively strip the key from existing Contact.customFields JSON.
Broadcasts
7 endpointsOne-shot campaign messages sent to many contacts.
- get
/api/v1/broadcastsList broadcasts
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- post
/api/v1/broadcastsCreate a broadcast
Requires scope `broadcasts:write`. Created in status=draft (or scheduled, when `scheduledFor` is given); optionally seeds BroadcastRecipient rows from `contactIds`.
- get
/api/v1/broadcasts/{id}Get a broadcast
Any authenticated API key (read-scope enforcement not yet applied to this endpoint). Includes its recipients.
- patch
/api/v1/broadcasts/{id}Update a broadcast
Requires scope `broadcasts:write`. Only broadcasts in status draft or scheduled may be updated. Passing `contactIds` deletes all existing BroadcastRecipient rows for this broadcast and re-creates them (there is no separate add-recipients endpoint).
- delete
/api/v1/broadcasts/{id}Cancel a broadcast
Requires scope `broadcasts:write`. Despite the HTTP method this does NOT delete the row: only broadcasts in status draft or scheduled may be cancelled; it marks their pending BroadcastRecipient rows status=skipped and sets the broadcast status=cancelled. There is no separate /cancel endpoint.
- post
/api/v1/broadcasts/{id}/sendSend a broadcast now
Requires scope `broadcasts:write`. Sets status=sending for the /api/cron/engage broadcasts pass to pick up immediately.
- post
/api/v1/broadcasts/{id}/scheduleSchedule a broadcast
Requires scope `broadcasts:write`. Sets status=scheduled and scheduledFor for the cron pass to claim once due.
Sequences
6 endpointsMulti-step drip campaigns with delay/message/broadcast steps.
- get
/api/v1/sequencesList sequences
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- post
/api/v1/sequencesCreate a sequence
Requires scope `sequences:write`. Created in status=draft (unless `status` is given) with the given inline `steps` (defaults to an empty array).
- get
/api/v1/sequences/{id}Get a sequence
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- patch
/api/v1/sequences/{id}Update a sequence
Requires scope `sequences:write`. Sets `status` to active/paused/draft here — there are no separate activate/pause endpoints.
- delete
/api/v1/sequences/{id}Delete a sequence
Requires scope `sequences:write`. Hard-deletes the row (there is no cancelled status for sequences); rejected with 409 while any enrollment is still status=active.
- post
/api/v1/sequences/{id}/enrollEnroll a contact in a sequence
Requires scope `sequences:write`. Upserts a single SequenceEnrollment row on (sequenceId, contactId) with status=active, currentStep=0, and nextRunAt derived from the first step. There is no separate unenroll endpoint or bulk form — call this once per contactId, and set the enrollment's status via a client-side record if you need to track unenrollment (the API itself has no route for it yet).
Automations
6 endpointsComment-to-DM automation rules and their trigger logs.
- get
/api/v1/automations/comment-to-dmList comment-to-DM automations
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- post
/api/v1/automations/comment-to-dmCreate a comment-to-DM automation
Requires scope `automations:write`.
- get
/api/v1/automations/comment-to-dm/{id}Get a comment-to-DM automation
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
- patch
/api/v1/automations/comment-to-dm/{id}Update a comment-to-DM automation
Requires scope `automations:write`.
- delete
/api/v1/automations/comment-to-dm/{id}Delete a comment-to-DM automation
Requires scope `automations:write`. Cascades to its trigger logs.
- get
/api/v1/automations/comment-to-dm/{id}/logsList an automation's trigger logs
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
AdAccounts
5 endpointsPer-network connected ad accounts (Meta, Google, TikTok, LinkedIn, Pinterest, X).
- get
/api/v1/ad-accountsList ad accounts
Requires scope `ad-accounts:read`.
- post
/api/v1/ad-accountsConnect an ad account
Requires scope `ad-accounts:write`. `credentials` is validated live via the network adapter's `connectAdAccount()` when that network is configured server-side (`<NETWORK>_ADS_*` env vars present); otherwise it is stored as given. Always AES-256-GCM encrypted at rest and never returned.
- get
/api/v1/ad-accounts/{id}Get an ad account
Requires scope `ad-accounts:read`.
- patch
/api/v1/ad-accounts/{id}Update an ad account
Requires scope `ad-accounts:write`.
- delete
/api/v1/ad-accounts/{id}Disconnect an ad account
Requires scope `ad-accounts:write`. Sets status=disconnected; cascades to its campaigns, ads, audiences, and tracking tags.
AdCampaigns
6 endpointsAd campaigns: objective, budget, and pause/resume state.
- get
/api/v1/ad-campaignsList ad campaigns
Requires scope `ad-campaigns:read`.
- post
/api/v1/ad-campaignsCreate an ad campaign
Requires scope `ad-campaigns:write`. `adAccountId` must belong to the caller's tenant. Created in status=draft.
- get
/api/v1/ad-campaigns/{id}Get an ad campaign
Requires scope `ad-campaigns:read`.
- patch
/api/v1/ad-campaigns/{id}Update an ad campaign
Requires scope `ad-campaigns:write`. Status transitions go through `POST /v1/ad-campaigns/{id}/status`, not this endpoint.
- delete
/api/v1/ad-campaigns/{id}Delete an ad campaign
Requires scope `ad-campaigns:write`. Detaches (does not delete) any ads currently assigned to it.
- post
/api/v1/ad-campaigns/{id}/statusPause or resume an ad campaign
Requires scope `ad-campaigns:write`. Body `{ action: "pause" | "resume" }`. `pause` is only valid from status=active and `resume` only from status=paused; any other current status is a 400. Dispatches to the network adapter (best-effort when configured), sets status accordingly, and emits `ad.status_changed`.
Ads
11 endpointsIndividual ads, including boosted posts, lead forms, targeting search, and conversion forwarding.
- get
/api/v1/adsList ads
Requires scope `ads:read`.
- post
/api/v1/adsCreate an ad
Requires scope `ads:write`. `adAccountId` (and `campaignId`, when given) must belong to the caller's tenant, and `campaignId`'s adAccountId must match. Created in status=draft unless `status` is given.
- get
/api/v1/ads/{id}Get an ad
Requires scope `ads:read`.
- patch
/api/v1/ads/{id}Update an ad
Requires scope `ads:write`. Only permitted while the ad's current status is draft or paused. Emits `ad.status_changed` when `status` changes.
- delete
/api/v1/ads/{id}Delete an ad
Requires scope `ads:write`.
- get
/api/v1/ads/{id}/analyticsGet an ad's daily analytics
Requires scope `ads:read`. Reads from AdMetric.
- post
/api/v1/ads/boost-postBoost an organic post into a paid ad
Requires scope `ads:write`. `postId` must belong to the caller's tenant (404, not leaked, on mismatch). Creates an `Ad` with `postId` set.
- post
/api/v1/ads/create-lead-formCreate a lead form
Requires scope `ads:write`. `adAccountId` must belong to the caller's tenant.
- post
/api/v1/ads/lead-forms/{id}/submissionsSubmit a lead against a lead form
Requires scope `ads:write`. The lead form `{id}` must belong to the caller's tenant (404, not leaked, on mismatch). `data` is validated against the form's field specs — required fields must be present and unknown keys are rejected. Optional `adId` links the lead to a tenant-owned ad. Creates a `Lead` and emits the `lead.received` webhook.
- post
/api/v1/ads/search-targetingSearch network targeting suggestions
Requires scope `ads:write`. `adAccountId` must belong to the caller's tenant. Returns a cached echo of the query when the account's network has no credentials configured, so this endpoint is exercisable without ad creds.
- post
/api/v1/ads/send-conversionsSend conversion events
Requires scope `ads:write`. `trackingTagId` must belong to the caller's tenant (404, not leaked, on mismatch). Any `userData.email`/`userData.phone` in an event's payload is SHA-256 hashed before persist and before being forwarded. Persists one `AdConversion` per event and forwards them to the network CAPI via the adapter when configured (`forwarded` reflects the outcome; unconfigured networks persist with `forwarded=false`).
AdAudiences
6 endpointsCustom/lookalike/saved audiences and hashed-member uploads.
- get
/api/v1/ad-audiencesList ad audiences
Requires scope `ad-audiences:read`.
- post
/api/v1/ad-audiencesCreate an ad audience
Requires scope `ad-audiences:write`. `adAccountId` must belong to the caller's tenant. Created in status=building.
- get
/api/v1/ad-audiences/{id}Get an ad audience
Requires scope `ad-audiences:read`.
- patch
/api/v1/ad-audiences/{id}Update an ad audience
Requires scope `ad-audiences:write`.
- delete
/api/v1/ad-audiences/{id}Delete an ad audience
Requires scope `ad-audiences:write`. Cascades to its members.
- post
/api/v1/ad-audiences/{id}/uploadUpload audience members
Requires scope `ad-audiences:write`. `id` must belong to the caller's tenant (404, not leaked, on mismatch). Raw email/phone are SHA-256 hashed immediately; only the hash is ever persisted (`AdAudienceMember.hashedValue`) or handed to the network adapter. `memberCount` is bumped by the number of newly-accepted (previously unseen) hashes.
Comments
4 endpointsInbound comments on connected accounts' native posts, and replies to them.
/api/v1/commentsList comments
Any authenticated API key (read-scope enforcement not yet applied to this endpoint).
/api/v1/comments/{id}Get a comment
Includes its threaded replies (Comment rows with parentId set) as `replies`, oldest first.
/api/v1/comments/{id}Delete/hide a comment
Requires scope `comments:write`. Marks the comment status = deleted; does not remove the row.
/api/v1/comments/{id}/replyReply to a comment
Requires scope `comments:write`. Persists the reply as a new Comment row (isOwn=true, parentId set to the original), flips the original's status to replied, and dispatches `comment.replied`. 409 if the original comment is already status=deleted.