Artifacts
Turn a remote URL or local bytes into an organization-owned Artifact — the only way binary media enters a run.
Run inputs are Artifact-only: binary media crosses the run boundary as a
{ kind, ref } Artifact, never as inline bytes,
base64, or a data: URI. These endpoints turn media you have — a fetchable
URL, or bytes on disk — into an organization-owned, first-party Artifact you
pass unchanged to POST /v1/runs or POST /v1/runs/node.
Two paths:
- Remote import — you have a public URL; blitflow fetches it server-side.
- Signed upload — you have local bytes; you upload them directly to storage with a short-lived, single-pathname authorization. The bytes never transit blitflow's API servers.
Import a remote URL
/v1/artifacts/importFetch a public http(s) URL server-side and copy it into first-party storage as an owned Artifact.
Request body
| Field | Type | Description |
|---|---|---|
url | string | The http(s) URL to import. Must be publicly reachable |
org | string | Organization slug; omit for your personal organization |
kind | string | Override the kind derived from the content type: image, audio, video, or file |
retention | string | temporary (default) or durable — see retention |
Private and internal destinations (localhost, private IP ranges, cloud metadata endpoints) are rejected, including on redirects. The same size cap and content-type allowlist as direct uploads apply.
curl -X POST "https://studio.blitflow.com/api/v1/artifacts/import" \
-H "Authorization: Bearer $BLITFLOW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/tower.jpg"}'Response
{
"id": "9c2b1e1a-…",
"artifact": {
"kind": "image",
"ref": "https://…/blitflow/uploads/2026-08/1f9d….jpg",
"mimeType": "image/jpeg"
},
"contentType": "image/jpeg",
"sizeBytes": 482133,
"expiresAt": "2026-09-17T12:00:00.000Z"
}Pass artifact unchanged as a run input:
{ "inputs": { "photo": { "kind": "image", "ref": "https://…/1f9d….jpg" } } }Errors
| Status | Cause |
|---|---|
400 | Unreachable URL, blocked (private/internal) destination, or fetch failure |
413 | Remote file over the 50 MB cap |
415 | Content type outside the allowlist |
Upload local bytes
Local bytes use a two-step signed upload: prepare an authorization, PUT the bytes directly to storage, then complete to record the Artifact.
Prepare
/v1/artifacts/uploadsCreate an upload intent and mint a short-lived token constrained to one randomized pathname, the declared content type, and the declared size.
| Field | Type | Description |
|---|---|---|
contentType | string | MIME type of the bytes (see allowlist) |
sizeBytes | integer | Size of the bytes; the upload is rejected beyond it. Max 52428800 (50 MB) |
org | string | Organization slug; omit for your personal organization |
kind | string | Override the kind derived from the content type: image, audio, video, or file |
retention | string | temporary (default) or durable |
{
"id": "5d0a7c22-…",
"pathname": "blitflow/uploads/2026-08/8ba3….png",
"uploadUrl": "https://vercel.com/api/blob/blitflow/uploads/2026-08/8ba3….png",
"token": "vercel_blob_client_…",
"contentType": "image/png",
"maxSizeBytes": 482133,
"tokenExpiresAt": "2026-08-18T13:00:00.000Z",
"expiresAt": "2026-08-18T14:00:00.000Z"
}The token authorizes exactly one pathname with the declared content type
and size cap, and expires at tokenExpiresAt (1 hour). The intent itself
expires at expiresAt (2 hours): complete before then or the intent — and any
uploaded bytes — is discarded.
Upload the bytes
Directly to storage — not to blitflow's API. With @vercel/blob:
import { put } from "@vercel/blob/client";
await put(prepared.pathname, bytes, {
access: "public",
token: prepared.token,
});Or raw HTTP:
curl -X PUT "$UPLOAD_URL" \
-H "Authorization: Bearer $UPLOAD_TOKEN" \
-H "x-content-type: image/png" \
-H "x-vercel-blob-access: public" \
--data-binary @./tower.pngComplete
/v1/artifacts/uploads/:id/completeVerify the uploaded blob against the intent and record the owned Artifact.
Verifies the blob at the prepared pathname — existence, size within the
declared cap, matching content type, your organization — then records exactly
one artifact ledger row and returns the same shape as /v1/artifacts/import.
Idempotent: completing the same intent again (a retry, a race) returns the same Artifact.
| Status | Cause |
|---|---|
400 | Nothing uploaded at the prepared pathname yet |
404 | Unknown intent, or an intent belonging to another organization |
409 | Uploaded blob doesn't match the declared size cap or content type |
410 | Intent expired before completion |
Limits and retention
| Limit | Value |
|---|---|
| Max size | 50 MB per artifact (uploads and imports) |
| Content types | image/*, audio/*, video/*, application/pdf, application/octet-stream |
Retention follows the retention field:
temporary(default) — the artifact expires 30 days after creation, the same window as run outputs. Right for one-off run inputs.durable— never expires; for workflow Library and recipe assets.
A temporary artifact referenced by a workflow graph is promoted to durable instead of expiring — the same part-of-the-recipe rule that protects run outputs.
The hosted MCP server exposes these operations as artifacts_import,
artifacts_uploads_prepare, and artifacts_uploads_complete. No tool accepts
base64, byte arrays, data: URIs, or local file paths — models must never
serialize file bytes into tool arguments.