Alongside its articles (posts in the API), a Paragraph publication keeps a library of short-form content: X posts and X threads, LinkedIn posts, one-off emails, and X articles. Teams see it in the app under Content, and Paragraph fills it as it works.

The /v1/content endpoints let you fill it too. Draft something anywhere (a script, a CI job, your own editor) and it lands in the library ready to review.

Uploading creates a draft. Nothing is posted, emailed, or scheduled until someone approves it, in the app or with the publish and approve endpoints.

Kinds and their bodies

Every piece has a kind, and body carries the content in the shape that kind uses. title names the piece in the library and isn't published anywhere.

KindBody
tweettext for a single post (at most 280 characters when creating; an update can go up to 25,000 when the X account can post long), or tweets for a thread (one entry per X post, each following the same limit as a single post). Send one or the other, never both.
linkedintext
newslettersubject, an optional preheader, and either Markdown body or a complete Tiptap bodyJson document
x_articletitle (the headline X publishes), body as CommonMark markdown, and optional canonicalUrl and coverImageUrl

Bodies are validated the same way the app validates them, so a thread with a 400-character entry or an Article missing its headline comes back with the same explanation you'd see while editing.

For a newsletter with merged table cells, inline formatting, or embedded media, use bodyJson to preserve its structured document. Provide either body or bodyJson, never both. An article copied to a newsletter keeps its rich document, subject, preview text, and cover without rewriting its words. Inline videos become usable links to the exact saved cut in email.

Keep the existing body format when updating a saved newsletter. Read it first and retain every unchanged node; replacing a rich document with flattened Markdown is refused. Unsupported formatting or unsafe destinations are also refused rather than silently removed.

Create a draft

curl -X POST "https://public.api.paragraph.com/api/v1/content" \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "tweet",
    "title": "Thread on writing in public",
    "body": { "tweets": ["Writing in public changes what you write.", "Here is what changed for me."] }
  }'
{
  "id": "7f3a2c18-5b9e-4c21-9a0d-8e6b1f4d2a55",
  "kind": "tweet",
  "title": "Thread on writing in public",
  "excerpt": "Writing in public changes what you write.",
  "status": "draft",
  "scheduled": false,
  "lockedReason": null,
  "publishedAt": null,
  "url": null,
  "archivedAt": null,
  "bucketId": null,
  "createdAt": "2026-08-19T09:12:44.108Z",
  "updatedAt": "2026-08-19T09:12:44.108Z",
  "body": { "tweets": ["Writing in public changes what you write.", "Here is what changed for me."] }
}

List your library

curl "https://public.api.paragraph.com/api/v1/content?kind=tweet&status=draft&limit=20" \
  -H "Authorization: Bearer your-api-key"

Filter with kind and status, and page with cursor. status takes all (the default), draft, published, or archived:

  • published means the piece has been sent somewhere. It keeps that status even if it's archived afterwards.
  • archived means it was put away without being sent.
  • all is everything except pieces archived without being sent.

A sent piece also has url, where it went live, and publishedAt. url is null for drafts and for one-off emails, which have no page to link to. A thread links to its first post.

Lists leave bodies out. Fetch a single piece with GET /v1/content/{id} to read one.

Group a piece with the post it came from

In the app, an article (a post in this API) and everything made from it (the X thread, the LinkedIn version, the newsletter) show as one stacked row under Content. That's a content group, and its ID is the bucketId field.

Create the group from the post. The call is safe to repeat, and a post that already has a group returns the same ID:

curl -X POST "https://public.api.paragraph.com/api/v1/posts/3T2PQZlsdQtigUp4fhlb/bucket" \
  -H "Authorization: Bearer your-api-key"
{ "bucketId": "c4e1a9d2-30b7-4f68-8a15-2d9c6b0e7f43" }

Then pass that ID as bucketId on everything you draft from the post. PATCH takes bucketId too, for a draft you made before the group existed.

Unknown fields are rejected, not ignored. A misspelled key comes back as a 400 naming it, rather than a draft that quietly isn't grouped.

Read a group with GET /v1/buckets/{bucketId} to see what's already in it, so you don't draft a version that exists:

{
  "id": "c4e1a9d2-30b7-4f68-8a15-2d9c6b0e7f43",
  "title": "Writing in public",
  "members": [
    { "kind": "post", "id": "3T2PQZlsdQtigUp4fhlb", "channel": "post", "status": "published", "position": 0 },
    { "kind": "content", "id": "7f3a2c18-5b9e-4c21-9a0d-8e6b1f4d2a55", "channel": "tweet", "status": "draft", "position": 1 }
  ]
}

Members also carry title, url, and timestamps. kind tells you where to read a member: post from /v1/posts/{id}, content from /v1/content/{id}. Videos and a few older members report other, which this API can't fetch yet.

GET /v1/buckets lists every group, most recently active first, and GET /v1/posts/{postId}/bucket returns a post's group (or null) without creating one. Removing a piece from a group is done in the app.

Edit a draft

PATCH renames a piece, replaces its body, or both. body replaces the content entirely, in the same shape POST takes, so send all of it rather than just what changed. Images added in the app are kept.

curl -X PATCH "https://public.api.paragraph.com/api/v1/content/7f3a2c18-5b9e-4c21-9a0d-8e6b1f4d2a55" \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"body": {"text": "Rewritten, and shorter."}}'

A piece with a send already queued goes out exactly as written, so editing its body is refused, and lockedReason explains why on every read. Cancel the schedule in the app or through chat first. Renaming always works.

Archive and restore

POST /v1/content/{id}/archive puts a piece away without deleting it and removes it from the app's Content view. POST /v1/content/{id}/restore brings it back as a draft.

Publish and approve

Publishing or approving with an API key counts the same as approving in the app and runs the same checks before anything goes out. Anyone holding the key can do it, so keep keys with the people who approve your posts.

  • POST /v1/content/{id}/publish posts an X post, X article, or LinkedIn post now. Pass publishAt to schedule an X or LinkedIn post instead, up to 30 days out.
  • GET /v1/schedule lists what's on the Calendar: what your agents made and proposed times for (needsApproval), what's scheduled, what's on hold, what went out, and what failed.
  • POST /v1/schedule/{id}/approve approves one at its proposed time. Add "publishNow": true to send it now, which also retries a failed send. Approving also releases a hold.
  • POST /v1/schedule/approve approves everything waiting for approval, articles and newsletters included, except pieces on hold. Pass entries to approve only some.
  • POST /v1/schedule/{id}/reschedule moves one to a new publishAt. It needs approval again at its new time.
  • POST /v1/schedule/{id}/unschedule takes one off the Calendar without sending it. The draft stays in your library.
  • POST /v1/schedule/{id}/hold pauses one, with an optional reason, until someone approves it by name.
  • POST /v1/schedule/{id}/account posts an X or LinkedIn piece from another account, picked from GET /v1/schedule/accounts. It needs approval again.

Pass entryRevision and contentRevision from the list when approving, so the approval is refused if the piece changed after you read it.

From the CLI

The CLI wraps the same endpoints. --kind picks the kind, and the body flags follow it: --tweet (repeat it for a thread), --subject and --preheader for a newsletter, --headline and --canonical-url for an Article. Long text comes from --text, --file, or stdin.

paragraph content create --kind newsletter --title "October update" --subject "What we shipped" --file ./body.md
paragraph content update <id> --text "Rewritten, and shorter."
paragraph content archive <id>
paragraph content restore <id>
paragraph content publish <id> --at 2026-12-01T16:00:00Z

paragraph schedule list --status needs_approval
paragraph schedule approve --all
paragraph schedule retry <entry-id>
paragraph schedule reschedule <entry-id> --at 2026-12-02T16:00:00Z
paragraph schedule hold <entry-id> --reason "Waiting on legal"
paragraph schedule unschedule <entry-id>
paragraph schedule account <entry-id> --account <account-id>

paragraph content bucket create <post-id>
paragraph content create --kind tweet --title "Thread" --tweet "First." --bucket <bucket-id>
paragraph content bucket for-post <post-id>

Errors

StatusWhat it means
400The body doesn't match its kind, or a queued send has locked the piece. msg says which.
401Missing or invalid API key.
404No such piece, post, or group in this publication, or the publication hasn't been opened in the app yet.
409The piece is already grouped with another post. Remove it from that group in the app first.

On this page