GenuineAIGenuineAI
For usersFor developers
  • Overview
  • Guides
  • How-to
  • API reference
Documentation
  • Quickstart
  • Authentication
  • Errors
  • Rate limits
  • API reference
Platform
  • Platform overview
  • Solutions
  • Pricing
  • Sign in
Company
  • Status
  • Trust Center
  • Contact
  • Terms of Service
  • Privacy Policy

© 2026 GenuineAI Ventures LLC

support@genuinehq.com
Media library
Uploading filesOrganizing and finding assetsDelivering assetsPublishing assets
AI
AI enrichmentPeople and facesAgents and conversations
Knowledge
Knowledge basesHow agents use knowledge
Automation
Autonomous AIExternal dataExtensions
Creating and deploying
Generating contentDeployed AI
Operations
Usage and credits
How-to

Delivering assets

Getting bytes out follows the same rule as getting them in: they do not travel through the API. You request a signed URL and fetch from storage.

Download one file

Code
GET /files/{id}/download-url

Answers with a signed, expiring URL. The optional size parameter (small, medium or large) requests a rendition instead of the original, which is what you want for anything being displayed rather than downloaded. It applies to images; other file types return the original whatever you request.

The response body is the URL as a bare JSON string, "https://…", not an object wrapping it. Parse it as a string.

Fetch the signed URL without your API credentials, because the signature is the authorization. These URLs last 15 minutes and should not be stored. Request a fresh one when you need the bytes: a URL cached in your database is a link that stops working and, until it does, a credential you did not intend to keep.

For a specific version, GET /files/{id}/versions/{version_id}/download-url does the same for one point in the file's history.

When you cannot fetch cross-origin

GET /files/{id}/image-data streams the bytes through the API instead. It exists for callers that cannot fetch cross-origin, most often a browser canvas that would be tainted by the request. Request a rendition rather than the original where one exists, since this path does carry the bytes through the API.

Download many files

Two endpoints, differing in how long the work takes.

POST /files/actions/download packs a small selection and streams the ZIP back in the response. It suits a handful of files.

POST /download-jobs hands the work to a background worker and returns a job to poll. Use it for anything substantial, up to 5,000 files per job:

Then poll GET /download-jobs/{id}:

Code
{ "job_id": "7b444c0d-…", "status": "completed", "progress": 100, "file_count": 412, "size": "original", "created_at": "2026-08-10T09:12:44Z", "download_url": "https://…", "expires_at": "2026-08-11T09:41:02Z" }

download_url appears only once status is completed. A failed job carries error instead. DELETE /download-jobs/{id} cancels one still running.

A job belongs to whoever created it: the person who started it is the person who can poll it.

Files you cannot read are dropped from the selection rather than failing the call, so file_count on the job may be lower than the list you sent. Compare the two if the count matters.

Share links

A share link publishes to someone with no account and no key. There are two kinds, chosen with share_type.

Download links

TerminalCode
curl -X POST https://api.genuineai.app/api/v1/share-links \ -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \ -H "Content-Type: application/json" \ -d '{ "share_type": "download", "name": "Q3 press kit", "file_ids": ["8a1b6e5c-…"], "expires_at": "2026-09-01T00:00:00Z", "password": "…" }'

Up to 500 files per link. The response includes kiosk_url, which is the address to send the recipient:

Code
{ "share_id": "…", "share_token": "…", "share_type": "download", "share_file_count": 12, "share_expires_at": "2026-09-01T00:00:00Z", "share_password_set": true, "share_view_count": 0, "share_download_count": 0, "kiosk_url": "https://share.genuineai.app/public/share/<token>" }

Two defaults are worth knowing: links expire after 7 days unless you pass expires_at, and a password is optional. Omit it and anyone with the URL can open the link. share_password_set reports which applies, and the password itself is never returned.

share_view_count and share_download_count are maintained for you, so a link doubles as a lightweight measure of whether what you sent was opened.

Upload links

share_type: "upload" runs the other way: a link that lets someone outside send files in without an account. It uses the same three-step upload described in uploading files, performed against the public surface with a token instead of a key.

TerminalCode
curl -X POST https://api.genuineai.app/api/v1/share-links \ -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \ -H "Content-Type: application/json" \ -d '{ "share_type": "upload", "name": "Event photographer drop", "album_ids": ["3f2a9c1e-…"], "pre_tags": {"campaign": ["summer-2026"]}, "max_files": 200, "max_size": 5368709120, "allow_change_tags": true }'
Option
album_idsAlbums the arriving files land in.
pre_tagsMedia tags applied on arrival, so contributions are filed correctly without the sender having to do anything.
max_files, max_sizeCaps on the drop.
allow_change_tags, allow_change_albumsWhether the sender may change any of the above, or just accept them.

Files arriving this way enter the library like any other, with the same statuses, the same processing and the same AI analysis.

The public endpoints

The recipient's browser talks to the unauthenticated /public surface: GET /public/share/{token} opens a link, POST /public/share/{token}/verify checks a password, and the download endpoints mirror the ones above. Upload links have the matching set under /public/upload/{token}.

You need these only if you are building your own recipient experience instead of sending people to kiosk_url. They are rate-limited more tightly than the rest of the API. See rate limits.

Manage links

GET /share-links lists your own links. PATCH /share-links/{id} changes expiry, password or contents, and DELETE /share-links/{id} revokes one immediately. Revoke a link that has been forwarded somewhere it should not go: a URL cannot be recalled, but it can be made to stop answering.

Next steps

  • Uploading files is the ingest side of the same flow.
  • Organizing and finding assets is how you select what to deliver.
  • Sharing and access explains how ACLs decide who sees what.
Last modified on October 8, 2026
Organizing and finding assetsPublishing assets
On this page
  • Download one file
    • When you cannot fetch cross-origin
  • Download many files
  • Share links
    • Download links
    • Upload links
    • The public endpoints
    • Manage links
  • Next steps
curl "https://api.genuineai.app/api/v1/files/<file-id>/download-url?size=medium" \ -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>"
curl -X POST https://api.genuineai.app/api/v1/download-jobs \ -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>" \ -H "Content-Type: application/json" \ -d '{"file_ids": ["8a1b6e5c-…", "3f2a9c1e-…"], "size": "original"}'
JSON
async function awaitZip(jobId) { for (;;) { const job = await fetch( `https://api.genuineai.app/api/v1/download-jobs/${jobId}`, { headers }, ).then(r => r.json()); if (job.status === "completed") return job.download_url; if (job.status === "failed") throw new Error(job.error); await new Promise(r => setTimeout(r, 3000)); } }
JSON