Run every post request through these steps, in this order. Skip a question only when the user's own words in this conversation already answered it.
Connection first. Confirm the target channel is connected (STEP 1). If it isn't, offer the connect flow (STEP 1.5) before anything else.
Plan second. Check PUBLISH_POST / SCHEDULE_POST (STEP 2). If SCHEDULE_POST is disabled, never present scheduling as an option. AI generation is never plan-gated; never tell the user it is.
Ask: own or generated? "Do you already have the post text (and image), or should I generate it?" Wait for the answer.
If generating, ask: idea or asset? "From an idea, or built around one of your site's assets (a product, blog post, event, booking, or coupon)?" Offer the asset option explicitly, every time. Wait for the answer.
The subject is a hard gate. Before any generation call you need, in the user's own words, what the post is about: an idea or a chosen asset. A reply that answers only part of a question (an account pick, a bare "you generate it") does not supply the subject. Never derive a topic from the site's profile or content on your own. Ask again and wait.
Route exactly one way:
generate-post-data with userInput (STEP 3a; returns caption and image).generate-post-data with siteAssets (STEP 3a).generate-text (STEP 3b).generate-image (STEP 3c; requires a source image URL).Any request to generate an image for the post means generate-post-data. generate-image cannot create an image from text alone (it returns 400 without a source image); no endpoint can.
Present the tool's output to the user: the caption and the image (rendered inline, or its URL). Never hand-write the caption or claim generated content you didn't get from the API. Get explicit approval on the final content before publishing (STEP 6).
The rest of this recipe is the reference for executing each step.
This recipe creates a post — called an item — and publishes it to one of a site's connected social channels, or schedules it for a future date. You can generate the post content with AI (STEP 3) or bring your own. Each item targets a single channel, and its type and content must match a combination the channel supports.
Base URL for all endpoints: https://www.wixapis.com/social-publisher/v1.
Prerequisites:
static.wixstatic.com); see Media handling in STEP 4. Images edited in STEP 3c are already hosted there.Flow: STEP 1 confirm the channel is connected (connect if needed) → STEP 2 check premium features → STEP 3 generate content (offer this proactively) → STEP 4 pick channel/type → STEP 5 create the draft → STEP 6 publish or schedule. Checking connection and premium first avoids generating content for a channel that can't receive it or an action the plan doesn't allow.
Not covered here: editing or analyzing already-published posts, post analytics/insights, or connecting a channel as a standalone goal (connection is handled here only as a precondition for publishing).
Determine the target channel from the request (INSTAGRAM, FACEBOOK, YOUTUBE, LINKEDIN, PINTEREST, GBP, TIKTOK, TWITTER), then get the account to publish to and confirm the channel is connected. TWITTER (X) is available only through July 31, 2026 — see the cutoff rule in STEP 4 and STEP 6. Do this first — you can't publish to an unconnected channel, and connecting is an interactive step best surfaced up front.
API Endpoint: GET https://www.wixapis.com/social-publisher/v1/accounts?channelName=INSTAGRAM
(Channel-name placement differs by endpoint: accounts and features take it as a query param; connect-url and long-lived-token-status take it as a path segment.)
Expected response:
Decision point:
id as channel.accountId in STEP 5.settings.default: true, or ask the user which page/account to post to, and use that id as channel.accountId. To change the default (what the dashboard's page picker does), call Update Account Settings: PATCH https://www.wixapis.com/social-publisher/v1/{channelName}/settings with the channel-specific default field:
{ "instagram": { "defaultAccountId": "<id>" } }{ "facebook": { "defaultPageId": "<id>" } }{ "pinterest": { "defaultBoardId": "<id>" } }{ "gbp": { "defaultLocationId": "<id>" } }{ "tiktok": { "defaultAccountId": "<id>" } }{ "linkedin": { "defaultChannelId": "<id>" } }accounts is empty → the channel isn't connected — offer to connect it (STEP 1.5). If long-lived-token-status is already VALID but this still returns empty (or NO_PAGES_FOR_USER), the token was created but no Facebook Page with a linked Instagram Business/Creator account was granted during authorization — treat it as "not connected" and re-run STEP 1.5, telling the user to grant the Page.400 USER_NOT_EXIST_FOR_CHANNEL error (instead of an empty list) → same meaning: this channel has no connected user. Treat it exactly like "not connected" and offer STEP 1.5.This call is per-channel — an empty list or a USER_NOT_EXIST_FOR_CHANNEL error tells you only about the channel you queried, nothing about others. Scope statements to that channel ("Pinterest isn't connected yet"); never say "no accounts connected on this site" from a single-channel check.
Note the extra IDs some channels need in STEP 5: Facebook facebook.page.id, Pinterest pinterest.board.id, Google Business Profile gbp.location.id.
Ask the user if they'd like to connect the channel now. If yes, run the OAuth connect flow — the site owner must authorize in their browser; it can't be completed server-side alone.
Get the authorization URL — GET https://www.wixapis.com/social-publisher/v1/INSTAGRAM/connect-url (path segment is the channel name):
A 428 INELIGIBLE_FOR_FEATURE here means the site has hit its plan's cap on how many channels it can connect (free plans allow one), not that this channel is blocked. Since the cap is full, another channel is already connected — offer only two options: upgrade the plan, or post to the already-connected channel (find it via List Accounts on the other channels, or ask). Don't suggest connecting a different new channel (same cap, same 428) or disconnecting the existing one.
Surface connectUrl to the user and ask them to open it and authorize the channel. The channel's OAuth redirect completes the connection server-side.
Poll the connection status — GET https://www.wixapis.com/social-publisher/v1/INSTAGRAM/long-lived-token-status every few seconds, for up to ~2 minutes, until status is VALID:
status values: NEVER_CREATED (not connected yet — keep polling until the timeout), VALID (connected — proceed), INVALID (connection expired — reconnect with the same flow), DISCONNECTED (was disconnected — reconnect). If it's still not VALID after ~2 minutes, stop and tell the user the connection wasn't completed and they can retry.
Re-run STEP 1 (List Accounts) to get the now-connected account's id for channel.accountId.
If the user declines to connect, stop: the post can't be published to an unconnected channel.
One call tells you what the site's plan allows — whether you can publish or schedule — so you fail fast before creating anything. (AI generation is not gated by this call — see STEP 3 — so there's no need to check for it here.)
API Endpoint: GET https://www.wixapis.com/social-publisher/v1/features?featureTypes=PUBLISH_POST&featureTypes=SCHEDULE_POST
Expected response:
quotaInfo is present only when the feature is metered — when quotas don't apply, monetizationEnabled is false and each entry carries just type and enabled. period is one of NO_PERIOD, MILLISECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR (NO_PERIOD means the quota doesn't reset).
Decision point:
PUBLISH_POST (publish now) or SCHEDULE_POST (schedule) — enabled: false → the plan doesn't include it; advise upgrading the social media marketing plan.SCHEDULE_POST is enabled: false, drop scheduling from the conversation entirely — treat "publish now" as the only action. Don't offer a "publish now or schedule?" choice anywhere in the flow, and don't present scheduling while noting it's unavailable. At most mention once that scheduling would need a plan upgrade.monetizationEnabled: true and that action's quotaInfo.remainingUsage is 0 → quota exhausted; tell the user when it resets (period, unless NO_PERIOD) or to upgrade.monetizationEnabled is false, quotas aren't enforced; rely on enabled.AI generation isn't gated by the plan (no premium check applies), so offer it by default. THE PROTOCOL at the top of this recipe governs this step: ask own-or-generated (rule 3), offer idea-or-asset (rule 4), hold the subject gate (rule 5), and route per rule 6. Never hand-write the caption or title; producing the post means calling the API below and presenting its output.
Produces ready-to-use, per-channel payloads that drop straight into STEP 5. This is the default — lead with it for any "create a post" request, offering both the idea-based and asset-based paths.
API Endpoint: POST https://www.wixapis.com/social-publisher/v1/generate-post-data
userInput — free-text idea to base the post on.
siteAssets — the site's own content to ground the post in, as { "id": "<asset-guid>", "type": "<asset-type>" }. Exactly one asset per call (multiple assets aren't supported yet). This recipe doesn't look assets up for you — resolve the id from the owning app's query API first, then pick the asset the user meant. The linked method docs are authoritative: read them for the exact request/response, or if an inline call stops working.
type | Owning app | Endpoint | Docs |
|---|---|---|---|
STORES_PRODUCT | Wix Stores | POST https://www.wixapis.com/stores/v3/products/query (body { "query": {} }) | Query Products |
STORES_CATEGORY | Wix Stores | POST https://www.wixapis.com/categories/v1/categories/query — requires treeReference, e.g. { "treeReference": { "appNamespace": "@wix/stores" } } | Query Categories |
STORES_COUPON | Wix Stores | POST https://www.wixapis.com/stores/v2/coupons/query (body { "query": {} }) | Query Coupons |
BLOG_POST | Wix Blog | POST https://www.wixapis.com/blog/v3/posts/query (body { "paging": { "limit": 10 } }) | Query Posts |
EVENT | Wix Events | POST https://www.wixapis.com/events/v3/events/query (body { "query": { "paging": { "limit": 10 } } } — paging.limit must be > 0) | Query Events |
BOOKINGS_SERVICE | Wix Bookings | POST https://www.wixapis.com/bookings/v2/services/query (body { "query": {} }) | Query Services |
media — candidate images to include, as { "type": "IMAGE", "url": "<public-url>" } (images only).
channels — which channels to generate for. Omit to get one result for each of INSTAGRAM, FACEBOOK, LINKEDIN, PINTEREST, GBP, and TIKTOK.
Provide userInput, siteAssets, or both.
Scope: this method produces standard image-post payloads for the six channels above only. It does not generate YouTube content or story/reel/video formats — for those, use 3b (caption) and 3c (image edit) and assemble the content object yourself in STEP 5.
Example — from an idea + an image, for Instagram and Facebook:
Example — from a site asset (a Stores product):
Expected response — one entry per requested channel, each holding a channel-specific payload (instagramPost, facebookPost, …) shaped exactly like the content in STEP 5 (Instagram shown below; the two-channel request above also returns a facebookPost entry):
Use the payload for your chosen channel as the content object in STEP 5. Note that when you pass no media of your own, generate-post-data returns an AI-generated image in mediaWrapper.media[] — review the whole payload with the user and surface that media, not just the caption (see STEP 6). The generated image's URL may live on a temporary external host rather than static.wixstatic.com — treat it like any external image: import it to the Media Manager first (see Media handling in STEP 4) and use the imported URL in the post.
Use when the user just wants caption suggestions to place into a post.
API Endpoint: POST https://www.wixapis.com/social-publisher/v1/generate-text
userInput is required. Options: channelName, tone (e.g. professional, playful), languageCode (ISO 639-1, e.g. en), and additionalContent (url, withHashtags, addLinkInBio).
Expected response: { "results": [ { "caption": "…", "title": "…" } ] }. Put the chosen caption (and title where the channel uses one) into the content object in STEP 5.
Transforms an existing source image according to a text prompt (e.g. add a sale banner to a product photo, restyle a background). This is AI image-to-image editing — it requires a source image and does not generate an image from a prompt alone. Runs asynchronously.
API Endpoint: POST https://www.wixapis.com/social-publisher/v1/generate-image
Both userInput (prompt) and imageUrl (the source image to edit) are required:
Response: { "status": "OK", "executionId": "b7c2f0a1-4e6d-4a8b-9c3f-2d5e1a7b0c94" }.
Poll for the result: GET https://www.wixapis.com/social-publisher/v1/generated-image/{executionId} about every few seconds, for up to ~2 minutes, until status is READY or FAILED:
status values: IN_PROGRESS (keep polling; if still IN_PROGRESS after ~2 minutes, stop and report), READY (use imageUrl — and fileId — as the post media), FAILED. A 404 GENERATED_IMAGE_NOT_FOUND means the executionId is invalid or expired. The generated image is also saved to the site's "Social Marketing AI Media" Media Manager folder.
For the connected channel from STEP 1, pick the item type and the matching content object (from STEP 3's output or the user's own content).
Channel (channel.name) | type | Content object | Main content fields |
|---|---|---|---|
INSTAGRAM | POST | instagramPost | caption, imageUrl/videoUrl or mediaWrapper (media required) |
INSTAGRAM | STORY | instagramStory | mediaWrapper (media required) |
FACEBOOK | POST | facebookPost | caption, pageId, imageUrl/videoUrl/link or mediaWrapper |
FACEBOOK | REEL | facebookReel | description, pageId, videoUrl or mediaWrapper |
FACEBOOK | STORY | facebookStory | pageId, mediaWrapper |
YOUTUBE | VIDEO | youtubeVideo | title, description, videoUrl, channelId |
YOUTUBE | SHORT | youtubeShort | title, description, videoUrl, channelId |
LINKEDIN | POST | linkedinPost | caption, authorId, mediaWrapper or linkMetadata |
TWITTER (X) | POST | twitterPost | caption, imageUrl/videoUrl/link or mediaWrapper — available only until July 31, 2026 (see STEP 6) |
PINTEREST | POST | pinterestPost | title, description, boardId, link, mediaWrapper |
GBP | POST | gbpPost | locationId, description and/or mediaWrapper, optional callToAction |
TIKTOK | POST | tiktokPhoto | title, description, privacyLevel, mediaWrapper |
TIKTOK | VIDEO | tiktokVideo | description, privacyLevel, mediaWrapper |
Notes:
mediaWrapper.media[] are { "type": "IMAGE" | "VIDEO", "url": "<public-url>" }.description and/or media). No endpoint creates an image from text alone (STEP 3c only edits an existing image), so if the user has none, source one — see Media handling below.caption is the text field for Instagram, Facebook, LinkedIn, and X/Twitter; description for YouTube, Pinterest, Google Business Profile, and TikTok.pageId (Facebook), boardId (Pinterest), locationId (Google Business Profile) come from the account object in STEP 1.privacyLevel must be one of the account's privacyLevelOptions from STEP 1 — one of PUBLIC_TO_EVERYONE, FOLLOWER_OF_CREATOR, MUTUAL_FOLLOW_FRIENDS, SELF_ONLY.caption ≤ 2200 (about 30 hashtags / 20 mentions); Pinterest title ≤ 100, description ≤ 800, link ≤ 2048; Google Business Profile description ≤ 1500; TikTok photo title ≤ 90 and description ≤ 4000, TikTok video description ≤ 2200. AI-generated captions/titles (STEP 3b) are ≤ 1000.callToAction (optional) is one of BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP.TWITTER (X) works like any other channel only through July 31, 2026; after that it's no longer functional (see STEP 6). Treat an UNSUPPORTED_CHANNEL error as authoritative.Media handling. Post media must be a Wix Media Manager URL — static.wixstatic.com for images, video.wixstatic.com for videos. Always use one, even for media that came from elsewhere. (The API only validates a generic URL and won't reject a non-Wix link, but don't rely on that: an external URL has to stay reachable when the post publishes — a real risk for scheduled posts — and skips the Media Manager processing channels expect. Treat a Wix URL as required.) In the content object, url is that Wix URL; fileId is derived from it.
Getting media into the Media Manager:
static.wixstatic.com url directly.POST https://www.wixapis.com/site-media/v1/files/import with { "url": "<external-url>", "mimeType": "image/jpeg", "displayName": "post.jpg" }.POST https://www.wixapis.com/site-media/v1/files/generate-upload-url, then PUT the raw file bytes to the returned uploadUrl (set Content-Type to the file's MIME type); the response returns the file descriptor. See the Media Manager Upload API.After import/upload the file returns operationStatus: PENDING — poll GET https://www.wixapis.com/site-media/v1/files/{id} until operationStatus is READY, then use its static.wixstatic.com url in the post.
Create the post as a draft. The response returns the draft's id, which STEP 6 publishes.
API Endpoint: POST https://www.wixapis.com/social-publisher/v1/items
Build the request from item.channel (name + the accountId from STEP 1), item.type (from the STEP 4 table), and exactly one channel-specific content object.
Assembling the content object:
generate-post-data), the returned per-channel object (e.g. instagramPost) is the content object — pass it through as-is.type: the text field (caption for Instagram/Facebook/LinkedIn, description for YouTube/Pinterest/GBP/TikTok), the media (mediaWrapper, or imageUrl/videoUrl for a single item), and any channel-specific fields — the ID from the STEP 1 account object (pageId for Facebook, boardId for Pinterest, locationId for GBP), plus authorId (LinkedIn), privacyLevel (TikTok, one of the account's privacyLevelOptions), and title where the channel uses one.Idempotency (retry safety). To make create/publish safe to retry, set a stable, caller-defined item.referenceId. Publishing a second item with the same referenceId fails with REFERENCE_ID_ALREADY_EXIST instead of posting a duplicate — so if a call times out, retry with the same referenceId rather than risk double-posting.
Example — Instagram image post:
For multiple media, use mediaWrapper instead of imageUrl/videoUrl:
Example — Facebook image post (needs pageId from the STEP 1 account):
Example — TikTok photo post (needs privacyLevel; description is the text field):
Expected response (abridged) — save id:
Only offer scheduling if SCHEDULE_POST was enabled: true in STEP 2 (per that step's decision point); otherwise publish now is the only option.
Confirm before publishing. Publishing immediately is public and can't be undone (you can only delete the post afterward). Before this call, show the user the final content — the caption, the target channel, and the media: render the image inline if the surface supports it, otherwise post its URL as a clickable link. Never ask them to approve a post whose image they haven't seen. Scheduling is reversible, so a lighter confirmation is fine there, but still surface the media.
API Endpoint: POST https://www.wixapis.com/social-publisher/v1/publish-by-id
Publish immediately — omit scheduledDate:
Schedule for a future date — include scheduledDate (ISO 8601, must be in the future; confirm SCHEDULE_POST in STEP 2). Compute it from the current date/time, resolving the user's relative wording ("next Monday 9am") in the site's timezone if known, otherwise UTC, and confirm the resolved date with the user:
X (Twitter) cutoff. X is no longer functional after July 31, 2026. Don't publish to X once that date has passed, and don't schedule an X post with a scheduledDate after it — a post scheduled to fire past the cutoff won't be published.
Expected response: the item with an updated status:
PUBLISHED — live on the channel; externalItemUrl links to the post.SCHEDULED — will publish automatically at scheduledDate.PROCESSING — publishing in progress; final status follows asynchronously.FAILED — publishing failed; surface the error.(The full status set is DRAFT, SCHEDULED, PROCESSING, IN_QUEUE, PUBLISHED, CANCELED, FAILED, DELETED.)
The post appears on the site's Social Media Marketing page in the dashboard. To reschedule a SCHEDULED item, call POST https://www.wixapis.com/social-publisher/v1/items/{id}/reschedule with body { "scheduledDate": "<new-date>" } (the item moves to SCHEDULED at the new date). To cancel it, call PATCH https://www.wixapis.com/social-publisher/v1/items/{id}/cancel with an empty body {} (the item moves to CANCELED).
| Symptom | Cause | Fix |
|---|---|---|
STEP 1 returns empty accounts | Channel not connected | Run STEP 1.5 to connect the channel, or ask the owner to connect it in the dashboard, then retry |
400 USER_NOT_EXIST_FOR_CHANNEL on List Accounts | The queried channel has no connected user (same as "not connected") | Treat as not-connected for that channel only; offer STEP 1.5. Don't conclude the whole site has no connected accounts — the check is per-channel |
428 INELIGIBLE_FOR_FEATURE on Get Connect Url | Site has hit its plan's cap on number of connected channels (e.g. free = 1), not a channel-specific block | Explain the channel-count limit; offer to upgrade, or find the already-connected channel (List Accounts / ask) and offer to post there. Don't suggest connecting a different new channel (same cap), don't suggest disconnecting/switching, don't retry the connect flow |
FAILED_PRECONDITION / NO_PAGES_FOR_USER on List Accounts | Connected Facebook/Instagram user has no page with a linked postable account | Ask the owner to grant a Facebook page (with a linked Instagram Business/Creator account) during authorization, then retry |
Generate Image poll returns 404 GENERATED_IMAGE_NOT_FOUND | executionId invalid or expired | Re-run Generate Image and poll the new executionId |
STEP 2 shows the publish/schedule feature enabled: false or remainingUsage: 0 | Plan doesn't allow the action, or quota used up | Advise upgrading, or wait for quota reset |
FAILED_PRECONDITION / INELIGIBLE_FOR_FEATURE on publish or schedule | Site's plan doesn't cover publishing/scheduling this post | Check STEP 2 first; advise upgrading the plan |
429 RESOURCE_EXHAUSTED / PUBLISH_LIMIT_EXCEEDED | Publishing rate limit hit | Back off and retry later |
ALREADY_EXISTS / REFERENCE_ID_ALREADY_EXIST on publish | An item with the same referenceId already exists | Expected when safely retrying a publish that already succeeded — don't re-publish with a new referenceId |
FAILED_PRECONDITION / UNSUPPORTED_CHANNEL | Targeting an unsupported channel, or X/Twitter after its July 31, 2026 cutoff | Use a supported channel; target X only before the cutoff |
| Create item rejected for missing media | Instagram and story types require media (GBP needs description and/or media) | Provide a public image/video URL (or edit one from a source image in STEP 3c) |
Reschedule/cancel returns ITEM_NOT_EXISTS, ITEM_IS_PUBLISHED, or ITEM_IS_DELETED | The item can't be rescheduled/canceled in its current state | Only reschedule/cancel items still in SCHEDULED status |
Publish returns status: FAILED | Content/type mismatch or channel rejected the post | Verify the type + content object match the channel's supported combination and that media URLs are public |
Last updated: 7 July 2026