Documentation Index
Fetch the complete documentation index at: https://apidocs.scripe.io/llms.txt
Use this file to discover all available pages before exploring further.
Posts
A post is the LinkedIn-ready version of a note. Posts move through
a state machine — draft → waiting approval → approved → scheduled →
published — and the v1 API exposes every state.
The API is read-only in v1: triggering post creation, editing
content, scheduling, and publishing are all dashboard-only operations.
Phase 3 introduces the write endpoints.
GET /v1/posts
List posts inside a project. projectId is required.
curl -i 'https://api.scripe.io/v1/posts?projectId=proj_01J9ZA…&status=draft,scheduled' \
-H "Authorization: Bearer scripe_sk_live_…" \
-H "Scripe-Api-Version: 2026-08-01"
Query parameters
| Name | Type | Required | Default | Notes |
|---|
projectId | string | yes | — | proj_* id. 404 if it doesn’t belong to the workspace. |
status | string | no | — | Comma-separated list of statuses (see below). Unknown values → 400 invalid_request. |
dateFrom | string | no | — | YYYY-MM-DD. Filters by post createdAt. Inclusive. |
dateTo | string | no | — | YYYY-MM-DD. Inclusive end-of-day UTC. |
limit | integer | no | 50 | 1…200. |
cursor | string | no | — | Opaque cursor from a previous response. |
Status values
The legacy status enum carries one of:
| Value | Meaning |
|---|
waitingProcessing | The post is being generated by the AI pipeline. |
draft | Editable draft, not yet sent for approval or scheduling. |
waitingApproval | Out for review (only used in workspaces with approval flows). |
approved | Approved by the reviewer; awaiting scheduling. |
rejected | Reviewer rejected the draft. Stays in the dashboard for edit. |
scheduled | Scheduled to publish at scheduledAt. |
published | Published to LinkedIn. |
suggested | AI-suggested but not yet acted on by the user. |
We’re moving towards a custom status system in the dashboard
(customStatusId), but v1 only exposes the legacy enum. The two are
synced bidirectionally inside the dashboard, so filtering by the legacy
status here will catch the right rows. Custom statuses will surface in
v1.x as an additive customStatus field; existing clients won’t break.
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"id": "post_01J9ZA…",
"projectId": "proj_01J9ZA…",
"status": "draft",
"platform": "LINKEDIN",
"contentType": "EDUCATIONAL",
"title": "Three lessons from rolling out a pricing change",
"content": "1/ Don't ship in December …",
"scheduledAt": null,
"createdAt": "2026-05-15T08:21:00.000Z",
"updatedAt": "2026-05-15T09:11:42.000Z"
}
],
"pagination": {
"next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2L…",
"has_more": false
}
}
Field reference
| Path | Type | Notes |
|---|
id | string (post_*) | |
projectId | string (proj_*) | null | The owning project. null only for legacy rows pre-Phase 1; modern posts always carry one. |
status | string | Legacy status enum (see above). |
platform | string | LINKEDIN. New platforms are additive; treat unknown values as opaque. |
contentType | string | One of EDUCATIONAL, PERSONAL, BUSINESS, INSPIRATIONAL, NEWS, UNKNOWN. |
title | string | Display title. May be empty for very early drafts. |
content | string | Body of the post. Plain text with newlines; no HTML. |
scheduledAt | string | null | ISO 8601 UTC. null unless status === "scheduled". |
createdAt | string | ISO 8601 UTC. |
updatedAt | string | null | ISO 8601 UTC. null if never edited. |
Internal fields like customStatusId, dfyStatus, dfyLiked,
chosenHook, postLength, postCreativity, rating, reasoning, the
multi-image asset payloads, and reshare context are intentionally not
in the response. They’re dashboard-only state and may be removed or
restructured without notice.
- Order is
(createdAt DESC, id DESC).
- Changing any filter (
projectId, status, dateFrom, dateTo,
limit) mid-loop will likely emit 400 bad_cursor. Restart the loop.
GET /v1/posts/{postId}
Single post read. Same shape, wrapped in data.
curl -i https://api.scripe.io/v1/posts/post_01J9ZA… \
-H "Authorization: Bearer scripe_sk_live_…" \
-H "Scripe-Api-Version: 2026-08-01"
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"id": "post_01J9ZA…",
"projectId": "proj_01J9ZA…",
"status": "scheduled",
"platform": "LINKEDIN",
"contentType": "EDUCATIONAL",
"title": "Three lessons from rolling out a pricing change",
"content": "1/ Don't ship in December …",
"scheduledAt": "2026-05-20T13:00:00.000Z",
"createdAt": "2026-05-15T08:21:00.000Z",
"updatedAt": "2026-05-19T17:42:11.000Z"
}
}
Errors
| Status | Code |
|---|
| 400 | invalid_request (bad status), bad_cursor, bad_pagination |
| 401 | unauthenticated, invalid_token, key_revoked, key_expired |
| 403 | scope_missing (need posts:read), plan_not_eligible |
| 404 | not_found |
| 429 | rate_limited |
POST /v1/posts
Create a draft post (or schedule one for a future timestamp). Required
scope: posts:write.
curl -i https://api.scripe.io/v1/posts \
-X POST \
-H "Authorization: Bearer scripe_sk_live_…" \
-H "Scripe-Api-Version: 2026-08-01" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a-unique-id-from-your-side" \
-d '{
"projectId": "proj_01J9ZA…",
"title": "Three lessons from rolling out a pricing change",
"content": "1/ Don'"'"'t ship in December…",
"scheduledFor": "2026-06-01T13:00:00Z"
}'
Request body
| Field | Type | Required | Notes |
|---|
projectId | string (proj_*) | yes | Must belong to the workspace; otherwise 404 not_found. |
content | string | no | TipTap-compatible body. Up to 100,000 chars. Defaults to a single space (TipTap rejects truly-empty content). |
title | string | no | Up to 500 chars. Empty by default. |
contentType | string | no | One of PERSONAL, BUSINESS_INTERNAL, BUSINESS_EXTERNAL, EDUCATIONAL, UNKNOWN. Defaults to PERSONAL. |
scheduledFor | string (ISO 8601) | no | Future timestamp. When set, the post status becomes scheduled and a calendar slot is created. Past timestamps → 422 unprocessable. |
Response
HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: false
{
"data": {
"id": "post_01J9ZA…",
"projectId": "proj_01J9ZA…",
"status": "scheduled",
"platform": "LINKEDIN",
"contentType": "PERSONAL",
"title": "Three lessons from rolling out a pricing change",
"content": "1/ Don't ship in December…",
"scheduledAt": "2026-06-01T13:00:00.000Z",
"createdAt": "2026-05-15T08:21:00.000Z",
"updatedAt": null
}
}
LinkedIn-token caveat
API-driven scheduling does NOT validate the project’s LinkedIn token at
create-time. The post-scheduler cron picks the post up at publish time
and refreshes the token. If the refresh permanently fails, the post
moves to failed_to_publish (same path as the dashboard). This is the
only intentional behavioural delta between API-scheduled and
dashboard-scheduled posts.
Errors
| Status | Code |
|---|
| 400 | invalid_request (missing projectId) |
| 401 | unauthenticated, invalid_token, key_revoked, key_expired |
| 403 | scope_missing (need posts:write), plan_not_eligible |
| 404 | not_found (project not in workspace) |
| 409 | idempotency_key_conflict |
| 413 | payload_too_large (content > 100KB or title > 500 chars) |
| 422 | unprocessable (bad shape, bad enum, past scheduledFor) |
| 429 | rate_limited |
POST /v1/posts/generations
Generate a post draft from a piece of free-form text or a saved Note.
The request kicks off an async job — the response is a Job
envelope you poll until status === "DONE" (then result.postId
points to the freshly-created Post). Required scope: posts:write.
curl -i https://api.scripe.io/v1/posts/generations \
-X POST \
-H "Authorization: Bearer scripe_sk_live_…" \
-H "Scripe-Api-Version: 2026-08-01" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a-unique-id-from-your-side" \
-d '{
"projectId": "proj_01J9ZA…",
"source": {
"type": "text",
"text": "Three lessons we learned shipping a pricing change…"
},
"options": {
"contentType": "EDUCATIONAL"
}
}'
Request body
| Field | Type | Required | Notes |
|---|
projectId | string (proj_*) | yes | Must belong to the workspace; otherwise 404 not_found. |
source.type | string | yes | text or note. |
source.text | string | when type=text | Up to 100,000 chars. Past the cap → 413 payload_too_large. |
source.noteId | string (note_*) | when type=note | Note must belong to the same project; otherwise 422 unprocessable. |
scheduledFor | string (ISO 8601) | no | Future timestamp. Stored on the resulting post; v1 doesn’t auto-schedule via this path yet. |
options.language | string | no | Short language code (≤16 chars). Defaults to the project’s saved preference. |
options.contentType | string | no | One of PERSONAL, BUSINESS_INTERNAL, BUSINESS_EXTERNAL, EDUCATIONAL, UNKNOWN. |
The worker already loads the project’s tone-of-voice profile, voice
samples, content pillars, and knowledge base before drafting. There
is intentionally no tone or preferredHookStyle option — tone
is configured per-project in the dashboard, and the hook generator
picks its own top-ranked hook. If you want to steer a specific draft,
put the steering text inside source.text (audience, format hints,
example phrases, etc.) — the worker weaves it into the prompt.
| wait_for_completion_ms | integer | no | If set, the API holds the connection up to this many ms (capped server-side at 25,000 ms) waiting for the job to finish. Useful for short-running flows that want a synchronous result. Falls through to standard polling on timeout. |
Response
HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: false
{
"data": {
"id": "job_01J9ZA…",
"type": "POST_GENERATION",
"status": "QUEUED",
"projectId": "proj_01J9ZA…",
"startedAt": null,
"completedAt": null,
"progress": null,
"result": null,
"errorCode": null,
"errorMessage": null,
"attemptCount": 0,
"estimatedCompletionMs": 25000,
"createdAt": "2026-05-15T08:21:00.000Z",
"updatedAt": "2026-05-15T08:21:00.000Z"
}
}
When the job finishes, result becomes { "postId": "post_01J9ZA…" }
and you can fetch the post via GET /v1/posts/{postId}.
Errors
| Status | Code |
|---|
| 400 | invalid_request (missing projectId, missing source fields) |
| 401 | unauthenticated, invalid_token, key_revoked, key_expired |
| 402 | spend_cap_exceeded (workspace daily AI cap reached) |
| 403 | scope_missing (need posts:write), plan_not_eligible |
| 404 | not_found (project / note not in workspace) |
| 409 | idempotency_key_conflict |
| 413 | payload_too_large (source.text > 100KB) |
| 422 | unprocessable (bad shape, unknown enum, note belongs to a different project, past scheduledFor) |
| 429 | rate_limited |
What’s NOT here (yet)
- Engagement metrics (likes, comments, views). They live on a
separate
tPostLinkedIn row and will surface as an analytics
endpoint in Phase 4.
- Variations and version history. v1 returns the canonical post
body. Per-version reads are queued for the dashboard’s content
studio integration.
- Approval / reviewer state. The
status === "waitingApproval"
signal exists, but the reviewer queue, decisions, and notes are
dashboard-only in v1.
- Update / delete endpoints.
PATCH / DELETE are out of scope
for v1. Use the dashboard for edits.