OpenAPI reference · Media
Add your own image to the media library
/mediaRegister an image the customer supplied as a media-library asset of the project, so it can be attached to a post.
This is the producer the library was missing on this surface.
POST /v1/uploads accepts image/*, but its handle is consumed
by POST /v1/sources and POST /v1/knowledge — neither of
which produces media — and PATCH /v1/posts/{postId}/media
takes either a library img_… id or a stored file key, never an
upload handle. Every other library writer is first-party (the
dashboard, the LinkedIn sync, POST /v1/media/generations).
Two ways in, exactly one of them per call:
content_base64— the bytes inline, for SMALL files only (≤ ~3 MB decoded ≈ 4 MB as base64). Larger request bodies are rejected at the platform edge (~4.5 MB on the wire) with a bare HTTP 413 that carries no error envelope, so pre-check the file size instead of retrying inline. An optionalsha256of the decoded bytes is verified after decode.uploadId— anupl_…handle fromPOST /v1/uploads, after the bytes have been PUT to its signed URL. Preferred for anything larger than the inline cap; the signed PUT takes the full 25 MB image cap.
The stored object is copied into the project's
image-library/ prefix with a file extension, which is what the
publish step needs in order to tell an image from a document.
The response is the same MediaAsset object GET /v1/media
lists.
The asset is returned status: PROCESSING and is attachable
immediately — attach resolves the stored file, not the status.
Cloudflare re-hosting and vision tagging run asynchronously;
GET /v1/media (READY only) lists it once they finish.
Sending the same bytes, or the same upload handle, twice returns
the asset already created rather than a duplicate row. Re-sending
the bytes of an asset that was deleted (MCP delete_media_asset)
restores it under a freshly minted storage key, so an object an
existing post already points at is never overwritten; the rare
case where that restore cannot be recorded answers 409.
Images only: SVG is refused (it is a script-bearing document and
LinkedIn rejects it), and a non-image content type is refused
naming the endpoint that takes it. Requires media:write.
Authorization
AuthorizationstringrequiredBearer token in the Authorization header.
Pass
Authorization: Bearer scripe_sk_live_<...>(orscripe_sk_test_<...>for test keys) on every request. Keys are scoped to a single workspace and can be revoked from the Scripe dashboard.The same header also accepts an OAuth 2.1 access token (
scripe_oat_*); both credentials share one scope vocabulary and every operation below documents the scope it requires. An API key can hold every scope named on this surface exceptwebhooks:manage, which is grantable to OAuth tokens only today — the webhook-endpoint operations answer403 scope_missingto every API key. Operations that name no scope accept any valid token of the workspace.
Header parameters
Scripe-Api-VersionstringPin the API version. Format
YYYY-MM-DD. Omit to receive the currently rolling default. Unknown versions return400 version_unsupported.
Request bodyapplication/json
projectIdstringProject whose library receives the image. Optional for an OAuth principal with a default project pinned at consent time.
content_base64stringBase64-encoded image bytes, ≤ ~3 MB decoded (larger request bodies are rejected at the platform edge as a bare HTTP 413 — use the
uploadIdpath instead). Mutually exclusive withuploadId.sha256stringOptional hex SHA-256 of the DECODED image bytes (inline path only; refused beside
uploadId). Strongly recommended when the caller can compute it — base64 relayed through model output corrupts silently, and a digest mismatch is rejected naming both digests instead of storing the wrong bytes.uploadIdstringupl_…handle fromPOST /v1/uploads, after the bytes have been PUT to its signed URL. Mutually exclusive withcontent_base64. A handle whose object does not exist is a 422, not a 404 — the handle is valid, the upload never happened.fileNamestringOriginal filename. On the inline path it is also how the content type is inferred when
mimeTypeis omitted.mimeTypestringImage MIME type for the inline path. Ignored on the
uploadIdpath, where the stored object's own content type (bound into the signature at mint time) is authoritative.altstringAlt text, carried onto the post at attach time.
titlestring
Responses
- 200
The created (or already-existing) asset.
- 400
Malformed request (bad cursor, bad limit, etc.).
- 401
Missing, malformed, expired, or revoked API key.
- 403
Plan not eligible, scope missing, or workspace mismatch.
- 404
Resource not found in this workspace.
- 409
A deleted asset could not be restored: its record of superseded storage keys is full (too many delete/restore cycles) or unreadable. Both keep the earlier copies reachable for erasure, so the restore is refused rather than dropping them. See
conflict. - 422
Body shape was JSON but failed validation (
unprocessable). - 429
Sliding-window rate limit exceeded.
Example request
curl --request POST \
--url 'https://api.scripe.io/v1/media' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"projectId": "proj_a1b2c3d4e5f6g7h8",
"content_base64": "string",
"sha256": "string",
"uploadId": "upl_org_2aSH--30e8e643de8b4f01",
"fileName": "keynote-stage.jpg",
"mimeType": "image/png",
"alt": "string",
"title": "string"
}'Example response (200)
{
"data": {
"id": "img_a1b2c3d4e5f6g7h8",
"projectId": "string",
"source": "UPLOAD",
"status": "string",
"title": "string",
"fileName": "string",
"mimeType": "string",
"width": 0,
"height": 0,
"alt": "string",
"tags": [
"string"
],
"aiCaption": "string",
"reusability": "string",
"generatedAssetKind": "string",
"provenance": {
"provider": "string",
"authorName": "string",
"permalink": "string"
},
"displayUrl": "string",
"createdAt": "string"
}
}