Artifact storage

Stores artifacts and attachments with signed uploads, access grants, retention, and deletion. $0.04 per GB-month + $30 per million writes + $3 per million reads + $0.18 per GB delivered. The base URL is https://api.motor.dev/artifact_storage, and the OpenAPI 3.1 specification describes each endpoint.

Overview

Store private files produced or used by your applications and agents, with metadata and an optional expiry. File contents are immutable; upload a new artifact when the contents change.

How it works

Create an upload to reserve storage and receive signed upload URLs. You send the bytes directly to storage, then complete the upload so Motor can verify them. Once the artifact is ready, issue short-lived download URLs for authorized users or agents.

File access and expiry

Keep your Motor key on your backend and never send it to storage URLs. An upload session lasts 24 hours. Report a file as saved only after completion returns ready status.

Download URLs last at most 300 seconds. For links in emails, use an application URL that authorizes the user and issues a fresh download URL. Storage is billed until you delete a file or it reaches its expires_at. If its billing key can't pay, Motor deletes the file 30 days later.

Pricing

  • Storage: $0.04 per GB-month (a decimal GB for 30 days), metered for each second an artifact is ready and billed each day, for at least 128 KiB per artifact.
  • Writes: $30 per million. One upload admission, including its first single-part PUT, completion, and cleanup; each multipart part URL or replacement upload URL; and each paid change, such as PATCH or a new billing key.
  • Reads: $3 per million. Each control read or page, each estimate, each download URL created, and each successful GET or HEAD request recorded in S3's access logs, including retries and repeated uses of a URL.
  • Delivery: $0.18 per GB of recorded bytes sent on successful GET requests, including partial responses. HEAD and 304 responses don't incur delivery charges.

Grant revocation, deletion, aborts, and reconciliation are included in the upload and storage prices. The Motor-Operation-Cost-USD response header reports the request charge. Upload and download-grant responses also include a cost object. POST /estimates prices a workload for $0.000003.

Limits

  • Requests: 100 a second per team (bursts of 200), 50 per key (bursts of 100), and 10 new uploads a second (bursts of 20).
  • Metadata: 16 pairs, keys of 64 bytes, values of 256, 4 KiB in all. Filenames of 255 bytes, media types of 127, run IDs of 128.
  • Lists: 50 items by default, at most 100, newest first; cursors work for 24 hours.
  • Operations and receipts stay 35 days after they finish.

GET /limits shows these limits and the rates, and, before a change, the rates that start next in rates.next. A request's optional rate_version must match the rates of the time that Motor receives it, or Motor answers 409 rate_changed. Usage takes the rates of the time it happened, even when Motor bills it later.

Examples

Store a PDF for seven days

Save a PDF for seven days with a filename and metadata. These commands use curl, jq, and OpenSSL on your backend; MOTOR_KEY is your secret key. Responses are trimmed. Replace sample IDs and save each Idempotency-Key to reuse if a request must be retried.

For a file up to 8 MiB, compute its exact byte count and Base64 SHA-256 before creating the upload:

FILE=output.pdf
SIZE=$(wc -c < "$FILE" | tr -d ' ')
SHA256=$(openssl dgst -sha256 -binary "$FILE" | openssl base64 -A)
jq -n --argjson size "$SIZE" --arg sha "$SHA256" \
  '{size_bytes:$size, sha256:$sha, content_type:"application/pdf", filename:"output.pdf", metadata:{job:"export-42", kind:"report"}, expires_at:"2026-10-24T09:00:00Z"}' > upload.json
curl https://api.motor.dev/artifact_storage/uploads \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" --data-binary @upload.json
HTTP 201

{
  "id": "aup_...",
  "artifact_id": "art_...",
  "transport": "single",
  "etag": "aup_...:1",
  "grant": {
    "url": "<returned presigned URL>",
    "required_headers": {
      "content-length": "<file size>",
      "content-type": "application/pdf",
      "if-none-match": "*",
      "x-amz-checksum-sha256": "<computed checksum>",
      "x-amz-server-side-encryption": "AES256"
    }
  }
}

Set GRANT_URL from grant.url and send every required header exactly. Send your Motor key only to Motor, never to the storage URL:

curl -X PUT "$GRANT_URL" \
  -H "content-length: $SIZE" -H 'content-type: application/pdf' \
  -H 'if-none-match: *' -H "x-amz-checksum-sha256: $SHA256" \
  -H 'x-amz-server-side-encryption: AES256' --data-binary @"$FILE"
HTTP 200
ETag: "<storage provider tag>"

Complete the upload using the ETag returned by Motor. S3’s ETag cannot be used for this request:

curl -i -X POST "https://api.motor.dev/artifact_storage/uploads/aup_.../complete" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "aup_...:1"'
HTTP 202

{
  "operation": {
    "id": "aop_...",
    "status": "pending"
  },
  "artifact_id": "art_..."
}
curl -i "https://api.motor.dev/artifact_storage/operations/aop_..." \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "id": "aop_...",
  "status": "succeeded",
  "artifact_id": "art_..."
}

Poll until the operation succeeds, then confirm the artifact is ready and save its ID:

curl -i "https://api.motor.dev/artifact_storage/artifacts/art_..." \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "id": "art_...",
  "status": "ready",
  "filename": "output.pdf",
  "metadata": {
    "job": "export-42",
    "kind": "report"
  }
}

Set expires_at to the time when you no longer need the file, or leave it out to keep the file until you delete it. A screenshot uses the same steps with image/png and its own hash. Report success only after the artifact is ready.

Create a download URL when the user clicks

Use the artifact from the first example. Keep a stable application URL in emails; authenticate and authorize the user in your backend, then create a short-lived grant and redirect to it.

curl -i "https://api.motor.dev/artifact_storage/artifacts/art_.../grants" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"method": "GET", "lifetime_seconds": 120, "accept_delayed_usage": true, "disposition": "attachment"}'
HTTP 201

{
  "url": "<returned presigned URL>",
  "required_headers": {},
  "reusable": true,
  "usage_reporting": "delayed"
}

Your application responds to the authorized user with:

HTTP 302
Location: <returned presigned URL>
Cache-Control: private, no-store

The grant can be reused until expiry; reads and delivered bytes are billed later. A key’s limit stops new grants, not use of an existing one.

API reference

Try it

Sign in to try the API here with one of your team's keys. Requests use that key's permissions and spending limit. Standard charges apply.

Uploads

Large files and resumable uploads

A file of more than 8 MiB is a multipart upload: create the upload without sha256, then register each part to get its URL. Every part except the last must contain part_size_bytes bytes. Motor chooses a part size of at least 8 MiB so the file fits within 10,000 parts. The largest file is 10,000 parts of 5 GiB, about 48.8 TiB.

curl https://api.motor.dev/artifact_storage/uploads/aup_.../parts \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"part_number": 1, "size_bytes": 8388608, "sha256": "<Base64 SHA-256 of the part>"}'

Each upload URL includes a signature covering the part’s length and SHA-256 checksum. S3 rejects bytes that don’t match. Send parts in any order and in parallel; we suggest at most 8 at once. A part's URL is valid for 60 seconds by default (15 to 300 with lifetime_seconds): start the request before it expires. To replace an expired URL, register the part again with its original length and checksum. Each newly issued upload URL costs one write, even if you don't use it; replaying the same idempotent request doesn't add a charge.

To resume after a crash, call POST /uploads/{upload_id}/reconcile and wait for its operation to succeed. Then read GET /uploads/{upload_id}/parts for the parts S3 reported. Upload the missing parts, read the upload for its current ETag, and complete it. An upload session lasts 24 hours.

Integrity and completion

Motor doesn't issue download URLs until completion verifies the uploaded bytes. For a single-part upload, Motor checks the object’s length and full-object SHA-256 checksum in S3. For a multipart upload, Motor checks each part's length and SHA-256, completes the upload with the ordered parts, and checks the object's composite checksum: the Base64-encoded SHA-256 of the concatenated binary part digests, followed by - and the number of parts. GET /artifacts/{artifact_id}/parts lists the manifest for your own check. S3's ETags are opaque tags, not hashes.

If completion can't find the bytes or any required parts, the operation fails as retryable and the upload reopens: send the bytes and complete again. If the stored content doesn't match the signed upload, the artifact fails permanently and Motor schedules its deletion. A ready artifact's bytes never change; a new version is a new artifact. Filenames, the media type, metadata, and the run ID can change with PATCH, each change with a new ETag.

Motor doesn't scan, preview, or transform content. Treat bytes as untrusted when you parse or run them.

POST/artifact_storage/uploads

Create an upload

Costs one write, and returns the upload. The artifact's storage is metered once it's ready. A single upload of at most 8 MiB returns its presigned PUT in grant: send the bytes straight to S3 with every header of required_headers, then complete the upload. A larger upload is multipart: register each part to get its URL. If Motor can't immediately confirm multipart creation at S3, the response is 202 with an operation, and the upload's provider_state is creating until a reconciliation finishes it. Costs one write. Storage is metered for max(size, 128 KiB) bytes while the artifact is ready.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "size_bytes": 3,
  "content_type": "text/plain",
  "filename": "result.txt",
  "sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
  "expires_at": "2026-10-24T09:00:00Z",
  "run_id": "run_42",
  "metadata": {
    "stage": "final"
  }
}
FieldTypeDescription
size_bytesrequiredintegerThe exact size: 0 to 53,687,091,200,000 bytes (10,000 parts of 5 GiB).
sha256stringThe Base64 of a 32-byte SHA-256 digest, as S3's x-amz-checksum-sha256 takes it.
content_typestringA media type of at most 127 bytes. Default application/octet-stream.
filenamestringA label of at most 255 bytes, not a path.
metadataobjectAt most 16 string pairs: keys of 1 to 64 UTF-8 bytes, values of at most 256, and at most 4 KiB in all. Metadata never grants access.
run_idstringAt most 128 bytes, for the list's filter.
expires_atWhen Motor deletes the artifact, a time in the future. Leave it out or send null to keep the artifact until you delete it.
lifetime_secondsintegerThe first PUT URL's lifetime. Default 60.
rate_versionstringThe rates that you expect; an older one is refused with 409 rate_changed.
Responses

201The upload, with its first PUT for a single upload.

{
  "id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "object": "upload",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "status": "open",
  "transport": "single",
  "size_bytes": 3,
  "part_size_bytes": 3,
  "part_count": 1,
  "content_type": "text/plain",
  "provider_state": "ready",
  "observed_at": null,
  "operation_id": null,
  "expires_at": "2026-10-18T09:00:00.000Z",
  "artifact_expires_at": "2026-10-24T09:00:00.000Z",
  "last_url_expires_at": "2026-10-17T09:01:00.000Z",
  "version": "1",
  "etag": "aup_5dMQVQFzdmYHPBV3rcQ25H:1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null,
  "grant": {
    "id": "agr_4m6yyjwaqkpJfPwCB62bV2",
    "object": "grant",
    "method": "PUT",
    "url": "https://motor-galadriel-blobs-prod.s3.us-east-1.amazonaws.com/artifact_storage/content/42/art_MAhBep7JB6wMAsAY63cEPM/body?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261017%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261017T090000Z&X-Amz-Expires=60&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bif-none-match%3Bx-amz-checksum-sha256%3Bx-amz-server-side-encryption&x-motor-grant=agr_4m6yyjwaqkpJfPwCB62bV2&X-Amz-Signature=5b1f6c3e0a8d4e2f9c7b1a0d3e6f8a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f",
    "required_headers": {
      "content-length": "3",
      "content-type": "text/plain",
      "if-none-match": "*",
      "x-amz-checksum-sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
      "x-amz-server-side-encryption": "AES256"
    },
    "expires_at": "2026-10-17T09:01:00.000Z",
    "size_bytes": 3,
    "part_number": 1
  },
  "operation": null,
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.00003",
    "meter_quantities": {
      "writes": 1,
      "reads": 0,
      "storage_byte_days": 0,
      "delivery_bytes": 0
    }
  }
}

202A multipart upload that Motor is still creating at S3. Poll the upload or the operation.

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

GET/artifact_storage/uploads/{upload_id}

Get an upload

Returns the upload's state, without any URL. Costs one read.

ParameterLocationDescription
upload_idpath
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200The upload.

{
  "id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "object": "upload",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "status": "open",
  "transport": "single",
  "size_bytes": 3,
  "part_size_bytes": 3,
  "part_count": 1,
  "content_type": "text/plain",
  "provider_state": "ready",
  "observed_at": null,
  "operation_id": null,
  "expires_at": "2026-10-18T09:00:00.000Z",
  "artifact_expires_at": "2026-10-24T09:00:00.000Z",
  "last_url_expires_at": "2026-10-17T09:01:00.000Z",
  "version": "1",
  "etag": "aup_5dMQVQFzdmYHPBV3rcQ25H:1",
  "created_at": "2026-10-17T09:00:00.000Z",
  "completed_at": null,
  "parts_registered": 1,
  "active_grants": 1
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/uploads/{upload_id}/parts

Register a part and get its URL

Registers a part's immutable length and SHA-256 and returns a presigned UploadPart for it. Parts can go in any order and in parallel; we suggest at most 8 at once. A replacement URL must use the part's original length and checksum. For a single upload, part 1 reissues its PUT. Each newly issued upload URL costs one write, even if unused. Replaying the same idempotent request doesn't add a charge. It advances the upload's version, so read the upload again before you complete or abort it.

ParameterLocationDescription
upload_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "part_number": 3,
  "size_bytes": 3222784,
  "sha256": "LXEWQrcmsEQBYnyp+6wy9chTD7GQPMTbAiWHF5IaSIE=",
  "lifetime_seconds": 300
}
FieldTypeDescription
part_numberrequiredinteger
size_bytesrequiredintegerThe part's exact size: part_size_bytes, or the rest for the last part.
sha256requiredstringThe Base64 of a 32-byte SHA-256 digest, as S3's x-amz-checksum-sha256 takes it.
lifetime_secondsinteger
rate_versionstring
Responses

201The part's presigned URL.

{
  "object": "upload_part_grant",
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "part": {
    "number": 1,
    "size_bytes": 3,
    "sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
  },
  "grant": {
    "id": "agr_8iXoRA7o4TbZV8Q671iCTJ",
    "object": "grant",
    "method": "PUT",
    "url": "https://motor-galadriel-blobs-prod.s3.us-east-1.amazonaws.com/artifact_storage/content/42/art_MAhBep7JB6wMAsAY63cEPM/body?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261017%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261017T090000Z&X-Amz-Expires=60&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bif-none-match%3Bx-amz-checksum-sha256%3Bx-amz-server-side-encryption&x-motor-grant=agr_8iXoRA7o4TbZV8Q671iCTJ&X-Amz-Signature=5b1f6c3e0a8d4e2f9c7b1a0d3e6f8a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f",
    "required_headers": {
      "content-length": "3",
      "content-type": "text/plain",
      "if-none-match": "*",
      "x-amz-checksum-sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
      "x-amz-server-side-encryption": "AES256"
    },
    "expires_at": "2026-10-17T09:01:00.000Z",
    "size_bytes": 3,
    "part_number": 1
  },
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.00003",
    "meter_quantities": {
      "writes": 1,
      "reads": 0,
      "storage_byte_days": 0,
      "delivery_bytes": 0
    }
  }
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

GET/artifact_storage/uploads/{upload_id}/parts

List an upload's parts

Returns the registered parts in order, with what S3 reported at the last reconciliation. After a crash, reconcile the upload and wait for that operation to succeed, then upload any missing parts. Costs one read for each page.

ParameterLocationDescription
upload_idpath
limitquery1 to 100 items, 50 by default.
cursorquerynext_cursor of the previous page. It works for 24 hours, with the same filters.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200A page of parts.

{
  "data": [
    {
      "number": 1,
      "size_bytes": 3,
      "sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
      "observed": null,
      "last_grant_id": "agr_8iXoRA7o4TbZV8Q671iCTJ"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "observed_at": null
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/uploads/{upload_id}/reconcile

Reconcile an upload with S3

Checks S3 for the parts and bytes already uploaded. A pending reconciliation, or one that observed the upload in the last 60 seconds, is returned instead of a new one. Included in the upload's price.

ParameterLocationDescription
upload_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{}
Responses

202The reconciliation.

{
  "id": "aop_VrECHafG8pjo1mGQYQFGnN",
  "object": "operation",
  "kind": "reconcile",
  "status": "pending",
  "phase": "observe",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "retryable": false,
  "error": null,
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.00",
    "meter_quantities": {
      "writes": 0,
      "reads": 0,
      "storage_byte_days": 0,
      "delivery_bytes": 0
    }
  },
  "usage_reporting": "delayed",
  "deadline_at": "2026-10-18T09:00:00.000Z",
  "created_at": "2026-10-17T09:00:00.000Z",
  "updated_at": "2026-10-17T09:00:00.000Z",
  "finished_at": null
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

POST/artifact_storage/uploads/{upload_id}/complete

Complete an upload

Freezes the manifest and starts verification: Motor reads the object's checksum at S3 (after completing the multipart upload) and publishes the artifact as ready. Poll the operation, and use the artifact only after it's ready. Bytes that didn't arrive fail the operation as retryable and reopen the upload. Included in the upload's price.

ParameterLocationDescription
upload_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{}
Responses

202The verification, and the upload.

{
  "operation": {
    "id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
    "object": "operation",
    "kind": "finalize",
    "status": "pending",
    "phase": "verify",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false,
    "error": null,
    "cost": {
      "rate_version": "2026-10-22",
      "charged_usd": "0.00",
      "meter_quantities": {
        "writes": 0,
        "reads": 0,
        "storage_byte_days": 0,
        "delivery_bytes": 0
      }
    },
    "usage_reporting": "delayed",
    "deadline_at": "2026-10-18T09:00:01.000Z",
    "created_at": "2026-10-17T09:00:01.000Z",
    "updated_at": "2026-10-17T09:00:01.000Z",
    "finished_at": null
  },
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "upload": {
    "id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "object": "upload",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "status": "completing",
    "transport": "single",
    "size_bytes": 3,
    "part_size_bytes": 3,
    "part_count": 1,
    "content_type": "text/plain",
    "provider_state": "ready",
    "observed_at": "2026-10-17T09:00:01.000Z",
    "operation_id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
    "expires_at": "2026-10-18T09:00:00.000Z",
    "artifact_expires_at": "2026-10-24T09:00:00.000Z",
    "last_url_expires_at": "2026-10-17T09:01:00.000Z",
    "version": "3",
    "etag": "aup_5dMQVQFzdmYHPBV3rcQ25H:3",
    "created_at": "2026-10-17T09:00:00.000Z",
    "completed_at": null
  }
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

POST/artifact_storage/uploads/{upload_id}/abort

Abort an upload

Stops new URLs and starts the erasure of whatever S3 received. Included in the upload's price.

ParameterLocationDescription
upload_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{}
Responses

202The erasure, and the upload.

{
  "operation": {
    "id": "aop_76NR3WaFxkBv4QpNZnjkGS",
    "object": "operation",
    "kind": "abort",
    "status": "pending",
    "phase": "uploads",
    "artifact_id": "art_9zugjD6c5aeRNXzv5SvUmP",
    "upload_id": "aup_BS2XfdVt7An32SmNKxunJh",
    "retryable": false,
    "error": null,
    "cost": {
      "rate_version": "2026-10-22",
      "charged_usd": "0.00",
      "meter_quantities": {
        "writes": 0,
        "reads": 0,
        "storage_byte_days": 0,
        "delivery_bytes": 0
      }
    },
    "usage_reporting": "delayed",
    "deadline_at": "2026-10-18T09:00:12.000Z",
    "created_at": "2026-10-17T09:00:12.000Z",
    "updated_at": "2026-10-17T09:00:12.000Z",
    "finished_at": null
  },
  "artifact_id": "art_9zugjD6c5aeRNXzv5SvUmP",
  "upload": {
    "id": "aup_BS2XfdVt7An32SmNKxunJh",
    "object": "upload",
    "artifact_id": "art_9zugjD6c5aeRNXzv5SvUmP",
    "status": "aborted",
    "transport": "multipart",
    "size_bytes": 20000000,
    "part_size_bytes": 8388608,
    "part_count": 3,
    "content_type": "application/zip",
    "provider_state": "ready",
    "observed_at": "2026-10-17T09:00:11.000Z",
    "operation_id": "aop_76NR3WaFxkBv4QpNZnjkGS",
    "expires_at": "2026-10-18T09:00:11.000Z",
    "artifact_expires_at": "2026-10-24T09:00:00.000Z",
    "last_url_expires_at": "2026-10-17T09:05:11.000Z",
    "version": "4",
    "etag": "aup_BS2XfdVt7An32SmNKxunJh:4",
    "created_at": "2026-10-17T09:00:11.000Z",
    "completed_at": null
  }
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

Artifacts

Expiry and storage billing

An artifact lasts until you delete it, or until its optional expires_at. Set expires_at when you create the upload, or change it with PATCH /artifacts/{artifact_id}: send a future time, ornull to keep the artifact until you delete it. At expires_at, Motor stops issuing URLs for the artifact and deletes it.

Storage is metered for each second an artifact is ready, at least 128 KiB per artifact, and recorded for its billing key once each UTC day ends. It stops when the deletion starts. POST /artifacts/{artifact_id}/billing_key assigns another active key from your team to pay for the artifact's storage from now on, and for downloads through newly issued URLs. Existing download URLs keep their original billing key.

When the billing key can't pay

If the billing key reached its monthly spending limit, or your team's billing is suspended or has no payment card, Motor starts a grace period for the key:

  • Motor keeps the key's artifacts for 30 days and doesn't charge their storage during that time.
  • Motor emits artifact_storage.storage.grace_started with its deletes_at, and emails your team's admins.
  • When the key can pay again, such as after you raise its limit or a new month starts, the grace period ends and the storage is billed again from the next day.
  • If the grace period reaches deletes_at, Motor deletes the key's artifacts with the reason unpaid, including artifacts that Motor services reference.

To keep the artifacts, raise the key's limit, fix your billing, or move them to another key before deletes_at.artifact_storage.storage.grace_ended reports the end with end_reason paid or deleted.

Keep an audit log export

An export's artifacts expire with the export. Take over a ready export artifact after its service releases it, then remove its expiry. First create an export using the Audit logs export documentation. Use the artifact IDs returned for its manifest and parts.

curl -i "https://api.motor.dev/artifact_storage/artifacts/art_export" \
  -H "Authorization: Bearer $MOTOR_KEY"
HTTP 200

{
  "id": "art_export",
  "status": "ready",
  "owner": {
    "service": "audit_logs",
    "resource_type": "export",
    "resource_id": "aex_..."
  },
  "reference_count": 0,
  "expires_at": "2026-10-18T09:00:00.000Z",
  "etag": "art_export:2"
}
curl -i -X POST "https://api.motor.dev/artifact_storage/artifacts/art_export/adopt" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "art_export:2"'
HTTP 200

{
  "id": "art_export",
  "owner": null,
  "etag": "art_export:3"
}
curl -i -X PATCH "https://api.motor.dev/artifact_storage/artifacts/art_export" \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'If-Match: "art_export:3"' \
  -H "Content-Type: application/json" \
  -d '{"expires_at": null}'
HTTP 200

{
  "id": "art_export",
  "expires_at": null,
  "etag": "art_export:4"
}

Repeat for the manifest and every part. Adoption fails while reference_count is nonzero.

Deletion and erasure

DELETE /artifacts/{artifact_id} with If-Match denies new URLs at once and returns an operation. Motor records the deletion outside its database first, so a restore from a backup can't bring the artifact back. Motor then deletes the stored content and verifies that S3 no longer has it. Existing download URLs may remain usable until they expire or S3 deletes the object; downloads already in progress may finish. Poll the deletion operation for confirmation. The artifact's labels and metadata are removed during cleanup; billing records remain.

POST /deletions deletes up to 100 artifacts, each at its expected version. Each item has its own result, such as accepted or conflict, and the operation succeeds when every accepted item is erased. Deletion and cleanup are included in the upload's price.

An attempt to delete an artifact still referenced by a Motor service returns 409 artifact_in_use with the service's references. An artifact that a service made for you shows its owner; manage its expiry yourself with POST /artifacts/{artifact_id}/adopt once the service released it.

Events

Subscribe to artifact_storage.* with POST /meta/event_subscriptions and service: "artifact_storage". Events are artifact_storage.artifact.ready, .failed, and .deleted; artifact_storage.upload.expired; and artifact_storage.storage.grace_startedand .grace_ended. Their bodies have IDs, states, reasons, times, and costs, never filenames, metadata, checksums, or URLs. Poll the resource or operation for its current state: an event can arrive late, and download usage can still arrive after a deleted event.

GET/artifact_storage/artifacts

List artifacts

Artifacts newest first. Without status, artifacts in deleting or deleted state are excluded. Pagination is live, not a snapshot. Costs one read for each page.

ParameterLocationDescription
statusquery
run_idquery
owner_servicequery
created_afterquery
created_beforequery
metadata_keyqueryWith metadata_value: an exact match of one metadata pair.
metadata_valuequery
limitquery1 to 100 items, 50 by default.
cursorquerynext_cursor of the previous page. It works for 24 hours, with the same filters.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200A page of artifacts.

{
  "data": [
    {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "ready",
      "size_bytes": 3,
      "billable_bytes": 131072,
      "content_type": "text/plain",
      "filename": "result.txt",
      "checksum": {
        "algorithm": "SHA256",
        "type": "FULL_OBJECT",
        "value": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
        "part_count": 1
      },
      "metadata": {
        "stage": "final"
      },
      "run_id": "run_42",
      "owner": null,
      "reference_count": 0,
      "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
      "billing_key_id": "key_JDyuT92k9U2Q54apCc8EgT",
      "created_at": "2026-10-17T09:00:00.000Z",
      "ready_at": "2026-10-17T09:00:02.000Z",
      "expires_at": "2026-10-24T09:00:00.000Z",
      "deletion": null,
      "failure": null,
      "last_operation_id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
      "cost": {
        "rate_version": "2026-10-22",
        "charged_usd": "0.000031223338"
      },
      "version": "3",
      "etag": "art_MAhBep7JB6wMAsAY63cEPM:3"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

GET/artifact_storage/artifacts/{artifact_id}

Get an artifact

Returns the artifact. An unavailable artifact with a deletion record returns 410 with details in error.artifact; check its deletion status to distinguish pending cleanup from confirmed erasure. Costs one read.

ParameterLocationDescription
artifact_idpath
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200The artifact.

{
  "id": "art_MAhBep7JB6wMAsAY63cEPM",
  "object": "artifact",
  "status": "ready",
  "size_bytes": 3,
  "billable_bytes": 131072,
  "content_type": "text/plain",
  "filename": "result.txt",
  "checksum": {
    "algorithm": "SHA256",
    "type": "FULL_OBJECT",
    "value": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
    "part_count": 1
  },
  "metadata": {
    "stage": "final"
  },
  "run_id": "run_42",
  "owner": null,
  "reference_count": 0,
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "billing_key_id": "key_JDyuT92k9U2Q54apCc8EgT",
  "created_at": "2026-10-17T09:00:00.000Z",
  "ready_at": "2026-10-17T09:00:02.000Z",
  "expires_at": "2026-10-24T09:00:00.000Z",
  "deletion": null,
  "failure": null,
  "last_operation_id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.000031223338"
  },
  "version": "3",
  "etag": "art_MAhBep7JB6wMAsAY63cEPM:3"
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

PATCH/artifact_storage/artifacts/{artifact_id}

Update an artifact's labels

Updates filename, content_type, metadata, run_id, and expires_at. Bytes never change. A new media type applies to later download URLs. Costs one write.

ParameterLocationDescription
artifact_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "filename": "final.txt",
  "metadata": {
    "stage": "published"
  },
  "expires_at": "2026-11-17T09:00:00Z"
}
FieldTypeDescription
filename
content_type
metadata
run_id
expires_atWhen Motor deletes the artifact, a time in the future, or null to keep it until you delete it. It can't come before the end of a Motor service's reference.
rate_versionstring
Responses

200The artifact.

{
  "id": "art_MAhBep7JB6wMAsAY63cEPM",
  "object": "artifact",
  "status": "ready",
  "size_bytes": 3,
  "billable_bytes": 131072,
  "content_type": "text/plain",
  "filename": "final.txt",
  "checksum": {
    "algorithm": "SHA256",
    "type": "FULL_OBJECT",
    "value": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
    "part_count": 1
  },
  "metadata": {
    "stage": "published"
  },
  "run_id": "run_42",
  "owner": null,
  "reference_count": 0,
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "billing_key_id": "key_JDyuT92k9U2Q54apCc8EgT",
  "created_at": "2026-10-17T09:00:00.000Z",
  "ready_at": "2026-10-17T09:00:02.000Z",
  "expires_at": "2026-11-17T09:00:00.000Z",
  "deletion": null,
  "failure": null,
  "last_operation_id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.000046223338"
  },
  "version": "4",
  "etag": "art_MAhBep7JB6wMAsAY63cEPM:4"
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

DELETE/artifact_storage/artifacts/{artifact_id}

Delete an artifact

Denies new URLs at once and starts the erasure, which the operation confirms. Existing download URLs may remain usable until they expire or S3 deletes the object. Downloads already in progress may finish. Poll the deletion operation for confirmation; billing records remain after content deletion. An artifact that Motor services reference answers 409 artifact_in_use. Included in the upload's price.

ParameterLocationDescription
artifact_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

202The erasure.

{
  "operation": {
    "id": "aop_NK2eo56rsu9WmkjUfRGg4w",
    "object": "operation",
    "kind": "delete",
    "status": "pending",
    "phase": "uploads",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false,
    "error": null,
    "cost": {
      "rate_version": "2026-10-22",
      "charged_usd": "0.00",
      "meter_quantities": {
        "writes": 0,
        "reads": 0,
        "storage_byte_days": 0,
        "delivery_bytes": 0
      }
    },
    "usage_reporting": "delayed",
    "deadline_at": "2026-10-18T09:00:17.000Z",
    "created_at": "2026-10-17T09:00:17.000Z",
    "updated_at": "2026-10-17T09:00:17.000Z",
    "finished_at": null
  },
  "artifact": {
    "id": "art_MAhBep7JB6wMAsAY63cEPM",
    "object": "artifact",
    "status": "deleting",
    "deletion": {
      "reason": "customer",
      "requested_at": "2026-10-17T09:00:17.000Z",
      "state": "erasure_pending",
      "erased_at": null,
      "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
    }
  }
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

GET/artifact_storage/artifacts/{artifact_id}/parts

Get an artifact's integrity manifest

Returns each part's length and SHA-256. A single upload is one part; a multipart artifact's checksum is the composite of its parts. Costs one read for each page.

ParameterLocationDescription
artifact_idpath
limitquery1 to 100 items, 50 by default.
cursorquerynext_cursor of the previous page. It works for 24 hours, with the same filters.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200A page of the manifest.

{
  "data": [
    {
      "number": 1,
      "size_bytes": 3,
      "sha256": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "checksum": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
  "checksum_type": "FULL_OBJECT",
  "part_count": 1
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/artifacts/{artifact_id}/billing_key

Update the billing key

Assigns another active key from your team to pay for the artifact's storage from now on, and for downloads through newly issued URLs. Existing download URLs keep their original billing key. Costs one write.

ParameterLocationDescription
artifact_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "key_id": "key_8Fq2RkV7xYz3LmN4pQs9Td"
}
FieldTypeDescription
key_idrequiredstringkey_..., an active key of your team that can use Artifact storage.
rate_versionstring
Responses

200The artifact.

{
  "id": "art_MAhBep7JB6wMAsAY63cEPM",
  "object": "artifact",
  "status": "ready",
  "size_bytes": 3,
  "billable_bytes": 131072,
  "content_type": "text/plain",
  "filename": "final.txt",
  "checksum": {
    "algorithm": "SHA256",
    "type": "FULL_OBJECT",
    "value": "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=",
    "part_count": 1
  },
  "metadata": {
    "stage": "published"
  },
  "run_id": "run_42",
  "owner": null,
  "reference_count": 0,
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "billing_key_id": "key_1bdzEY5E2uzM13vhCQbPv7",
  "created_at": "2026-10-17T09:00:00.000Z",
  "ready_at": "2026-10-17T09:00:02.000Z",
  "expires_at": "2026-11-23T09:00:00.000Z",
  "deletion": null,
  "failure": null,
  "last_operation_id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.000096466218"
  },
  "version": "7",
  "etag": "art_MAhBep7JB6wMAsAY63cEPM:7"
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/artifacts/{artifact_id}/adopt

Take over a service's artifact

Changes a service-managed artifact to customer-managed after all service references have been released. Your team already owns the artifact; adoption transfers responsibility for its expiry and deletion without copying the content. Costs one write.

ParameterLocationDescription
artifact_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{}
Responses

200The artifact.

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/deletions

Delete artifacts in bulk

Deletes up to 100 artifacts, each at its expected version. Each item is accepted or refused on its own. The operation succeeds when every accepted item is erased. Included in the uploads' price.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "artifacts": [
    {
      "id": "art_KmfnmrWSi9FirysMYsAS7Q",
      "version": "3"
    },
    {
      "id": "art_unknown",
      "version": "1"
    }
  ]
}
FieldTypeDescription
artifactsrequiredarray
Responses

202The bulk deletion with each item's result.

{
  "id": "aop_5aEzmK7rkpvXq9C1Kbywvq",
  "object": "operation",
  "kind": "bulk_delete",
  "status": "pending",
  "phase": "erasing",
  "artifact_id": null,
  "upload_id": null,
  "retryable": false,
  "error": null,
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.00",
    "meter_quantities": {
      "writes": 0,
      "reads": 0,
      "storage_byte_days": 0,
      "delivery_bytes": 0
    }
  },
  "usage_reporting": "delayed",
  "deadline_at": "2026-10-18T09:00:16.000Z",
  "created_at": "2026-10-17T09:00:16.000Z",
  "updated_at": "2026-10-17T09:00:16.000Z",
  "finished_at": null,
  "items": [
    {
      "artifact_id": "art_KmfnmrWSi9FirysMYsAS7Q",
      "result": "accepted",
      "state": "pending",
      "message": null
    },
    {
      "artifact_id": "art_unknown",
      "result": "not_found",
      "state": "skipped",
      "message": "The artifact doesn't exist."
    }
  ]
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

Download URLs

A download URL is a presigned S3 GET or HEAD that's valid for 15 to 300 seconds (60 by default), never past the artifact's expires_at. It's a bearer credential: anyone who holds it can use it, any number of times, until it expires, and a transfer that started can finish after that. There's no single-use URL and no hard cap on what reused URLs cost. Use these URLs only if you can accept charges beyond a spending limit for requests made before they expire.

Creating a download URL costs one read, charged at once to the artifact's billing key, and each download with it costs another read. So a URL used three times costs four reads, and an unused one costs one. Motor bills the downloads from S3's server access logs: each successful GET or HEAD (including 206 and 304) is a read, and the recorded bytes sent on those requests incur delivery charges. The logs arrive late, usually within hours, so GET /grants/{grant_id} shows observed.observed_through, the latest request time recorded so far, not a guarantee that all earlier usage has arrived. That's why a URL request needs accept_delayed_usage: true. A spending limit stops new URLs; it can't stop a URL already issued. Usage is charged to whichever key was the artifact’s billing key when Motor issued the URL, even if that key is later revoked or replaced. Duplicate log records aren't charged twice, and missing records aren't charged. Records can settle up to 30 days after the request.

disposition is attachment by default; inline works only for text/plain and application/octet-stream. range: [start, end] signs one inclusive byte range. POST /grants/{grant_id}/revoke prevents Motor from replaying that grant's URL. It doesn't prevent a new grant for the artifact, and the existing S3 URL may remain usable until provider_url_valid_until. If S3's logs stop arriving, Motor pauses new download URLs with 503 downloads_paused until they recover; uploads and URLs already issued keep working.

POST/artifact_storage/artifacts/{artifact_id}/grants

Get a download URL

Issues a presigned GET or HEAD straight to S3, for 15 to 300 seconds and never past the artifact's expires_at. It's a bearer credential that can be reused until it expires. Creating a download URL costs one read, charged at once to the artifact's billing key, and a key at its spending limit gets 402; an unused URL costs only that read. Each successful GET or HEAD request recorded in S3's access logs is another read, including retries, 206 responses, and 304 responses. Recorded bytes on successful GET requests incur delivery charges; HEAD and 304 responses do not. Duplicate records aren't charged twice, and missing records aren't charged. Usage may be charged up to 30 days later. The charge uses whichever key was the artifact’s billing key when the URL was issued, even if that key is later revoked or replaced. accept_delayed_usage: true is required.

ParameterLocationDescription
artifact_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "method": "GET",
  "accept_delayed_usage": true,
  "lifetime_seconds": 120,
  "range": [
    0,
    1
  ]
}
FieldTypeDescription
methodDefault GET.
accept_delayed_usagerequiredRequired: the URL can be reused until it expires, and Motor bills its observed requests and bytes later. A spending limit stops new URLs, not a URL already issued.
lifetime_secondsinteger
dispositionDefault attachment; inline only for text/plain and application/octet-stream.
rangearrayInclusive byte offsets [start, end], signed as the Range header.
rate_versionstring
Responses

201The download URL.

{
  "id": "agr_KqdusJoZHmSTdZXkxVCGd3",
  "object": "grant",
  "method": "GET",
  "url": "https://motor-galadriel-blobs-prod.s3.us-east-1.amazonaws.com/artifact_storage/content/42/art_MAhBep7JB6wMAsAY63cEPM/body?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261017%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261017T090005Z&X-Amz-Expires=120&X-Amz-SignedHeaders=host%3Brange&response-cache-control=private%2C%20no-store&response-content-disposition=attachment%3B%20filename%3D%22final.txt%22%3B%20filename%2A%3DUTF-8%27%27final.txt&response-content-type=text%2Fplain&x-motor-grant=agr_KqdusJoZHmSTdZXkxVCGd3&X-Amz-Signature=5b1f6c3e0a8d4e2f9c7b1a0d3e6f8a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f",
  "required_headers": {
    "range": "bytes=0-1"
  },
  "expires_at": "2026-10-17T09:02:05.000Z",
  "size_bytes": 3,
  "part_number": null,
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "range": [
    0,
    1
  ],
  "disposition": "attachment",
  "provider_url_valid_until": "2026-10-17T09:02:05.000Z",
  "reusable": true,
  "usage_reporting": "delayed",
  "budget_enforcement": "issuance_only",
  "cost": {
    "rate_version": "2026-10-22",
    "issuance_usd": "0.000003",
    "estimated_single_use_usd": "0.00000300036"
  }
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

409A state, idempotency, rate, or reference conflict, such as upload_completed, artifact_in_use, idempotency_conflict, or rate_changed.

{
  "error": {
    "code": "upload_completed",
    "message": "The upload is complete. Use its artifact.",
    "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
    "retryable": false
  }
}

410The artifact was deleted or expired, or the upload session expired.

{
  "error": {
    "code": "artifact_deleted",
    "message": "The artifact is no longer available. Check its deletion status for cleanup progress.",
    "retryable": false,
    "artifact": {
      "id": "art_MAhBep7JB6wMAsAY63cEPM",
      "object": "artifact",
      "status": "deleting",
      "deletion": {
        "reason": "customer",
        "requested_at": "2026-10-17T09:00:17.000Z",
        "state": "erasure_pending",
        "erased_at": null,
        "operation_id": "aop_NK2eo56rsu9WmkjUfRGg4w"
      }
    }
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

503The provider or Motor is temporarily unavailable, or download URLs are paused. Wait for Retry-After when present; otherwise retry after a few seconds.

{
  "error": {
    "code": "provider_unavailable",
    "message": "Artifact storage isn't configured in this environment.",
    "retryable": true,
    "retry_after_seconds": 5
  }
}

GET/artifact_storage/grants/{grant_id}

Get a grant

Returns the grant's state and the usage that S3's logs showed so far, without its URL. Usage arrives later than the requests. Costs one read.

ParameterLocationDescription
grant_idpath
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200The grant.

{
  "id": "agr_KqdusJoZHmSTdZXkxVCGd3",
  "object": "grant",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "upload_id": null,
  "part_number": null,
  "method": "GET",
  "purpose": "download",
  "range": [
    0,
    1
  ],
  "disposition": "attachment",
  "status": "active",
  "issued_at": "2026-10-17T09:00:05.000Z",
  "expires_at": "2026-10-17T09:02:05.000Z",
  "revoked_at": null,
  "provider_url_valid_until": "2026-10-17T09:02:05.000Z",
  "payer_key_id": "key_JDyuT92k9U2Q54apCc8EgT",
  "reusable": true,
  "usage_reporting": "delayed",
  "budget_enforcement": "issuance_only",
  "observed": {
    "requests": 0,
    "bytes": 0,
    "observed_through": null
  },
  "rate_version": "2026-10-22",
  "version": "1",
  "etag": "agr_KqdusJoZHmSTdZXkxVCGd3:1"
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/grants/{grant_id}/revoke

Revoke a grant

Prevents Motor from replaying this grant's URL. The existing S3 URL may remain usable until provider_url_valid_until; revocation does not prevent new grants for the artifact. Included in the service price.

ParameterLocationDescription
grant_idpath
Idempotency-Keyheader1 to 128 printable ASCII characters: a new value for each operation, and the same value when you retry it. Records last 35 days.
If-MatchheaderThe resource's entity tag in quotes, such as "art_...:3".
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{}
Responses

200The grant.

{
  "id": "agr_KqdusJoZHmSTdZXkxVCGd3",
  "object": "grant",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "upload_id": null,
  "part_number": null,
  "method": "GET",
  "purpose": "download",
  "range": [
    0,
    1
  ],
  "disposition": "attachment",
  "status": "revoked",
  "issued_at": "2026-10-17T09:00:05.000Z",
  "expires_at": "2026-10-17T09:02:05.000Z",
  "revoked_at": "2026-10-17T09:00:06.000Z",
  "provider_url_valid_until": "2026-10-17T09:02:05.000Z",
  "payer_key_id": "key_JDyuT92k9U2Q54apCc8EgT",
  "reusable": true,
  "usage_reporting": "delayed",
  "budget_enforcement": "issuance_only",
  "observed": {
    "requests": 0,
    "bytes": 0,
    "observed_through": null
  },
  "rate_version": "2026-10-22",
  "version": "2",
  "etag": "agr_KqdusJoZHmSTdZXkxVCGd3:2"
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

412If-Match names an older version. Read the resource again.

{
  "error": {
    "code": "precondition_failed",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

428If-Match is missing.

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header, such as If-Match: \"ff:ffe_...:7\". Read the resource to get its current ETag.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

Operations

Errors have a code, a message, and retryable. A 412 means that your ETag is stale: read the resource and retry with its new ETag. A 409 rate_changed means that your rate_version is outdated. For 429 and 503, wait for Retry-After when present, or retry after a few seconds. Reuse the same Idempotency-Key when retrying the same request.

S3 answers presigned requests with its own XML errors:

  • AccessDenied with "Request has expired": ask for a new URL. For a part, register it again with the same checksum.
  • SignatureDoesNotMatch: send every header listed in required_headers with its exact value. Don't change the signed URL.
  • BadDigest: the bytes don't match the SHA-256 that you registered.
  • 412 PreconditionFailed on a single PUT: an object already exists at that key. Complete the upload so Motor can verify it.
  • NoSuchUpload: the multipart upload ended. Read the upload; it may be complete or aborted.

GET/artifact_storage/operations/{operation_id}

Get an operation

Returns the operation's status, its resources, its error, and what it charged. Costs one read.

ParameterLocationDescription
operation_idpath
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200The operation.

{
  "id": "aop_QD8pYT1vdYyMTDkDWgKPM1",
  "object": "operation",
  "kind": "finalize",
  "status": "pending",
  "phase": "verify",
  "artifact_id": "art_MAhBep7JB6wMAsAY63cEPM",
  "upload_id": "aup_5dMQVQFzdmYHPBV3rcQ25H",
  "retryable": false,
  "error": null,
  "cost": {
    "rate_version": "2026-10-22",
    "charged_usd": "0.00",
    "meter_quantities": {
      "writes": 0,
      "reads": 0,
      "storage_byte_days": 0,
      "delivery_bytes": 0
    }
  },
  "usage_reporting": "delayed",
  "deadline_at": "2026-10-18T09:00:01.000Z",
  "created_at": "2026-10-17T09:00:01.000Z",
  "updated_at": "2026-10-17T09:00:01.000Z",
  "finished_at": null
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

404The resource doesn't exist for your team.

{
  "error": {
    "code": "not_found",
    "message": "The grant doesn't exist.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

Limits and pricing

GET/artifact_storage/limits

Get limits and rates

Returns the sizes, the request rates, and the published rates. Costs one read.

ParameterLocationDescription
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Responses

200The limits.

{
  "object": "limits",
  "rates": {
    "rate_version": "2026-10-22",
    "storage_usd_per_gb_month": "0.04",
    "writes_usd_per_million": "30",
    "reads_usd_per_million": "3",
    "delivery_usd_per_gb": "0.18",
    "download_url_reads": 1,
    "minimum_billable_bytes": 131072,
    "storage_billing": "metered per second while an artifact is ready, and recorded per key each UTC day",
    "download_usage_reporting": "delayed",
    "next": null
  },
  "sizes": {
    "max_object_bytes": 53687091200000,
    "single_upload_max_bytes": 8388608,
    "max_parts": 10000,
    "max_part_bytes": 5368709120,
    "min_part_bytes": 8388608,
    "upload_session_hours": 24,
    "url_lifetime_seconds": {
      "default": 60,
      "min": 15,
      "max": 300
    },
    "storage_grace_days": 30,
    "max_metadata_pairs": 16,
    "max_metadata_bytes": 4096,
    "max_bulk_deletions": 100
  },
  "request_rates": {
    "team_per_second": 100,
    "team_burst": 200,
    "key_per_second": 50,
    "key_burst": 100,
    "uploads_per_second": 10,
    "uploads_burst": 20
  },
  "downloads_paused": false,
  "usage_feed": {
    "reporting": "delayed",
    "observed_through": null,
    "logs_listed_at": null,
    "typical_delay": "S3 delivers access logs within a few hours, sometimes later; Motor settles records up to 30 days late."
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}

POST/artifact_storage/estimates

Estimate a workload

Prices an upload, its storage for storage_days days (30 by default), its reads, its download URLs, and its downloads with every meter, at the rates of now. An estimate doesn't reserve anything or authorize a later transfer; the estimate expires after five minutes and does not lock in a price. Costs one read.

ParameterLocationDescription
Motor-ModeheaderArtifact storage is live-only: omit it or send live.
Request body
{
  "size_bytes": 1000000,
  "storage_days": 7,
  "downloads": 1
}
FieldTypeDescription
size_bytesinteger
storage_daysintegerDays of storage to price. Default 30.
upload_part_urlsintegerPart URLs to issue; default the upload's part count.
readsintegerControl reads.
downloadsintegerSuccessful downloads.
download_urlsintegerDownload URLs to create; default one for each download. Each costs a read.
download_bytesintegerDelivered bytes; default size_bytes for each download.
rate_versionstring
Responses

200The estimate.

{
  "object": "estimate",
  "transport": "single",
  "billable_bytes": 1000000,
  "lines": [
    {
      "meter": "writes",
      "quantity": 1,
      "cost_usd": "0.00003"
    },
    {
      "meter": "reads",
      "quantity": 2,
      "cost_usd": "0.000006"
    },
    {
      "meter": "storage_byte_days",
      "quantity": 7000000,
      "cost_usd": "0.000009333334"
    },
    {
      "meter": "delivery_bytes",
      "quantity": 1000000,
      "cost_usd": "0.00018"
    }
  ],
  "total_usd": "0.000225333334",
  "estimate_cost_usd": "0.000003",
  "meter_quantities": {
    "writes": 1,
    "reads": 2,
    "storage_byte_days": 7000000,
    "delivery_bytes": 1000000
  },
  "rates": {
    "rate_version": "2026-10-22",
    "storage_usd_per_gb_month": "0.04",
    "writes_usd_per_million": "30",
    "reads_usd_per_million": "3",
    "delivery_usd_per_gb": "0.18",
    "download_url_reads": 1,
    "minimum_billable_bytes": 131072,
    "storage_billing": "metered per second while an artifact is ready, and recorded per key each UTC day",
    "download_usage_reporting": "delayed",
    "next": null
  },
  "quote_expires_at": "2026-10-17T09:05:10.000Z",
  "note": "An estimate reserves nothing. Download usage is observed and billed later; a URL can be reused until it expires."
}

400The body isn't valid JSON, or an Idempotency-Key is missing.

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Expected a JSON value at byte 0.",
    "retryable": false
  }
}

401The key is missing, invalid, or revoked.

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked.",
    "retryable": false
  }
}

402The work would pass the paying key's spending limit, or your team doesn't have a payment card.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC.",
    "retryable": false
  }
}

403A publishable key, a key that can't use Artifact storage, or a suspended team.

{
  "error": {
    "code": "forbidden",
    "message": "This publishable key cannot access this API. Check the key's allowed APIs. For server-side requests, use the secret key.",
    "retryable": false
  }
}

413The request body is larger than 64 KiB.

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 65536 bytes.",
    "retryable": false
  }
}

415The body isn't application/json.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json.",
    "retryable": false
  }
}

422A field, the cursor, or Motor-Mode isn't valid.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be uploading, verifying, ready, deleting, deleted, failed, expired, or aborted.",
    "retryable": false
  }
}

429Too many requests; retry after Retry-After.

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds.",
    "retryable": true,
    "retry_after_seconds": 1
  }
}