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

# Tasks and artifact downloads

> Distinguish acceptance from completion, inspect tasks, and download artifacts.

These APIs submit and inspect gateway-persisted tasks, list artifacts, and download them. They do not accept arbitrary provider task IDs and are not OpenAI Batch / Files APIs. Use the public ID returned by submission under the same user and project scope.

## Submit and inspect

Set up the [REST runtime](/en/guides/http-runtime). Use `submit` only when the operator confirms that the plugin supports the legacy generic entry. `task-request.json` must contain that plugin's normalized request object with `model`; other fields are plugin-specific. Arbitrary native provider bodies are not interchangeable with it. Prefer the documented [image](/en/guides/image-generation), [video](/en/guides/video-generation), or [Responses](/en/guides/background-responses) protocol when available.

```bash theme={null}
export API_PLUGIN_KEY="YOUR_PLUGIN_KEY"
export API_TASK_REQUEST_FILE="./task-request.json"
node examples/tasks.mjs submit
export API_TASK_ID="TASK_ID_FROM_RESPONSE"
node examples/tasks.mjs status
node examples/tasks.mjs artifacts
export API_ARTIFACT_KEY="ARTIFACT_KEY_FROM_LIST"
node examples/tasks.mjs head
export API_ARTIFACT_OUTPUT="./task-artifact.bin"
node examples/tasks.mjs download
```

Run the commands step by step, submit only once, and paste the actual returned ID. Submission's `queued` is a fixed acceptance receipt; even immediate completion needs a status read. Status values are uppercase `NOT_START`, `SUBMITTED`, `QUEUED`, `IN_PROGRESS`, `SUCCESS`, `FAILURE`, `NEEDS_REVIEW`, or `UNKNOWN`. `NEEDS_REVIEW` requires manual investigation; `UNKNOWN` is not success. Failure reasons may contain sensitive data and should not be copied into public logs.

The example queries once without polling. A successful `status` exit means a valid observation; only `completed: true` means `SUCCESS`. If adding polling, bound the total deadline, interval, and attempts. A timeout or disconnect does not cancel a task. There are no generic inference endpoints to cancel, delete, or list all tasks.

## Artifacts and downloads

`artifacts`, `head`, and `download` first require `SUCCESS`. Successful tasks may still have no artifacts, for example because the plugin exposes none or did not retain results. Do not hard-code `video` or invent keys. Entries contain `key`, `type`, optional `mime_type`, and `content_url`. The example prints only the first three and constructs an authenticated gateway content path from the listed key, without following redirects.

`content_url` may contain a temporary signature. Treat it as a temporary access credential; do not log it or append an API key. The example does not use it. Put deployment path prefixes in `API_ROOT_URL`. GET downloads the whole artifact, while HEAD checks availability. Validate Range / conditional requests separately using the reference; HEAD success cannot guarantee future availability.

The download example allows up to 20 MiB, creates a new file only after reading the entire response, and rejects overwrites, empty content, non-200 responses, HTML / JSON, and MIME mismatches. JSON file artifacts require a separate explicit handler. It does not decode or validate media contents or open downloaded files.

References: [submit](/en/api-reference/tasks-submit), [status](/en/api-reference/tasks-retrieve), [list](/en/api-reference/task-artifacts), [GET](/en/api-reference/task-artifact-content), [HEAD](/en/api-reference/task-artifact-head).

```javascript theme={null}
import { readFile, writeFile, access } from 'node:fs/promises';
import { request, requestJSON, readBody } from './http-client.mjs';

try {
  const mode = process.argv[2] || 'status';
  if (!['submit', 'status', 'artifacts', 'head', 'download'].includes(mode)) throw new Error('Invalid mode.');
  const task = process.env.API_TASK_ID;
  const idPattern = /^[A-Za-z0-9_-][A-Za-z0-9._~-]{0,127}$/;
  if (mode === 'submit') {
    const plugin = process.env.API_PLUGIN_KEY;
    if (!/^[A-Za-z0-9_-]{1,30}$/.test(plugin || '')) throw new Error('Set API_PLUGIN_KEY.');
    const raw = await readFile(process.env.API_TASK_REQUEST_FILE);
    if (!raw.length || raw.length > 1024 * 1024) throw new Error('Invalid request file size.');
    const body = JSON.parse(raw);
    if (!body || typeof body !== 'object' || Array.isArray(body)
      || typeof body.model !== 'string' || !body.model.trim()) throw new Error('Provide a plugin-specific JSON object with model.');
    const result = await requestJSON(`/v1/tasks/${encodeURIComponent(plugin)}`, body);
    if (typeof result.task_id !== 'string' || !idPattern.test(result.task_id)
      || result.id !== result.task_id || result.status !== 'queued') throw new Error('Invalid submission receipt.');
    console.log(JSON.stringify({ task_id: result.task_id, accepted: true }));
  } else {
    if (!idPattern.test(task || '')) throw new Error('Set API_TASK_ID.');
    const path = `/v1/tasks/${encodeURIComponent(task)}`;
    const result = await requestJSON(path);
    const states = ['NOT_START', 'SUBMITTED', 'QUEUED', 'IN_PROGRESS', 'SUCCESS', 'FAILURE', 'NEEDS_REVIEW', 'UNKNOWN'];
    if (result.task_id !== task || !states.includes(result.status)) throw new Error('Invalid task status.');
    if (mode === 'status') {
      console.log(JSON.stringify({ task_id: task, status: result.status, completed: result.status === 'SUCCESS' }));
    } else {
      if (result.status !== 'SUCCESS') throw new Error('Task is not successful; do not resubmit automatically.');
      const listing = await requestJSON(`${path}/artifacts`);
      if (listing.task_id !== task || !Array.isArray(listing.artifacts) || listing.artifacts.length > 64) throw new Error('Invalid artifacts.');
      const seen = new Set();
      for (const item of listing.artifacts) {
        if (!item || typeof item.key !== 'string' || !idPattern.test(item.key) || seen.has(item.key)
          || !['image', 'audio', 'video', 'file'].includes(item.type) || typeof item.content_url !== 'string' || !item.content_url
          || (item.mime_type !== undefined && (typeof item.mime_type !== 'string' || /[\r\n]/.test(item.mime_type)))) throw new Error('Invalid artifact entry.');
        seen.add(item.key);
      }
      if (mode === 'artifacts') {
        // Avoid logging signed URLs. Use authenticated gateway paths below.
        console.log(JSON.stringify(listing.artifacts.map(({ key, type, mime_type }) => ({ key, type, mime_type }))));
      } else {
        const artifact = listing.artifacts.find((item) => item.key === process.env.API_ARTIFACT_KEY);
        if (!artifact) throw new Error('Choose API_ARTIFACT_KEY from the listing.');
        const output = process.env.API_ARTIFACT_OUTPUT || 'task-artifact.bin';
        if (mode === 'download') {
          try { await access(output); throw new Error('Output already exists.'); }
          catch (error) { if (error.code !== 'ENOENT') throw error; }
        }
        // Never send the API key to a provider URL or a signed content_url.
        const response = await request(`${path}/artifacts/${encodeURIComponent(artifact.key)}/content`,
          { method: mode === 'head' ? 'HEAD' : 'GET' });
        if (response.status !== 200) throw new Error('Expected a complete response, not a partial file.');
        if (mode === 'head') {
          console.log(JSON.stringify({ available: true, content_type: response.headers.get('content-type') }));
        } else {
          const type = (response.headers.get('content-type') || '').split(';')[0].trim();
          if (!type || type === 'text/html' || type === 'application/json' || type.endsWith('+json')
            || (artifact.mime_type && type !== artifact.mime_type.split(';')[0].trim())) throw new Error('Unexpected artifact content type.');
          const bytes = await readBody(response, 20 * 1024 * 1024);
          if (!bytes.length) throw new Error('Empty artifact.');
          await writeFile(output, bytes, { flag: 'wx' });
          console.log('Artifact saved.');
        }
      }
    }
  }
} catch {
  console.error('Task request failed or returned invalid data. No automatic resubmission; check task status.');
  process.exitCode = 1;
}
```

[阅读中文版](/zh/guides/tasks)


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