Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

External Links

Share content with people who don't have a ScaiDrive account. External links are public URLs with optional password, expiry, IP allow-list, email allow-list, download cap, and access notifications.

Base path: /api/v1/external/

Three link modes:

Type Who can do what
VIEW Preview only. No content download
DOWNLOAD View + download the file/folder
UPLOAD Drop-zone: recipients upload to a folder, nothing else

DOWNLOAD and VIEW target a file, folder, or entire share. UPLOAD targets a folder — recipients place new files into it.

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl -X POST $SCAIDRIVE_URL/api/v1/external/links \
  -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_type": "file",
    "resource_id": "fil_01J3K",
    "share_id": "shr_01J3H",
    "link_type": "DOWNLOAD",
    "password": "hunter2",
    "expires_in_days": 7,
    "max_downloads": 50,
    "custom_name": "Quarterly Report (draft)"
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
resp = httpx.post(
    f"{url}/api/v1/external/links",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "resource_type": "file",
        "resource_id": "fil_01J3K",
        "share_id": "shr_01J3H",
        "link_type": "DOWNLOAD",
        "password": "hunter2",
        "expires_in_days": 7,
        "max_downloads": 50,
    },
)
link = resp.json()
print(link["url"])
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const resp = await fetch(`${url}/api/v1/external/links`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    resource_type: "file",
    resource_id: "fil_01J3K",
    share_id: "shr_01J3H",
    link_type: "DOWNLOAD",
    password: "hunter2",
    expires_in_days: 7,
    max_downloads: 50,
  }),
});
const link = await resp.json();

Response:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "id": "lnk_01J5U",
  "token": "opaque-share-token",
  "short_code": "abc123",
  "url": "https://scaidrive.scailabs.ai/share/opaque-share-token",
  "short_url": "https://scaidrive.scailabs.ai/s/abc123",
  "password_required": true,
  "expires_at": "2026-04-30T10:15:00Z",
  "stats": {"view_count": 0, "download_count": 0}
}

Creating a link requires SHARE permission on the resource.

Field Type Effect
resource_type (required) file, folder, share What's being shared
resource_id (required) string ID of the resource
share_id (required) string Share containing the resource
link_type (required) VIEW, DOWNLOAD, UPLOAD Access mode
password string Recipients must supply this to access
expires_in_days integer Convenience: expires N days from creation
expires_at ISO timestamp Explicit expiry time
max_downloads integer Stop working after N downloads
max_views integer Stop working after N views
allowed_ips array of CIDR strings Deny if caller IP isn't in any range
allowed_emails array of strings Require an email match (with require_email: true)
require_email boolean Prompt for email before access
custom_name string Display name in share UI instead of the filename
custom_message string Message shown to recipients
show_download_button boolean For VIEW links, whether to show a download-nag UI
allow_preview boolean For DOWNLOAD links, whether the preview is available
notify_on_access boolean Email the owner on first access
notify_on_download boolean Email the owner on each download
notify_on_upload boolean Email the owner on each upload (UPLOAD links)
notification_email string Override the default notification recipient
upload_folder_id string For UPLOAD links: destination folder (required)
max_file_size integer For UPLOAD links: max bytes per upload
allowed_extensions array of strings For UPLOAD links: allowed file extensions (["pdf", "docx"])

Recipients hit GET /s/{token} in a browser. The web frontend then resolves the token to a resource and serves the appropriate preview, download, or upload UI. For programmatic access:

bash
1
curl "$SCAIDRIVE_URL/api/v1/external/access/opaque-share-token/info"

Returns information about the link (resource name, link type, whether a password is required, whether email verification is required). No authentication; no password needed for the info endpoint itself.

bash
1
2
3
curl -X POST "$SCAIDRIVE_URL/api/v1/external/access/opaque-share-token" \
  -H "Content-Type: application/json" \
  -d '{"password": "hunter2"}'
python
1
2
3
4
5
resp = httpx.post(
    f"{url}/api/v1/external/access/{token}",
    json={"password": "hunter2"},
)
session = resp.json()

Returns a LinkAccessResponse carrying link_type, resource_name, preview/download permissions, and (for require_email links that pass verification) a session_token. The web frontend uses this to gate the next step — for DOWNLOAD and VIEW links, it streams the file content; for UPLOAD links, it presents the drop-zone UI.

Download and upload#

Downloads and uploads through external links happen through the web frontend at /s/{token} after the access step above, not through direct API paths. If you need to script downloads for an external link from a server-side process, the cleanest path is to authenticate as the link creator and call GET /api/v1/files/{file_id}/content directly — same content, no link plumbing.

For uploads through UPLOAD links, files land in the upload_folder_id specified at link creation, owned by the link creator.

List#

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
     "$SCAIDRIVE_URL/api/v1/external/links?share_id=shr_01J3H"

Get details#

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
     $SCAIDRIVE_URL/api/v1/external/links/lnk_01J5U

Response includes stats (views, downloads) and recent access_log entries.

Update#

bash
1
2
3
4
curl -X PATCH $SCAIDRIVE_URL/api/v1/external/links/lnk_01J5U \
  -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expires_at": "2026-05-15T00:00:00Z"}'

Every option except resource_id and link_type can be changed after creation. Passing "password": null removes password protection.

Revoke#

bash
1
2
curl -X DELETE $SCAIDRIVE_URL/api/v1/external/links/lnk_01J5U \
  -H "Authorization: Bearer $SCAIDRIVE_TOKEN"

Revoked links fail immediately with EXTERNAL_LINK_REVOKED. Revocation is permanent — you can't re-enable.

Access log#

Every access to a link creates a GuestSession record: IP address, user agent, access time, email if required. Read the log:

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
     $SCAIDRIVE_URL/api/v1/external/links/lnk_01J5U/sessions

Useful for "who accessed this link?" audits and compliance.

Common recipes#

Time-limited download with notification#

json
1
2
3
4
5
6
7
8
9
{
  "resource_type": "file",
  "resource_id": "fil_01J3K",
  "share_id": "shr_01J3H",
  "link_type": "DOWNLOAD",
  "expires_in_days": 3,
  "max_downloads": 1,
  "notify_on_download": true
}

One-download, three-day link with email confirmation.

IP-restricted preview#

json
1
2
3
4
5
6
7
8
{
  "resource_type": "folder",
  "resource_id": "fld_01J3I",
  "share_id": "shr_01J3H",
  "link_type": "VIEW",
  "allowed_ips": ["198.51.100.0/24", "203.0.113.42/32"],
  "expires_in_days": 30
}

Partner office can preview; nobody else can even see the folder exists.

External upload drop-zone#

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "resource_type": "folder",
  "resource_id": "fld_01J3I",
  "share_id": "shr_01J3H",
  "link_type": "UPLOAD",
  "upload_folder_id": "fld_01J3I",
  "max_file_size": 104857600,
  "allowed_extensions": ["pdf", "docx", "xlsx"],
  "require_email": true,
  "notify_on_upload": true
}

Vendors upload contracts; you get an email on each upload, file-size capped at 100 MB, doc types only.

Error cases#

Code When
EXTERNAL_LINK_NOT_FOUND Token doesn't match
EXTERNAL_LINK_EXPIRED Past expires_at or disabled
EXTERNAL_LINK_REVOKED Explicitly revoked
EXTERNAL_LINK_PASSWORD_REQUIRED Password required but not supplied
EXTERNAL_LINK_PASSWORD_INCORRECT Password mismatch
EXTERNAL_LINK_IP_DENIED Caller IP outside allowed CIDRs
EXTERNAL_LINK_EMAIL_DENIED Caller email not in allow-list
EXTERNAL_LINK_MAX_DOWNLOADS Download cap reached
EXTERNAL_LINK_MAX_VIEWS View cap reached

What's next#

Updated 2026-05-18 15:04:14 View source (.md) rev 2