---
title: External Links
path: api-guides/external-links
status: published
---

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/`

## Link types

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.

## Creating a link

```bash
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
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
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
{
  "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.

## All link options

| 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"]`) |

## Using a link — recipients

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:

### Get link metadata

```bash
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.

### Access the link

```bash
curl -X POST "$SCAIDRIVE_URL/api/v1/external/access/opaque-share-token" \
  -H "Content-Type: application/json" \
  -d '{"password": "hunter2"}'
```

```python
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.

## Managing links

### List

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

### Get details

```bash
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
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
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
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
{
  "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
{
  "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
{
  "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

- [Sharing](/docs/scaidrive/api-guides/sharing) — internal shares and members.
- [External Links Reference](/docs/scaidrive/reference/external-links) — all endpoints.
- [Enterprise Compliance](/docs/scaidrive/advanced/enterprise-compliance) — DLP on outbound links.