API documentation

Read and write access to vessel feeds, posts, media, and comments. Build integrations, widgets, custom readers, and publishing tools.

authentication

All requests use a Bearer token in the Authorization header:

Authorization: Bearer <token>

OAuth grants cover one vessel. Account settings, personal filters, and activity attributed to another vessel are unavailable to apps. Public content remains readable subject to its access rules. Reauthorizing replaces the approved scopes and invalidates the previous tokens.

An OAuth app can call the endpoints on this page and nothing else: every other route answers 403 endpoint_not_available. A token that is expired, revoked or malformed answers 401 invalid_token (with WWW-Authenticate) everywhere, public reads included, so refresh and retry rather than reading it as logged out.

personal use → your own token

Building something that only you use? Use your personal API token. It acts as you with full access to all your vessels. There's no scoping, so don't share it. You can rotate the token by logging out with the "Log out all other devices" option checked.

Sign in to get your API key.

building for other users → create an OAuth app

If your code acts on behalf of other people, you must register an OAuth app. Each user grants your app access to a specific vessel with read, write and, for staff tools, manage scope (manage needs write), and they can revoke any time.

Register at /developer/apps · step-by-step curl walkthrough at /developer/oauth.

Each endpoint below carries a read, write or manage chip showing the OAuth scope it requires. Personal API keys are unscoped and can call every endpoint on this page; a handful of browser-session-only endpoints (and direct messages) require a web session.

what's stable

The endpoints documented on this page (plus the OAuth flow at /developer/oauth) are the public, stable surface: feeds, follow, posts, media, likes, comments, and forums. Their paths, fields, error codes, and block types won't change incompatibly once published.

Event attendee exports are also stable and require event-manager permissions.

ticket lookup, check-in and undo are also stable and require check-in staff permissions.

The app serves other /api/v1/* routes (RSVP, polls, reports, bans/mutes, collections, the map, event browsing and attendee UI lists, search) to power its own web client. OAuth apps cannot call them at all (403 endpoint_not_available). A personal token can reach them because it's unscoped, but they're internal and may change without notice: don't build against them. Direct messages are intentionally unreachable by any token.

idempotency

If we save a post but the connection drops before you see the response, you can’t safely retry — you might post twice. Send an Idempotency-Key header on POST /api/v1/pst/create, /api/v1/pst/repost and /api/v1/pst/quote and retrying is safe.

Idempotency-Key: 9f2b7c1e-4a55-4d0e-9c1a-3b7e5f0d2a11
  • Any 1–255 printable ASCII characters, no spaces. A malformed key is a 400, never silently ignored — you’re never unprotected without being told.
  • Keys are scoped to your app/account and the target vessel, and are kept for 24 hours.
  • Same key, same payload → the original response replayed, with meta.idempotent_replay: true and an Idempotency-Replayed header. No second post, no second quota charge.
  • Same key, different payload → idempotency_key_reused (409).
  • Retried while the first is still running → idempotency_in_progress (409) with Retry-After.
  • idempotency_unavailable (503) is always safe to retry with the same key.

Always retry with the same key. Minting a new one for a retry is exactly the duplicate this prevents.

request ids

Every response carries a correlation id as the X-Request-Id header and as meta.request_id in the envelope, on successes and errors alike. It’s in our server logs too, so quoting one in a bug report finds the exact request.

You can supply your own with X-Request-ID: 1–64 characters from A-Z a-z 0-9 . _ -. Anything else is replaced with a generated id rather than rejected.

Publicly cacheable responses carry no id, by design — they’re served from a shared cache, so a per-request value in one would be handed to every later reader.

response format

All endpoints return a standard envelope. The shape of data depends on the endpoint:

single object (e.g. vessel profile)

{
"ok": true,
"data": { "short": "...", "name": "...", ... }
}

paginated list (e.g. feed endpoints)

{
"ok": true,
"data": { "items": [...] },
"meta": {
"page": {
"nextCursor": "eyJ...",
"hasMore": true
}
}
}

For paginated endpoints, pass ?c=<nextCursor> to fetch the next page.

error

{
"ok": false,
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}

error with details (e.g. password-protected vessel)

{
"ok": false,
"error": {
"code": "vessel_access_required",
"message": "Access required for this vessel.",
"details": { "vesselShort": "abc123XYZ45" }
}
}

Some errors include error.details with machine-readable context. See password-protected vessels for the unlock flow.

lexicon

nnyyxx
The primordial Greek goddess of night, a figure of immense power who personifies darkness.
account
A person on nnyyxx. One account can own and collaborate on multiple vessels.
vessel
A profile or site on nnyyxx. Vessels have their own URL, theme, posts, and followers. An account can own multiple vessels.
owner
Full access to a vessel. Cannot be restricted. Every vessel has exactly one owner account.
admin
Can manage a vessel. Granular permissions: manage admins, manage collaborators, change settings, edit any post.
collaborator
Can contribute to a vessel. Granular permissions: publish, edit own posts, delete own posts, moderate, RSVP, like, show via attribution.
theme
A vessel's visual style: accent color, background color, title color, font, layout, background image, and post display settings.
post
A published piece of content on a vessel. Posts can be text, photosets, videos, links, or events.
draft
An unpublished post that's still being worked on. Drafts are only visible to the vessel's members.
queue
A line of posts waiting to be auto-published at regular intervals throughout the day, based on the vessel's queue settings.
scheduled
A post set to publish at a specific date and time chosen by the author.

vessels

vessel profile

GET /api/v1/vsl/:slug

Look up a vessel by its slug and get profile info including name, description, links, pages, and theme.

read

your vessels

GET /api/v1/act/vsls

List all vessels your account has access to, with your role, permissions, and via attribution for each.

read

reading posts

vessel posts

GET /api/v1/feed/vsl/:vessel_short/pst

Paginated JSON feed of published posts for a vessel. Returns structured post objects with media, tags, location, and type-specific data.

read
Response shape
{
"type": "post",
"subType": "text|photoset|video|event|link",
"short": "aBc123dEfGh",
"ts": 1712000000000,
"created": "2026-04-01T12:00:00.000Z",
"edited": "2026-04-01T13:00:00.000Z",
"vslName": "My Vessel",
"vslUrl": "my-vessel",
"content": "<p>Post HTML content</p>",
"media": [{ "short", "fileShort", "url", "w", "h", "th", "caption", "ext" }],
"ps": [{ ... }],       // photoset images
"psl": "321",          // photoset layout
"video": { "status", "w", "h", "duration", "src", "th" },
"link": { "url", "title", "description", "image", "domain" },
"event": { ... },
"loc": { "name", "venue", "geo", "lat", "lon" },
"tags": [{ "display": "Art", "canon": "art", "type": 0 }],
"client": { "kind": "app", "name", "url", "icon" }  // only on API-created posts; {"kind":"api"} for a personal token
}

tag feed

GET /api/v1/feed/t/:tag

Global tag feed as JSON. Returns posts from all vessels tagged with the given tag, newest first. Tag names are auto-canonicalized (lowercased, trimmed).

read

drafts

GET /api/v1/feed/vsl/:vessel_short/drafts

Unpublished draft posts for a vessel. Requires authentication and publish permission on the vessel.

read

queue

GET /api/v1/feed/vsl/:vessel_short/queue

Posts in the publish queue for a vessel, waiting to be auto-published. Requires authentication and publish permission.

read

scheduled

GET /api/v1/feed/vsl/:vessel_short/scheduled

Posts scheduled for future publication. Each item includes a scheduledDate field. Requires authentication and publish permission.

read

event attendees

manage

export attendees

GET /api/v1/event/:vesselShort/:postShort/attendees/export

Retrieve the attendee CSV fields as paginated JSON, including RSVP responses, check-in status and check-in time. Use the hosting vessel's short code and the event post's short code.

register an OAuth app and follow the Authorization Code + PKCE flow to obtain an access token with write scope on the hosting vessel, even though this is a GET request.

the authorizing account needs event-manager access: an owner, an admin with post-editing permission, or a collaborator with editing permission on their own event. permissions and authoring identities are restricted to the grant's vessel. check-in permission alone and read-only grants cannot export attendees.

Follow meta.page.nextCursor with a URL-encoded ?c= until meta.page.hasMore is false, including when a page is empty. Page size is server-controlled. Rows are newest RSVP first and read live on each request. Different attendees may RSVP as the same vessel; each remains a separate row.

response example with email sharing enabled

{
"ok": true,
"data": {
"items": [{
"vessel_name": "Guest",
"vessel_handle": "guest",
"response": "going",
"plus_ones": 1,
"responded_at": "2026-07-01T12:00:00.000Z",
"email": "[email protected]",
"checked_in": true,
"checked_in_at": "2026-08-01T20:15:00.000Z"
}]
},
"meta": { "page": { "nextCursor": null, "hasMore": false } }
}

vessel_name / vessel_handle: the attendee's chosen vessel identity.

response: going, maybe (interested), cant-go, or waitlist.

plus_ones: additional guests, excluding the responder. responded_at: RSVP creation time in ISO 8601 UTC, or null when unavailable.

checked_in: a boolean. checked_in_at: ISO 8601 UTC or null. A checked-in ticket can have an unavailable timestamp. Non-ticketed events return false and null.

email: included only while the event's email sharing is enabled. It is null for responders who did not consent or whose email is unavailable. Account IDs, staff identities, ticket codes and notes are never included. The internal attendee JSON/HTML lists do not export emails.

email access requires the event's current sharing setting and that individual RSVP's consent stamp. new RSVP saves require an explicit acknowledgement of the sharing notice; missing or malformed consent keeps the email private. consent never carries across attendees using the same vessel. disabling email sharing suppresses emails on subsequent export requests. ticket lookup and check-in never return emails, even with consent.

Errors: 401 unauthorized without authentication; 404 not_found for missing or unauthorized events, including insufficient OAuth scope; 400 invalid_request for an invalid cursor; 403 wrong_vessel for a custom-domain session bound to another vessel. Normal API rate limits apply (429 rate_limited). responses and CSV downloads are private, no-store.

ticket check-in

manage

build your own scanner

use the same QR tickets and live check-in state as the nnyyxx door page. register an OAuth app and follow the Authorization Code + PKCE flow. send the resulting access token using Authorization: Bearer YOUR_OAUTH_ACCESS_TOKEN.

all three operations require an owner, admin, or collaborator with verify event attendance (ck) permission on the hosting vessel. this covers all its events and does not grant attendee exports. OAuth requires the manage scope on that vessel, including for lookup. the QR identifies a ticket; it does not authorize staff access.

1. look up a scan

GET /api/v1/tickets/:code

the QR encodes https://nyx.now/tickets/CODE. parse the URL, verify its origin and /tickets/CODE path, then extract the opaque, case-sensitive code. call the API on your configured nnyyxx origin; do not follow a scanned URL with your authorization header. the printed ticket serial is a different identifier and cannot replace the code.

all three operations return live ticket state

{
"ok": true,
"data": {
"ticket": {
"serial": "TICKET_SHORT",
"event": {
"vessel_short": "VESSEL_SHORT",
"post_short": "POST_SHORT",
"title": "Meetup",
"checkin_enabled": true
},
"attendee": {
"vessel_short": "GUEST_SHORT",
"vessel_name": "Guest",
"vessel_handle": "guest"
},
"response": "going",
"plus_ones": 2,
"party_size": 3,
"valid": true,
"checked_in": false,
"checked_in_at": null,
"can_check_in": true
}
}
}

attendee is the chosen RSVP vessel, or null if deleted. different people can use the same vessel identity; identify each ticket by its code. account IDs, staff identities, emails, notes and the code itself are not returned.

response is going, maybe, cant-go, or waitlist (null for an unknown legacy value). plus_ones excludes the responder; party_size includes them. one check-in consumes the entire party's ticket.

valid means ticketing is enabled and the current response is going, even if already checked in. can_check_in additionally requires an unused ticket. checked_in_at is ISO 8601 UTC or null when unchecked or unavailable. a checked-in ticket may have no timestamp.

2. check in for the selected event

POST /api/v1/event/:vesselShort/:postShort/tickets/:code/checkin

no body is required. use the event selected in your scanner, identified by the hosting vessel and event post's short codes. compare it with the lookup result before offering admission. the server returns 404 not_found for another event's ticket, even if the staff account manages both events.

only one scanner can consume an unused ticket. repeat scans return 409 already_checked_in and preserve the original timestamp. the server checks the live RSVP under the same lock as response changes. check-in stays available after the event ends and when RSVPs are closed.

3. undo a mistake

DELETE /api/v1/event/:vesselShort/:postShort/tickets/:code/checkin

no body is required. clears check-in status and time so a valid ticket can be used again. an unused ticket also succeeds. staff can undo a check-in after the RSVP changes away from going.

errors and retries

401 unauthorized: authenticate first. 404 not_found: missing, cancelled or replaced ticket, missing event, wrong event for a mutation, or insufficient staff/OAuth permissions. these cases return the same error. 403 wrong_vessel: a custom-domain session belongs to another vessel.

409 checkin_inactive: ticketing is off. 409 invalid_ticket: the live response is not going. 409 already_checked_in: do not admit again. 409 ticket_busy: another change holds the RSVP lock; retry shortly. normal API rate limits (429 rate_limited) and account suspension rules apply.

use the live check-in result to admit a party, not a cached can_check_in value. after a timeout or lost mutation response, look up the ticket again before acting; do not assume failure and admit again or blindly repeat an undo. Idempotency-Key does not replay ticket mutations. responses are private, no-store. cancelling an RSVP invalidates its code; a new RSVP gets a new code.

creating posts

Create posts with a single request. All post types use the same endpoint — specify the type in the request body. Images for photosets are uploaded inline with the request.

POST /api/v1/pst/create
write

text

Create a text post with HTML content. Supports @mentions and #hashtags which are auto-detected and linked.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "text",
"content": "<p>Hello from the API!</p>",
"tags": ["api", "hello-world"]
}' \
'https://nyx.now/api/v1/pst/create'

photoset

Upload 1–10 images in a single request using multipart form data. Text content, a layout string, and captions are all optional.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-F 'vessel={slug}' \
-F 'type=photoset' \
-F 'content=<p>Beach day</p>' \
-F '[email protected]' \
-F '[email protected]' \
-F '[email protected]' \
-F 'layout=21' \
-F 'image_caption=["Sunset","Sandy beach","Seashells"]' \
-F 'tags=["beach","summer"]' \
'https://nyx.now/api/v1/pst/create'

images — The @ prefix tells curl to read a file from disk. @photo1.jpg reads photo1.jpg from the current directory. Use a full path like @/Users/me/photos/photo1.jpg for files elsewhere.

content — Optional text to accompany the photos.

layout — Each digit is the number of images in that row. "21" = 2 images on the first row, 1 on the second. Auto-generated if omitted.

image_caption — JSON array of captions matching upload order.

event

Create an event with cover images, date, location, and optional RSVP. Uses multipart form data. A description can be added with content.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-F 'vessel={slug}' \
-F 'type=event' \
-F '[email protected]' \
-F 'event={
"title": "Summer Concert",
"summary": "Live music in the park",
"startDate": "2027-06-15T19:00:00Z",
"endDate": "2027-06-15T23:00:00Z",
"timezone": "America/New_York",
"locationType": "in_person",
"location": "Central Park, NYC",
"ticket": {"type": "rsvp"}
}' \
-F 'lat=77.1755' \
-F 'lon=-61.1311' \
-F 'location_name=Worm City' \
-F 'tags=["music","summer"]' \
'https://nyx.now/api/v1/pst/create'

event — Required. JSON object with event details (see fields below).

images — 1–10 cover images. Same @ syntax as photoset posts.

content — Optional longer description for the event.

event fields

title — Required. Event name (max 200 chars).

summary — Required. Short description (max 140 chars).

startDate — Required. ISO 8601 date (YYYY-MM-DDTHH:MM:SSZ).

endDate — Optional. ISO 8601 date.

timezone — Required. IANA timezone (e.g. "America/New_York").

locationType — Required. "in_person", "online", or "tba".

location — Venue or address text (in-person events).

onlineUrl — URL for online events.

ticket — Optional. {"type": "rsvp"}, {"type": "free"}, or {"type": "paid", "price": 25, "currency": "USD"}.

rsvp — Optional. RSVP config. {"allowMaybe": true}. Enable +1s with {"allowPlusOnes": true, "maxPlusOnes": 3} (per-RSVP cap, 1–5).

video

Create a video post from a publicly reachable URL. Pass video_source_url and we fetch, transcode, and host the file for you — there is no direct upload endpoint. The post is returned in "processing" state and auto-publishes when encoding finishes. Poll GET /api/v1/video/:short with the returned post_video_short to watch progress.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "video",
"video_source_url": "https://",
"content": "<p>Optional caption</p>",
"tags": ["clips"]
}' \
'https://nyx.now/api/v1/pst/create'

video_source_url — Required. Public http(s) URL to the video file. Must be reachable without auth.

content — Optional caption HTML shown alongside the video.

If you set status to "draft", "queue", or "schedule", the post enters that state immediately and the video metadata populates when processing finishes.

response (HTTP 201)

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"type": "video",
"status": "processing",
"url": "/v/my-vessel/aBc123dEfGh",
"video": {
"post_video_short": "vId987xYzAb",
"status": "processing"
}
}
}

audio

Audio posts are a two-step flow: first upload each track via POST /api/v1/audio/upload to get back a short per track, then pass those shorts in audio_tracks here. Posts hold 1–25 tracks played as a playlist, with an optional shared album art image.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "audio",
"audio_tracks": [
{"short": "aUdC7x3bKpR", "title": "Intro"},
{"short": "aUdF9k2mNqZ", "title": "Ocean"}
],
"album_art_short": "iMgAbC123dE",
"content": "<p>Optional description</p>",
"tags": ["demo"]
}' \
'https://nyx.now/api/v1/pst/create'

audio_tracks — Required. Array of {"short","title"} objects, 1–25 entries, in playlist order. The short must come from a prior upload on a vessel the caller can publish to.

album_art_short — Optional. A TempPostImage short from POST /api/images/post. If omitted, the feed renders a themed fallback using the vessel's colors.

content — Optional description HTML shown alongside the player.

Tracks are already counted against the daily/monthly audio quota at upload time — publishing this post does not re-reserve them.

response (HTTP 201)

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"type": "audio",
"status": "published",
"url": "/v/my-vessel/aBc123dEfGh",
"audio": {
"tracks": [
{ "short": "aUdC7x3bKpR", "title": "Intro", "duration": 27, "ext": "mp3" },
{ "short": "aUdF9k2mNqZ", "title": "Ocean", "duration": 183, "ext": "mp3" }
],
"album_art": { "short": "iMgXyZ789Ab", "ext": "jpg" }
}
}
}

poll

Polls hold 2–6 options and run for a set duration (5 minutes–14 days). poll_kind picks the layout: "text" renders a labeled list, "image" renders an image grid where each option carries a pre-uploaded image (use POST /api/images/post first, then pass each short as image_short). An optional content HTML body shows below the poll.

example — text poll

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "poll",
"poll_question": "Best beach day activity?",
"poll_kind": "text",
"poll_duration_seconds": 86400,
"poll_options": [
{"text": "Surf"},
{"text": "Read"},
{"text": "Nap"}
],
"tags": ["summer"]
}' \
'https://nyx.now/api/v1/pst/create'

example — image poll

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "poll",
"poll_question": "Which cover?",
"poll_kind": "image",
"poll_duration_seconds": 604800,
"poll_options": [
{"image_short": "iMgAbC123dE", "alt": "Sunrise"},
{"image_short": "iMgFgH456iJ", "alt": "Sunset"}
]
}' \
'https://nyx.now/api/v1/pst/create'

poll_question — Required. Up to 280 chars.

poll_kind — "text" (default) or "image". Image polls require an image_short per option.

poll_duration_seconds — Required. How long the poll stays open. Min 300 (5 min), max 1209600 (14 days).

poll_options — Required. 2–6 entries. Per-entry fields: text (option label, max 80 chars; required for text polls, used as caption for image polls), image_short (required for image polls; from POST /api/images/post), alt (image alt text, optional).

content — Optional HTML description shown under the poll. Supports inline images and #hashtags.

Once any vote is cast, options can’t be changed (text edits + duration tweaks still allowed).

response (HTTP 201)

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"type": "poll",
"status": "published",
"url": "/v/my-vessel/aBc123dEfGh",
"poll": {
"question": "Best beach day activity?",
"kind": "text",
"close_at": "2026-04-26T17:32:00Z",
"options": [
{ "text": "Surf" },
{ "text": "Read" },
{ "text": "Nap" }
]
}
}
}

inline images

Put images anywhere inside a post’s content HTML. Upload each image to /api/images/post to get a short code and a copy-paste placeholder, then embed it in your content. Optionally attach alignment and a caption per image via image_settings. Works with any text-bearing post type (text, event, link, video, poll).

POST /api/images/post
write

step 1 — upload

vessel_short is the vessel’s short code (same form used by the Likes endpoint), not the URL slug.

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-F '[email protected]' \
-F 'vessel_short={vessel}' \
'https://nyx.now/api/images/post'

response

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"url": "https://i.nyx.now/ab/c1/nyx_aBc123dEfGh.jpg",
"placeholder": "{{[[img:aBc123dEfGh]]}}",
"thumbhash": "...",
"width": 1600,
"height": 1200,
// "gps": { "lat": .., "lon": .. } — only when the image carries EXIF geotags
}
}

Copy the placeholder string; that’s what you embed in your content. A gps object is included only when the uploaded image has EXIF geotags.

step 2 — embed in content

<p>Check this out {{[[img:aBc123dEfGh]]}} cool photo!</p>

Place the placeholder anywhere in your HTML. Multiple images are fine; repeating the same placeholder inserts the same image twice.

step 3 — publish with optional alignment & caption

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "text",
"content": "<p>Cool shot {{[[img:aBc123dEfGh]]}}.</p>",
"image_settings": {
"aBc123dEfGh": { "align": "center", "caption": "Sunset over the bay" }
}
}' \
'https://nyx.now/api/v1/pst/create'

alignment & caption

image_settings is keyed by placeholder position ("0", "1"…) or by the short inside the {{[[img:…]]}} token — the short returned from the upload on create, or media[i].short from a read. Keys that address no image in content are rejected with invalid_request (400) before anything is saved; unknown align values fall back to the default.

On PATCH /api/v1/pst/:short the map merges: an image you don’t name keeps its stored caption and alignment, and a field you omit on an image you do name is left alone. Send "caption": "" to clear a caption.

align — "left", "center" (default), "right", or "stretch".

caption — text shown as the image’s caption on rendered feeds (also used as the alt attribute for accessibility).

constraints

Each uploaded image counts against your daily image cap — see /settings/billing for your live usage and cap.

Uploaded images that are never referenced by a published post are swept after a few days.

when the cap is reached

{
"ok": false,
"error": {
"code": "image_limit_reached",
"message": "Daily image limit reached.",
"details": {
"limit": <applicable cap>,
"current": <your count>,
"tier": "free" | "paid",
"cta": { "label": <label>, "url": "/settings/billing" }
}
}
}

See Rate Limits for the full error taxonomy.

hashtags & mentions

#word in a body becomes a linked tag, and @vessel-url becomes a linked mention that notifies its vessel. Both are detected automatically. Two optional fields let you decide instead — useful when you’re republishing text that happens to contain a # or @ word that isn’t meant as a tag or a mention.

tag_settings

{
"content": "<p>#25yrsago a thing happened. #history</p>",
"tag_settings": { "history": { "display": "History" } }
}

An object keyed by hashtag. Supply it and it’s authoritative: it decides which #words are linked and which become tags on the vessel’s tag pages.

Omitted — every #word is detected (the default). {} — nothing is tagged; #25yrsago stays literal text. A partial map — only the hashtags you name.

display sets the spelling shown in the link; omit it for the hashtag’s own lowercased form.

Every key must appear in content as #<key>; a key naming no hashtag is rejected with invalid_request (400).

On PATCH it replaces the inline tag set, and applies even without a content edit, so you can un-tag a word in place. Your explicitly attached tags are a separate set and are never touched by it.

mention_settings

{ "mention_settings": ["someone"] }

An allow-list of vessel urls: an array, or an object whose keys are urls.

Omitted — every @url that resolves to a real vessel is linked and notified (the default). [] or {} — no mentions are linked and no mention notifications are sent. A partial list — only the urls you name.

Every entry must appear in content as @<url>; an entry naming no mention is rejected with invalid_request (400).

This list can only narrow what’s detected. Every surviving url is still resolved against a real vessel server-side, so a mention always points at the vessel that url actually belongs to — you can decline a mention, never invent one. Values in the object form are ignored for the same reason.

Both fields are refused on repost rows, whose body is a frozen mirror of the original.

request fields

Field Type Description
vessel string Required. Vessel URL slug.
type string Required. "text", "photoset", "event", "link", "video", "audio", or "poll".
content string HTML content. Required for text posts, optional for others. May include inline-image placeholders — see Inline Images.
image_settings object Optional. Alignment & caption for inline images, keyed by placeholder position or image short code. Merges on update. See Inline Images.
status string "published" (default), "draft", "queue", or "schedule".
tags string[] Array of tag strings (max 25). Merged with #hashtags in content.
images files Photoset and event posts. 1–10 image files (multipart). Max 20MB each.
event object Event posts only. JSON object with title, summary, startDate, timezone, locationType. See event fields.
layout string Photoset layout. Each digit = images in that row, e.g. "321".
image_caption string[] Captions for photoset images, matching upload order.
url string Link posts only. The URL to share.
video_source_url string Video posts only. Public http(s) URL to a video file. We fetch and transcode it.
schedule_date string ISO 8601 date. Required when status is "schedule".
lat number Latitude (-90 to 90). Required if lon is provided.
lon number Longitude (-180 to 180). Required if lat is provided.
location_name string Optional display name for the location (e.g. "Central Park, NYC"). Shown alongside the post.
source_url string Optional content source attribution: where the content came from. Normalized to an absolute http(s) URL; rendered on the post as the site's icon or source: domain. Reposts inherit the original's. Also accepted on PATCH /api/v1/pst/:short (null clears).

statuses

Control when your post goes live by setting the status field:

"published" Publish immediately (default).
"draft" Save as draft. Edit and publish later.
"queue" Add to your vessel's queue. Published automatically based on queue settings.
"schedule" Schedule for a specific time. Requires schedule_date — a full UTC timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ. The date must be in the future.

example of scheduling a post

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "text",
"content": "<p>Scheduled from the API</p>",
"status": "schedule",
"schedule_date": "2027-10-27T00:00:00Z"
}' \
'https://nyx.now/api/v1/pst/create'

location

Tag any post with a latitude and longitude. Posts with location data appear on the map. Add an optional location_name for display.

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"type": "text",
"content": "<p>Posting from the ice sheet</p>",
"lat": 77.1755,
"lon": -61.1311,
"location_name": "Camp Century, Greenland"
}' \
'https://nyx.now/api/v1/pst/create'

auto-features

  • @vessel-url mentions in content are auto-detected and linked (up to 30 per post).
  • #hashtag patterns in content are auto-detected and merged with explicit tags.
  • Fenced code blocks (```) are converted to formatted code blocks.

response

Returns the created post on success (HTTP 201):

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"type": "text",
"status": "published",
"url": "/v/my-vessel/aBc123dEfGh",
"created": "2026-04-15T12:00:00.000Z"
}
}

follow

Follow or unfollow a vessel or tag as one of your vessels. Follows are capped at 200 per account per day (unfollows don’t count), and a vessel can accumulate at most 5,000 follows; both return rate_limited. Following a follower-approval vessel creates a pending request rather than an immediate follow.

follow or unfollow

POST /api/v1/follow
write

Toggle a follow from one of your vessels.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel_short": "{vessel}",
"target_short": "xYz789wVuTs",
"target_type": "vessel",
"action": "follow"
}' \
'https://nyx.now/api/v1/follow'

vessel_short — The vessel doing the following (must be one of yours).

target_short — The vessel short code, or the tag name when target_type is "tag".

target_type — "vessel" or "tag".

action — "follow" or "unfollow".

response

{
"ok": true,
"data": { "following": true }
}

Bulk variant: POST /api/v1/follow/bulk takes a vessel_shorts array (max 25) against one target and returns a per-vessel result map.

likes

Likes are vessel-scoped — a vessel likes a post, not an account. You specify both sides in every request: vessel_short is the vessel doing the liking (one of your own), and post_vessel_short identifies the vessel that owns the post. Both endpoints are idempotent — liking an already-liked post or unliking one you haven’t liked is a no-op.

like a post

POST /api/v1/like
write

Like a published post from one of your vessels.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel_short": "{vessel}",
"post_short": "aBc123dEfGh",
"post_vessel_short": "xYz789wVuTs"
}' \
'https://nyx.now/api/v1/like'

vessel_short — The vessel doing the liking (must be one of yours). The short from /api/v1/act/vsls.

post_short — The post to like. The short from any feed endpoint.

post_vessel_short — The vessel the post belongs to.

response

{
"ok": true,
"data": {
"liked": true,
"count": 12
}
}

unlike a post

DELETE /api/v1/like
write

Remove a like from a post. Same body as the like endpoint.

example

curl -X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel_short": "{vessel}",
"post_short": "aBc123dEfGh",
"post_vessel_short": "xYz789wVuTs"
}' \
'https://nyx.now/api/v1/like'

response

{
"ok": true,
"data": {
"liked": false,
"count": 11
}
}

comments

Comments live on a post. Every request identifies the post by post_vessel_short + post_short. Replies use parent_short at any depth. Lists paginate with the same cursor envelope as feeds — see pagination.

All reads honor password-protected vessel access — anonymous callers without the access cookie get the same gate as post reads do. Write endpoints additionally require the write OAuth scope (personal API keys bypass scoping).

Likes are one per account and one per vessel, the post rule. The vessel_short on a like is the vessel it is attributed to (the “liked from” avatar). Re-liking from a different vessel moves the like (no count change, switchedFrom names the vessel it left); a vessel another member of a shared vessel already liked from answers 409 like_taken. The unlike endpoint removes your own like wherever it sits, or, given a vessel_short, the like that vessel holds when you made it or own or administer the vessel (403 like_locked otherwise).

comment object

Returned by every read endpoint (in data.items[]) and by create / update (in data.comment).

{
"type": "comment",
"short": "aBc123dEfGh",
"parentShort": null,
"topShort": null,
"depth": 0,
"ts": 1747000000000,
"edited": null,
"status": 1,
"vsl": { "short": "...", "name": "...", "url": "...", "avatarUrl": "...", "accent": "#..." },
"via": null,
"body": "hello",
"image": null,
"likeCnt": 4,
"replyCnt": 3,
"viewerLiked": false,
"viewerLikedFromVesselShort": null,
"chip": null,
"canEdit": false,
"canDelete": false
}

status: 1 = published, 2 = deleted by author, 3 = removed by mod (rows with status > 1 carry body: "" and image: null). viewerLiked / viewerLikedFromVesselShort / canEdit / canDelete are populated only when the request is authenticated. chip: "author" | "collab" | "follows" | null.

list comments

GET /api/v1/cmt/:post_vessel_short/:post_short
read

Paginated list of top-level comments on a post. Auth is optional; password-protected vessels still require the access cookie.

example

curl 'https://nyx.now/api/v1/cmt/xYz789wVuTs/aBc123dEfGh?sort=top&limit=50'

sort — top (default, most-liked) | newest | oldest.

c — Opaque cursor from meta.page.nextCursor on the previous page.

limit — Items per page (default 50).

response

{
"ok": true,
"data": { "items": [ /* comment objects */ ] },
"meta": {
"page": { "nextCursor": "eyJ...", "hasMore": true },
"totalCount": 509,
"sort": "top"
}
}

list replies

GET /api/v1/cmt/:post_vessel_short/:post_short/:top_short/r
read

Every descendant of a top-level comment (oldest first), flat. top_short is the short of the top-level comment whose thread you want.

example

curl 'https://nyx.now/api/v1/cmt/xYz789wVuTs/aBc123dEfGh/cMt456pQrSt/r'

c — Opaque cursor for the next page.

limit — Items per page (default 50).

response

{
"ok": true,
"data": { "items": [ /* comment objects */ ] },
"meta": {
"page": { "nextCursor": "eyJ...", "hasMore": false },
"totalCount": 12
}
}

list reposters

GET /api/v1/cmt/:post_vessel_short/:post_short/:comment_short/reposters
read

The quote posts carrying a comment (the who-reposted panel): rendered rows, oldest first, with count = the comment's quoteCnt. Hidden or unpublished quote posts and banned / muted reposters are omitted at read time. Post comments only. The sibling /likers and /collections panels share the path shape.

example

curl 'https://nyx.now/api/v1/cmt/xYz789wVuTs/aBc123dEfGh/cMt456pQrSt/reposters'

c — Opaque cursor for the next page.

post a comment

POST /api/v1/cmt
write

Post a new comment on a published post. Body and/or a staged image are required (empty body is allowed only with an image). To reply to an existing comment, pass parent_short.

access: Your account must hold a role (owner / admin / collab) on vessel_short. perm_publish is not required — a collab who can’t publish posts can still comment.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"post_vessel_short": "xYz789wVuTs",
"post_short": "aBc123dEfGh",
"vessel_short": "{vessel}",
"body": "great post"
}' \
'https://nyx.now/api/v1/cmt'

post_vessel_short — The vessel that owns the post.

post_short — The post being commented on.

vessel_short — The vessel you’re commenting as. Must be one you own / admin / collab on.

body — Comment text (max 10,000 chars). Optional if temp_image_short is set.

parent_short — Optional. The short of the comment you’re replying to.

temp_image_short — Optional. Attach a previously-uploaded image (see inline images).

response

{
"ok": true,
"data": { "comment": { /* comment object */ } }
}

edit a comment

PATCH /api/v1/cmt/:short
write

Edit body and/or attached image on an existing comment.

access: Author only. Even moderators (admins with perm_edit_posts) cannot edit other users’ comments — they can only delete. Deleted comments cannot be re-edited.

example

curl -X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"post_vessel_short": "xYz789wVuTs",
"post_short": "aBc123dEfGh",
"vessel_short": "{vessel}",
"body": "edited!"
}' \
'https://nyx.now/api/v1/cmt/cMt456pQrSt'

post_vessel_short, post_short, vessel_short — Required, same as create.

body — Optional. New text (max 10,000 chars).

temp_image_short — Optional. Attach a new image (replaces the existing one).

remove_image — Optional true to drop the current image.

response

{
"ok": true,
"data": { "comment": { /* updated comment object */ } }
}

delete a comment

DELETE /api/v1/cmt/:short
write

Soft-delete a comment. The row stays so the thread reads in order, but the body and any attached image are dropped. Idempotent on already-deleted comments.

access: Author OR moderator. Mods are owners and admins with perm_edit_posts on the post’s vessel.

example

curl -X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY' \
'https://nyx.now/api/v1/cmt/cMt456pQrSt?post_vessel_short=xYz789wVuTs&post_short=aBc123dEfGh'

post_vessel_short, post_short — Required (accepted as query or body).

response

{
"ok": true,
"data": { "deleted": true, "html": "<article ...>" }
}

like a comment

POST /api/v1/cmt/like
write

Like a comment from one of your vessels. One like per account and one per vessel: re-posting with a different vessel_short moves your like there and returns switched: true plus switchedFrom (the like count does not change); a vessel another member already liked from answers 409 like_taken.

access: The chosen vessel_short must have perm_like (owners and admins always do; collabs must have the l flag).

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel_short": "{vessel}",
"comment_short": "cMt456pQrSt",
"post_short": "aBc123dEfGh",
"post_vessel_short": "xYz789wVuTs"
}' \
'https://nyx.now/api/v1/cmt/like'

vessel_short — The vessel the like is attributed to (your own, with perm_like).

comment_short — The comment to like.

post_short, post_vessel_short — The comment’s post.

response

{
"ok": true,
"data": {
"liked": true,
"switched": false,
"vesselShort": "vsLAbc12345",
"count": 5
}
}

unlike a comment

DELETE /api/v1/cmt/like
write

Remove a like from a comment. Without vessel_short your own like goes wherever it sits. With it, the like that vessel holds goes: yours, or another member’s when you own or administer the vessel (403 like_locked otherwise).

example

curl -X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"comment_short": "cMt456pQrSt",
"post_short": "aBc123dEfGh",
"post_vessel_short": "xYz789wVuTs"
}' \
'https://nyx.now/api/v1/cmt/like'

response

{
"ok": true,
"data": {
"liked": false,
"count": 4
}
}

forums

Vessels can host forums — flat or threaded discussion boards, optionally split into sections. The stable surface lets you list a vessel's forums, read threads, start threads, and reply. Bodies are HTML run through the same sanitizer as comments; inline images ride the placeholder flow.

Access follows the vessel: visibility settings and passwords gate reads and writes alike, and a forum or section can additionally restrict who views or posts (expect 403s). Some forums allow or force anonymous posting: pass "anon": true where it's allowed (refused loudly where it isn't), and anonymous rows render as "Anon" with a per-thread tag. Locked threads refuse replies. Forum owners can set daily posting caps — a 429 with "Daily limit reached" means come back tomorrow.

Votes, moderation (pin/lock/move), read tracking, presence, and forum/section management stay internal.

read

list a vessel's forums

GET /api/v1/forum/list?vessel_short=VESSEL_SHORT

Returns the forums you can reach on the vessel with their short codes (the coordinates every other forum call wants), names, kind, and the sections you can reach (sections[]: short, name, url, parentShort). Authentication is optional; the vessel's access rules apply, and forums or sections you cannot see are left out.

read

list threads

GET /api/v1/forum/{vessel_short}/{forum_short}/threads?c=<cursor>

Standard cursor pagination. Ranked forums also accept sort=hot|top|new|activity (invalid sorts fall back to the default).

read

list a thread's posts

GET /api/v1/forum/{vessel_short}/t/{thread_short}/posts?c=<cursor>

Posts carry short, HTML body, author (or the anonymous tag), and reply linkage. Threaded forums nest via parent_short; ranked replies accept a sort= param.

write

start a thread

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"forum_vessel_short": "FORUM_VESSEL_SHORT",
"vessel_short": "YOUR_VESSEL_SHORT",
"forum_short": "FORUM_SHORT",
"title": "tape trading thread",
"body": "<p>post your lists</p>"
}' \
'https://nyx.now/api/v1/forum/thread'

forum_vessel_short — Required. The vessel the forum lives on (from list forums).

vessel_short — Required. The vessel you're posting as (your acting identity).

forum_short — Required. From list forums. Add section_short to post into a section.

title + body — Required. Body is sanitized HTML.

anon — Optional. true posts anonymously where the forum allows it.

response (HTTP 200)

{
"ok": true,
"data": {
"thread": { "short": "aBc123dEfGh", "url": "/v/.../f/.../tape-trading-thread/aBc123dEfGh" },
"redirect": "..."
}
}
write

reply to a thread

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"forum_vessel_short": "VESSEL_SHORT",
"vessel_short": "YOUR_VESSEL_SHORT",
"body": "<p>list incoming</p>"
}' \
'https://nyx.now/api/v1/forum/thread/THREAD_SHORT/posts'

forum_vessel_short — Required. The vessel hosting the forum.

vessel_short — Required. The vessel you're replying as.

body — Required. Sanitized HTML, same rules as comments.

parent_short — Optional. Reply to a specific post (threaded forums nest it).

anon — Optional, where the forum allows it.

Returns the created post as data.post (plus rendered data.html). Locked threads refuse with a 400.

video status

After creating a video post, poll this endpoint with the returned post_video_short to check transcoding progress.

GET /api/v1/video/:post_video_short
read

Returns the current processing state. "ready" and "error" are terminal. A reasonable cadence is every 5s for the first minute, then every 30s.

example

curl -H 'Authorization: Bearer YOUR_API_KEY' \
'https://nyx.now/api/v1/video/vId987xYzAb'

while processing

{
"ok": true,
"data": {
"post_video_short": "vId987xYzAb",
"status": "processing",
"post_short": "aBc123dEfGh"
}
}

when ready

{
"ok": true,
"data": {
"post_video_short": "vId987xYzAb",
"status": "ready",
"post_short": "aBc123dEfGh",
"src": "https://v.nyx.now/p720/ab/cd/nyx_abc.mp4?v=my-vessel&p=aBc123dEfGh",
"width": 1920,
"height": 1080,
"duration": 45.2,
"aspect_ratio": "16:9",
"has_audio": true,
"thumbnail_url": "https://i.nyx.now/.../nyx_imgShort.jpg",
"thumbhash": "lzhashbase64"
}
}

when processing fails

{
"ok": true,
"data": {
"post_video_short": "vId987xYzAb",
"status": "error",
"post_short": "aBc123dEfGh",
"error_code": "video_source_unreachable",
"error_message": "Could not download input URL."
}
}

When a video fails, the owning post transitions from "processing" to "draft" so you can retry or adjust — the post is not deleted.

error codes

video_source_unreachable — We could not download the URL (DNS, 4xx/5xx, TLS, not publicly reachable).

video_source_too_large — Duration exceeds the 4-hour cap.

video_mux_rejected — File rejected (unsupported codec, corrupt, no playable rendition).

video_processing_timeout — Processing did not finish within the retry budget.

audio tracks

Upload audio files, manage temp tracks, then attach them to an audio post. Each track is a separate upload; files are validated server-side with ffprobe — no transcoding. Limits:

  • MP3, M4A (AAC), or WAV (PCM 16/24-bit LE/BE, 32-bit float LE)
  • Max size: 50 MB · Max duration: 30 minutes
  • Exactly 1 audio stream (MP3 attached-picture cover art allowed, 0 other video streams)
  • Channels: mono or stereo · Sample rate: 8 kHz–96 kHz

Each upload reserves one track against the caller's daily + monthly audio quota (audio_limit_reached / audio_monthly_limit_reached on overflow). Identical files are hash-deduplicated — two uploads of the same bytes share one R2 object.

upload track

POST /api/v1/audio/upload
write

Upload one audio file. Returns a short you can pass in audio_tracks when creating the post. Unused tracks are garbage-collected after 3 days.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-F 'vessel_short={slug}' \
-F '[email protected]' \
'https://nyx.now/api/v1/audio/upload'

vessel_short — Required. The short code of the vessel that will own this track. The caller must have publish permission on it.

audio — Required. The audio file. The @ prefix tells curl to read from disk (see photoset example).

response (HTTP 200)

{
"ok": true,
"data": {
"short": "aUdC7x3bKpR",
"tracking_id": "abc123def456ghi789jkl012",
"title": "track",
"duration": 183,
"channels": 2,
"sample_rate": 44100,
"ext": "mp3",
"container": "mp3",
"codec": "mp3",
"url": "https://a.nyx.now/aud/ab/cd/nyx_abCdEfGh12345678.mp3?v=my-vessel"
}
}

title is derived from the uploaded filename (stem, no extension) — override it per-track in audio_tracks at publish time. url is live immediately; no processing wait.

rejection reasons

A rejected upload returns HTTP 400 with audio_rejected. The details.reason field gives a stable subcode:

file_too_large — Exceeds 50 MB.

file_empty — Zero bytes.

duration_too_long — Exceeds 30 minutes.

duration_invalid — Missing, zero, or unreadable duration.

container_not_allowed — Not MP3, MP4/M4A, or WAV.

codec_not_allowed — Codec doesn't match the container's allowlist.

major_brand_not_allowed — MP4 container with an unsupported major_brand.

stream_count_invalid — Zero or multiple audio streams.

video_stream_not_allowed — Contains a video stream other than MP3 attached-picture cover art.

channels_not_allowed — More than 2 channels.

sample_rate_not_allowed — Outside 8–96 kHz.

audio_unreadable — ffprobe couldn't parse the file.

ffprobe_timeout / ffprobe_output_too_large / ffprobe_unavailable — Server-side validator failure; retryable.

track metadata

GET /api/v1/audio/:short
read

Fetch metadata for a track the caller controls. Useful to re-hydrate client state after a page reload when you have a short but not the rest.

example

curl -H 'Authorization: Bearer YOUR_API_KEY' \
'https://nyx.now/api/v1/audio/aUdC7x3bKpR'

response

{
"ok": true,
"data": {
"short": "aUdC7x3bKpR",
"vessel_short": "my-vessel",
"title": "Ocean",
"position": null,
"bound": false,
"duration": 183,
"channels": 2,
"sample_rate": 44100,
"ext": "mp3",
"container": "mp3",
"codec": "mp3",
"url": "https://a.nyx.now/aud/ab/cd/nyx_abCdEfGh12345678.mp3?v=my-vessel"
}
}

bound is true once the track has been attached to a post; position is its 0-indexed slot in the post's playlist.

delete track

DELETE /api/v1/audio/:short
write

Delete a temp (unbound) track. Tracks already attached to a post must be removed by deleting or editing the post — deleting them here would race the post's track ordering.

example

curl -X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY' \
'https://nyx.now/api/v1/audio/aUdC7x3bKpR'

response

{
"ok": true,
"data": { "short": "aUdC7x3bKpR", "deleted": true }
}

Calling on a bound track returns invalid_request with a message pointing to post edit instead.

managing posts

Delete a post, publish a draft, or change the time on a scheduled post. Full post editing isn’t exposed on the API yet — for other changes, delete and recreate.

delete post

DELETE /api/v1/pst/:short
write

Hard delete. Attached images, videos, tags, location, RSVPs, likes, and feed index entries are all cleaned up automatically. Not idempotent — a second delete returns 404.

example

curl -X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}"
}' \
'https://nyx.now/api/v1/pst/POST_SHORT'

response

{
"ok": true,
"data": { "deleted": true, "short": "aBc123dEfGh" }
}

Collaborators can only delete posts they authored. Owners and admins with perm_edit_posts can delete anything on the vessel.

pin post

POST /api/v1/pst/:short/pin
DELETE /api/v1/pst/:short/pin
manage

Pin a published post to the top of its vessel's posts tab, or take it down. A vessel pins at most 3 posts, most recently pinned first (unpin and pin again to move one to the top). Pinned posts lead page 1 of the profile feed with pinned: true and pinnedAt, and meta.pinnedCount says how many; the chronological stream never repeats them. Both verbs are idempotent.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}"
}' \
'https://nyx.now/api/v1/pst/POST_SHORT/pin'

response

{
"ok": true,
"data": { "pinned": true }
}

The owner and admins with perm_change_settings can pin; collaborators cannot. A repost is a 400 (pin the original), an unpublished post a 409 invalid_state, a full block a 409 pin_limit_reached with details.limit and details.current.

publish draft

POST /api/v1/pst/:short/publish
write

Transition a post from draft to published. Nothing else changes — no content edits, no other status transitions. Video posts must have a "ready" video before publishing.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}"
}' \
'https://nyx.now/api/v1/pst/POST_SHORT/publish'

response

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"status": "published",
"url": "/v/my-vessel/aBc123dEfGh"
}
}

errors

invalid_state (409) — post isn’t in draft state. details.current_status names the actual state.

video_not_ready (409) — video is still processing. Poll Video Status until "ready", then retry.

reschedule post

POST /api/v1/pst/:short/reschedule
write

Change the publish time on a post that’s already in scheduled state. Content and status are untouched; only schedule_date moves.

example

curl -X POST \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"vessel": "{slug}",
"schedule_date": "2026-11-03T18:00:00Z"
}' \
'https://nyx.now/api/v1/pst/POST_SHORT/reschedule'

response

{
"ok": true,
"data": {
"short": "aBc123dEfGh",
"status": "schedule",
"schedule_date": "2026-11-03T18:00:00Z",
"url": "/v/my-vessel/aBc123dEfGh"
}
}

notes

schedule_date must be a valid ISO 8601 timestamp strictly in the future.

Only posts in scheduled state can be rescheduled. Drafts, queued, and published posts return invalid_state.

post types

subType Description
photoset Photo album with grid layout (ps + psl fields)
text Rich text with optional inline images
video Video with streaming URL, dimensions, duration, thumbhash
audio Audio playlist with 1–25 tracks and optional album art (audio.tracks + audio.art fields)
link Link preview with OG metadata (title, description, image)
event Event with date, location, RSVP, and optional ticketing
poll Poll with 2–6 text or image options, closing at poll.closeAt (create payload: poll)
quote A frozen quote of a comment or forum post with the reposter's commentary (read-only in feeds)

pagination

All feed endpoints use cursor-based pagination. To fetch the next page, pass the nextCursor from the response as the c query parameter:

GET /api/v1/feed/vsl/:vessel_short/pst?c=eyJ...

When meta.page.hasMore is false, you've reached the end.

rate limits

API requests are rate-limited. If you exceed the limit, the API returns a 429 status with:

{
"ok": false,
"error": {
"code": "rate_limited",
"message": "Too many requests."
}
}

Current request-rate limits (all per one-minute window). Each request is charged to the buckets that fit who is calling:

120 / min per user of your app: an OAuth app's calls for one user (one grant), reads and writes together. This is the budget that follows the person; an app pays no per-IP limit, so a server sending every user's calls from one address is fine.

300 / min per account: across every app the same account has authorized.

6000 / min reads, 1200 / min writes, per app: a circuit breaker for the whole app, all of its users together.

70 / min per IP plus 120 / min per account: personal-token calls (and the web client). The account limit holds however many addresses the calls come from.

70 / min per IP: requests with no credentials.

30 / min per IP: the OAuth provider endpoints (/oauth/authorize, /oauth/token, /oauth/revoke), and requests whose token is rejected.

10 / min (20 / min on a paid plan): search queries, per account. Every executed query counts, pagination pages included.

forum daily caps: forum owners can cap threads/replies per day; the 429 message reads "Daily limit reached."

Authenticated responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds) for the tightest bucket the request paid, and every 429 carries Retry-After. Per-app limits are the defaults; nnyyxx can raise them for an app that needs more. These are throttling limits, separate from the daily usage caps below. Internal endpoints carry additional, tighter buckets of their own: another reason not to build on them.

Best practices:

  • Cache responses on your end rather than re-fetching on every page load
  • Use cursor pagination instead of polling the first page repeatedly
  • Back off exponentially when you receive a 429

Separate from request-rate throttling, daily per-account usage limits apply to posts, images, video uploads, and audio-track uploads. Caps differ by plan and your live usage is visible on /settings/billing. When you hit one, the API returns 429 with one of these codes:

post_limit_reached — daily post-creation cap reached (all post types count).

image_limit_reached — daily image-upload cap reached (photoset + inline + event cover + page images).

video_limit_reached — daily video-post cap reached.

audio_limit_reached — daily audio-track upload cap reached (one per uploaded track, reserved at upload time — deleting a temp track does not refund it).

audio_monthly_limit_reached — monthly audio-track upload cap reached.

All three carry a structured details payload with your current count, the applicable cap, your plan tier, and a CTA linking back to billing:

{
"limit": <applicable cap>,
"current": <your current count>,
"tier": "free" | "paid",
"cta": {
"label": <tier-appropriate label>,
"url": "/settings/billing"
}
}

Counters reset at UTC midnight. See /settings/billing for your current usage against the caps.

password-protected vessels

Some vessels are gated by a password set by their owner. Any API request that touches a gated vessel — reading its feed, following it, liking a post on it, RSVP'ing to its events, voting on its polls, collecting its posts — is rejected with 403 vessel_access_required until the caller proves access.

three ways to satisfy the gate

  • membership — an authenticated account with any role (owner, admin, collaborator) on the vessel passes through automatically.
  • access cookie — once a viewer unlocks a vessel via the web gate or the password param below, a pw-<vesselShort> cookie is set and applied to subsequent requests for the same vessel.
  • password — send the password in an X-Vessel-Password header (preferred: a GET has no body, and a query string ends up in logs and caches), the request body, or the query string. Verified server-side; on a successful match the request continues normally.

example — like a post on a password-protected vessel

# first attempt — no password
curl -X POST https://nyx.now/api/v1/like \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"vessel_short":"...","post_short":"...","post_vessel_short":"abc123XYZ45"}'
# → 403 { ok:false, error:{ code:"vessel_access_required", details:{ vesselShort:"abc123XYZ45" }}}
# retry with the password param
curl -X POST https://nyx.now/api/v1/like \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"vessel_short":"...","post_short":"...","post_vessel_short":"abc123XYZ45","password":"trustno1"}'

example — fetch a password-protected vessel's posts

GET /api/v1/feed/vsl/:vessel_short/pst rejects the request with 403 vessel_access_required until the caller has unlocked the vessel. Send the password in an X-Vessel-Password header to unlock and read in one shot (?password= also works).

# locked
curl https://nyx.now/api/v1/feed/vsl/abc123XYZ45/pst
# → 403 { ok:false, error:{ code:"vessel_access_required", details:{ vesselShort:"abc123XYZ45" }}}
# unlocked — full feed
curl -H "X-Vessel-Password: trustno1" https://nyx.now/api/v1/feed/vsl/abc123XYZ45/pst

The gate is orthogonal to authentication. A request authenticated by personal token, OAuth access token, or session cookie will still be denied if the target vessel is locked and the caller hasn't unlocked it. A wrong password value yields the same 403.

Wrong-password attempts are rate-limited per vessel and address (IPv6 per /64), with a ceiling per vessel across all addresses. After a small number of failures within a 15-minute window the endpoint stops verifying and returns 429 rate_limited with a Retry-After header (and error.details.retryAfter in seconds) until the window expires. A correct password resets the counter.