Skip to content

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.

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.

{
"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://..."
}
  • fileType is derived from the extension: markdown, html, text, image, video, audio, json, csv, pdf, or other.
  • searchStatus is pending, processing, ready, or failed and tracks content indexing for search and insights.
  • content is included when the endpoint returns file content (create, update, and single-asset reads). An empty text document returns content: "" and fileSize: 0; an omitted content field means content was not included. Collection lists omit inline content. For binary files it is base64.
  • storageUrl and url appear when the file is held in cloud storage.
  • Treat storageKey and 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.
  • entityLinks appears when you ask for links, and lists the entities the asset’s collection belongs to.

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. Use POST /api/v1/assets/:assetId/links for that.

Request body

NameTypeRequiredDescription
fileNamestringYesFile name.
fileExtensionstringYesExtension without meaning-changing tricks, e.g. md, png, pdf. Determines the file type and whether content is treated as binary.
contentstringYesFile content. Plain text for text formats, base64 for binary formats, or a URL when isRemoteUrl is true.
collectionIdUUIDOne of the two homesCollection to store the asset in.
entityLinksarrayOne of the two homesObjects with entityType (job, organization, or project) and entityId.
descriptionstringNoDescription stored on the asset.
metadataobjectNoFree-form metadata.
createdByAgentTypestringNoLabel recording which automation produced the file.
isRemoteUrlbooleanNotrue when content is a URL to an externally hosted file. Defaults to false.
owningOrganizationIdUUIDNoOwning organisation. Normally derived from the collection or links.
visibilityScopestringNoorganization, client, public, or private.
sourceTypestringNoWhere the file came from, e.g. upload.
externalSourcestringNoName of an external system of record.
externalIdstringNoID in that external system.
isArchivedbooleanNoCreate the asset already archived.
tagIdsarray of UUIDsNoTags to apply immediately.
folderIdsarray of UUIDsNoFolders to place the asset in.
primaryFolderIdUUIDNoWhich 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

Terminal window
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

  • 400 if a required field is missing or invalid, if neither collectionId nor entityLinks is given, or if the referenced collection does not exist.
  • 400 with error placeholder_content if text content is an unfilled scaffolding marker.
  • 403 if you do not have access to the collection, a linked entity, or the owning organisation.
  • 413 with code STORAGE_QUOTA_EXCEEDED if storing the file would take the organisation over its storage limit. Delete unused files or contact support.
  • 413 with code KNOWLEDGE_CONTENT_CAPACITY_EXCEEDED if retained file history has reached its limit. The file change is not saved. Contact support.
  • 413 with code KNOWLEDGE_CONTENT_TOO_LARGE if the stored content is too large. Reduce the content before retrying.

List assets in a collection, or the assets linked to an entity. Provide collectionId, or both entityType and entityId.

Query parameters

NameTypeRequiredDescription
collectionIdUUIDOne of the two filtersLists the collection’s assets.
entityTypestringWith entityIdjob, organization, or project.
entityIdUUIDWith entityTypeLists assets linked to this entity.
ownerOrganizationIdUUIDNoFor 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

Terminal window
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

  • 400 if you supply neither collectionId nor the entityType and entityId pair.
  • 403 if you do not have access to the collection or entity.
  • 404 if the collection does not exist.

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

NameTypeRequiredDescription
entityTypestringYesjob, organization, or project.
entityIdUUIDYesEntity the file is linked to.
fileNamestringYesExact file name.

Example request

Terminal window
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

  • 403 if you do not have access to the entity.
  • 404 if no asset with that name is linked to the entity.

List an entity’s assets grouped by file type.

Query parameters

NameTypeRequiredDescription
entityTypestringYesjob, organization, or project.
entityIdUUIDYesEntity to inspect.

Example request

Terminal window
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

  • 403 if you do not have access to the entity.

Download a ZIP of current files you can read. Supply exactly one scope:

QueryRequired
collectionIdA collection UUID, or use the entity pair below
entityType and entityIdjob, project or organization and its UUID, when collectionId is absent
creatorOptional agent type, or manual for files without an agent creator
ownerOrganizationIdOptional 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.

List folders, either for a collection or for an entity that owns a folder tree.

Query parameters

NameTypeRequiredDescription
collectionIdUUIDOne of the two filtersLists the collection’s folders.
ownerEntityTypestringWith ownerEntityIdorganization, project, or job.
ownerEntityIdUUIDWith ownerEntityTypeLists the entity’s folders.

Example request

Terminal window
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

  • 400 if you supply neither collectionId nor the owner pair.
  • 403 if you do not have access to the collection or entity.
  • 404 if the collection does not exist.

Create a folder, optionally nested under a parent folder.

Request body

NameTypeRequiredDescription
namestringYesFolder name.
collectionIdUUIDOne of the two homesCreates the folder in the collection’s organisation tree.
ownerEntityTypestringWith ownerEntityIdorganization, project, or job.
ownerEntityIdUUIDWith ownerEntityTypeEntity that owns the folder tree.
parentFolderIdUUIDNoParent folder for nesting.

Example request

Terminal window
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

  • 400 if name is missing, or neither collectionId nor the owner pair is given.
  • 403 if you do not have access to the collection or entity.

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

NameTypeRequiredDescription
deleteAssetsbooleanNotrue also deletes every asset in the folder.

Example request

Terminal window
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

  • 403 if you do not have access to the folder’s owner.
  • 404 if the folder does not exist.

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

NameTypeRequiredDescription
qstringYesNatural-language search text.
entityTypestringNoLimit results to assets linked to one entity type: job, organization, or project. Use with entityId.
entityIdstring (UUID)NoThe entity to limit results to. Use with entityType.
organizationIdstring (UUID)NoThe organisation to search in. Defaults to your key’s organisation.
tagIdsstringNoComma-separated tag ids; only assets with those tags are returned.
folderIdstring (UUID)NoOnly assets in this folder.
limitintegerNoMaximum results, 1 to 50. Default 20.

Example request

Terminal window
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

  • 400 if q is missing or empty.
  • 403 if you ask for an entity you cannot access.

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=json forces a JSON response for any file (binary content comes back base64-encoded).

Path parameters

NameTypeRequiredDescription
assetIdUUIDYesAsset ID.

Query parameters

NameTypeRequiredDescription
formatstringNojson forces a JSON response even for binary files.
includeLinksbooleanNotrue includes the asset’s entityLinks in JSON responses.
downloadstringNo1 returns the file as an attachment with its original name, as raw bytes for any file type.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset, or if the asset is private and you are not allowed to see it (see Private files).
  • 404 if the asset does not exist.

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

NameTypeRequiredDescription
assetIdUUIDYesAsset ID.

Example request

Terminal window
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

  • 400 if the asset is a remote link rather than a stored file.
  • 403 if your organisation does not own the asset, or if the asset is private and you are not allowed to see it.
  • 404 if the asset does not exist.

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)

NameTypeRequiredDescription
contentstringNoNew file content. Text formats reject unfilled scaffolding markers.
fileNamestringNoNew file name.
descriptionstringNoNew description.
metadataobjectNoReplaces the stored metadata.
changelogstringNoMessage recorded on the new version.
updatedByAgentTypestringNoLabel 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

Terminal window
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

  • 400 with error placeholder_content if the new text content is an unfilled scaffolding marker.
  • 403 if your organisation does not own the asset, or if the asset is private and you are not allowed to change it.
  • 404 if the asset does not exist.
  • 413 with code STORAGE_QUOTA_EXCEEDED if the new content would take the organisation over its storage limit.
  • 413 with code KNOWLEDGE_CONTENT_CAPACITY_EXCEEDED if retained file history has reached its limit. The file change is not saved. Contact support.
  • 413 with code KNOWLEDGE_CONTENT_TOO_LARGE if the stored content is too large. Reduce the content before retrying.

Delete an asset.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

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

NameTypeRequiredDescription
entityTypestringYesjob, organization, or project.
entityIdUUIDYesEntity to link.
requestIdstringNoFree-form reference echoed back on the response.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset, if you do not have access to the target entity, or if you lack ASSET_UPLOAD on the destination collection. A target entity that does not exist also returns 403, not 404: the access check runs first and does not distinguish “not yours” from “not there”, so the endpoint cannot be used to probe for ids.
  • 404 if the asset does not exist.

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/assets with collectionId), 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. ..."
}

Apply existing tags to an asset. The first five supplied tags become the asset’s primary tags.

Request body

NameTypeRequiredDescription
tagIdsarray of UUIDsYesAt least one tag ID.

Example request

Terminal window
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

  • 400 if tagIds is missing, empty, or contains a non-UUID value.
  • 403 if your organisation does not own the asset.
  • 404 if 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

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

Place an asset in one or more folders. Use replaceExisting to move it rather than add it.

Request body

NameTypeRequiredDescription
folderIdsarray of UUIDsYesAt least one folder ID.
primaryFolderIdUUIDNoWhich of folderIds is the primary folder.
replaceExistingbooleanNotrue removes the asset from all current folders first (a move).

Example request

Terminal window
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

  • 400 if folderIds is missing or empty.
  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

Queue an asset for content reindexing. Use this if search results or the summary excerpt look stale.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

Get an asset’s derived information: AI summary excerpt, indexing status, tags, and folders.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

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

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if 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

NameTypeRequiredDescription
assetIdUUIDYesAsset ID.
versionIdUUIDYesVersion to restore, from the versions list.

Example request

Terminal window
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

  • 403 if your organisation does not own the asset.
  • 404 if the asset does not exist.

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-store and X-Content-Type-Options: nosniff.
  • Content-Disposition: inline unless you asked for download=1, with the original file name (non-ASCII names are encoded as filename*).
  • 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.

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.

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.

An end-to-end file lifecycle. Replace the placeholders with real IDs.

  1. Create a collection to hold the files:
Terminal window
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" }'
  1. Upload a file into it:
Terminal window
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>"
}'
  1. Update the file, which creates version 2 automatically:
Terminal window
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" }'
  1. Find files by listing the collection, or search asset content with natural language:
Terminal window
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.

  1. Download the content. A single file:
Terminal window
curl "https://api.runnit.io/api/v1/assets/<asset-id>" \
-H 'Authorization: Bearer rnk_your_key' -o campaign-brief.md

Or everything linked to a project as a zip:

Terminal window
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'

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.

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.

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.