---
title: Storage and data retention
description: "Where generated media lives, how long signed URLs last, the trash and permanent deletion, thumbnails, and your own storage."
---

# Storage and data retention

Keep asset ids in your application and fetch download URLs when you need them. A signed URL's expiry controls access to the bytes; it is separate from when the asset is deleted.

## Generated media

Uploaded and generated assets are stored in Google Cloud Storage (GCS). `signed_url` is stable for about an hour: reads within the same clock hour return the same signed URL so the media can be cached across refreshes. Each asset read signs for the current window, with at least one hour of validity remaining; `expires_at` reports the real expiry.

| Property | Value |
| --- | --- |
| Durable identifier | `Asset.id` |
| Download location | `signed_url` |
| Exact deadline | `expires_at` |
| Refresh | Read `GET /assets/{id}` again |
| Thumbnail | `thumbnail_url` for an image downscale or video poster; null when absent |
| Attachment download | `GET /assets/{id}?disposition=attachment` |

> [!WARNING]
> Never store a signed URL as the permanent address of a file. Store the asset id, re-read the asset for a current URL, and follow `expires_at`. An expired URL does not mean that the asset has been deleted. Use a [share link](./file-access.html#share-links) when another person needs durable access.

<!-- gen:fields schema=Asset only=id,signed_url,expires_at,thumbnail_url,mime_type,size_bytes,width,height,duration_seconds,has_audio,status,deleted_at,created_at -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `signed_url` | string | Yes | Time-limited GCS signed URL for download.… |
| `expires_at` | string | Yes | Expiry of `signed_url`. |
| `mime_type` | string | No |  |
| `size_bytes` | integer, nullable | No |  |
| `width` | integer, nullable | No |  |
| `height` | integer, nullable | No |  |
| `duration_seconds` | number, nullable | No | Media duration in seconds for video/audio assets.… |
| `has_audio` | boolean, nullable | No | Whether the asset's media carries an audio stream.… |
| `thumbnail_url` | string, nullable | No | Time-limited signed URL for a server-generated thumbnail (image downscale or video poster frame).… |
| `status` | `AssetStatus` | No |  |
| `deleted_at` | string, nullable | No | When the asset was moved to the trash (soft-deleted). Omitted for live assets; only trash listings (`GET /assets?trashed=true`) return trashed assets. Trashed assets are purged permanently 30 days after this timestamp. |
| `created_at` | string | Yes |  |
<!-- /gen -->

Optional media metadata can lag behind the bytes. Uploaded or older videos may have no duration, audio verdict or poster until the asynchronous media sweep has inspected them. A missing `thumbnail_url` is not a failed asset.

## Request payloads

The asset stores the prompt. For images, `prompt` preserves the customer's words; `enhanced_prompt` contains the composed prompt that actually ran when it differs. For video and audio, `prompt` is the dispatched prompt and `enhanced_prompt` is absent or null. Uploaded assets may have no prompt.

There is no switch to stop storing prompts and no per-request expiry header.

| Field | What is retained |
| --- | --- |
| `prompt` | Customer image prompt, or dispatched video/audio prompt |
| `enhanced_prompt` | Composed image prompt, only when different from `prompt` |
| `created_at` | When the asset was created |
| `deleted_at` | When it entered trash; omitted on live assets |

## Trash and deletion

The lifecycle is **live → trash → permanent deletion**. `DELETE /assets/{id}` moves an asset to trash and keeps its stored bytes for recovery. The API purges trashed assets 30 days after `deleted_at`, or you can call the permanent-delete endpoint earlier.

| Action | Endpoint | Result |
| --- | --- | --- |
| Move to trash | `DELETE /assets/{id}` | `204`; hidden from default reads and listings |
| List trash | `GET /assets?trashed=true` | Only soft-deleted assets |
| Restore | `POST /assets/{id}/restore` | `200` asset with a current signed URL; already-live assets are an idempotent no-op |
| Delete forever | `DELETE /assets/{id}/permanent` | `204`; works on both live and trashed assets |

The trash filter is **`trashed=true`**. The separate `status` parameter chooses `uploading` or `ready`; it does not select trash. Deleting an already-trashed asset returns `404`.

### Move an asset to trash and restore it

| Property | Value |
| --- | --- |
| Input | `ASSET_ID`, an asset in your current Library scope |
| Delete response | `204`, no response body |
| Restore response | `200`, an `Asset` |
| Recovery window | Restore before the 30-day purge or a permanent deletion |

These examples perform both operations in order. The CLI supports `assets delete`; it has no restore command, so its tab uses the HTTP restore call. The Rust example configures an empty-body `Content-Length` for the generated bodyless POST.

```bash tab="curl" title="Trash and restore example"
$ curl --fail-with-body -sS -X DELETE "https://api.nolgia.ai/v1/assets/$ASSET_ID" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
$ curl --fail-with-body -sS -X POST "https://api.nolgia.ai/v1/assets/$ASSET_ID/restore" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" --data ''
```
```bash tab="CLI" title="Trash and HTTP restore example"
$ nolgia assets delete "$ASSET_ID"
$ curl --fail-with-body -sS -X POST "https://api.nolgia.ai/v1/assets/$ASSET_ID/restore" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" --data ''
```
```ts tab="TypeScript" title="Trash and restore example"
import { createNolgiaClient } from "@nolgia/sdk";

const nolgia = createNolgiaClient(process.env.NOLGIA_TOKEN!);
const params = { path: { id: process.env.ASSET_ID! } };
const { error: deleteError } = await nolgia.DELETE("/assets/{id}", { params });
if (deleteError) throw new Error(`${deleteError.title}: ${deleteError.detail ?? ""}`);
const { data: asset, error } = await nolgia.POST("/assets/{id}/restore", { params });
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
console.log(asset.id, asset.signed_url);
```
```python tab="Python" title="Trash and restore example"
import os
from http import HTTPStatus
from uuid import UUID
from nolgia import AuthenticatedClient
from nolgia.api.assets import delete_asset, restore_asset
from nolgia.models import Asset

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])
asset_id = UUID(os.environ["ASSET_ID"])
deleted = delete_asset.sync_detailed(asset_id, client=client)
if deleted.status_code != HTTPStatus.NO_CONTENT:
    raise SystemExit(f"refused: {deleted.parsed}")
asset = restore_asset.sync(asset_id, client=client)
if not isinstance(asset, Asset):
    raise SystemExit(f"refused: {asset}")
print(asset.id, asset.signed_url)
```
```rust tab="Rust" title="Trash and restore example"
use nolgia_client::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_LENGTH};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut headers = HeaderMap::new();
    headers.insert(AUTHORIZATION, HeaderValue::from_str(
        &format!("Bearer {}", std::env::var("NOLGIA_TOKEN")?),
    )?);
    headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
    let http = reqwest::Client::builder().default_headers(headers).build()?;
    let client = Client::new_with_client("https://api.nolgia.ai/v1", http);
    let id = std::env::var("ASSET_ID")?.parse::<uuid::Uuid>()?;
    client.delete_asset().id(id).send().await?;
    let asset = client.restore_asset().id(id).send().await?.into_inner();
    println!("{} {}", asset.id, asset.signed_url);
    Ok(())
}
```

Deletion returns `204 No Content`, with no JSON. Restoration returns an asset; this is a schema-built excerpt, not a captured restore. The omitted `model` field records upload provenance for this uploaded image.

```json title="200 restored Asset — schema-built excerpt"
{
  "id": "11111111-1111-4111-8111-111111111111",
  "user_id": "22222222-2222-4222-8222-222222222222",
  "modality": "image",
  "signed_url": "https://storage.googleapis.com/example/reference.png?…",
  "expires_at": "2026-09-21T05:00:00Z",
  "status": "ready",
  "created_at": "2026-09-20T03:30:00Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id`, `user_id` | The original asset and its creator |
| `modality`, `status` | Media kind and upload-completion state |
| `signed_url`, `expires_at` | Current download URL and deadline |
| `created_at` | Original creation time, preserved by restoration |
| `deleted_at` | Cleared by restoration and omitted for this live asset |

> [!WARNING]
> Permanent deletion cannot be undone. It removes the asset row and deletes the stored bytes immediately when possible, otherwise through the deferred cleanup queue. A share link to trashed or deleted media answers `410`.

<!-- gen:endpoints paths=/assets/{id},/assets/{id}/restore,/assets/{id}/permanent -->
| Method | Path | What it does |
| --- | --- | --- |
| [GET](../api/#tag/assets/get/assets/{id}) | `/assets/{id}` | Fetch one of the current user's assets with a fresh signed URL. |
| [PATCH](../api/#tag/assets/patch/assets/{id}) | `/assets/{id}` | Update one of the current user's assets (tags, display name, prompt, metadata, favorite). |
| [DELETE](../api/#tag/assets/delete/assets/{id}) | `/assets/{id}` | Move one of the current user's assets to the trash (soft delete). |
| [POST](../api/#tag/assets/post/assets/{id}/restore) | `/assets/{id}/restore` | Restore a trashed asset back into the library. |
| [DELETE](../api/#tag/assets/delete/assets/{id}/permanent) | `/assets/{id}/permanent` | Permanently delete one of the current user's assets. |
<!-- /gen -->

## Bring your own storage

Enterprise organizations can connect an S3-compatible bucket. An owner or admin creates the connection, tests its credentials, and enables mirroring so assets are copied as they become ready. Existing ready assets can be backfilled, and objects in the bucket can be imported into the Library. The original Nolgia asset remains the id you use with the API.

| Property | Value |
| --- | --- |
| Availability | Enterprise organization; owner or admin manages connections |
| Providers | AWS S3 or compatible endpoints such as R2, MinIO and GCS interoperability |
| Automatic copy | Enable `mirror_enabled` on the connection |
| Destination layout | `<prefix>/<yyyy>/<mm>/<asset_id>.<ext>` |
| Progress | Read the asset's `mirrors` array on `GET /assets/{id}` |
| Existing assets | Queue a backfill for ready, non-trashed organization assets |

`mirrors` is present only for an organization asset with at least one storage connection. Each entry names the connection, its copy status and, once available, the remote object key. Read that status before assuming the copy has completed.

<!-- gen:fields schema=AssetMirror -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `connection_id` | string | Yes |  |
| `status` | `AssetMirrorStatus` | Yes |  |
| `remote_key` | string, nullable | No | Object key in the customer's bucket once copied. |
| `last_error` | string, nullable | No | Last copy failure; the worker retries up to five times with backoff before settling on `error`. |
| `updated_at` | string | Yes |  |
<!-- /gen -->

<!-- gen:endpoints prefix=/organizations/{id}/storage-connections -->
| Method | Path | What it does |
| --- | --- | --- |
| [GET](../api/#tag/organizations/get/organizations/{id}/storage-connections) | `/organizations/{id}/storage-connections` | List the organization's bring-your-own storage connections (owner or admin; Enterprise). |
| [POST](../api/#tag/organizations/post/organizations/{id}/storage-connections) | `/organizations/{id}/storage-connections` | Connect an S3-compatible bucket to the organization (owner or admin; Enterprise). |
| [PATCH](../api/#tag/organizations/patch/organizations/{id}/storage-connections/{connection_id}) | `/organizations/{id}/storage-connections/{connection_id}` | Update a storage connection (owner or admin; Enterprise). |
| [DELETE](../api/#tag/organizations/delete/organizations/{id}/storage-connections/{connection_id}) | `/organizations/{id}/storage-connections/{connection_id}` | Disconnect a storage connection (owner or admin; Enterprise). |
| [POST](../api/#tag/organizations/post/organizations/{id}/storage-connections/{connection_id}/test) | `/organizations/{id}/storage-connections/{connection_id}/test` | Probe a storage connection (owner or admin; Enterprise). |
| [POST](../api/#tag/organizations/post/organizations/{id}/storage-connections/{connection_id}/import) | `/organizations/{id}/storage-connections/{connection_id}/import` | Import objects from the connected bucket into the organization library (owner or admin; Enterprise). |
| [POST](../api/#tag/organizations/post/organizations/{id}/storage-connections/{connection_id}/backfill) | `/organizations/{id}/storage-connections/{connection_id}/backfill` | Queue every ready organization asset for mirroring to this connection (owner or admin; Enterprise). |
<!-- /gen -->

## Summary

| Data type | Default retention or lifetime | Control |
| --- | --- | --- |
| Live uploaded or generated asset | Kept in the Library; download URL expiry does not delete it | Trash or permanently delete the asset |
| Trashed asset and its bytes | Purged 30 days after `deleted_at` | Restore before purge, or permanently delete sooner |
| Asset download and thumbnail URLs | Stable for about an hour; exact download expiry in `expires_at` | Re-read the asset for a current signature |
| Signed upload URL | 30 minutes | Create a new upload after expiry |
| Prompt and enhanced image prompt | Stored with the asset | No storage opt-out or per-request expiry header |
| Public share link | 30 days by default; 1–365 days at creation | Set `expires_in_days` or revoke it |
| Organization mirror | A copy in your connected bucket | Manage the storage connection and your bucket's own lifecycle |

:::cards
- [Uploads and files](./uploads.html): Put small or large reference media into the Library.
- [File access controls](./file-access.html): Choose Library access or a durable share link.
- [Teams and organizations](./organizations.html): Manage a shared Library and storage connections.
:::
