Skip to content

API Endpoints

Base URL: https://app.nuelink.com/api/public/v1 Contract: OpenAPI 3.0.3, version 1.2.0

The API has 17 operations. 1.3.0-alpha added six: validate a token, read a brand’s weekly schedule, list posts across a brand, reschedule, re-queue, or draft a post, delete a post, and read published results with engagement.

The full machine-readable contract is published at /openapi-v1.2.0.yaml. Point a client generator or a request validator at it to work against the same spec these pages describe.

MethodPathDoesIntroduced
GET/meGet the authenticated profileOriginal alpha
GET/authValidate a token1.3.0-alpha
GET/brandsList brandsOriginal alpha
GET/brands/{brand_id}/scheduleRead the weekly queue schedule1.3.0-alpha
GET/brands/{brand_id}/channelsList channelsOriginal alpha
GET/brands/{brand_id}/collectionsList collectionsOriginal alpha
POST/brands/{brand_id}/collectionsCreate a collection1.2.0-alpha
GET/brands/{brand_id}/automationsList feed automations1.2.0-alpha
POST/brands/{brand_id}/automationsCreate a feed automation1.2.0-alpha
GET/brands/{brand_id}/mediaList media1.2.0-alpha
POST/brands/{brand_id}/mediaUpload mediaOriginal alpha
GET/brands/{brand_id}/collections/{collection_id}/postsList a collection’s posts1.2.0-alpha
GET/brands/{brand_id}/postsList a brand’s posts1.3.0-alpha
POST/brands/{brand_id}/collections/{collection_id}/postsCreate a postOriginal alpha
PATCH/brands/{brand_id}/posts/{post_id}Reschedule, re-queue, or draft a post1.3.0-alpha
DELETE/brands/{brand_id}/posts/{post_id}Delete a post1.3.0-alpha
GET/brands/{brand_id}/published-postsList published results1.3.0-alpha

All paths are relative to https://app.nuelink.com/api/public/v1.

Every request needs:

Authorization: Bearer YOUR_API_KEY
Accept: application/json

Requests with a JSON body (POST and PATCH) also need:

Content-Type: application/json

Media uploads are the exception: they’re sent as multipart/form-data.

Every list operation accepts page and per_page. See Pagination, Rate Limits, and Errors for the conventions that apply across all of them.

The API uses several time formats, and they don’t share a timezone. Mixing them up is the usual way a post lands an hour off.

FieldFormatTimezone
scheduledAt (you send it)Y-m-d H:i:s, e.g. 2026-10-12 09:00:00The brand’s
postDate (you read it back)Y-m-d H:i:sUTC
created_from, created_to, published_from, published_toY-m-d H:i:sUTC
Queue times: queues[].time, and time in the scheduleHH:mmThe brand’s
createdAt, updatedAt, joinedAt, publishedAtISO 8601, e.g. 2026-10-05T12:00:00.000000ZUTC
lastRun (automations)Unix timestamp, in seconds—

A post scheduled for 2026-10-12 09:00:00 in a brand set to Europe/London (UTC+1 in October) reads back as "postDate": "2026-10-12 08:00:00". Read the brand’s timezone from GET /brands before you schedule or compare times.


Returns the user the API key belongs to.

{
"status": "success",
"data": {
"id": "user_id",
"name": "Example User",
"joinedAt": "2026-01-01T00:00:00.000000Z",
"timezone": "UTC"
}
}

The id is an external hashed identifier, not the internal user ID.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/me"
FieldTypeDescription
idstringOpaque user identifier. Not the internal user ID; treat it as a string.
namestringDisplay name on the Nuelink account.
joinedAtstringISO 8601 timestamp of account creation.
timezonestring | nullThe user’s timezone as stored by Nuelink, usually an IANA name such as Europe/London. Treat it as an opaque string that can be null. This is the user’s timezone, not a brand’s: schedule with the brand’s.

/me and /auth count only against the 60-per-minute route limit, not the 30-per-minute resource limit, which makes them cheap health checks. Their X-RateLimit-* headers describe that larger bucket. See Rate Limits.

Validates a token. It returns the same profile as GET /me, so use whichever name reads better in your code: a 200 means the key works, and a 401 says why it doesn’t in errors.authorization.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/auth"

The 401 bodies are listed in Authentication.


Returns the brands the key’s user is an active member of, newest first.

{
"status": "success",
"data": [
{
"id": 1001,
"title": "Example Brand",
"description": "Example workspace",
"timezone": "Europe/London",
"queueStatus": "ACTIVE",
"createdAt": "2026-01-01T00:00:00.000000Z",
"updatedAt": "2026-01-15T12:00:00.000000Z"
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}

timezone is the one used to interpret scheduledAt for every post in that brand. Read it before you schedule anything.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands?page=1&per_page=25"
FieldTypeDescription
idintegerBrand identifier. Use as brand_id in every nested path.
titlestringDisplay name of the brand. Match on this when resolving a brand by name.
descriptionstring | nullOptional free-text description.
timezonestring | nullIANA timezone this brand schedules in, e.g. Europe/London. scheduledAt and queue times are interpreted here.
queueStatusstringACTIVE or PAUSED. While PAUSED, no queued post publishes from any of the brand’s collections.
createdAtstringISO 8601 timestamp.
updatedAtstringISO 8601 timestamp.

Every weekly queue slot in the brand, from every collection, grouped by day and ordered by time. Times are in the brand’s timezone. It isn’t paginated: one call returns the whole week.

{
"status": "success",
"data": {
"timezone": "Europe/London",
"queueStatus": "ACTIVE",
"schedule": {
"MON": [
{ "time": "09:00", "queueId": 55001, "collectionId": 2001, "collectionTitle": "Product Launches", "collectionStatus": "ACTIVE" }
],
"TUE": [],
"WED": [
{ "time": "14:30", "queueId": 55002, "collectionId": 2001, "collectionTitle": "Product Launches", "collectionStatus": "ACTIVE" }
],
"THU": [],
"FRI": [],
"SAT": [],
"SUN": []
}
}
}
Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/schedule"
FieldTypeDescription
timezonestring | nullThe brand’s timezone. Every time below is in it.
queueStatusstringACTIVE or PAUSED, as on GET /brands.
scheduleobjectKeyed MON through SUN. A day with no slots is an empty array.
schedule.*[].timestringHH:mm, 24-hour.
schedule.*[].queueIdintegerThe slot’s ID, the same value as queues[].id on the collection.
schedule.*[].collectionIdintegerThe collection that owns the slot.
schedule.*[].collectionTitlestringThat collection’s title.
schedule.*[].collectionStatusstringACTIVE or PAUSED.

A slot only publishes queued posts when the brand’s queueStatus and the slot’s collectionStatus are both ACTIVE.

To change slots, use the dashboard. The API sets a collection’s slots when it creates the collection, but can’t edit them afterwards.


Returns the brand’s active and inactive channels, newest first.

{
"status": "success",
"data": [
{
"id": 5001,
"name": "Example Instagram Account",
"status": "ACTIVE",
"type": "Instagram",
"createdAt": "2026-01-01T09:00:00.000000Z",
"updatedAt": "2026-01-15T14:30:00.000000Z"
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}

Channels are for inspection and for building collections; the create-post endpoint does not accept channel IDs.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/channels?page=1&per_page=25"
FieldTypeDescription
idintegerChannel identifier. Use it in channels when creating a collection, and in platforms.youtube.playlists[].channelId.
namestringDisplay name of the connected social account.
statusstringACTIVE (connected and working) or INACTIVE (disconnected, or needs re-auth). An INACTIVE channel will not receive posts.
typestringPlatform type. See the values below.
createdAtstringISO 8601 timestamp of when the channel was connected.
updatedAtstringISO 8601 timestamp.

Facebook, Instagram, Threads, LinkedIn, Google Business, YouTube, TikTok, Pinterest, X, Mastodon, Bluesky, Telegram, Social Media.

Switch on these, but handle an unrecognized type without failing: new platforms are added over time.


Returns the brand’s collections, newest first, each with its status, evergreen settings, channels, weekly queue, and post counts.

{
"status": "success",
"data": [
{
"id": 2001,
"title": "Product Launches",
"description": "Launch content",
"status": "ACTIVE",
"evergreen": { "enabled": true, "maxRepublish": 2 },
"maxRepublish": 2,
"timezone": "Europe/London",
"createdAt": "2026-01-01T09:00:00.000000Z",
"updatedAt": "2026-01-02T10:30:00.000000Z",
"channels": [
{ "id": 5001, "name": "Example Instagram Account", "status": "ACTIVE", "type": "Instagram" }
],
"queues": [
{ "id": 55001, "day": "MON", "time": "09:00", "date": "MON 09:00", "status": "PENDING" },
{ "id": 55002, "day": "WED", "time": "14:30", "date": "WED 14:30", "status": "PENDING" }
],
"postsCount": { "queued": 12, "scheduled": 1, "drafts": 3, "published": 40 }
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}

The embedded channels array is where a post to this collection will go.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/collections?page=1&per_page=25"
FieldTypeDescription
idintegerCollection identifier. Use as collection_id when creating or listing posts.
titlestringDisplay name. Match on this when resolving a collection by name.
descriptionstring | nullOptional free-text description.
statusstringACTIVE or PAUSED. A paused collection doesn’t publish queued posts.
evergreen.enabledbooleanWhether published posts are added back to the queue.
evergreen.maxRepublishinteger0 turns evergreen off, -1 republishes forever, and any other value N re-adds published posts after N weeks.
maxRepublishintegerThe same value as evergreen.maxRepublish.
timezonestring | nullTimezone of the queue times: the brand’s.
channelsarrayChannels this collection publishes to, active or inactive. Each carries id, name, status, and type, with no timestamps. Unavailable channels and Pinterest account-container records are left out.
queuesarrayWeekly slots, ordered Monday to Sunday.
queues[].idintegerSlot identifier, the same value as queueId in GET /schedule.
queues[].daystringMON through SUN.
queues[].timestringHH:mm, in the brand’s timezone.
queues[].datestringday and time together, e.g. MON 09:00.
queues[].statusstringPENDING or PUBLISHED.
postsCountobjectqueued, scheduled, drafts, and published counts. Returned by this list only, not by create.
createdAtstringISO 8601 timestamp.
updatedAtstringISO 8601 timestamp.

queues holds one collection’s slots. For every slot in the brand at once, across collections, use GET /schedule.

Create a collection, with its default channels and its weekly queue, in one call.

Body

FieldTypeRequiredNotes
titlestringYes1–255 characters
descriptionstring | nullNoMax 1000 characters
maxRepublishinteger | nullNoEvergreen. 0, the default, turns it off; N re-adds published posts to the queue after N weeks
channelsinteger[] | nullNoChannel IDs from this brand. Defaults to []
queuesstring[] | nullNoWeekly slots in the brand’s timezone, format Mon 09:00: a three-letter day and 24-hour time. Defaults to []

Queue limits. Verified users can create up to 10 slots per collection per day, unverified users up to 5. Duplicate or overlapping slots are not rejected, so de-duplicate before you send.

Terminal window
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/collections" \
--data '{
"title": "Example Collection",
"description": "Example collection description",
"maxRepublish": 2,
"channels": [5001, 5002],
"queues": ["Mon 09:00", "Wed 14:30", "Fri 18:00"]
}'

Returns 201 with the created collection, in the shape GET /collections returns, minus postsCount. Each queue slot gets its own id, and the day comes back uppercase: Mon 09:00 is returned as "day": "MON", "time": "09:00", "date": "MON 09:00".

{
"status": "success",
"data": {
"id": 2002,
"title": "Example Collection",
"description": "Example collection description",
"status": "ACTIVE",
"evergreen": { "enabled": true, "maxRepublish": 2 },
"maxRepublish": 2,
"timezone": "Europe/London",
"createdAt": "2026-10-05T09:00:00.000000Z",
"updatedAt": "2026-10-05T09:00:00.000000Z",
"channels": [
{ "id": 5001, "name": "Example Instagram Account", "status": "ACTIVE", "type": "Instagram" }
],
"queues": [
{ "id": 55101, "day": "MON", "time": "09:00", "date": "MON 09:00", "status": "PENDING" },
{ "id": 55102, "day": "WED", "time": "14:30", "date": "WED 14:30", "status": "PENDING" },
{ "id": 55103, "day": "FRI", "time": "18:00", "date": "FRI 18:00", "status": "PENDING" }
]
}
}

Field meanings are in the list response table.

Errors worth handling

  • 400 - a channel ID doesn’t belong to this brand, or a day has more slots than the user’s limit allows.
  • 403 - the brand hit its collections limit on the current plan.
  • 404 - brand not found, or you’re not a member of it.

Lists the brand’s feed automations, newest first. Automations of other kinds aren’t returned.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/automations?page=1&per_page=25"

Each automation returns id, name, collectionId, status, type, feedUrl, runCount, importAsType, lastRun (Unix timestamp), title, caption, createdAt, and updatedAt.

{
"status": "success",
"data": [
{
"id": 8812,
"name": "NASA Breaking News",
"collectionId": 720,
"status": "ACTIVE",
"type": "RSS",
"feedUrl": "https://www.nasa.gov/rss/dyn/breaking_news.rss",
"runCount": 42,
"importAsType": "IMAGE",
"lastRun": 1767225600,
"title": "{{title}}",
"caption": "{{title}} {{link}}",
"createdAt": "2026-01-01T09:00:00.000000Z",
"updatedAt": "2026-01-15T14:30:00.000000Z"
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}
FieldTypeDescription
idintegerAutomation identifier.
namestringThe automation’s name.
collectionIdintegerThe collection imported posts land in.
statusstringCurrent state of the automation, e.g. ACTIVE.
typestringFeed source, one of the 27 values listed under POST /automations.
feedUrlstringThe source being polled.
runCountintegerHow many times the automation has run.
importAsTypestring | nullLINK, IMAGE, VIDEO, or CAROUSEL.
lastRunintegerUnix timestamp in seconds, not an ISO 8601 string like the other date fields on this page. 0 means it hasn’t run yet.
titlestring | nullTitle template applied to each imported post.
captionstring | nullCaption template applied to each imported post.
createdAtstringISO 8601 timestamp.
updatedAtstringISO 8601 timestamp.

Create a feed automation. Nuelink fetches and parses the feed during the request, so an unreachable or malformed feed fails here rather than later.

Body

FieldTypeRequiredNotes
namestringYes1–255 characters
collectionIdintegerYesA collection in this brand. Imported posts land there
typestringYesThe feed source, see below
feedUrlstring (uri)YesMust be fetchable and parseable
importAsTypestringYesLINK, IMAGE, VIDEO, or CAROUSEL
titlestring | nullNoTitle template for each imported post, max 255 characters. Defaults to {{title}}
captionstring | nullNoCaption template for each imported post, max 255 characters
loadOldPostsboolean | nullNoDefaults to false: only new feed items are imported. true also imports the items already in the feed, in the background
addPostsAsDraftboolean | nullNoDefaults to false. true imports posts as drafts instead of queueing them
refreshRateinteger | nullNoHours between checks: 1, 6, 12, or 24, the default

Supported type values

YOUTUBE, RSS, ATOM, MEDIUM, SHOPIFY, WORDPRESS, WOOCOMMERCE, ETSY, GHOST, SUBSTACK, ANCHOR, TRANSISTOR, CAPTIVATE, SOUNDCLOUD, BLOGGER, WIX, SQUARESPACEBLOG, SQUARESPACESHOP, WEEBLY, TUMBLR, SHOPIFY2, WOOCOMMERCE2, PRESTASHOP, BUZZSPROUT, RSSCOM, CASTOS, PODCASTCO

Terminal window
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/automations" \
--data '{
"name": "NASA Breaking News",
"collectionId": 720,
"type": "RSS",
"feedUrl": "https://www.nasa.gov/rss/dyn/breaking_news.rss",
"importAsType": "IMAGE",
"title": "{{title}}",
"caption": "{{title}} {{link}}",
"loadOldPosts": false,
"addPostsAsDraft": true
}'

Templates shape each imported post. title and caption take placeholders that Nuelink fills from each feed item at import time. {{title}}, {{description}}, and {{link}} are the common ones; which placeholders a feed offers depends on its type.

Returns 201 with the created automation, in the same shape GET /automations returns:

{
"status": "success",
"data": {
"id": 8812,
"name": "NASA Breaking News",
"collectionId": 720,
"status": "ACTIVE",
"type": "RSS",
"feedUrl": "https://www.nasa.gov/rss/dyn/breaking_news.rss",
"runCount": 0,
"importAsType": "IMAGE",
"lastRun": 0,
"title": "{{title}}",
"caption": "{{title}} {{link}}",
"createdAt": "2026-10-05T09:00:00.000000Z",
"updatedAt": "2026-10-05T09:00:00.000000Z"
}
}

lastRun is 0 until the first run. The automation starts polling after creation, so runCount and lastRun change on their own. Field meanings are in the list response table.

Errors worth handling

  • 400 - Nuelink couldn’t fetch or parse the feed. Check the URL in a browser first.
  • 403 - the account hit its automations limit on the current plan.
  • 404 - the brand or the target collection doesn’t exist, or isn’t yours.
  • 422 - a field failed validation. Under the 1.2.0 contract, a request in the 1.2.0-alpha shape is invalid: name is required, and FEED isn’t a type value.

Lists the media in a brand’s library, newest first. Each item returns an opaque string id, name, type, size, createdAt, and updatedAt.

Pass the optional type query parameter to filter the library: IMAGE, VIDEO, GIF, APPLICATION, DOCUMENT, or CSV. APPLICATION returns everything stored as a document; DOCUMENT narrows that to office and document file types. Omit type to return everything.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/media?type=IMAGE&page=1&per_page=25"

The returned string id can be passed unchanged as media[].id when creating a post. Treat it as opaque: do not decode it, coerce it to a number, or infer storage details from its value.

{
"status": "success",
"data": [
{
"id": "encoded_media_id",
"name": "launch.jpg",
"type": "image/jpeg",
"size": 245678,
"createdAt": "2026-01-01T09:00:00.000000Z",
"updatedAt": "2026-01-01T09:00:00.000000Z"
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}
FieldTypeDescription
idstringOpaque media ID. Pass unchanged as media[].id when creating a post.
namestring | nullOriginal filename as uploaded, when known.
typestringMIME type, e.g. image/jpeg, not the type filter value. The filter takes categories (IMAGE, VIDEO, …); the field returns a MIME type.
sizeintegerFile size in bytes.
createdAtstringISO 8601 timestamp.
updatedAtstringISO 8601 timestamp.
POST /brands/{brand_id}/media
Content-Type: multipart/form-data

The multipart field name is media, one file per request: JPEG, PNG, BMP, MP4, MOV, or PDF, up to 100 MiB (104,857,600 bytes). Infrastructure limits can reject a large upload before it reaches that size, so test with your real files. PDF is accepted for LinkedIn only, where it publishes as a document post; other platforms in the collection will skip it.

webp is no longer accepted. Convert those files to PNG or JPEG first.

The REST API never takes media as base64. Upload the file as multipart, or skip uploading altogether and pass a public url in the post’s media array.

Terminal window
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/media" \
-F "media=@./launch.jpg"
{
"status": "success",
"data": {
"id": "encoded_media_id",
"type": "image/jpeg",
"size": 245678
}
}

Keep this id. Media IDs are opaque strings throughout list, upload, and post creation. Pass the value unchanged as media[].id.

FieldTypeDescription
idstringOpaque media ID. Pass unchanged as media[].id when creating a post.
typestringMIME type detected by the server, e.g. image/jpeg.
sizeintegerFile size in bytes.

Rejected uploads come back as 422, with the accepted types in the message. Read that list rather than hardcoding one — it is the authoritative answer at runtime:

{
"message": "The given data was invalid.",
"errors": {
"media": ["The media must be a file of type: ..."]
}
}
{
"message": "The given data was invalid.",
"errors": {
"media": ["The media file may not be greater than 100 megabytes."]
}
}

GET /brands/{brand_id}/collections/{collection_id}/posts

Section titled “GET /brands/{brand_id}/collections/{collection_id}/posts”

Lists a collection’s posts, newest first. Filter on the server with the query parameters below. Every filter is optional, and they combine with AND.

ParameterValuesNotes
viewQUEUE, SCHEDULED, DRAFT, PUBLISHEDShortcut for queued posts, posts with a fixed date, drafts, or published posts
statusPENDING, DRAFT, PUBLISHEDLifecycle status
post_typeTEXT, IMAGE, VIDEO, MULTIMEDIA, SHORT, DOCUMENT, THREAD, SLIDES, REEL, TIKTOK, STORY, POLL, ENGAGE_COMMENTContent type
posting_typeNOW, SCHEDULE, QUEUE, DRAFTPublishing mode
created_fromY-m-d H:i:s, UTCCreated at or after this time
created_toY-m-d H:i:s, UTCCreated at or before this time. Can’t precede created_from
sort_byid (default), title, post_date, created_at, updated_atTies break on post ID, in the same direction
sort_orderdesc (default), asc

Plus page and per_page. Encode the space in a timestamp filter (created_from=2026-10-01%2000:00:00), or let your HTTP client build the query string.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/collections/$COLLECTION_ID/posts?view=DRAFT&page=1&per_page=25"

Each post returns id, collectionId, title, body, media[], postType, postingType, status, postDate, createdAt, updatedAt, and a poll object on polls.

{
"status": "success",
"data": [
{
"id": 4364709,
"collectionId": 2001,
"title": "Spring Collection Launch",
"body": "Our new Spring Collection is live.",
"media": [{ "id": "encoded_media_id" }],
"postType": "IMAGE",
"postingType": "DRAFT",
"status": "DRAFT",
"postDate": null,
"createdAt": "2026-01-01T09:00:00.000000Z",
"updatedAt": "2026-01-01T09:00:00.000000Z"
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}
FieldTypeDescription
idintegerPost identifier, the same value POST .../posts returned.
collectionIdintegerThe collection this post belongs to.
titlestring | nullOptional title.
bodystring | nullThe caption. You send caption on create and read body back — the field is named differently in each direction. On a poll, body is null and the caption is in poll.caption.
mediaarrayAttached media, each item carrying an id only.
postTypestringDerived by the server from the media, poll, story, and thread inputs, e.g. IMAGE, THREAD, or POLL. The full list is under post_type above.
postingTypestringNOW, SCHEDULE, QUEUE, or DRAFT. A post created with IMMEDIATE reads back as NOW.
statusstringLifecycle status, e.g. PENDING, DRAFT, or PUBLISHED.
postDatestring | nullThe scheduled time in UTC, formatted Y-m-d H:i:s: not ISO 8601, and not the brand-local value you sent as scheduledAt. null when no time is set, e.g. on a draft.
createdAtstringISO 8601 timestamp.
updatedAtstringISO 8601 timestamp.
pollobjectPresent only on polls, with caption, question, options, period, correctOptionIndex, explanation.

This is how you verify a create actually did what you asked. A saved draft reads back as:

{
"postingType": "DRAFT",
"status": "DRAFT",
"postDate": null
}

Read it back without a view or status filter. If the mode was wrong, a filter would hide the post instead of showing you the mistake. With the default sort a post you just created is normally first, but match on the id the create returned rather than on position: another client can create a post between your two calls.

Lists posts across every collection in the brand. It takes the same filters as the collection endpoint, plus collection_id to narrow it to one collection, and returns the same post shape.

view answers the common questions in one call: QUEUE for what’s waiting in the queues, SCHEDULED for what’s booked at a fixed time, DRAFT for what’s waiting for review, and PUBLISHED for what went out.

Terminal window
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/posts?view=SCHEDULED&sort_by=post_date&sort_order=asc&page=1&per_page=25"

Sorting on post_date ascending lists scheduled posts earliest first. postDate is UTC, so convert it to the brand’s timezone before you show it to anyone.

POST /brands/{brand_id}/collections/{collection_id}/posts

Section titled “POST /brands/{brand_id}/collections/{collection_id}/posts”

Creates a text, media, story, thread, document, or poll post in a collection. The collection’s channels receive it; you don’t pass channel IDs. The rules:

  • publishMode is required, one of IMMEDIATE, SCHEDULE, QUEUE, DRAFT.
  • scheduledAt is required when publishMode=SCHEDULE: format Y-m-d H:i:s, in the brand’s timezone, and at least 10 minutes in the future. Nuelink stores it in UTC, and you read it back as postDate.
  • caption is required unless you provide a poll. Max 3000 characters.
  • Each media[] item must have exactly one of id or url.
  • A caption split by triple newlines becomes a thread of up to 10 segments. See Threads.
  • A brand can hold a maximum of 15,000 posts in total. Collection, scheduled-post, and plan limits also apply.

Optional content fields: title (max 255), alt (max 255), link (max 1000), postAsStory, autoThreadText.

Optional structured fields: poll, comment, and platforms for Instagram, Facebook, TikTok, YouTube, and Google Business Profile.

Those three are covered in full, with field rules and JSON examples for every platform, in Platform-Specific Options. Platform options are stored for compatible channels; each platform’s own publishing constraints still apply when the post goes out.

FieldTypeRequiredDescription
publishModestringYesQUEUE, SCHEDULE, IMMEDIATE, or DRAFT.
captionstringYes (unless poll is set)Main post text. 1–3000 characters. null counts as missing.
pollobjectRequired if no captionPoll configuration. A caption sent alongside becomes the poll’s caption. See Polls and quizzes.
scheduledAtstringYes (if publishMode is SCHEDULE)Format Y-m-d H:i:s, e.g. 2026-10-10 12:12:00. Interpreted in the brand’s timezone, and must be at least 10 minutes in the future.
titlestringNoUsed by YouTube, Pinterest, and similar. Max 255 characters.
altstringNoAlt text for accessibility. Max 255 characters.
linkstringNoDestination URL, max 1000 characters. Google Business Profile uses it for the post’s LEARN_MORE button, and Pinterest as the Pin’s destination link. See Links.
postAsStorybooleanNoPublish as a Story on channels that support them (Instagram and Facebook). Other platforms in the collection publish a regular feed post.
autoThreadTextbooleanNoTurns on Nuelink’s automatic text splitting. See Threads.
commentobjectNoAuto-comment posted after publishing. delay and comment are both required when it’s present. See Global auto-comment.
mediaarrayNoAttachments. Each item carries an id from /media or a public url, never both. Nuelink downloads a url during the request.
platformsobjectNoPer-platform non-content options. See Platform-Specific Options.
Terminal window
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/collections/$COLLECTION_ID/posts" \
--data '{
"title": "Spring Collection Launch",
"caption": "Our new Spring Collection is live. Link in bio to shop.",
"alt": "Flat lay of skincare bottles on fresh herbs.",
"publishMode": "SCHEDULE",
"scheduledAt": "2026-10-10 12:12:00",
"media": [{ "id": "encoded_media_id" }]
}'

A DRAFT version of the same call is in the Quick Start, and a multi-platform example is in Platform-Specific Options.

{
"status": "success",
"data": {
"id": 4364709,
"message": "Post created successfully"
}
}

The id is the Nuelink post ID. The response doesn’t report delivery: read the post back with GET .../posts to confirm what was created, and once it has gone out, check each channel’s result with GET /published-posts filtered on this id. That endpoint returns only PUBLISHED entries by default, so ask for the other statuses too before you conclude every channel received the post.

Missing caption with no poll — 422

{
"message": "The given data was invalid.",
"errors": {
"caption": ["The caption field is required when poll is not present."]
}
}

scheduledAt less than 10 minutes away — 422

{
"status": "error",
"message": "Scheduled post date must be at least 10 minutes in the future",
"errors": {
"scheduledAt": "Scheduled post date must be at least 10 minutes in the future"
}
}

Before 1.3.0-alpha this case was documented as 400. Match it as a 422.

A media[].url Nuelink could not fetch — 400

{
"status": "error",
"message": "Failed to download media from url: https://cdn.example.com/missing.jpg",
"errors": {
"media": "Failed to download media from url: https://cdn.example.com/missing.jpg"
}
}

A media[].id that isn’t in this brand — 404

{
"status": "error",
"message": "Media with id encoded_media_id not found",
"errors": {
"media": "Media with id encoded_media_id not found"
}
}

Brand post limit reached — 403

{
"status": "error",
"message": "This brand has reached the maximum total number of posts allowed (15,000).",
"errors": {
"limits": "This brand has reached the maximum total number of posts allowed (15,000)."
}
}

A 403 also covers the scheduled-and-queued post limit on your plan, and a brand you’re not a member of (errors.brand_id: You are not a member of this brand).

The two 422 bodies above have different shapes: the missing caption maps the field to an array, the scheduling rule to a string. See Errors.

Changes when, or whether, a post publishes: reschedule it, put it in its collection’s queue, move it to the front or back of that queue, or turn it into a draft. It works on drafts, queued posts, and scheduled posts. A post’s content — caption, media, options — can’t be edited through the API.

Body. Send at least one of publishMode or queuePosition.

FieldTypeNotes
publishModestringDRAFT turns the post into a draft. SCHEDULE reschedules it and needs scheduledAt. QUEUE puts it in its collection’s queue. IMMEDIATE isn’t accepted here.
scheduledAtstringRequired with SCHEDULE, and not allowed without it. Brand-local Y-m-d H:i:s, at least 10 minutes in the future.
queuePositionstringFRONT or BACK. Moves a queued post to that end of its collection’s queue, on its own or together with publishMode: QUEUE. There’s no way to set an arbitrary position.
Terminal window
curl --fail-with-body --silent --show-error \
-X PATCH \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/posts/$POST_ID" \
--data '{
"publishMode": "SCHEDULE",
"scheduledAt": "2026-10-12 09:00:00"
}'

The other bodies you’ll send most:

ToSend
Make a queued post the next one out{ "queuePosition": "FRONT" }
Take a post out of the schedule or queue{ "publishMode": "DRAFT" }
Queue a draft and publish it next{ "publishMode": "QUEUE", "queuePosition": "FRONT" }

Returns 200 with the updated post, in the same shape the post lists return. The post rescheduled above, in a Europe/London brand, reads back with its postDate in UTC:

{
"status": "success",
"data": {
"id": 4364709,
"collectionId": 2001,
"title": "Spring Collection Launch",
"body": "Our new Spring Collection is live.",
"media": [{ "id": "encoded_media_id" }],
"postType": "IMAGE",
"postingType": "SCHEDULE",
"status": "PENDING",
"postDate": "2026-10-12 08:00:00",
"createdAt": "2026-10-05T09:00:00.000000Z",
"updatedAt": "2026-10-05T14:00:00.000000Z"
}
}

Errors worth handling

  • 409 - the post is already published. Only draft, queued, or scheduled posts can be changed.
  • 422 - the body broke a rule: neither publishMode nor queuePosition, scheduledAt missing with SCHEDULE or sent without it, or a time less than 10 minutes away.
  • 403 - you’re not a member of the brand, or a scheduled-post limit was reached.
  • 404 - the post doesn’t exist in this brand.
{
"status": "error",
"message": "Only draft, queued or scheduled posts can be edited",
"errors": {
"post_id": "Only draft, queued or scheduled posts can be edited"
}
}

Permanently deletes a post. It can’t be undone.

Before you delete, read the post back and confirm the brand and post ID. If you only need it not to go out, PATCH it to DRAFT instead: that’s reversible.

Terminal window
curl --fail-with-body --silent --show-error \
-X DELETE \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/posts/$POST_ID"

Returns 200:

{
"status": "success",
"data": {
"id": 4364709,
"message": "Post deleted successfully"
}
}

Errors worth handling

  • 403 - sensitive actions aren’t enabled for the account. The body names the permissions key, shown below.
  • 404 - the post doesn’t exist in this brand.
{
"status": "error",
"message": "Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it.",
"errors": {
"permissions": "Deleting posts through the API is disabled. Enable sensitive actions for AI in your API settings to allow it."
}
}

What actually went out: one entry per channel, with the platform link and the engagement results. Use it to confirm delivery, find failures, and compare performance. Results are newest first, and include only PUBLISHED entries unless you ask for another status.

ParameterValuesNotes
statusPUBLISHED (default), FAILED, RETRYING, SKIPPED, SENTDelivery status, one per request. Entries in other statuses are left out
collection_idinteger
channel_idinteger
post_idintegerThe Nuelink post the entries came from: the id that POST .../posts returned
post_typestring
searchstring, max 255Case-insensitive match on the published text
published_fromY-m-d H:i:s, UTCPublished at or after this time
published_toY-m-d H:i:s, UTCPublished at or before this time. Can’t precede published_from
sort_byid (default), created_at, likes, comments, shares
sort_orderdesc (default), asc

Plus page and per_page.

Terminal window
curl --fail-with-body --silent --show-error --get \
-H "Authorization: Bearer $NUELINK_API_KEY" \
-H "Accept: application/json" \
--data-urlencode "sort_by=likes" \
--data-urlencode "published_from=2026-09-01 00:00:00" \
--data-urlencode "per_page=25" \
"https://app.nuelink.com/api/public/v1/brands/$BRAND_ID/published-posts"
{
"status": "success",
"data": [
{
"id": 120045,
"postId": 4364709,
"collectionId": 2001,
"channel": { "id": 5001, "name": "Example Instagram Account", "type": "Instagram" },
"postType": "IMAGE",
"body": "Our new Spring Collection is live.",
"url": "https://www.instagram.com/p/EXAMPLE/",
"status": "PUBLISHED",
"message": null,
"publishedAt": "2026-10-10T11:12:00.000000Z",
"results": { "likes": 42, "comments": 5, "shares": 3 }
}
],
"pagination": { "currentPage": 1, "perPage": 25, "total": 1, "lastPage": 1, "nextPageUrl": null, "prevPageUrl": null }
}
FieldTypeDescription
idintegerThis entry’s ID: one per post per channel. Not the post ID.
postIdinteger | nullThe Nuelink post this was published from.
collectionIdinteger | nullThat post’s collection.
channelobjectThe channel it went to: id, name, and type, any of which can be null.
postTypestring | nullContent type, e.g. IMAGE.
bodystring | nullThe text as published.
urlstring | nullLink to the post on the platform.
statusstringOne of the status values above.
messagestring | nullA status message, when there is one.
publishedAtstringISO 8601 timestamp.
resultsobjectEngagement counts: likes, comments, and shares.

Three questions it answers:

  • Did my post go out everywhere? Filter on post_id, but one call isn’t enough: with the default status, you only see the channels where the post was published. Repeat it with status set to FAILED, RETRYING, SKIPPED, and SENT, walking every page of each, or compare the published entries with the collection’s channels to find the ones missing.
  • Did anything fail? Filter on status=FAILED to see which channels a post didn’t reach.
  • What performed best? Sort on likes, comments, or shares, inside a published_from and published_to window.