staqlydocs

TypeScript SDK

Write, schedule and publish posts, connect channels and react to webhooks from TypeScript or JavaScript code.

Edit on GitHub

@staqly/sdk is the typed client for staqly in TypeScript and JavaScript. Use it when your agent or app is written in TS/JS and should write posts, put them on the calendar, publish through your own automation or react to webhooks. For shell scripts and cron pick the CLI, for AI agents that speak MCP use the MCP server, and from other languages call the REST API or use the Python SDK.

The SDK has no runtime dependencies (only fetch and Web Crypto) and runs on Node 20.19+, Bun, Deno, browsers and edge runtimes. Its types are generated from the API's OpenAPI contract.

Get started

Install the package

npm install @staqly/sdk

The package is ESM-only. CommonJS code can still require('@staqly/sdk') on Node 20.19+ or 22.12+.

Start staqly

Run a server on your computer with staqly serve (or Docker) and give the SDK its access token, which staqly prints at start, in STAQLY_API_KEY:

export STAQLY_API_KEY=$(staqly serve token)   # Docker: $(docker exec staqly server token)

Connect a channel, write a post, publish it

publish.ts
import { Staqly } from '@staqly/sdk'

const staqly = new Staqly() // http://localhost:3333, the token from STAQLY_API_KEY

const channel = await staqly.channels.connect({ provider: 'custom', name: 'My publisher' })
await staqly.collections.create({ name: 'Product updates' })

const post = await staqly.posts.create({
  collection: 'Product updates',
  content: 'v1.4 is out: schedules, channels and a calendar.',
  targets: [{ channel_id: channel.id }],
  publish_now: true,
})

const done = await staqly.posts.waitForPublished(post.id) // resolves when every channel settled

for (const t of done.targets) {
  console.log(t.channel_name, t.status, t.external_url ?? t.error)
}

Watch it in the dashboard

Open http://localhost:8080: the post is on the Schedule page and in Posts, with a status per channel.

Configure the client

import { Staqly } from '@staqly/sdk'

const staqly = new Staqly({
  baseUrl: 'https://app.staqly.ai', // default: $STAQLY_API_URL, then http://localhost:3333
  apiKey: process.env.STAQLY_API_KEY, // default: $STAQLY_API_KEY; null = ignore the env var
  timeout: 60_000, // per-request timeout in ms
  maxRetries: 2, // retries for 408/429/5xx/network errors
  fetch: myFetch, // custom fetch (tests, proxies, tracing)
  defaultHeaders: { 'X-Team': 'growth' },
  userAgent: 'my-agent/1.0', // appended to "staqly-sdk-ts/<version>"
})

apiKey is an API key in cloud mode (stk_...), or the Community Edition's access token (stk_local_...). From a container on the staqly Docker network, use baseUrl: 'http://api:3333'.

Every method takes an optional last argument with per-request overrides:

await staqly.posts.list({ status: 'scheduled' }, { timeout: 5_000, maxRetries: 0, signal })
await staqly.posts.create({ collection: 'Posts', content: 'x' }, { idempotencyKey: `job-${jobId}` })

Request bodies and query parameters use the API's snake_case names (publish_at, channel_id), exactly as in the REST docs. SDK options such as timeout and idempotencyKey are camelCase.

staqly.health() checks liveness, staqly.ready() checks readiness, and staqly.whoami() returns the server mode, the workspace and the calling principal with its scopes. The reference lists every method.

Work with posts

// a draft with a text of its own on one channel (x and publisher are channels from channels.list())
const post = await staqly.posts.create({
  collection: 'Product updates', // ID or exact name
  content: 'Three days, every avatar half price.',
  media: [media.id], // from staqly.media.upload
  targets: [
    { channel_id: x.id, content: 'Three days. Half price.' },
    { channel_id: publisher.id }, // settings: { ... } sets the provider's per-post fields
  ],
  tags: ['sale'],
  metadata: { campaign: 'spring' },
})

await staqly.posts.preview(post.id) // the text per channel and what each platform objects to
await staqly.posts.schedule(post.id, '2026-11-03T09:00:00Z') // onto the calendar
await staqly.posts.cancel(post.id) // back to draft
await staqly.posts.publish(post.id) // now (202: a job does the work)
await staqly.posts.retryTarget(post.id, x.id) // one failed channel again
await staqly.posts.update(post.id, { content: 'Fixed', expected_version: post.version }) // 409 when someone edited it
const copy = await staqly.posts.duplicate(post.id) // a new draft; published posts are read-only

// list, search and page through everything
await staqly.posts.list({ status: 'scheduled', publish_after: '2026-11-02', publish_before: '2026-11-09' }) // the calendar
await staqly.posts.search('half price', { collection_id: post.collection_id })

for await (const p of staqly.posts.iterate({ channel_id: x.id, status: 'published' })) {
  console.log(p.content)
}

await staqly.posts.history(post.id) // its events, newest first
await staqly.posts.tags() // every tag with its count

The client also covers channels, providers, schedules, jobs, media, events, webhooks, alerts, exports, workspace, plans, apiKeys, billing, sessions and deviceLinks; see the reference. staqly.http.request({ method, path, body }) sends any other request with the client's auth, retries and error mapping.

Channels and schedules

const providers = await staqly.providers.list() // the catalog: auth, status, limits, fields
// your own automation; token platforms take their credentials in `fields` once they are available
const publisher = await staqly.channels.connect({ provider: 'custom', name: 'My publisher' })
await staqly.channels.test(publisher.id) // against the platform; marks needs_reconnect on failure
await staqly.channels.disable(publisher.id) // or enable(), update(id, { name, settings })

const schedule = await staqly.schedules.create({
  collection: 'Product updates',
  name: 'Weekday mornings',
  kind: 'recurring',
  rule: { times: ['09:00'], weekdays: [1, 2, 3, 4, 5], tz: 'Europe/Warsaw' },
  channels: [publisher.id],
  pick: 'next',
  source: 'agent',
})
await staqly.schedules.run(schedule.id) // now; the regular next run is unchanged

OAuth platforms need a person in a browser: channels.oauthStart('x') returns the platform's sign-in URL to open, and the browser lands on the dashboard when done; providers.createLink() makes a connect link for someone without access. See Developer apps.

Wait for a post to settle

import { WaitTimeoutError } from '@staqly/sdk'

try {
  const post = await staqly.posts.waitForPublished(id, { timeout: 30 * 60_000 }) // ms, default 10 min

  if (post.status === 'failed') {
    for (const t of post.targets.filter((t) => t.status === 'failed')) {
      console.log(t.channel_name, t.error)
    }
  }
} catch (e) {
  if (e instanceof WaitTimeoutError) {
    console.log('still publishing', e.post)
  }
}

waitForPublished polls the post until it is published or failed (every 2 seconds; pollInterval changes it, in ms). Pass signal to cancel or onPoll to log progress.

For long-running services, push beats waiting: subscribe a webhook to post.published and post.failed.

Be your own publisher

A channel on your own automation hands posts to your code:

for (const w of await staqly.work.list()) {
  if (w.kind === 'target') {
    try {
      const url = await publishSomewhere(w.post, w.channel)
      await staqly.posts.reportResult(w.post.id, w.channel.id, { status: 'published', url })
    } catch (e) {
      await staqly.posts.reportResult(w.post.id, w.channel.id, { status: 'failed', error: String(e) })
    }
  } else {
    // a schedule found nothing to publish: write drafts into w.collection
    await staqly.posts.create({ collection: w.collection.id, content: await draftOne() })
  }
}

work.list() is GET /v1/work. With a webhook on post.publish_requested you get the same request pushed to you.

Verify webhooks

Every webhook delivery is signed with the webhook's secret (whsec_...). verifyWebhook checks the X-Staqly-Signature and X-Staqly-Timestamp headers, rejects timestamps more than 300 s away, and returns the parsed event. It throws WebhookVerificationError otherwise.

app/api/staqly/route.ts
import { verifyWebhook, WebhookVerificationError, type PostEventData } from '@staqly/sdk/webhooks'

export async function POST(req: Request) {
  const body = await req.text() // the raw body, exactly as received

  try {
    const event = await verifyWebhook<PostEventData>(process.env.STAQLY_WEBHOOK_SECRET!, body, req.headers)

    if (event.type === 'post.publish_requested') {
      await publishMyself(event.data.post, event.data.channel)
    }

    return new Response('ok')
  } catch (e) {
    if (e instanceof WebhookVerificationError) {
      return new Response('invalid signature', { status: 400 })
    }

    throw e
  }
}
  • Always pass the raw body. JSON that was parsed and serialized again will not match.
  • headers can be a Headers object or a plain object (Express req.headers works with express.raw({ type: 'application/json' })).
  • A fourth argument { tolerance } changes the time window in seconds (0 disables it).
  • Deliveries are at-least-once and can arrive out of order: de-duplicate on event.id (also sent as X-Staqly-Event-Id) and order by event.created_at (Delivery guarantees).
  • isValidWebhook() returns a boolean instead of throwing; computeWebhookSignature() helps you test your handler. The same functions verify a schedule endpoint request with the schedule's endpoint_secret.

@staqly/sdk/webhooks is a small standalone entry point, handy for edge functions.

Handle errors

Every error extends StaqlyError. HTTP errors extend APIError and carry status, code, message, fields, details, requestId, headers and body.

ClassWhen
BadRequestError400 malformed request
AuthenticationError401 missing, invalid or revoked key
PlanLimitError402 plan_limit_exceeded, with limit, max, used, plan
PermissionDeniedError403 missing scope, or the action needs a dashboard session
NotFoundError404
ConflictError409, e.g. the post is published, the version changed (post_changed), the platform is soon
GoneError410
PayloadTooLargeError / UnsupportedMediaTypeError413 / 415 uploads
ValidationError422, with fields (a target over its provider's limit is keyed targets.<channel_id>.content)
RateLimitError429, with retryAfter (seconds)
InternalServerError5xx
APIConnectionError / TimeoutErrorno response / request timed out
APIUserAbortErroryour AbortSignal fired
WaitTimeoutErrorwaitForPublished or waitForWorkspaceExport gave up (id, waited in seconds, post)
ExportFailedErrora workspace export ended failed or expired (job)
WebhookVerificationErrorbad signature or timestamp
import { ConflictError } from '@staqly/sdk'

try {
  await staqly.posts.update(id, { content: 'x', expected_version: 3 })
} catch (e) {
  if (e instanceof ConflictError) {
    // someone edited the post meanwhile; reload it
  } else {
    throw e
  }
}

Retries, timeouts and idempotency

  • Requests that fail with 408, 429, 500, 502, 503, 504, a network error or a timeout are retried up to maxRetries times (default 2) with exponential backoff and jitter. A Retry-After of 60 s or less is honored; a longer one throws RateLimitError right away.
  • Every POST, PATCH or DELETE with a JSON body gets an automatic Idempotency-Key, reused across its retries, so a retry never applies the request twice. Pass { idempotencyKey } to choose the key yourself, for example to de-duplicate across process restarts. JSON bodies are compared by content (key order and spacing don't matter), so a key can be shared with the Python SDK.
  • Uploads (media.upload) are retried only on 429.
  • timeout (default 60 s) applies to each attempt.

Types

All models come from the OpenAPI contract and are exported: Post, PostTarget, Collection, Channel, Provider, Schedule, Job, WorkItem, Event, Webhook, Delivery, Alert, Workspace, APIKey, Plan and more. Webhook payloads are typed by WebhookEvent<D> and PostEventData.

Reference

Every method takes RequestOptions (timeout, maxRetries, idempotencyKey, headers, signal) as its last argument. Methods that return a list resolve to an array; methods named list on paginated endpoints (posts, events, jobs) resolve to a page { data, next_cursor }.

Client

MethodWhat it does
new Staqly(options?)See Configure the client
health(), live(), ready()GET /health, /livez, /readyz
setup()GET /v1/setup: mode, whether signup is open, billing
whoami()GET /v1/session: mode, workspace, principal and scopes
openapi()The raw OpenAPI document as text
http.request({ method, path, query, body, headers })Any request with the client's auth, retries and error mapping

posts

MethodEndpoint
list(params?), iterate(params?)GET /v1/posts (collection_id, status, channel_id, tag, publish_after, publish_before, q, sort, limit, cursor)
search(q, params?)GET /v1/posts?q=
get(id)GET /v1/posts/{id}
create(input)POST /v1/posts (collection, content, media, targets, publish_at, status, publish_now, tags, metadata, source, external_ref, repeat_every, thread)
update(id, patch)PATCH /v1/posts/{id} (expected_version refuses a stale edit)
schedule(id, publishAt), cancel(id)POST .../schedule, POST .../cancel
publish(id)POST .../publish (202)
duplicate(id)POST .../duplicate
preview(id)GET .../preview
history(id, { limit?, cursor? })GET .../events (newest first)
retryTarget(id, channelId)POST .../targets/{channel}/retry (202)
reportResult(id, channelId, { status, url?, external_id?, error? })POST .../targets/{channel}/result
waitForPublished(id, { timeout?, pollInterval?, signal?, onPoll? })Polls GET /v1/posts/{id} until the post is published or failed
delete(id), deleteMany(ids), purge(id)DELETE /v1/posts/{id}, POST /v1/posts/delete, POST .../purge
metrics(id, { refresh? }), reportMetrics(id, channelId, metrics)GET .../metrics, PUT .../targets/{channel}/metrics (numbers per channel)
createPreviewLink(id, { expires_in_days? }), previewLinks(id), revokePreviewLink(...)Preview links anyone can open and comment on
comments(id), addComment(id, input), resolveComment(), reopenComment(), deleteComment()Reviewers' comments on the post
tags()GET /v1/tags

work

MethodEndpoint
list()GET /v1/work: targets waiting for a result (kind: target) and schedules asking for posts (kind: posts)

collections

MethodEndpoint
list({ include_archived? })GET /v1/collections
get(idOrName)GET /v1/collections/{id}
create({ name, description?, icon?, color?, processing_timeout_seconds? })POST /v1/collections
update(id, patch)PATCH /v1/collections/{id}
archive(id), unarchive(id)PATCH with archived
delete(id, confirmName)DELETE /v1/collections/{id}?confirm=
listPosts(idOrName, params?)GET /v1/collections/{id}/posts

channels and providers

MethodEndpoint
providers.list()GET /v1/providers
providers.setApp(id, { client_id, client_secret }), providers.deleteApp(id)PUT / DELETE /v1/providers/{id}/app
providers.createLink({ note?, providers?, group_id?, expires_in_hours?, max_uses? }), providers.links(), providers.revokeLink(id)/v1/channel-links (connect links)
channels.oauthStart(provider, { instance? })GET /v1/channels/oauth/{provider}/start: the URL a person opens to connect an OAuth platform
channels.listGroups(), createGroup(), updateGroup(), deleteGroup()Channel groups
channels.list({ group? }), channels.get(id)GET /v1/channels, GET /v1/channels/{id}
channels.connect({ provider, name?, fields?, settings? })POST /v1/channels (token platforms and custom)
channels.update(id, { name?, settings?, status?, fields? }), channels.enable(id), channels.disable(id)PATCH /v1/channels/{id}
channels.test(id)POST /v1/channels/{id}/test
channels.remove(id)DELETE /v1/channels/{id}

schedules

MethodEndpoint
list({ collection_id? }), get(id)GET /v1/schedules, GET /v1/schedules/{id}
create(input)POST /v1/schedules
update(id, patch)PATCH /v1/schedules/{id}
run(id)POST /v1/schedules/{id}/run (202)
delete(id)DELETE /v1/schedules/{id}

jobs

MethodEndpoint
list({ status?, kind?, limit?, cursor? }), get(id)GET /v1/jobs, GET /v1/jobs/{id}
stats()GET /v1/jobs/stats
retry(id), cancel(id)POST /v1/jobs/{id}/retry, /cancel

media

MethodEndpoint
upload(file, { filename? })POST /v1/media (a Blob, File or fileFromPath() from @staqly/sdk/node)
list(params?), info(id), update(id, patch), usage(id)GET /v1/media, GET / PATCH /v1/media/{id}/info, .../usage
download(id), downloadResponse(id), url(id)GET /v1/media/{id} (bytes, the raw Response, or the URL)
delete(id, { permanent? }), restore(id)DELETE /v1/media/{id} (to Deleted for 30 days, or for good), restore it

events

MethodEndpoint
list(params?), iterate(params?)GET /v1/events (post_id, channel_id, schedule_id, collection_id, type, type_prefix, actor_type, actor_id, agent, from, to, limit, cursor)

webhooks

MethodEndpoint
list(), get(id)GET /v1/webhooks, GET /v1/webhooks/{id}
create({ url, events?, collection_ids?, description?, active?, timeout_ms?, max_attempts? })POST /v1/webhooks (the response carries the secret once)
update(id, patch), rotateSecret(id)PATCH /v1/webhooks/{id}
test(id)POST /v1/webhooks/{id}/test
delete(id)DELETE /v1/webhooks/{id}
deliveries.list({ webhook_id?, status?, post_id?, limit? }), deliveries.retry(id)GET /v1/webhook-deliveries, POST .../{id}/retry

alerts

MethodEndpoint
list(), get(id)GET /v1/alerts, GET /v1/alerts/{id}
create({ name, type, config, events?, collection_ids?, digest_window_seconds?, enabled? })POST /v1/alerts
update(id, patch), enable(id), disable(id)PATCH /v1/alerts/{id}
test(id)POST /v1/alerts/{id}/test
deliveries(id, { limit? })GET /v1/alerts/{id}/deliveries
delete(id)DELETE /v1/alerts/{id}

exports

MethodEndpoint
events(params?), eventsResponse({ format?, ...params })GET /v1/events/export: an async iterator of events, or the raw NDJSON / CSV Response (save it with saveResponse from @staqly/sdk/node)
createWorkspaceExport(), listWorkspaceExports(), getWorkspaceExport(id)/v1/workspace/export, /v1/workspace/exports
waitForWorkspaceExport(id, { timeout? }), downloadWorkspaceExport(id), workspaceExportResponse(id)Poll, then GET .../download (bytes, or the raw Response for saveResponse)
exportWorkspace({ timeout? })Start and wait in one call; resolves to the ready job

workspace, workspaces and plans

MethodEndpoint
workspace.get(), workspace.rename(name), workspace.update({ name, timezone })GET / PATCH /v1/workspace
workspace.usage()GET /v1/workspace/usage
workspace.members()GET /v1/members (cloud)
workspace.agents(), workspace.forgetAgent(id)GET /v1/agents, DELETE /v1/agents/{id}
workspace.delete(confirmName)DELETE /v1/workspace (dashboard session only)
workspace.accessToken(), workspace.rotateAccessToken()Community Edition: GET /v1/access-token, POST /v1/access-token/rotate
workspaces.create({ name, setup_token? })POST /v1/workspaces (cloud signup)
plans.list()GET /v1/plans
MethodEndpoint
billing.get(), billing.details(), billing.checkout({ plan?, interval? }), billing.portal()/v1/billing, /details, /checkout, /portal
billing.xPosts(), billing.creditsCheckout({ posts })/v1/billing/x-posts, /credits/checkout (extra posts to X)
apiKeys.list(), apiKeys.create({ name, scopes, expires_at? }), apiKeys.rotate(id), apiKeys.revoke(id)/v1/api-keys
sessions.current(), sessions.list(), sessions.revoke(id), sessions.logout()/v1/session, /v1/sessions
sessions.signInWithApiKey(key), sessions.recover(recoveryKey)POST /v1/sessions/api-key, POST /v1/sessions/recover
deviceLinks.create(), deviceLinks.redeem(token)/v1/device-links, /redeem

Next steps

On this page