Assets API
The Assets API manages the files in the Runnit asset library: uploading, reading, downloading, updating with automatic version history, deleting, and organising with entity links, tags, and folders.
Assets always live inside a collection. Create or look up a collection with the Asset Collections API first, then upload into it.
Authentication: every endpoint requires an API key. See API Keys & Authentication.
Permissions: your key stays bound to its organisation. Reads require asset
view access and the file’s source and privacy permissions. Creating, updating,
deleting, restoring versions, changing tags and requesting reindexing also require
asset:upload on the file’s collection. Image-edit and video job requests check
source access and write permission before starting a job. A revoked permission
applies on the next request and returns 403 without making the change.
An active agency-to-client relationship can authorise client collection writes
through the agency’s permissions. The actual collection owner determines the
destination: a contradictory owningOrganizationId returns 400, and a missing
or deleted collection returns 404. Legacy entity uploads and link copies check
write permission before creating a destination collection. A link copy requires
read access to its source and write access to its destination. Private-file,
Finance document and AI Directives restrictions still apply.
AI transformations of an asset (POST /api/v1/assets/:assetId/edit and
POST /api/v1/assets/:assetId/convert-to-video) start generation jobs and are
covered by Image Generation and
Video Generation.
Saved-asset transformation returns 403 for private files, expense receipts and
protected Finance files, including requests by their authorised reader. No job
is created. Sources must be live images in a live collection; old files without
a collection must be moved first. Queued work rechecks your live access and
source identity before submitting it and saving output. A changed source or
revoked access fails the job without saving a generated file. The same rules
apply through call_public_api. The original file is unchanged.
Upload request size
Section titled “Upload request size”JSON requests have a 20 MiB (20,971,520-byte) limit, including metadata, JSON
escaping and encoded file content. Base64 expands binary data, so this is not a
20 MiB source-file allowance. Oversized create or update requests return 413 with
code: "PAYLOAD_TOO_LARGE", details.limitBytes: 20971520, a message and
requestId. No file or version is saved. Reduce the payload before retrying.
The same response is available through
MCP call_public_api. Its separate 25 MiB bridge limit
still applies. Empty text documents remain supported through this API.
The asset object
Section titled “The asset object”{ "id": "5595c819-...", "fileName": "style-guide", "fileType": "markdown", "fileExtension": "md", "mimeType": "text/markdown", "isBinary": false, "isRemoteUrl": false, "storageProvider": "aws", "storageKey": "assets/ddf75d91-.../d5df4edc-.../style-guide", "fileSize": 32, "description": "Tone of voice and formatting rules.", "metadata": {}, "createdByAgentType": null, "createdByUserId": "213ae491-...", "createdByUserName": "Mark Tolson", "updatedByAgentType": null, "createdAt": "2026-07-08T02:31:50.117Z", "updatedAt": "2026-07-08T02:31:58.598Z", "version": 1, "owningOrganizationId": "ddf75d91-...", "visibilityScope": "organization", "sourceType": "upload", "isArchived": false, "summaryExcerpt": "A short AI summary of the content.", "searchStatus": "ready", "folderPath": null, "lastIndexedAt": "2026-07-08T12:31:58.597Z", "folderIds": [], "storageUrl": "https://...", "url": "https://..."}fileTypeis derived from the extension:markdown,html,text,image,video,audio,json,csv,pdf, orother.searchStatusispending,processing,ready, orfailedand tracks content indexing for search and insights.contentis included when the endpoint returns file content (create, update, and single-asset reads). An empty text document returnscontent: ""andfileSize: 0; an omitted content field means content was not included. Collection lists omit inline content. For binary files it is base64.storageUrlandurlappear when the file is held in cloud storage.- Treat
storageKeyand storage URLs as opaque values. Use the asset ID as the file’s identity and fetch its current metadata after an update; storage locations can change when content is replaced. entityLinksappears when you ask for links, and lists the entities the asset’s collection belongs to.
POST /api/v1/assets
Section titled “POST /api/v1/assets”Upload a file. Send a JSON body; for binary files (images, PDFs, Office
documents, media) put the base64-encoded bytes in content.
Give the file a home in one of two ways:
collectionId(preferred): the asset is stored in that collection.entityLinks(legacy): Runnit finds or creates a suitable collection for the linked job, project, or organisation and stores the asset there. The first entity in job, project, organisation order decides the home; the others do not attach the same file in more than one place. UsePOST /api/v1/assets/:assetId/linksfor that.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| fileName | string | Yes | File name. |
| fileExtension | string | Yes | Extension without meaning-changing tricks, e.g. md, png, pdf. Determines the file type and whether content is treated as binary. |
| content | string | Yes | File content. Plain text for text formats, base64 for binary formats, or a URL when isRemoteUrl is true. |
| collectionId | UUID | One of the two homes | Collection to store the asset in. |
| entityLinks | array | One of the two homes | Objects with entityType (job, organization, or project) and entityId. |
| description | string | No | Description stored on the asset. |
| metadata | object | No | Free-form metadata. |
| createdByAgentType | string | No | Label recording which automation produced the file. |
| isRemoteUrl | boolean | No | true when content is a URL to an externally hosted file. Defaults to false. |
| owningOrganizationId | UUID | No | Owning organisation. Normally derived from the collection or links. |
| visibilityScope | string | No | organization, client, public, or private. |
| sourceType | string | No | Where the file came from, e.g. upload. |
| externalSource | string | No | Name of an external system of record. |
| externalId | string | No | ID in that external system. |
| isArchived | boolean | No | Create the asset already archived. |
| tagIds | array of UUIDs | No | Tags to apply immediately. |
| folderIds | array of UUIDs | No | Folders to place the asset in. |
| primaryFolderId | UUID | No | Which of folderIds is the primary folder. |
Text uploads are rejected only when the content is an unfilled scaffolding
marker (for example FULL_CONTENT_HERE or [CONTENT HERE]). Short stub
documents such as TBD or TODO are accepted.
Example request
curl -X POST https://api.runnit.io/api/v1/assets \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "fileName": "style-guide", "fileExtension": "md", "content": "# Style guide\n\nUse sentence case.", "collectionId": "<collection-id>", "description": "Tone of voice and formatting rules." }'Response
Returns 201 with the full asset, including content. The upload is also
recorded as version 1 in the asset’s version history, and content indexing is
queued (searchStatus starts as pending).
{ "success": true, "asset": { "id": "5595c819-...", "fileName": "style-guide", "version": 1 } }Errors
400if a required field is missing or invalid, if neithercollectionIdnorentityLinksis given, or if the referenced collection does not exist.400with errorplaceholder_contentif text content is an unfilled scaffolding marker.403if you do not have access to the collection, a linked entity, or the owning organisation.413with codeSTORAGE_QUOTA_EXCEEDEDif storing the file would take the organisation over its storage limit. Delete unused files or contact support.413with codeKNOWLEDGE_CONTENT_CAPACITY_EXCEEDEDif retained file history has reached its limit. The file change is not saved. Contact support.413with codeKNOWLEDGE_CONTENT_TOO_LARGEif the stored content is too large. Reduce the content before retrying.
GET /api/v1/assets
Section titled “GET /api/v1/assets”List assets in a collection, or the assets linked to an entity. Provide
collectionId, or both entityType and entityId.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| collectionId | UUID | One of the two filters | Lists the collection’s assets. |
| entityType | string | With entityId | job, organization, or project. |
| entityId | UUID | With entityType | Lists assets linked to this entity. |
| ownerOrganizationId | UUID | No | For organization entities: only files in collections owned by this organisation (you need access to it). Without it you get files in collections owned by the organisation that authorised your request plus the entity organisation’s own. |
Example request
curl "https://api.runnit.io/api/v1/assets?collectionId=<collection-id>" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "collectionId": "d5df4edc-...", "assets": [ { "id": "5595c819-...", "fileName": "style-guide" } ] }The entity form omits collectionId. Asset objects in lists do not include
content; fetch an asset by ID to read it.
Errors
400if you supply neithercollectionIdnor theentityTypeandentityIdpair.403if you do not have access to the collection or entity.404if the collection does not exist.
GET /api/v1/assets/file
Section titled “GET /api/v1/assets/file”Fetch one file by name for an entity. Text files come back as JSON with
content; images and binary documents come back as the raw bytes with the
file’s own content type.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| entityType | string | Yes | job, organization, or project. |
| entityId | UUID | Yes | Entity the file is linked to. |
| fileName | string | Yes | Exact file name. |
Example request
curl "https://api.runnit.io/api/v1/assets/file?entityType=project&entityId=<project-id>&fileName=style-guide" \ -H 'Authorization: Bearer rnk_your_key'Response
For text formats:
{ "success": true, "asset": { "id": "5595c819-...", "fileName": "style-guide", "content": "# Style guide..." } }For images, PDFs, Office documents, and other binary formats, the response
body is the file itself (Content-Disposition: inline). Remote-URL assets
always return JSON with the URL in content.
You can also fetch a file with a clean path instead of query parameters:
GET /api/v1/assets/:entityType/:entityId/:fileName. That form always serves
the file content directly (redirecting for remote-URL assets), which suits
embedding in src attributes.
Add download=1 to either form to receive the file as an attachment
(Content-Disposition: attachment) with its original name. With download=1,
text files are returned as raw bytes rather than JSON. See
How stored files are served for the headers
every file response carries.
Errors
403if you do not have access to the entity.404if no asset with that name is linked to the entity.
GET /api/v1/assets/by-type
Section titled “GET /api/v1/assets/by-type”List an entity’s assets grouped by file type.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| entityType | string | Yes | job, organization, or project. |
| entityId | UUID | Yes | Entity to inspect. |
Example request
curl "https://api.runnit.io/api/v1/assets/by-type?entityType=project&entityId=<project-id>" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "assetsByType": { "markdown": [ { "id": "5595c819-...", "fileName": "style-guide" } ], "image": [ { "id": "8a41d2b0-...", "fileName": "hero.png" } ] }}Errors
403if you do not have access to the entity.
GET /api/v1/assets/download
Section titled “GET /api/v1/assets/download”Download a ZIP of current files you can read. Supply exactly one scope:
| Query | Required |
|---|---|
collectionId | A collection UUID, or use the entity pair below |
entityType and entityId | job, project or organization and its UUID, when collectionId is absent |
creator | Optional agent type, or manual for files without an agent creator |
ownerOrganizationId | Optional authorised collection owner for an organisation entity, to match an owner-filtered file list |
Mixed or incomplete scopes and malformed identifiers return400. Archived,
deleted and inaccessible private files are omitted. No readable files or a
missing collection returns404; an inaccessible organisation returns403.
The response is application/zip. Folder paths are not recreated, so identical
file names from different folders can collide. Filenames and bytes reflect current saved
content. Failed individual storage reads may omit entries from the archive.
Through call_public_api, pass these fields in query. The ZIP is returned as
bodyBase64 with encoding: "base64" and byteLength. Decode it to save the ZIP.
See Bulk download for the browser workflow.
GET /api/v1/assets/folders
Section titled “GET /api/v1/assets/folders”List folders, either for a collection or for an entity that owns a folder tree.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| collectionId | UUID | One of the two filters | Lists the collection’s folders. |
| ownerEntityType | string | With ownerEntityId | organization, project, or job. |
| ownerEntityId | UUID | With ownerEntityType | Lists the entity’s folders. |
Example request
curl "https://api.runnit.io/api/v1/assets/folders?ownerEntityType=organization&ownerEntityId=<org-id>" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "folders": [ { "id": "c1b2a3d4-...", "name": "Logos", "slug": "logos", "path": "/logos", "parentFolderId": null, "sortOrder": 0 } ]}Errors
400if you supply neithercollectionIdnor the owner pair.403if you do not have access to the collection or entity.404if the collection does not exist.
POST /api/v1/assets/folders
Section titled “POST /api/v1/assets/folders”Create a folder, optionally nested under a parent folder.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Folder name. |
| collectionId | UUID | One of the two homes | Creates the folder in the collection’s organisation tree. |
| ownerEntityType | string | With ownerEntityId | organization, project, or job. |
| ownerEntityId | UUID | With ownerEntityType | Entity that owns the folder tree. |
| parentFolderId | UUID | No | Parent folder for nesting. |
Example request
curl -X POST https://api.runnit.io/api/v1/assets/folders \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "ownerEntityType": "project", "ownerEntityId": "<project-id>", "name": "Approved" }'Response
Returns 201 with the new folder:
{ "success": true, "folder": { "id": "c1b2a3d4-...", "name": "Approved", "path": "/approved" } }Errors
400ifnameis missing, or neithercollectionIdnor the owner pair is given.403if you do not have access to the collection or entity.
DELETE /api/v1/assets/folders/:folderId
Section titled “DELETE /api/v1/assets/folders/:folderId”Delete a folder. By default the assets inside are kept (they just lose the
folder link); pass deleteAssets=true to delete them too.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| deleteAssets | boolean | No | true also deletes every asset in the folder. |
Example request
curl -X DELETE "https://api.runnit.io/api/v1/assets/folders/<folder-id>?deleteAssets=true" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true }Errors
403if you do not have access to the folder’s owner.404if the folder does not exist.
GET /api/v1/assets/search
Section titled “GET /api/v1/assets/search”Semantic search over asset content. Runnit indexes every asset’s content and description, so you can search with natural language rather than exact file names. Any organisation member can call it; results are limited to assets you can access.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Natural-language search text. |
| entityType | string | No | Limit results to assets linked to one entity type: job, organization, or project. Use with entityId. |
| entityId | string (UUID) | No | The entity to limit results to. Use with entityType. |
| organizationId | string (UUID) | No | The organisation to search in. Defaults to your key’s organisation. |
| tagIds | string | No | Comma-separated tag ids; only assets with those tags are returned. |
| folderId | string (UUID) | No | Only assets in this folder. |
| limit | integer | No | Maximum results, 1 to 50. Default 20. |
Example request
curl "https://api.runnit.io/api/v1/assets/search?q=brand%20tone%20of%20voice&limit=10" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "results": [ { "assetId": "5595c819-...", "fileName": "style-guide", "score": 0.87 } ]}Each result includes the matching asset’s identifying fields and a relevance
score. Fetch the full content with GET /api/v1/assets/:assetId.
Errors
400ifqis missing or empty.403if you ask for an entity you cannot access.
GET /api/v1/assets/:assetId
Section titled “GET /api/v1/assets/:assetId”Get one asset with its full content. The response format depends on the file:
- Text formats return JSON with
content. - Images, PDFs, Office documents, and other binary formats return the raw file bytes with the file’s content type.
- Remote-URL assets return JSON with the URL in
content. format=jsonforces a JSON response for any file (binary content comes back base64-encoded).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| assetId | UUID | Yes | Asset ID. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| format | string | No | json forces a JSON response even for binary files. |
| includeLinks | boolean | No | true includes the asset’s entityLinks in JSON responses. |
| download | string | No | 1 returns the file as an attachment with its original name, as raw bytes for any file type. |
Example request
curl "https://api.runnit.io/api/v1/assets/<asset-id>?format=json&includeLinks=true" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "asset": { "id": "5595c819-...", "fileName": "style-guide", "content": "# Style guide...", "version": 2, "entityLinks": [] } }Errors
403if your organisation does not own the asset, or if the asset isprivateand you are not allowed to see it (see Private files).404if the asset does not exist.
POST /api/v1/assets/:assetId/signed-url
Section titled “POST /api/v1/assets/:assetId/signed-url”Create a short-lived link to a file’s bytes that needs no credential. Use it when something outside Runnit has to fetch the file itself, for example a document viewer. The link expires five minutes after it is created and works for that one file only.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| assetId | UUID | Yes | Asset ID. |
Example request
curl -X POST "https://api.runnit.io/api/v1/assets/<asset-id>/signed-url" \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "url": "/api/assets/signed/<asset-id>?exp=1788000000&sig=<signature>", "expiresAt": "2026-09-02T04:05:00.000Z"}url is relative to the API host. Fetching it returns the file with the
headers described in How stored files are served.
Add download=1 to receive it as an attachment. An expired or altered link
answers 404.
Errors
400if the asset is a remote link rather than a stored file.403if your organisation does not own the asset, or if the asset isprivateand you are not allowed to see it.404if the asset does not exist.
PATCH /api/v1/assets/:assetId
Section titled “PATCH /api/v1/assets/:assetId”Update an asset’s content or details. Changing content (or fileName)
snapshots the previous state and creates a new version, so earlier versions
stay available for restore.
The current file and retained versions can have different storage locations. Updating content does not overwrite the content of a saved version. Retained versions also count in the Storage report.
Request body (all optional)
| Name | Type | Required | Description |
|---|---|---|---|
| content | string | No | New file content. Text formats reject unfilled scaffolding markers. |
| fileName | string | No | New file name. |
| description | string | No | New description. |
| metadata | object | No | Replaces the stored metadata. |
| changelog | string | No | Message recorded on the new version. |
| updatedByAgentType | string | No | Label recording which automation made the change. |
The connected user is recorded as the author. A supplied updatedByUserId
cannot change that attribution.
Changing content, file name, description, or metadata also queues reindexing,
so searchStatus returns to pending until the new content is indexed.
Example request
curl -X PATCH https://api.runnit.io/api/v1/assets/<asset-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "content": "# Style guide v2\n\nUse sentence case everywhere.", "changelog": "Second pass" }'Response
{ "success": true, "asset": { "id": "5595c819-...", "version": 2, "content": "# Style guide v2..." } }Errors
400with errorplaceholder_contentif the new text content is an unfilled scaffolding marker.403if your organisation does not own the asset, or if the asset isprivateand you are not allowed to change it.404if the asset does not exist.413with codeSTORAGE_QUOTA_EXCEEDEDif the new content would take the organisation over its storage limit.413with codeKNOWLEDGE_CONTENT_CAPACITY_EXCEEDEDif retained file history has reached its limit. The file change is not saved. Contact support.413with codeKNOWLEDGE_CONTENT_TOO_LARGEif the stored content is too large. Reduce the content before retrying.
DELETE /api/v1/assets/:assetId
Section titled “DELETE /api/v1/assets/:assetId”Delete an asset.
Example request
curl -X DELETE https://api.runnit.io/api/v1/assets/<asset-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "message": "Asset deleted successfully" }Errors
403if your organisation does not own the asset.404if the asset does not exist.
POST /api/v1/assets/:assetId/links
Section titled “POST /api/v1/assets/:assetId/links”Make an existing asset appear on another entity.
An asset belongs to one collection, and a collection is what links to a job, project, or organisation. Linking therefore places a copy of the asset in that entity’s collection (creating the project’s collection if it does not have one yet) and leaves the original where it is. The response tells you the new asset id, which is the one that appears on the entity.
The call is idempotent. If the file is already visible on the entity, nothing
is written and the response says already_linked.
This includes an image Ru generated in a conversation linked to the project.
Use the returned asset ID; a separate project copy is unnecessary. When a copy
is needed, it keeps the source’s AI creator attribution. The same behaviour is
available through MCP link_asset and call_public_api.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| entityType | string | Yes | job, organization, or project. |
| entityId | UUID | Yes | Entity to link. |
| requestId | string | No | Free-form reference echoed back on the response. |
Example request
curl -X POST https://api.runnit.io/api/v1/assets/<asset-id>/links \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "entityType": "project", "entityId": "<project-id>" }'Response
Returns 201 when a copy was placed, or 200 when the file was already there:
{ "success": true, "status": "copied", "link": { "assetId": "8f035e2c-...", "entityType": "project", "entityId": "1f0e9a52-...", "collectionId": "d5df4edc-...", "collectionName": "Project - Winter Campaign", "copiedFromAssetId": "5595c819-..." }}Errors
403if your organisation does not own the asset, if you do not have access to the target entity, or if you lackASSET_UPLOADon the destination collection. A target entity that does not exist also returns403, not404: the access check runs first and does not distinguish “not yours” from “not there”, so the endpoint cannot be used to probe for ids.404if the asset does not exist.
DELETE /api/v1/assets/:assetId/links
Section titled “DELETE /api/v1/assets/:assetId/links”Not supported. Returns 400.
Because an asset is attached to an entity by the collection it lives in, and lives in exactly one collection, there is no separate link to remove without removing the file.
- To take the file off the entity, delete that copy with
DELETE /api/v1/assets/:assetId. - To move it somewhere else, create it in the new collection first
(
POST /api/v1/assetswithcollectionId), then delete this copy.
Response
{ "success": false, "unsupported": true, "error": "Assets are attached to a job, project, or organisation by the collection they live in, so there is no separate link to remove. ..."}POST /api/v1/assets/:assetId/tags
Section titled “POST /api/v1/assets/:assetId/tags”Apply existing tags to an asset. The first five supplied tags become the asset’s primary tags.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| tagIds | array of UUIDs | Yes | At least one tag ID. |
Example request
curl -X POST https://api.runnit.io/api/v1/assets/<asset-id>/tags \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "tagIds": ["<tag-id>"] }'Response
{ "success": true, "tags": [ { "id": "7d31e0aa-...", "name": "Brand", "slug": "brand", "isSystem": false } ] }Errors
400iftagIdsis missing, empty, or contains a non-UUID value.403if your organisation does not own the asset.404if the asset does not exist.
DELETE /api/v1/assets/:assetId/tags/:tagId
Section titled “DELETE /api/v1/assets/:assetId/tags/:tagId”Remove one tag from an asset.
Example request
curl -X DELETE https://api.runnit.io/api/v1/assets/<asset-id>/tags/<tag-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true }Errors
403if your organisation does not own the asset.404if the asset does not exist.
POST /api/v1/assets/:assetId/folders
Section titled “POST /api/v1/assets/:assetId/folders”Place an asset in one or more folders. Use replaceExisting to move it
rather than add it.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| folderIds | array of UUIDs | Yes | At least one folder ID. |
| primaryFolderId | UUID | No | Which of folderIds is the primary folder. |
| replaceExisting | boolean | No | true removes the asset from all current folders first (a move). |
Example request
curl -X POST https://api.runnit.io/api/v1/assets/<asset-id>/folders \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "folderIds": ["<folder-id>"], "replaceExisting": true }'Response
{ "success": true, "folders": [ { "id": "c1b2a3d4-...", "name": "Approved", "path": "/approved" } ] }Errors
400iffolderIdsis missing or empty.403if your organisation does not own the asset.404if the asset does not exist.
POST /api/v1/assets/:assetId/reindex
Section titled “POST /api/v1/assets/:assetId/reindex”Queue an asset for content reindexing. Use this if search results or the summary excerpt look stale.
Example request
curl -X POST https://api.runnit.io/api/v1/assets/<asset-id>/reindex \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "message": "Asset reindex scheduled" }searchStatus moves to pending and returns to ready once indexing
completes.
Errors
403if your organisation does not own the asset.404if the asset does not exist.
GET /api/v1/assets/:assetId/insights
Section titled “GET /api/v1/assets/:assetId/insights”Get an asset’s derived information: AI summary excerpt, indexing status, tags, and folders.
Example request
curl https://api.runnit.io/api/v1/assets/<asset-id>/insights \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "insights": { "summaryExcerpt": "Tone of voice rules for all campaign copy.", "searchStatus": "ready", "lastIndexedAt": "2026-07-08T12:31:58.597Z", "tags": [ { "id": "7d31e0aa-...", "name": "Brand" } ], "folders": [ { "id": "c1b2a3d4-...", "name": "Approved" } ] }}Errors
403if your organisation does not own the asset.404if the asset does not exist.
GET /api/v1/assets/:assetId/versions
Section titled “GET /api/v1/assets/:assetId/versions”List an asset’s version history, newest first (up to 50 versions). Each version includes its full content, so you can inspect a version before restoring it.
Example request
curl https://api.runnit.io/api/v1/assets/<asset-id>/versions \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "versions": [ { "id": "367be2ca-...", "assetId": "5595c819-...", "versionNumber": 2, "fileName": "style-guide", "fileSize": 47, "content": "# Style guide v2...", "changelog": "Second pass", "createdByUserId": "213ae491-...", "createdAt": "2026-07-08T02:32:22.652Z" } ]}Errors
403if your organisation does not own the asset.404if the asset does not exist.
POST /api/v1/assets/:assetId/versions/:versionId/restore
Section titled “POST /api/v1/assets/:assetId/versions/:versionId/restore”Restore an asset to an earlier version. The asset’s content and name return to that snapshot.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| assetId | UUID | Yes | Asset ID. |
| versionId | UUID | Yes | Version to restore, from the versions list. |
Example request
curl -X POST https://api.runnit.io/api/v1/assets/<asset-id>/versions/<version-id>/restore \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "asset": { "id": "5595c819-...", "content": "# Style guide..." } }Errors
403if your organisation does not own the asset.404if the asset does not exist.
How stored files are served
Section titled “How stored files are served”Every response that returns a file’s bytes (the file routes above, the
asset-ID route for binary files, download=1, and signed links) carries the
same headers:
Cache-Control: private, no-storeandX-Content-Type-Options: nosniff.Content-Disposition: inlineunless you asked fordownload=1, with the original file name (non-ASCII names are encoded asfilename*).- HTML, SVG and XML files keep their content type but are sent with a
sandboxing
Content-Security-Policy. They render and their scripts run, but in an isolated context that cannot read Runnit cookies, storage, or call the API as the signed-in user. If you embed such a file in your own page, expect that isolation.
Credentials are never accepted in the URL. Use the Authorization header, or
a signed link when a third party has to
fetch the file.
Private files
Section titled “Private files”An asset with visibilityScope set to private can be read, downloaded,
changed or deleted only by:
- the person who uploaded it;
- the owner of a chat conversation the file is attached to; or
- an owner or admin of the organisation.
You also need permission to view assets. An explicit denial of that
permission applies even if you uploaded the file. Finance documents require
their Finance permissions. Anyone else, including organisation API keys,
receives 403; ZIP downloads omit files you cannot read.
An ordinary user-generated MCP credential can use read_file or
read_document for the same authorised private-file read. Private files
are included in listings and search only when the connected user can read them.
Internal sessions limited to a job, client or tool set continue to reject private-file
reads, except for a receipt explicitly bound to the active receipt-reading task.
Generated credentials stay bound to their organisation. Membership in another workspace does not let that credential read files or change folders there without an authorised client relationship. See MCP file tools.
Expense receipts
Section titled “Expense receipts”Upload a receipt with metadata: { "source": "expense_receipt" } to make it private
before you attach it to a claim. Attaching any file to an expense also makes it
private. This restriction cannot be removed by changing metadata or visibility.
An attachment must be a readable upload in the expense’s organisation; missing,
foreign, inaccessible, remote-link and Finance document sources are rejected.
Receipt reads, file history, insights, downloads, listings and search use the expense’s current submitter or reviewer permissions. Unattached and discarded receipt uploads remain private to their uploader with expense access. A copied file ID does not grant access. Organisation API keys cannot read receipts.
Copying a receipt to another collection and minting a public viewer link return
409, even for an authorised reader. Previously minted application viewer links
return 404. Use an authenticated download. These rules also apply through
call_public_api; named file readers and discovery use the same source access.
See Expenses.
Common workflows
Section titled “Common workflows”An end-to-end file lifecycle. Replace the placeholders with real IDs.
- Create a collection to hold the files:
curl -X POST https://api.runnit.io/api/v1/collections \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "ownerOrganizationId": "<org-id>", "name": "Winter Campaign Assets", "collectionType": "creative" }'- Upload a file into it:
curl -X POST https://api.runnit.io/api/v1/assets \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "fileName": "campaign-brief", "fileExtension": "md", "content": "# Winter campaign brief\n\nGoals, audience, and deliverables.", "collectionId": "<collection-id>" }'- Update the file, which creates version 2 automatically:
curl -X PATCH https://api.runnit.io/api/v1/assets/<asset-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "content": "# Winter campaign brief v2\n\nUpdated goals.", "changelog": "Scope revision" }'- Find files by listing the collection, or search asset content with natural language:
curl "https://api.runnit.io/api/v1/assets?collectionId=<collection-id>" \ -H 'Authorization: Bearer rnk_your_key'
curl "https://api.runnit.io/api/v1/assets/search?q=winter%20campaign%20goals" \ -H 'Authorization: Bearer rnk_your_key'AI agents can run the same search through the
MCP server with the same rnk_ key.
- Download the content. A single file:
curl "https://api.runnit.io/api/v1/assets/<asset-id>" \ -H 'Authorization: Bearer rnk_your_key' -o campaign-brief.mdOr everything linked to a project as a zip:
curl -L -o project-assets.zip \ "https://api.runnit.io/api/v1/assets/download?entityType=project&entityId=<project-id>" \ -H 'Authorization: Bearer rnk_your_key'Next steps
Section titled “Next steps”Organise files with the Asset Collections API, or report on the work they support with the Reports API.
Upload a new receipt when the source is an existing cloud file. Relabelling a cloud file as a receipt returns 409; attaching a generic cloud file to an expense returns 404. This prevents a previously public file from being treated as private while its original address still works.
SVG content
Section titled “SVG content”For fileExtension: "svg", send content as raw XML or base64-encoded file bytes.
An image/svg+xml base64 data URL is also accepted. JSON reads with format=json
return isBinary: false for raw XML or isBinary: true for base64 content.
fileSize is the decoded byte count. Updates may switch between these forms.
Use GET /api/v1/assets/:assetId?download=true for the original file bytes.
Previews, ZIP entries, copies and restored versions preserve those bytes,
including older uploads. Use the returned URL for stored SVGs; do not construct
an object-store URL. The SVG reader requires your current file access.
Through call_public_api, UTF-8 SVG downloads return text.
SVG bytes that cannot round-trip through UTF-8 return bodyBase64, encoding: "base64" and
byteLength. Decode that base64 to recover the original bytes. Named file reads
are described in MCP tools.
Supplier invoice originals
Section titled “Supplier invoice originals”A supplier-uploaded original requires cost access to every linked purchase order and bill, or organisation cost access when unattached. Asset-view access and an organisation role alone do not override this. File reads, versions, cached text, listings, search and authorised writes retain current source checks. Renaming or changing metadata does not remove the restriction. Cloud replacements remain private, and downloads use the authenticated asset route.
Public viewer links and collection copies return 409 for authorised readers;
previously minted viewer links return 404. Protected originals cannot become
component sources or image transformations. Bulk indexing and OCR refuse before
changing files when the requester lacks source access. A source whose upload
binding is missing remains unreadable. Generated REST credentials and MCP share
these rules. See Supplier Portal.