> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackshift.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Trim videos, create JPG posters and download MP4s

> Copyable REST recipes for start/end trimming with audio, timestamp JPEG posters, job polling and signed download URLs for external AI services.

## Goal

Create a finished video clip or poster and obtain the exact output file.

## Prerequisites

* Node.js 20+ for the examples; no SDK package is required
* A server-side token with assets:read, assets:process and assets:write; an authorized Assets space ID
* A clean, ready video with an authorized current native video package; sufficient processing allowance and advanced-transform entitlement

## Workflow

<Steps>
  <Step>
    Upload a video and prepare its current version for native processing.
  </Step>

  <Step>
    Render the trim or poster recipe below.
  </Step>

  <Step>
    Wait for completed status and use the derivative ID returned by that job.
  </Step>

  <Step>
    Download the output or mint its short-lived URL immediately before external processing.
  </Step>
</Steps>

## Before you render

For Cloudinary users: start/end offsets map to the trim operation; a poster offset maps to timestamp. These are JSON recipes submitted to the API, not Cloudinary transformation strings appended to a CDN URL.

Check GET /assets/capabilities and your plan before integrating. Native video packaging and Studio editing are separate operations. After upload, use POST /assets/\{assetID}/versions/\{versionID}/video when processing is needed, then GET /assets/\{assetID}/video until the current package is ready and review permits use. Follow the native video guide for package states; a completed upload alone is not an editable video.

Management requests use /api/v1 and return a success/data envelope. X-Asset-Space-ID selects your authorized space. The examples assume an existing video ID in that space. Do not put the permanent token in browser JavaScript.

## Shared request and job-polling helper

Run this helper first, then the recipe you need. Export STACKSHIFT\_ASSETS\_TOKEN and STACKSHIFT\_ASSET\_SPACE\_ID in your server environment. A 202 response is acceptance, not a finished file. The ten-minute polling budget is an example client timeout, not a processing guarantee. Resume the same job after that timeout rather than resubmitting it.

The transform service derives an idempotency key from the asset, source checksum and normalized recipe when you omit one. If you supply Idempotency-Key yourself, use a different key when the source or recipe changes.

```js theme={null}
// Node.js 20+; keep these credentials on your server.
const api = 'https://api.stackshift.cloud/api/v1';
const headers = {
  Authorization: `Bearer ${process.env.STACKSHIFT_ASSETS_TOKEN}`,
  'X-Asset-Space-ID': process.env.STACKSHIFT_ASSET_SPACE_ID,
  'Content-Type': 'application/json',
};
async function request(path, init = {}) {
  const response = await fetch(api + path, {
    ...init, headers: { ...headers, ...init.headers },
    signal: AbortSignal.timeout(60_000),
  });
  const body = await response.json();
  if (!response.ok || body.success === false) {
    throw new Error(`Assets request failed: HTTP ${response.status}`);
  }
  return body.data;
}
async function render(assetId, definition) {
  const submitted = await request(`/assets/${assetId}/transforms`, {
    method: 'POST', body: JSON.stringify({ definition }),
  });
  // Persist submitted.job_id in your application for recovery after a restart.
  const deadline = Date.now() + 10 * 60_000;
  while (Date.now() < deadline) {
    const job = await request(`/assets/jobs/${submitted.job_id}`);
    if (job.status === 'completed') {
      if (!job.result?.id || job.result.status !== 'available') {
        throw new Error('Job completed without an available derivative');
      }
      return job.result;
    }
    if (job.status === 'failed' || job.status === 'canceled') {
      throw new Error(`Render ${job.status}; inspect job ${submitted.job_id}`);
    }
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  throw new Error(`Still processing; resume polling job ${submitted.job_id}`);
}
```

## Trim from 5 seconds to 20 seconds, keeping audio

This produces a 15-second MP4. Start is inclusive of the selected range; end must be greater than start and within source duration. Leave muted false to retain the first audio track when present. Video is encoded as H.264 and audio as AAC stereo; this is not lossless stream copying or preservation of every audio track.

```js theme={null}
const videoId = process.env.VIDEO_ASSET_ID;
const clip = await render(videoId, {
  version: 2,
  media_kind: 'video',
  operations: [{ type: 'trim', start_seconds: 5, end_seconds: 20 }],
  output: { kind: 'video', format: 'mp4', muted: false, encoding: 'balanced' },
});
const clipId = clip.id;
```

## Create a JPEG poster at 12.5 seconds

Timestamp is relative to the original source video. To take a poster relative to an edited clip, first save that clip as an asset and use the new asset as the source after it is ready. Use format jpeg in the API even when your downloaded filename ends in .jpg. Choose a timestamp before the last frame.

```js theme={null}
const poster = await render(videoId, {
  version: 2,
  media_kind: 'video',
  operations: [{ type: 'timestamp', timestamp_seconds: 12.5 }],
  output: { kind: 'thumbnail', format: 'jpeg', quality: 90 },
});
const posterId = poster.id;
```

## Download the exact rendered file

The completed job result is the derivative record, including id, output\_kind, mime\_type, size and status. It is not the original asset ID. GET /assets/\{assetID}/derivatives also lists results, but do not select its first row: another recipe or source version can have produced it.

Set ASSET\_ID to the source video and OUTPUT\_ID to clipId or posterId. Change the output filename to poster.jpg when downloading the poster. This authenticated endpoint returns file bytes, not JSON. Use ?info=true separately to inspect output metadata.

```bash theme={null}
curl --fail-with-body \
  -H "Authorization: Bearer $STACKSHIFT_ASSETS_TOKEN" \
  -H "X-Asset-Space-ID: $STACKSHIFT_ASSET_SPACE_ID" \
  "https://api.stackshift.cloud/api/v1/assets/$ASSET_ID/derivatives/$OUTPUT_ID?download=true" \
  --output clip.mp4
```

## Give an external AI service a download URL

The signed derivative URL expires in 15 minutes and is bound to the issuer access, source version and output. Mint it shortly before the provider fetches the file. Send the returned URL to that provider using its own documented media-input field; never send your StackShift API token.

For a provider that queues retrieval beyond the expiry, download and upload the bytes using its file API, or mint a fresh URL when retrieval begins. Do not treat an expiring signed URL as a permanent database reference. Keep the asset and derivative IDs. Replacing the source or revoking access can invalidate the URL.

```js theme={null}
const signed = await request(
  `/assets/${videoId}/derivatives/${clipId}/signed-url`,
  { method: 'POST' },
);
// Pass signed.url to the external provider; do not log it.
// signed.expires_at tells you when the grant expires.
```

## Keep the edited video as a separate asset

POST /assets/\{assetID}/derivatives/\{outputID}/save copies a video-source Studio output into a new private asset in the default bucket. The response data is the new asset. Store its ID and wait for its own scan/processing readiness before delivery. This separates the saved output from the original source lifecycle; it does not make it public.

```js theme={null}
const savedClip = await request(
  `/assets/${videoId}/derivatives/${clipId}/save`,
  { method: 'POST' },
);
// Store savedClip.id in your application database.
```

## Expected result

<Check>
  Create a finished video clip or poster and obtain the exact output file.
</Check>

## Common failures

<Warning>
  * 403: check token scopes, space membership, source readiness/review and plan entitlement.
  * A trim end or poster timestamp outside source duration is rejected; do not retry unchanged.
  * A queued/running/waiting job is not a failed job. Inspect the same job and account quota instead of launching duplicates.
  * An expired signed URL or replaced source requires a new authorized delivery decision.
</Warning>

## Related guides

<CardGroup cols={2}>
  <Card title="Edit videos in Video Studio" href="/assets/video-studio">
    Trim, crop, resize, overlay, extract audio and create poster frames before packaging video for delivery.
  </Card>

  <Card title="Native video processing and secure playback" href="/assets/video-scanning-and-governance">
    Version-pinned video packages, scan and review gates, replacement behavior, playback sessions, API routes, and failure recovery.
  </Card>

  <Card title="Deterministic image, video, and audio transforms" href="/assets/deterministic-media-transforms">
    Define ordered v2 transforms for images, video renditions, audio extraction, and timestamp thumbnails; store eager presets or materialize durable derivatives.
  </Card>

  <Card title="Import generated videos from an external URL" href="/assets/import-videos-from-url">
    Copy an AI-generated MP4 into permanent Assets storage using a constrained remote-URL capability, then track readiness and deliver it safely.
  </Card>

  <Card title="Blur images and create a 1200×630 social preview" href="/assets/blur-and-social-preview">
    Build an exact-size JPEG social card with ordered crop and blur operations, then choose durable public delivery or private access.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.