Subchapter 8.7
references/blog/how-to-create-blog-posts.mdMarkdown15 KBView on GitHub
Article: Create and Publish Blog Posts with Rich Content and Images
Standard call shape (every curl below). The
<AUTH>placeholder is shorthand forAuthorization: Bearer <TOKEN>only. Body-bearing requests also needContent-Type: application/json.
This article demonstrates how to create and immediately publish blog posts using Wix Blog REST API, including handling external images, rich content formatting, and proper media management workflow.
IMPORTANT: When calling the Blog API as a 3rd-party app (not as the site owner), draftPost.memberId is required. The API will reject requests with “Missing post owner information” if omitted.
A memberId is the id of a site member (Wix Members). Only the Members API produces one. Run the two steps below in order and stop as soon as you have an id — do not go looking for an author id anywhere else (see What is not a memberId).
Query site members using List Members (opens in a new tab):
curl -X GET "https://www.wixapis.com/members/v1/members?fieldsets=PUBLIC&paging.limit=1" \
-H "Authorization: <AUTH>"If the response contains a member, use members[0].id as draftPost.memberId and skip step 2. That member becomes the post author.
If the site has no members yet — the response is {"members": [], "metadata": {"count": 0, "total": 0, ...}} — create the author member yourself with Create Member (opens in a new tab). This is the normal case on a new site or a site that only has Blog installed: nobody has signed up, so there is no member to reuse. Creating the author is a prerequisite of the post the user asked for, not a separate decision — create it, then say so in your summary.
curl -X POST "https://www.wixapis.com/members/v1/members" \
-H "Authorization: <AUTH>" \
-H "Content-Type: application/json" \
-d '{
"member": {
"loginEmail": "blogauthor@example.com",
"contact": { "firstName": "Blog", "lastName": "Author" }
}
}'Use member.id from the response as draftPost.memberId. The new member comes back with status: "APPROVED", and its profile.nickname is derived from the contact name. That nickname is the author name the blog displays, so pass the name the user wants shown (or set member.profile.nickname explicitly). If the user named an author, use their name; otherwise a generic author like the example above is fine.
These are the wrong places to look, and probing them costs a round trip each:
memberId.memberId. On a site with no members the contacts query is usually empty too.GET https://www.wixapis.com/identity/v1/users returns 404. There is no site-scoped identity endpoint to read the site owner from. Do not try it.Identify external image URLs from user input for cover images and embedded content images.
Import each external image using Import File (opens in a new tab). This converts external URLs to Wix Media IDs required for blog posts.
curl -X POST "https://www.wixapis.com/site-media/v1/files/import" \
-H "Authorization: <AUTH>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/image.jpg",
"mediaType": "IMAGE",
"displayName": "Cover Image.jpg"
}'The response will include a file.id field. Use this ID in blog post creation. Images with operationStatus: "PENDING" can be used immediately.
Store the returned file IDs for use in blog post creation.
You have two endpoints:
POST https://www.wixapis.com/blog/v3/draft-postsPOST https://www.wixapis.com/blog/v3/bulk/draft-posts/createUse the bulk endpoint when seeding multiple posts — one call replaces N single-post calls and avoids the per-call latency of ~25–30 s each.
curl -X POST "https://www.wixapis.com/blog/v3/draft-posts" \
-H "Authorization: <AUTH>" \
-H "Content-Type: application/json" \
-d '{
"draftPost": {
"title": "My Blog Post",
"memberId": "author-member-id",
"richContent": {
"nodes": [
{
"type": "PARAGRAPH",
"nodes": [{
"type": "TEXT",
"textData": {
"text": "This is a paragraph with some content.",
"decorations": []
}
}],
"paragraphData": {}
}
]
},
"media": {
"wixMedia": {
"image": { "id": "mediaId" }
},
"displayed": true,
"custom": true
}
},
"publish": true
}'⚠️ Body shape — read this carefully. Each item in
draftPostsis a FLAT post object:{title, memberId, richContent, media?, ...}. Do NOT wrap each item in adraftPostfield. Unlike the single-post endpoint (which uses{draftPost: {...}}because the request is one post), the bulk endpoint puts each post DIRECTLY inside thedraftPostsarray.
✅ CORRECT body shape (verified against the live API — returns 200 with results[].itemMetadata.success: true):
{
"draftPosts": [
{ "title": "First Post", "memberId": "...", "richContent": { /* … */ } },
{ "title": "Second Post", "memberId": "...", "richContent": { /* … */ } }
],
"publish": true
}❌ WRONG body shape (returns 400 Bad Request with draftPosts[i].title must not be empty because the API is looking for draftPosts[i].title directly and finds it nested under a draftPost field):
{
"draftPosts": [
{ "draftPost": { "title": "First Post", "memberId": "...", "richContent": { /* … */ } } },
{ "draftPost": { "title": "Second Post", "memberId": "...", "richContent": { /* … */ } } }
],
"publish": true
}The natural intuition is “the bulk endpoint reuses the single-post {draftPost: {...}} envelope, just inside an array” — that’s wrong. The bulk endpoint flattens the envelope away because the array IS the envelope. Use the FLAT shape: draftPosts[i] IS the post.
curl -X POST "https://www.wixapis.com/blog/v3/bulk/draft-posts/create" \
-H "Authorization: <AUTH>" \
-H "Content-Type: application/json" \
-d '{
"draftPosts": [
{
"title": "First Post",
"memberId": "author-member-id",
"richContent": { /* Ricos JSON — see below */ },
"media": { "wixMedia": { "image": { "id": "mediaId" } }, "displayed": true, "custom": true }
},
{
"title": "Second Post",
"memberId": "author-member-id",
"richContent": { /* Ricos JSON */ }
}
],
"publish": true
}'The response body is {results: [{itemMetadata: {id, originalIndex, success}}, ...]}. Each result’s itemMetadata carries the created post id and a success: boolean flag — the bulk call returns 200 even if some posts fail; check each results[i].itemMetadata.success individually.
Common URL-shape mistakes (do not use these — both return 404):
/blog/v3/draft-posts/bulk ✗/blog/v3/draft-posts/bulk-create ✗/blog/v3/bulk/draft-posts/create (note: bulk is a path segment between v3 and draft-posts, not a suffix on draft-posts).Structure rich content using Ricos JSON format. Reference Ricos documentation (opens in a new tab) for complete node structure. Common node types:
PARAGRAPH for text contentHEADING for section headersIMAGE for embedded images (requires Wix Media ID)ORDERED_LIST and BULLETED_LIST for listsBLOCKQUOTE for quoted textLIST_ITEM for individual list itemsCRITICAL: All TEXT nodes MUST be wrapped in PARAGRAPH nodes within their parent containers.
Correct Ricos structure example:
{
"nodes": [
{
"type": "PARAGRAPH",
"nodes": [
{
"type": "TEXT",
"textData": {
"text": "This is a paragraph with some content.",
"decorations": []
}
}
],
"paragraphData": {}
}
]
}Correct BLOCKQUOTE structure:
{
"type": "BLOCKQUOTE",
"nodes": [
{
"type": "PARAGRAPH",
"nodes": [
{
"type": "TEXT",
"textData": { "text": "Quote text here", "decorations": [] }
}
],
"paragraphData": {}
Correct LIST_ITEM structure:
{
"type": "LIST_ITEM",
"nodes": [
{
"type": "PARAGRAPH",
"nodes": [
{
"type": "TEXT",
"textData": { "text": "List item text", "decorations": [] }
}
],
"paragraphData": {}
For embedded images in rich content, use IMAGE nodes with Wix Media IDs:
{
"type": "IMAGE",
"nodes": [],
"imageData": {
"containerData": {
"width": { "size": "CONTENT" },
"alignment": "CENTER"
},
"image": {
"src": { "id": "mediaId" },
"width":
Set publish: true to immediately publish the post rather than saving as draft.
Resolve category IDs using List Categories (opens in a new tab) if user provides category names.
Resolve tag IDs using Query Tags (opens in a new tab) if user provides tag labels.
Include resolved IDs in categoryIds and tagIds arrays in the draft post object.
memberId is mandatory - get it from List Members (opens in a new tab), and if the site has no members yet, create one with Create Member (opens in a new tab) (see Part 0). Never substitute a contact id, a collaborator/contributor id, or a Wix user id for a memberIdwix:image://v1/ prefix) for both cover images and embedded imageswidth and height properties in the image object"operationStatus": "PENDING" from import can be used immediately in blog postspublish: true in the request to publish immediately rather than save as draftdraftPosts arrayfieldsets: ['URL'] to get post URLs in the responsedisplayName values during image import for better organization| Error | Cause | Solution |
|---|---|---|
| “Missing post owner information” | memberId not provided | Add draftPost.memberId - see Part 0 for how to get one |
| “memberIds … do not exist” | Invalid member ID | The id is not a site member’s id (e.g. a contact, contributor, or Wix user id). Re-resolve it with Part 0 |
List Members returns "total": 0 | Site has no members yet | Create the author member with Create Member - Part 0, step 2. Do not query contacts, contributors, or identity endpoints instead |
| “Expected a paragraph node but found TEXT” | Invalid Ricos structure | Wrap TEXT nodes in PARAGRAPH nodes (see structure rules above) |
| Image not displaying | Using external URL directly | Import image via Media Manager first, then use the returned file ID |