Tesselith
ProductSolutionsCompareFree toolsBlogPricing
Join the waitlist
  1. Home/
  2. Docs/
  3. Tesselith API: Getting Started Guide

REST API

Tesselith API: Getting Started Guide

Updated October 30, 2026

Authenticate, create a draft, schedule or queue it across networks, upload media and handle errors, limits and retries with the Tesselith REST API.

The Tesselith API lets your own code do what the composer does: create posts for any connected channel, schedule them for an exact time or add them to a channel's posting queue, upload images and video, and read how they performed. It's a JSON REST API at https://api.tesselith.com/v1. It goes through the same service layer as the app, so validation, permissions, plan limits and the audit log behave the same whether a post comes from the dashboard, a script or an AI assistant.

Key takeaways

  • Base URL: https://api.tesselith.com/v1. Interactive reference at /v1/docs, OpenAPI 3.1 at /v1/openapi.json.
  • Included on the Pro, Team and Agency plans. Other plans get 403 PLAN_UPGRADE_REQUIRED.
  • Auth: Authorization: Bearer tsl_live_…. Owners and admins create keys in Settings → Developers.
  • A key acts as its creator, limited to its scopes and never more than the creator's current role.
  • Errors are RFC 9457 application/problem+json. Switch on the code field, not the message.
  • Send Idempotency-Key on every POST to make retries safe for 24 hours. Rate limit per key: 120 requests a minute on Pro, 300 on Team, 600 on Agency.

Quick start: your first scheduled post in 4 calls

If you just want to see it work, this is the whole flow. Each step is explained in detail further down.

export TESSELITH_KEY="tsl_live_…"   # Settings → Developers → Create key
API=https://api.tesselith.com/v1

# 1. Find the channel ids you can post to
curl -s "$API/channels" -H "Authorization: Bearer $TESSELITH_KEY"

# 2. Create a draft for one or more channels
curl -s -X POST "$API/posts" \
  -H "Authorization: Bearer $TESSELITH_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-post-1" \
  -d '{"body":"We just shipped dark mode 🌙","channel_ids":["CHANNEL_ID"]}'

# 3a. Schedule it for an exact time…
curl -s -X POST "$API/posts/POST_ID/schedule" \
  -H "Authorization: Bearer $TESSELITH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scheduled_at":"2026-11-03T09:30:00-05:00"}'

# 3b. …or add it to the queue (next free slot on every channel)
curl -s -X POST "$API/posts/POST_ID/queue" \
  -H "Authorization: Bearer $TESSELITH_KEY" \
  -H "Content-Type: application/json" -d '{}'

Authentication and API keys

Every request needs a bearer token in the Authorization header. Tokens are API keys that start with tsl_live_ followed by 40 random characters. Workspace owners and admins create them under Settings → Developers → Create key. You choose a name, the access level and an optional expiry (30 days, 90 days, 1 year, or never). The full key is shown exactly once. Tesselith stores only a SHA-256 hash, so a lost key can't be recovered, only rotated.

Scopes

A key can have full access, read-only access, or a custom set of scopes. The scopes are the same permissions the app's roles use:

ScopeAllows
channel:readList connected channels
post:readList and read posts, queues and posting schedules
post:createCreate drafts
post:updateEdit posts, move them back to drafts, replace posting schedules
post:publishSchedule posts and add them to the queue
post:deleteDelete posts
media:uploadUpload images and video
analytics:readRead per-post metrics
webhook:manageCreate, list and delete webhook endpoints

A key can never do more than the person who created it. Effective access is the key's scopes intersected with the creator's role at the moment of each request. Demote the creator from admin to viewer and their keys immediately lose write access. Remove them from the workspace and their keys stop working. This is deliberate: a key is a delegation, not a separate identity that outlives the person who made it.

Rotating and revoking keys

Rotate issues a new secret with the same name, scopes and expiry and kills the old secret at once. Deploy the new key first if you need zero downtime, then rotate. Revoke ends a key permanently. Both take effect on the next request. Every call made with a key is written to the workspace audit log with the key's id, and the key list shows when each key was last used.

Channels: where posts can go

GET /channels returns every connected social account in the workspace with its id, platform, display name, handle, avatar and status. A status other than active means the network's authorization has expired and someone needs to reconnect it in the app. Posts targeting it will fail until they do, which is also what the channel.reconnect_required webhook tells you.

{
  "data": [
    {
      "id": "3qY0yJk1n2Lr8sVd7a9Bc",
      "platform": "instagram",
      "name": "Acme Coffee",
      "handle": "acmecoffee",
      "avatar_url": "https://…",
      "status": "active"
    }
  ]
}

Creating and editing posts

A post is created as a draft with POST /posts. It has a body, the channel_ids it targets, optional media_ids, an optional IANA timezone, and an optional thread of follow-up replies to publish under it. One post can target many channels: the same draft goes to Instagram, Threads, Bluesky and the rest.

const res = await fetch("https://api.tesselith.com/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TESSELITH_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    body: "Our autumn menu is here. Pumpkin cold brew is back.",
    channel_ids: [instagramId, threadsId, blueskyId],
    media_ids: [photoId],
    timezone: "America/New_York",
  }),
})
if (!res.ok) throw new Error((await res.json()).code)
const post = await res.json() // status: "draft"

PATCH /posts/{id} edits a post. Only the fields you send change, but channel_ids and thread replace the whole list when present. Use overrides to give a channel its own fields (for example a Reddit title or a Pinterest board), keyed by channel id. GET /posts lists posts newest first with optional status and channel_id filters, and GET /posts/{id} returns one.

Follow-up threads

To publish a reply chain under a post (a Bluesky or Threads thread, or a first comment), send thread items with a channel_id, body, sequence and optional delay_minutes. They publish in sequence order after the main post goes out on that channel.

Scheduling: exact time or queue

A draft becomes scheduled in one of two ways, and they mirror the two buttons in the composer.

  • `POST /posts/{id}/schedule` with scheduled_at pins the post to an exact moment. Always include an offset or Z; 2026-11-03T09:30:00-05:00 is unambiguous, while a bare 2026-11-03T09:30:00 is rejected.
  • `POST /posts/{id}/queue` gives the post the next free slot from its channels' weekly posting schedules. position: "last" (the default) takes the next free slot; position: "next" jumps the line and moves queued posts back one slot. A multi-channel post waits for a time that's free on every one of its channels, so it lands everywhere together. The chosen time comes back in scheduled_at.
  • `POST /posts/{id}/unschedule` moves a scheduled post back to drafts.

Both scheduling calls need the post:publish scope. That's stricter than editing, so you can give an integration permission to prepare drafts without letting it publish anything. If a channel has no free slot, the queue call returns 422 with code NO_QUEUE_SLOT. Add times with PUT /channels/{id}/schedule or schedule for an exact time instead.

Reading and changing the queue

GET /queue?channel_id=… returns a channel's upcoming items for the next two weeks in time order: scheduled posts and the free slot times between them. GET /channels/{id}/schedule returns the weekly slots and the workspace timezone. PUT /channels/{id}/schedule replaces them with a list of {day_of_week, time} pairs (0 is Sunday, time is HH:mm local), up to 168 a week. Queued posts re-flow into the new slots. Any that no longer fit keep their time as fixed-time posts; the response counts them in stranded_posts.

curl -s -X PUT "$API/channels/CHANNEL_ID/schedule" \
  -H "Authorization: Bearer $TESSELITH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slots":[{"day_of_week":1,"time":"09:00"},
                {"day_of_week":3,"time":"09:00"},
                {"day_of_week":5,"time":"12:30"}]}'

Uploading media

POST /media stores an image or video and returns an id to use in media_ids. Send the file as multipart file, or send JSON {"url": "https://…"} and Tesselith fetches it for you. Images (JPEG, PNG, GIF, WebP) can be up to 32 MB. Video (MP4, MOV, WebM, AVI, WMV, MPEG, 3GP, FLV), PDFs and caption files (VTT, SRT, TTML) can be up to 100 MB through the API. Optional alt_text, width, height and duration_seconds fields are stored with the file; alt text is used on networks that support it.

# From disk
curl -s -X POST "$API/media" -H "Authorization: Bearer $TESSELITH_KEY" \
  -F [email protected] -F alt_text="Pumpkin cold brew on a wooden table"

# From a public URL
curl -s -X POST "$API/media" -H "Authorization: Bearer $TESSELITH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/cover.jpg"}'

URL uploads only fetch from public HTTPS addresses. Private networks, loopback and cloud metadata addresses are refused, redirects aren't followed, and a fetch that takes longer than 15 seconds fails with URL_UNAVAILABLE.

Analytics

GET /analytics/posts/{id} returns the latest metrics for a published post, per channel and totalled: impressions, likes, comments, shares, saves, clicks, video plays and watch time. A null means the network didn't report that metric, not zero. synced_at tells you how fresh the numbers are.

Errors

Every error is an RFC 9457 problem document with Content-Type: application/problem+json. The code field is stable and safe to switch on; detail is written for people and may change. Validation errors add an errors array with the path and message for each field.

{
  "type": "https://tesselith.com/docs/api/errors#validation-error",
  "title": "Unprocessable content",
  "status": 422,
  "detail": "The request isn't valid.",
  "code": "VALIDATION_ERROR",
  "errors": [{ "path": "scheduled_at", "message": "Invalid ISO datetime" }]
}
StatusCodeWhat to do
401UNAUTHORIZEDKey missing, wrong, expired or revoked. Don't retry.
403FORBIDDENThe key's scopes or its creator's role don't allow this.
403POST_LIMIT_REACHEDThe workspace hit its plan's monthly post limit.
403PLAN_UPGRADE_REQUIREDThe workspace's plan doesn't include the API.
404NOT_FOUNDNo such object in this workspace.
409IDEMPOTENCY_IN_PROGRESSThe same Idempotency-Key is still running. Retry shortly.
422VALIDATION_ERRORFix the fields listed in errors.
422NO_QUEUE_SLOTThe channels share no free queue slot. Add times or pick one.
422IDEMPOTENCY_KEY_REUSEDThe key was used for a different request body.
422URL_UNAVAILABLEA media URL couldn't be fetched safely.
429RATE_LIMITEDWait for Retry-After seconds.

Idempotency: safe retries

Networks fail in the middle of requests. Without protection, a client that retries a timed-out POST /posts can create the same post twice. Send an Idempotency-Key header (any string up to 255 characters, typically a UUID or your own record id) on every POST. Tesselith stores the first successful response for 24 hours and returns it again for a repeat with the same key, marked with Idempotent-Replayed: true. Errors aren't stored, so you can fix a bad request and retry under the same key. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSED.

Rate limits

Each key can make 120 requests per minute on Pro, 300 on Team and 600 on Agency. Every response reports where you stand using the IETF RateLimit and RateLimit-Policy headers. When you go over, you get 429 with a Retry-After header giving the seconds until the window resets. Back off for that long instead of retrying in a tight loop.

RateLimit-Policy: "key";q=120;w=60
RateLimit: "key";r=87;t=34

Pagination

List endpoints return { "data": [...], "next_cursor": "…" }. Pass next_cursor back as cursor to get the next page, and stop when it's null. limit sets the page size, up to 100. Cursors are opaque: don't parse or construct them.

import os, requests

API = "https://api.tesselith.com/v1"
headers = {"Authorization": f"Bearer {os.environ['TESSELITH_KEY']}"}

cursor = None
while True:
    params = {"status": "published", "limit": 100}
    if cursor:
        params["cursor"] = cursor
    page = requests.get(f"{API}/posts", headers=headers, params=params).json()
    for post in page["data"]:
        print(post["id"], post["scheduled_at"], post["body"][:60])
    cursor = page["next_cursor"]
    if not cursor:
        break

Conventions

  • JSON fields are snake_case. Ids are opaque strings.
  • Timestamps are ISO 8601. Responses are always UTC (Z); requests must include an offset.
  • Versioning is in the path. Changes to /v1 are additive only; a breaking change would ship as /v2 with a deprecation window.
  • Webhooks deliver the other direction: see the webhooks guide.

Every error code, explained →

Open the interactive API reference →

Questions developers ask

Where do I get a Tesselith API key?
Workspace owners and admins create keys in Settings → Developers. The key is shown once; copy it into your secret manager straight away.
Is there an OpenAPI spec for generating a client?
Yes. https://api.tesselith.com/v1/openapi.json is an OpenAPI 3.1 document generated from the same schemas the server validates with, so generated clients match the real responses.
Can an API key access more than one workspace?
No. A key belongs to one workspace. Create a key in each workspace you need to automate.
Does the API publish immediately?
The API schedules posts for a time or a queue slot; the publishing pipeline sends them within a minute of their time. To publish as soon as possible, schedule for the current time.
What happens if my API call times out after creating a post?
Retry with the same Idempotency-Key. If the first attempt succeeded, you get its original response back instead of a second post.

Build on Tesselith

Create a workspace, connect your channels and generate an API key in Settings → Developers.

Join the waitlist

Related pages

Webhooks guideAPI error codesREST API overviewPosting queueAutomate social posting with an API

More docs

API Error CodesWebhooks: Real-Time Events From TesselithConnect Claude and Other AI Assistants to Tesselith (MCP)

Opening soon

Be first in when the doors open.

Join the waitlist and you'll be in the first wave of invites. One email when it opens, nothing else.

Join the waitlist
Tesselith

The social scheduler for creators, teams and agencies. One calendar, every network you post to.

Product

  • Composer
  • Calendar
  • Analytics
  • Automation
  • AI assistant
  • Posting queue
  • API & webhooks
  • MCP server
  • All features

Solutions

  • Creators
  • Agencies
  • Small business
  • E-commerce
  • All solutions

Compare

  • vs Buffer
  • vs Hootsuite
  • vs Later
  • vs Sprout Social
  • All comparisons
  • Alternatives

Free tools

  • Character counter
  • Best time to post
  • UTM builder
  • Engagement rate
  • All tools

Company

  • About
  • Blog
  • Developer docs
  • Pricing
  • Contact
  • Security
  • Status

Legal

  • Privacy
  • Terms
  • Cookies
  • Acceptable Use
  • DPA
  • Refunds

Schedule posts for

  • X (Twitter)
  • Instagram
  • Facebook Pages
  • LinkedIn
  • YouTube
  • TikTok
  • Threads
  • Bluesky
  • Mastodon
  • Pinterest
  • Reddit
  • Discord
  • Telegram
  • Dev.to
  • Hashnode
© 2026 Tesselith. All rights reserved.
PrivacyTermsCookiesSecurityData deletion
Tesselith