Scripe relaunch is live.See what's new

OpenAPI reference · Media

Add your own image to the media library

POST/media

Register 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).

Three ways in, exactly one of them per call:

  • sourceUrl — a public https:// address of the image; Scripe downloads it server-side (up to 25 MB), so the bytes never pass through the request. The address must open without signing in and serve the image file itself. Only https on port 443 to a public address is fetched: IP literals, private, loopback, link-local and metadata addresses are refused, every redirect hop (at most three) is judged the same way, and the type is read from the bytes (PNG, JPEG, GIF, WebP, AVIF), never from the server's Content-Type. A refusal is a 422 naming why; a file over the cap is a 413.
  • 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 optional sha256 of the decoded bytes is verified after decode.
  • uploadId — an upl_… handle from POST /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

  • Authorizationstringrequired

    Bearer token in the Authorization header.

    Pass Authorization: Bearer scripe_sk_live_<...> (or scripe_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 except webhooks:manage, which is grantable to OAuth tokens only today — the webhook-endpoint operations answer 403 scope_missing to every API key. Operations that name no scope accept any valid token of the workspace.

Header parameters

  • Scripe-Api-Versionstring

    Pin the API version. Format YYYY-MM-DD. Omit to receive the currently rolling default. Unknown versions return 400 version_unsupported.

Request bodyapplication/json

  • projectIdstring

    Project whose library receives the image. Optional for an OAuth principal with a default project pinned at consent time.

  • sourceUrlstring

    Public https:// address of the image, fetched server-side (see the operation description for what is refused). The file name defaults to the URL's last path segment. Mutually exclusive with content_base64 and uploadId; sha256 is refused beside it.

  • content_base64string

    Base64-encoded image bytes, ≤ ~3 MB decoded (larger request bodies are rejected at the platform edge as a bare HTTP 413 — use the uploadId path instead). Mutually exclusive with uploadId and sourceUrl.

  • sha256string

    Optional 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.

  • uploadIdstring

    upl_… handle from POST /v1/uploads, after the bytes have been PUT to its signed URL. Mutually exclusive with content_base64. A handle whose object does not exist is a 422, not a 404 — the handle is valid, the upload never happened.

  • fileNamestring

    Original filename. On the inline path it is also how the content type is inferred when mimeType is omitted.

  • mimeTypestring

    Image MIME type for the inline path. Ignored on the uploadId path, where the stored object's own content type (bound into the signature at mint time) is authoritative.

  • altstring

    Alt 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.

  • 413

    Request body exceeds the size cap.

  • 422

    Body shape was JSON but failed validation (unprocessable).

  • 429

    Sliding-window rate limit exceeded.

Example request

bash
curl --request POST \
  --url 'https://api.scripe.io/v1/media' \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "projectId": "proj_a1b2c3d4e5f6g7h8",
  "sourceUrl": "https://images.example.com/keynote-stage.jpg",
  "content_base64": "string",
  "sha256": "string",
  "uploadId": "upl_org_2aSH--30e8e643de8b4f01",
  "fileName": "keynote-stage.jpg",
  "mimeType": "image/png",
  "alt": "string",
  "title": "string"
}'

Example response (200)

json
{
  "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"
  }
}