---
title: File access controls
description: "Who can read a file: library scope, organization roles, and durable share links with an expiry and a revoke."
---

# File access controls

Library access follows the caller's personal or organization scope. To give someone access without an account, create a share link with a deadline, keep the returned URL, and revoke the link when it is no longer needed.

There is no per-file ACL on the storage object itself; access is the Library scope plus share links.

## How access works

| Request and caller | Result | Why |
| --- | --- | --- |
| `GET /assets/{id}` for your live asset in personal scope | `200` with an asset and signed URL | The asset is in your Library |
| `GET /assets/{id}` for a teammate's live asset in your active organization | `200` with an asset and signed URL | Organization assets belong to the shared Library; `user_id` still identifies the creator |
| Share management for an asset outside the caller's Library scope | `404` | The target must be visible in the current scope |
| Create a share link as a viewer or billing role | `403 Read-only role` | Reading the Library does not grant sharing authority |
| Create a share link as a member for another teammate's asset | `403 Forbidden` | Members may share only assets they created; owners/admins may share any |
| Public resolver for an active share link | `302` to temporary media | No account or bearer token is required |
| Public resolver for a revoked/expired link or trashed/deleted media | `410` | The link no longer provides access |
| Public resolver for an unknown token | `404` | No matching link exists |
| Public resolver over its per-IP limit | `429`, `Retry-After: 60` | Wait before trying again |

The `302` is the resolver response, not the media body. Follow its `Location` to fetch the bytes.

## Library scope

Personal assets belong to your personal Library. In organization context, teammates can read the shared Library, while the asset's `user_id` continues to name its creator. Choose the active context with `PUT /me/active-organization`; organization API keys stay bound to their organization. See [Teams and organizations](./organizations.html) for switching context and roles.

| Role or scope | Create a share link | Revoke a share link |
| --- | --- | --- |
| Personal asset owner | Own ready, untrashed assets | Links on own assets |
| Organization owner/admin | Any ready, untrashed asset in that organization | Links on any asset in that organization |
| Organization member | Assets they created | Links they created or links on assets they can change |
| Organization viewer/billing | Refused with `403 Read-only role` | Refused with `403 Read-only role` |

> [!NOTE]
> Being able to see a teammate's asset does not mean you can share it. A member must be its creator; an owner or admin can share any visible organization asset. Creating a link for unfinished or trashed media is refused with `409`.

## Share links

![Create a share link, resolve it to a short-lived media URL, and revoke it to return 410](../assets/diagrams/share-link-lifecycle.svg)

### Create a link

| Property | Value |
| --- | --- |
| Endpoint | `POST /assets/{id}/share` |
| Default lifetime | 30 days |
| Custom lifetime | `expires_in_days`, an integer from 1 through 365 |
| Success | `201` with a `ShareLink` |
| Returned once | `token` and `url`; later listings cannot recover either |

<!-- gen:fields schema=CreateShareLinkRequest -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expires_in_days` | integer | No | Lifetime of the link in days from now. Defaults to 30. Ignored when `never_expires` is true. |
| `never_expires` | boolean | No | A link with no end date. Refused (`400`, code `share_expiry_exceeds_organization_limit`) when the organization sets a maximum lifetime. |
| `password` | string | No | Viewers must enter this password before the media is served. Stored only as an argon2id hash; never returned. |
| `view_only` | boolean | No | Turn downloads off for this link. The share page offers no Download and the resolver refuses `download=true`. A viewer who can play media can still capture it, so treat this as a deterrent, not copy protection. |
<!-- /gen -->

Set `ASSET_ID` to a ready asset you are allowed to share. These request examples use a seven-day lifetime. The CLI currently has no share command; its tab uses curl with the same `NOLGIA_TOKEN` credential.

```bash tab="curl" title="Create share link example"
$ curl --fail-with-body -sS "https://api.nolgia.ai/v1/assets/$ASSET_ID/share" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" -d '{"expires_in_days":7}'
```
```bash tab="CLI" title="Share through the HTTP API example"
$ curl --fail-with-body -sS "https://api.nolgia.ai/v1/assets/$ASSET_ID/share" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" -d '{"expires_in_days":7}'
```
```ts tab="TypeScript" title="Create share link example"
import { createNolgiaClient } from "@nolgia/sdk";

const nolgia = createNolgiaClient(process.env.NOLGIA_TOKEN!);
const { data: link, error } = await nolgia.POST("/assets/{id}/share", {
  params: { path: { id: process.env.ASSET_ID! } },
  body: { expires_in_days: 7 },
});
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
console.log(link.id, link.url);
```
```python tab="Python" title="Create share link example"
import os
from uuid import UUID
from nolgia import AuthenticatedClient
from nolgia.api.sharing import create_asset_share_link
from nolgia.models import CreateShareLinkRequest, ShareLink

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])
link = create_asset_share_link.sync(
    UUID(os.environ["ASSET_ID"]), client=client,
    body=CreateShareLinkRequest(expires_in_days=7),
)
if not isinstance(link, ShareLink):
    raise SystemExit(f"refused: {link}")
print(link.id, link.url)
```
```rust tab="Rust" title="Create share link example"
use nolgia_client::ClientBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = ClientBuilder::new("https://api.nolgia.ai/v1")
        .bearer_token(std::env::var("NOLGIA_TOKEN")?)
        .build()?;
    let id = std::env::var("ASSET_ID")?.parse::<uuid::Uuid>()?;
    let link = client.create_asset_share_link().id(id)
        .body_map(|b| b.expires_in_days(7_u64))
        .send().await?.into_inner();
    println!("{} {:?}", link.id, link.url);
    Ok(())
}
```

This is the production create response from `share-create.json`; the token and the URL's token are redacted. The SDK snippets are examples, not separate live captures.

```json title="201 — share-create.json"
{
  "access_count": 0,
  "created_at": "2026-09-21T03:33:52.874514Z",
  "created_by": "dad27b53-a85b-4e3d-8fd6-b152c803a27c",
  "expires_at": "2026-09-28T03:33:52.873377Z",
  "id": "26d786f5-437c-4da4-9f04-60342191299b",
  "kind": "asset",
  "target_id": "f7bc037c-d7d7-434a-8843-26675574de0d",
  "token": "…",
  "token_prefix": "EJIkUdv1",
  "url": "https://nolgia.ai/s/…"
}
```

<!-- gen:fields schema=ShareLink -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes |  |
| `kind` | `ShareLinkKind` | Yes |  |
| `target_id` | string | Yes | The shared asset's id, or the shared render's id. |
| `token` | string | No | The share token (base64url, 128 bits of entropy). Create response only. |
| `token_prefix` | string | Yes | The first characters of the token, for recognising a link in a listing. |
| `url` | string | No | The public share URL to hand out (the nolgia.ai share page). Create response only. |
| `expires_at` | string | No | When the link stops resolving. Absent for a link that never expires. |
| `has_password` | boolean | No | Viewers must enter a password. |
| `view_only` | boolean | No | Downloads are turned off for this link. |
| `created_at` | string | Yes |  |
| `created_by` | string | Yes | The user who created the link. |
| `access_count` | integer | Yes | Successful public resolutions so far. |
| `last_accessed_at` | string | No | When the link was last resolved. Absent until the first visit. |
| `revoked_at` | string | No | Set once the link has been revoked. Listings only return unrevoked links. |
<!-- /gen -->

> [!WARNING]
> Save the returned share URL when creating the link. Tokens are stored hashed, so a later list call cannot reconstruct the URL. Anyone who has the link can resolve it until it expires or is revoked; keep it out of public logs unless you intend to publish it.

### List active links

| Property | Value |
| --- | --- |
| Endpoint | `GET /assets/{id}/share` |
| Order | Newest first |
| Included | Unrevoked, unexpired links |
| Omitted | Tokens and URLs |
| Management handle | Use each link's `id` to revoke it |

```bash title="List share links example"
$ curl --fail-with-body -sS "https://api.nolgia.ai/v1/assets/$ASSET_ID/share" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

```json title="200 — share-list.json"
{
  "links": [
    {
      "access_count": 1,
      "created_at": "2026-09-21T03:33:52.874514Z",
      "created_by": "dad27b53-a85b-4e3d-8fd6-b152c803a27c",
      "expires_at": "2026-09-28T03:33:52.873377Z",
      "id": "26d786f5-437c-4da4-9f04-60342191299b",
      "kind": "asset",
      "last_accessed_at": "2026-09-21T03:33:53.01188Z",
      "target_id": "f7bc037c-d7d7-434a-8843-26675574de0d",
      "token_prefix": "EJIkUdv1"
    }
  ]
}
```

| Response field | Meaning |
| --- | --- |
| `links` | Active links, each using the `ShareLink` fields above without `token` or `url` |
| `links[].id`, `links[].token_prefix` | Revoke handle and short recognition aid |
| `links[].kind`, `links[].target_id` | Whether the target is an asset/render and which target it is |
| `links[].created_by`, `links[].created_at`, `links[].expires_at` | Creator, creation time and share deadline |
| `links[].access_count`, `links[].last_accessed_at` | Successful public resolutions and the latest one; metadata reads do not increment the count |

### Resolve the media

| Property | Value |
| --- | --- |
| Endpoint | `GET /share/{token}`; no authentication |
| Success | `302` with a fresh signed media URL in `Location` |
| Media URL lifetime | About 15 minutes |
| Caching | `Cache-Control: no-store` on the redirect |
| Download | Add `?download=true` for a `Content-Disposition: attachment` media URL |

Set `SHARE_TOKEN` to the token returned at creation. This command shows the resolver response without following it; add `-L` to fetch the media.

```bash title="Inspect public resolver example"
$ curl -sS -D - -o /dev/null "https://api.nolgia.ai/v1/share/$SHARE_TOKEN"
```

These are the real response headers from `share-resolve.headers`; `location` is truncated.

```http title="302 — share-resolve.headers"
HTTP/2 302
access-control-allow-headers: Authorization, Content-Type, X-Request-Id, If-Match, Idempotency-Key
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-origin: *
cache-control: no-store
content-type: text/html; charset=utf-8
location: https://storage.googleapis.com/nolgia-generations-prod/dad27b53-a85b-4e…
x-cloud-trace-context: 978e41fdddedef4cd8db4b39631ad0cd
date: Mon, 21 Sep 2026 03:33:53 GMT
server: Google Frontend
content-length: 941
via: 1.1 google
alt-svc: h3=":443"; ma=2592000
```

| Response field | Meaning |
| --- | --- |
| HTTP status `302` | Follow the redirect; this response is not the media |
| `location` | Short-lived download URL, refreshed on each resolution |
| `cache-control` | `no-store`; do not cache the resolver response |
| `content-type`, `content-length` | The redirect's HTML body, not the media's MIME type or size |
| `date`, trace and infrastructure headers | Response metadata, not share-link settings |
| `access-control-*` | Cross-origin response headers |

A valid share URL can outlast many media URLs. Keep the share URL and resolve it when needed; do not persist the `Location` as a replacement for the share link.

### Revoke a link

| Property | Value |
| --- | --- |
| Endpoint | `DELETE /assets/{id}/share/{token}` |
| `{token}` accepts | The original token **or the link id from the listing** |
| Success | `204`, no response body; already revoked is also `204` |
| Wrong target | `404` if the link does not exist or belongs to another asset |
| Next public resolution | `410 Gone` |

The path parameter is named `token`, but the captured revoke used the link id `26d786f5-437c-4da4-9f04-60342191299b` and returned `204`. Use your own listing's id as `SHARE_LINK_ID`.

```bash title="Revoke by link id example"
$ curl --fail-with-body -sS -X DELETE \
  "https://api.nolgia.ai/v1/assets/$ASSET_ID/share/$SHARE_LINK_ID" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
```

```http title="Revoke result"
HTTP/2 204
```

There is no JSON body. The same public resolver request now returns the real problem captured in `share-gone.json`:

```bash title="Resolve a revoked link example"
$ curl -sS "https://api.nolgia.ai/v1/share/$SHARE_TOKEN"
```

```json title="410 — share-gone.json"
{
  "detail": "this share link has been revoked",
  "request_id": "localhost/7bnUUXx7b2-007297",
  "status": 410,
  "title": "Gone",
  "type": "about:blank"
}
```

| Response field | Meaning |
| --- | --- |
| `status`, `title` | `410 Gone`: the link no longer resolves |
| `detail` | This capture identifies revocation; expired links and deleted media give their own reason |
| `request_id` | Correlation id to include when asking for help |
| `type` | Problem type; `about:blank` here |

Revocation stops future resolutions. A previously issued signed media URL has its own short expiry; revoking the share does not change that URL's signature.

### Preview metadata and renders

`GET /share/{token}/meta` returns the title, modality, media URL and expiry for a preview page or embed, with an optional description, thumbnail, duration and size. It has the same unknown/revoked/rate-limit behavior as the resolver, but does not increment the link's access count.

<!-- gen:fields schema=ShareLinkMeta -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `ShareLinkKind` | Yes |  |
| `title` | string | Yes | The asset's display name (or a trimmed prompt), or the composition's name for a render. |
| `description` | string | No | A short description when one is known (the generation prompt, or the composition's description). |
| `model` | string | No | Catalog id of the model that generated the shared media (render it with the catalog's display name, never raw).… |
| `preset` | `ShareLinkPreset` | No |  |
| `modality` | `Modality` | Yes |  |
| `mime_type` | string | No |  |
| `media_url` | string | Yes | Freshly signed short-lived URL of the media itself. Play or embed it now; never store it. |
| `media_expires_at` | string | Yes | When `media_url` (and `thumbnail_url`) stop working. Fetch the metadata again for a new one. |
| `thumbnail_url` | string | No | Freshly signed short-lived poster image, when the media has one. |
| `duration_seconds` | number | No | Media duration for video and audio, when known. |
| `size_bytes` | integer | No |  |
| `expires_at` | string | No | When the share link itself stops resolving. Absent for a link that never expires. |
| `view_only` | boolean | No | Downloads are turned off for this link; offer no Download. |
| `created_at` | string | Yes |  |
| `creator_insider` | `InsiderBadge` | No | Present only when the link's owner is an active NOLGIA Insider (who chose a public profile) and the link is a personal one: their badge, linking to nolgia.ai/@handle. Absent for everyone else. |
<!-- /gen -->

Renders use the same create, list and revoke pattern under `/renders/{id}/share` and `/renders/{id}/share/{token}`. Their visibility follows the composition's Library scope. Both kinds resolve through the same public `/share/{token}` and `/share/{token}/meta` routes.

<!-- gen:endpoints tag=Sharing -->
| Method | Path | What it does |
| --- | --- | --- |
| [GET](../api/#tag/sharing/get/assets/{id}/share) | `/assets/{id}/share` | List the active share links on an asset. |
| [POST](../api/#tag/sharing/post/assets/{id}/share) | `/assets/{id}/share` | Create a durable public share link for an asset. |
| [DELETE](../api/#tag/sharing/delete/assets/{id}/share/{token}) | `/assets/{id}/share/{token}` | Revoke a share link on an asset. |
| [GET](../api/#tag/sharing/get/renders/{id}/share) | `/renders/{id}/share` | List the active share links on a render. |
| [POST](../api/#tag/sharing/post/renders/{id}/share) | `/renders/{id}/share` | Create a durable public share link for a finished render. |
| [DELETE](../api/#tag/sharing/delete/renders/{id}/share/{token}) | `/renders/{id}/share/{token}` | Revoke a share link on a render. |
| [GET](../api/#tag/sharing/get/me/share-links) | `/me/share-links` | List every active share link in the caller's library, newest first. |
| [GET](../api/#tag/sharing/get/me/share-links/policy) | `/me/share-links/policy` | The share-link rules that apply in the caller's current space. |
| [GET](../api/#tag/sharing/get/share/{token}) | `/share/{token}` | Public resolver (no authentication) that redirects to the shared media. |
| [GET](../api/#tag/sharing/get/share/{token}/meta) | `/share/{token}/meta` | Public metadata (no authentication) for a share link's preview page. |
| [POST](../api/#tag/sharing/post/share/{token}/unlock) | `/share/{token}/unlock` | Trade a share link's password for a short-lived viewing grant (no authentication). |
<!-- /gen -->

:::cards
- [Storage and data retention](./storage.html): Understand signed URLs, trash and permanent deletion.
- [Teams and organizations](./organizations.html): Choose Library scope and manage organization roles.
:::
