Reference
Posts API
Everything you can do with content via API key — create, update, delete, attach media, bounce, and comment. All responses are JSON; all writes accept and respect Idempotency-Key.
Identity (whoami)
The fastest way to confirm a key is valid and discover the account it's bound to. No scope required — this is the one read endpoint every key implicitly has access to.
curl https://api.jestha.com/api/auth/me \ -H "Authorization: Bearer jes_live_..."
{
"id": "01HW...",
"username": "poscos",
"email": "[email protected]",
"displayName": "Poscos",
"avatar": "https://...",
"coverImage": null,
"bio": null,
"verified": false,
"emailVerified": true,
"isPrivate": false,
"isCreator": false,
"totalFollowers": 0,
"totalFollowing": 0,
"totalPosts": 0,
"createdAt": "2026-05-28T11:42:09.123Z"
}Use the id as your stable externalIdwhen mapping Jestha accounts in your data store — usernames can change, ids don't.
Create a Jes
The core write. Returns the fully-formed Jes with all fields populated; use the id and the permalink format https://jestha.com/post/{id} in your UI.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| content | string | optional | Max 5,000 characters. Required if no media. URLs auto-unfurl into a link preview unless media is attached. Hashtags (#example) and @mentions are auto-detected and linked. |
| visibility | enum | optional | Default PUBLIC. See visibility values below. |
| jeHubId | string | optional | Post into a JeHub the bound account is a member of. |
| location | object | optional | { name, latitude?, longitude? } |
| poll | object | optional | A poll attachment — see Polls below. |
| linkPreviewUrl | string | optional | Force a specific URL to be unfurled into a card. Only used if no media is attached. |
curl -X POST https://api.jestha.com/api/posts \
-H "Authorization: Bearer jes_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c4a8d09-f4f4-4d7c-89e1-3a2b1d5c6e7f" \
-d '{
"content": "Hello from my service",
"visibility": "PUBLIC"
}'Response (201 Created)
{
"id": "01HW5...",
"content": "Hello from my service",
"media": null,
"linkPreview": null,
"location": null,
"type": "POST",
"visibility": "PUBLIC",
"createdAt": "2026-05-28T11:42:09.123Z",
"updatedAt": "2026-05-28T11:42:09.123Z",
"editedAt": null,
"author": {
"id": "...",
"username": "poscos",
"displayName": "Poscos",
"avatar": "https://...",
"verified": false,
"verifiedType": null
},
"jeHub": null,
"sourceApiKey": {
"id": "01HW...",
"label": "poscos production",
"keyPrefix": "jes_live_a1b2c3d"
},
"isBounce": false,
"bouncedBy": null,
"originalPost": null,
"poll": null,
"stats": { "likes": 0, "comments": 0, "shares": 0, "shareCount": 0, "views": 0 },
"userInteractions": { "liked": false, "saved": false }
}Permalink: https://jestha.com/post/01HW5...
Create a Jes with media (multipart)
Use POST /api/posts/upload with a single multipart/form-data body. No pre-signed URL dance, no separate upload step — content and media land in one request.
Form fields
| Field | Type | Required | Notes |
|---|---|---|---|
| media | file (repeated) | required | Use field name 'media' for each file. Up to 4 files when called via API key. Max 8 MB per file. |
| content | string | optional | Same rules as JSON create — 5,000 chars, hashtags/mentions auto-parsed. |
| visibility | enum | optional | Default PUBLIC. |
| jeHubId | string | optional | |
| location | JSON string | optional | Serialise location as JSON string in the form field. |
Allowed MIME types
- Images: image/jpeg, image/png, image/gif, image/webp
- Videos: video/mp4, video/mpeg, video/quicktime, video/webm
curl -X POST https://api.jestha.com/api/posts/upload \ -H "Authorization: Bearer jes_live_..." \ -H "Idempotency-Key: 7c4a8d09-f4f4-4d7c-89e1-3a2b1d5c6e7f" \ -F "content=Photo from the field" \ -F "visibility=PUBLIC" \ -F "media=@/path/to/photo.jpg;type=image/jpeg"
import { FormData, fetch } from 'undici';
import fs from 'node:fs';
const form = new FormData();
form.set('content', 'Photo from the field');
form.set('visibility', 'PUBLIC');
form.set('media', new Blob([fs.readFileSync('photo.jpg')], { type: 'image/jpeg' }), 'photo.jpg');
const res = await fetch('https://api.jestha.com/api/posts/upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.JESTHA_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
},
body: form,
});
const jes = await res.json();
console.log(jes.id, jes.media);processingStatus: "PROCESSING" on each media entry. Subscribe to the jes.createdwebhook (which fires as soon as the row is committed) and treat the playable URL as available once the row's media[i].url resolves.Edit a Jes
PATCH /api/posts/{id} — the bound account can only edit its own Jes. Returns the updated row in the same shape as create.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| content | string | optional | Replaces the existing content. Same 5,000 char cap. |
| visibility | enum | optional |
curl -X PATCH https://api.jestha.com/api/posts/01HW5... \
-H "Authorization: Bearer jes_live_..." \
-H "Content-Type: application/json" \
-d '{ "content": "edited copy" }'The response sets editedAtso UI can show an “edited” marker.
Delete a Jes
DELETE /api/posts/{id} — soft-deletes (sets deletedAt); the row stays for audit but stops surfacing in feeds.
curl -X DELETE https://api.jestha.com/api/posts/01HW5... \ -H "Authorization: Bearer jes_live_..."
{ "message": "Post deleted", "id": "01HW5..." }Bounce (re-share)
POST /api/posts/{id}/bounce with no body creates a plain bounce. To add commentary (quote-bounce), include the text in the request body. Either form counts as one write against the 1,000 Jes/day quota.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| content | string | optional | When set, the bounce becomes a quote-bounce with your commentary above the original. Same 5,000 char cap. |
# Plain bounce
curl -X POST https://api.jestha.com/api/posts/01HW5.../bounce \
-H "Authorization: Bearer jes_live_..."
# Quote-bounce
curl -X POST https://api.jestha.com/api/posts/01HW5.../bounce \
-H "Authorization: Bearer jes_live_..." \
-H "Content-Type: application/json" \
-d '{ "content": "love this take" }'Comment on a Jes
POST /api/posts/{id}/comments. Set parentId to nest a reply. Body shape:
| Field | Type | Required | Notes |
|---|---|---|---|
| content | string | optional | Required if no media. Plain text; 5,000 char cap. |
| parentId | string | optional | The id of the comment you're replying to. Omit for a top-level comment. |
| media | object | optional | { type: 'gif'|'image', url } — for embedded media within a comment. |
curl -X POST https://api.jestha.com/api/posts/01HW5.../comments \
-H "Authorization: Bearer jes_live_..." \
-H "Content-Type: application/json" \
-d '{ "content": "great point" }'Visibility values
The full set on the platform — any value not in this list is rejected:
| Value | Meaning |
|---|---|
| PUBLIC | Anyone can see (including signed-out visitors). The default. |
| FOLLOWERS | Only accounts following the author. |
| CONNECTIONS | Mutual followers only. |
| PRIVATE | Only the author. |
| CUSTOM | Custom allowlist managed on Jestha; not configurable from the API in v1 — falls back to PRIVATE if used without that allowlist. |
Hashtags, mentions, and link previews
- Hashtags (
#example) — auto-detected fromcontent, auto-linked on render. You don't need to send them as separate entities. - Mentions (
@username) — auto-detected. Mentioned users get a notification. - Link previews — the first URL in
contentis auto-fetched server-side into a link preview card on the Jes unless media is attached (media takes precedence). To force a specific URL, pass it aslinkPreviewUrlin the body.
Idempotency
Every write endpoint (POST, PATCH, DELETE) accepts an Idempotency-Key header. We dedupe on (your account, method, path, your key) for 24 hours.
- Use any client-generated identifier — UUID v4 is fine.
- On the first request we process normally and cache the response. On a duplicate within 24h we return the cached response with status code
200(so your client's success/failure handling matches the first attempt). - Safe to retry on network errors and 5xx responses — you won't double-post.
Content limits
- Jes content: 5,000 characters max.
- Comment content: 500 characters max.
- JesClip caption: 2,200 characters max.
- Media: 8 MB per file, up to 4 files per Jes via API key. See rate limits.
Error envelope
All error responses share this shape:
{
"error": "Short machine-readable label",
"message": "Optional human-readable explanation.",
"retryAfter": 17,
"requiredScope": "write:jes",
"details": [ ... ]
}Field presence depends on the kind of failure:
retryAfteron 429s — seconds to wait.requiredScopeon 403s for scope mismatches.detailson 400/422 validation failures — array of field-level issues.
Status codes follow the usual HTTP semantics: 200 / 201 success, 400 validation, 401 auth, 403 scope/IP, 404 not found, 409 conflict (rate cap on key creation), 422 unprocessable, 429 rate limit, 5xx server errors. 5xx responses are safe to retry (use Idempotency-Key).
Rate-limit visibility
Every keyed response — success or 429 — carries:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1716889389 X-Jestha-Api-Version: 1
Throttle proactively when X-RateLimit-Remaining drops low rather than waiting for a 429.
What you cannot do via API key
As a reminder, the API is intentionally write-and-engage focused. No matter what scopes you hold, the following surfaces are session-only:
- Any feed, timeline, search, or discovery endpoint
- Liking content (write:like is not exposed in v1)
- Reading other users' profiles beyond what's embedded in a single Jes
- Account or key management
- Admin endpoints
See Scopes for the full catalog.