> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bibleit.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bibleit.dev/_mcp/server.

# Upload a staged presentation asset

POST https://api.bibleit.app/v1/presentations/{presentation_id}/assets
Content-Type: application/octet-stream

**Required scopes:** `asset.create`, `presentation.update`.

Send raw bytes, not multipart or JSON. Images are bounded to 10 MiB, dimensions 8192 per side and 40 million pixels; videos to 64 MiB and 4096 per side. Actual bytes, media format and existing codec/container constraints are inspected, regardless of claimed Content-Type. Content-Encoding must be identity. Body reading has a 60-second budget. Existing organization media quotas and upload reservations apply; definitive rejections release reservations and bytes; an ambiguous keyed completion retains its bounded lease until retry/reconciliation or normal expiry. Keyed uploads read and hash the bounded body after authorization, before quota reservation, so full quotas cannot block replay. Fresh scopes and ownership are checked before reservation and completion. Returns safe metadata and a protected asset Location, never a storage key/path. Ready assets are staged for 24 hours; attach through conditional presentation PUT to retain them. Uploading does not change document revision/ETag or publish. Optional Idempotency-Key atomically records a successful creation for 24-hour replay; current authorization is rechecked. Changed payload or original If-Match returns 409 idempotency_conflict. Replay precedes stale-version and quota evaluation.

**Additional request headers:**

- `Content-Length` (required): Required exact raw body byte count. Missing returns 411, excessive returns 413. Chunked-only requests are unsupported.
- `Idempotency-Key` (optional): Optional case-sensitive retry key, retained 24 hours from successful commit. Scoped to credential principal (stable service-token ID; personal bt_ credential; delegated OAuth grant), account and concrete method/path. Payload, If-Match, query string and Content-Type must match. JSON object key order is ignored; queue reference casing/defaults are normalized; raw upload/import bytes are hashed. Replays retain original success body, Location and ETag with a fresh X-Request-Id, after current authority/ownership/processing/library checks. Same scope/key with changed input:409 idempotency_conflict. Concurrent requests serialize; a contended retry lock can return 409 idempotency_in_progress; bounded database waits can return 503. Errors are not recorded. After expiry, a key is treated as new; original resource/job lifetime is unchanged. No key means no retry guarantee.

**Response headers:**

- `Cache-Control` (201).
- `Location` (201): Bearer-protected asset resource URL.

Reference: https://docs.bibleit.dev/api-reference/organizations/presentation-assets/upload-presentation-asset

## Authentication

- `Authorization` header (bearer token, required) — Organization-owned service token. Issue the initial token in organization credential settings. Scopes are documented per operation via x-required-scopes; they are not OAuth scopes.

## Request

### Path parameters

- `presentation_id` (string, required) — Presentation UUID belonging to the service token organization.

### Query parameters

- `kind` (enum, required) — Image: up to 10 MiB. Video: up to 64 MiB.
  - Allowed values: `image`, `video`

### Headers

- `Idempotency-Key` (string, optional) — Optional case-sensitive retry key, retained 24 hours from successful commit. Scoped to credential principal (stable service-token ID; personal bt_ credential; delegated OAuth grant), account and concrete method/path. Payload, If-Match, query string and Content-Type must match. JSON object key order is ignored; queue reference casing/defaults are normalized; raw upload/import bytes are hashed. Replays retain original success body, Location and ETag with a fresh X-Request-Id, after current authority/ownership/processing/library checks. Same scope/key with changed input:409 idempotency_conflict. Concurrent requests serialize; a contended retry lock can return 409 idempotency_in_progress; bounded database waits can return 503. Errors are not recorded. After expiry, a key is treated as new; original resource/job lifetime is unchanged. No key means no retry guarantee.

### Body (application/octet-stream)

This endpoint expects binary data of type application/octet-stream.

- Binary request body.

## Response

### 201

Validated staged asset.

- `id` (string, required)
- `organization_id` (string, required)
- `presentation_id` (string, required)
- `kind` (enum, required)
  - Allowed values: `image`, `video`
- `mime` (enum, required)
  - Allowed values: `image/png`, `image/jpeg`, `image/gif`, `video/mp4`, `video/webm`
- `width` (integer, required)
- `height` (integer, required)
- `bytes` (integer, required)

## Errors

### 401 Unauthorized Error

Error response. 400 malformed JSON; 401 invalid/expired/revoked token; 403 denied scope or domain policy; 404 unknown/foreign resource; 405 unsupported method; 409 quota/conflict or stopped Live; 412 stale state validator; 428 missing If-Match; 413 body too large; 415 unsupported media type; 422 invalid fields; 429 admission budget exhausted; 503 unavailable dependency.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 403 Forbidden Error

Error response. 400 malformed JSON; 401 invalid/expired/revoked token; 403 denied scope or domain policy; 404 unknown/foreign resource; 405 unsupported method; 409 quota/conflict or stopped Live; 412 stale state validator; 428 missing If-Match; 413 body too large; 415 unsupported media type; 422 invalid fields; 429 admission budget exhausted; 503 unavailable dependency.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 404 Not Found Error

Error response. 400 malformed JSON; 401 invalid/expired/revoked token; 403 denied scope or domain policy; 404 unknown/foreign resource; 405 unsupported method; 409 quota/conflict or stopped Live; 412 stale state validator; 428 missing If-Match; 413 body too large; 415 unsupported media type; 422 invalid fields; 429 admission budget exhausted; 503 unavailable dependency.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 409 Conflict Error

Domain conflict, changed idempotency input (idempotency_conflict), or concurrent retry lock contention (idempotency_in_progress).

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 422 Unprocessable Entity Error

Invalid input or invalid_idempotency_key.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 429 Too Many Requests Error

Error response. 400 malformed JSON; 401 invalid/expired/revoked token; 403 denied scope or domain policy; 404 unknown/foreign resource; 405 unsupported method; 409 quota/conflict or stopped Live; 412 stale state validator; 428 missing If-Match; 413 body too large; 415 unsupported media type; 422 invalid fields; 429 admission budget exhausted; 503 unavailable dependency.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

### 503 Service Unavailable Error

Error response. 400 malformed JSON; 401 invalid/expired/revoked token; 403 denied scope or domain policy; 404 unknown/foreign resource; 405 unsupported method; 409 quota/conflict or stopped Live; 412 stale state validator; 428 missing If-Match; 413 body too large; 415 unsupported media type; 422 invalid fields; 429 admission budget exhausted; 503 unavailable dependency.

- `type` (any, required)
- `title` (string, required)
- `status` (integer, required)
- `code` (string, required)
- `request_id` (string, required)

## Examples

**Response**

```json
{
  "id": "string",
  "organization_id": "string",
  "presentation_id": "string",
  "kind": "image",
  "mime": "image/png",
  "width": 1,
  "height": 1,
  "bytes": 1
}
```

**SDK Code**

```python
import requests

url = "https://api.bibleit.app/v1/presentations/presentation_id/assets"

querystring = {"kind":"image"}

headers = {
    "Idempotency-Key": "integration-7c642889-60eb-47ed-a16c-13e290e3f054",
    "Authorization": "Bearer <token>",
    "Content-Type": "application/octet-stream"
}

response = requests.post(url, headers=headers, params=querystring)

print(response.json())
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.bibleit.app/v1/presentations/presentation_id/assets?kind=image"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("Idempotency-Key", "integration-7c642889-60eb-47ed-a16c-13e290e3f054")
	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/octet-stream")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```