DocumentationDirect media uploads

Direct media uploads

v1

Private direct uploads, verified files and durable assets for scheduled publishing.

Upload directly, then complete

Uploads have two steps: create a private PUT ticket, then verify the completed bytes. A pending asset cannot be published. Use media:write for uploads/completion/deletion and media:read for lists, metadata and private previews. Still images can be JPEG, PNG or WebP, up to 20 MiB and 24 megapixels. Videos can be MP4, MOV or WebM, up to 128 MiB.

TypeScript · server
import { readFile } from 'node:fs/promises';

const bytes = await readFile('./launch.mp4');
const { data: asset } = await castrook.media.upload(
  new Blob([bytes], { type: 'video/mp4' }),
  { filename: 'launch.mp4', content_type: 'video/mp4', profile_id: profile.id },
);

const { data: post } = await castrook.posts.create({
  profile_id: profile.id,
  text: 'Ready for launch.',
  account_ids: [account.id],
  media_items: [{ asset_id: asset.id, type: 'video' }],
  scheduled_at: new Date(Date.now() + 7 * 86400000).toISOString(),
}, { idempotencyKey: 'launch-video-v1' });

The underlying HTTP flow

HTTP
POST /api/v1/media/uploads
Authorization: Bearer YOUR_SERVER_KEY
Content-Type: application/json

{
  "filename": "launch.mp4",
  "size": 1843200,
  "content_type": "video/mp4",
  "sha256": "LOWERCASE_SHA256_HEX_OF_THE_FILE",
  "profile_id": "prf_customer_id"
}

The response is { data: { asset, upload: { url, method, headers, expires_at } } }. Send the file bytes to upload.url using the returned method and exact headers. Send no Castrook key to the PUT URL. Its lifetime is short; create another upload if the ticket expires.

TypeScript
const response = await fetch(ticket.upload.url, {
  method: ticket.upload.method,
  headers: ticket.upload.headers,
  body: file,
});
if (!response.ok) throw new Error('Upload failed');

const { data: ready } = await castrook.media.complete(ticket.asset.id);
// ready.status is 'ready'; completion checks size, type and checksum.

For browser uploads, your backend creates the ticket and returns it to an authenticated customer. The browser PUTs bytes directly to that URL, then asks your backend to complete the upload. Enforce customer/profile ownership in your backend. Never expose your bearer key in browser code.

Durable scheduled assets

Completed files are frozen so an old PUT URL cannot replace scheduled content. Unused files expire after 30 days. Queued and scheduled posts pin their assets through delivery; pending references prevent deletion. Canceling a pending post releases its references. The upload allowance is counted once; using the same asset in delivery does not add another media-ingest debit.

A post can use an asset_id or a public HTTPS URL per item. URL-based sources must remain accessible through ingestion; short-lived URLs are unsuitable for distant schedules. Prefer uploaded assets for durable scheduling.

Inspect and delete

TypeScript
const { data: assets } = await castrook.media.list({ profile_id: profile.id });
const { data: asset } = await castrook.media.get(assetId);

// preview_url requires a signed-in session or bearer authorization.
// It is not a public sharing link or a URL to send to social platforms.
await castrook.media.delete(assetId); // conflicts while a pending post uses it

Build a carousel · Schedule delivery · Media allowances