---
title: External Links Reference
path: reference/external-links
status: published
---

Public sharing via link tokens.

**Base path:** `/api/v1/external/`

## Link object

```json
{
  "id": "lnk_01J5U",
  "tenant_id": "tnt_01J3A",
  "share_id": "shr_01J3H",
  "resource_type": "file",
  "resource_id": "fil_01J3K",
  "link_type": "DOWNLOAD",
  "token": "opaque-share-token",
  "short_code": "abc123",
  "url": "https://scaidrive.scailabs.ai/share/opaque-share-token",
  "short_url": "https://scaidrive.scailabs.ai/s/abc123",
  "status": "active",
  "password_required": true,
  "max_downloads": 50,
  "download_count": 3,
  "max_views": null,
  "view_count": 12,
  "allowed_ips": null,
  "allowed_emails": null,
  "require_email": false,
  "custom_name": "Quarterly Report",
  "custom_message": null,
  "created_by": "usr_01J3N",
  "created_at": "2026-04-20T10:00:00Z",
  "expires_at": "2026-04-30T10:15:00Z"
}
```

`status`: `active`, `expired`, `revoked`, `disabled`.
`link_type`: `VIEW`, `DOWNLOAD`, `UPLOAD`.

## POST /api/v1/external/links

Create link. Returns 201.

**Required permission:** `SHARE` on resource.

### All fields

| Field | Type | Notes |
|-------|------|-------|
| `resource_type` | string, required | `file`, `folder`, `share` |
| `resource_id` | string, required | |
| `share_id` | string, required | |
| `link_type` | string, required | `VIEW`, `DOWNLOAD`, `UPLOAD` |
| `password` | string | Optional; hashed with bcrypt |
| `expires_in_days` | integer | Convenience |
| `expires_at` | ISO timestamp | Explicit |
| `max_downloads` | integer | |
| `max_views` | integer | |
| `allowed_ips` | array of CIDR strings | |
| `allowed_emails` | array of strings | |
| `require_email` | boolean | |
| `custom_name` | string | |
| `custom_message` | string | |
| `show_download_button` | boolean | For VIEW links |
| `allow_preview` | boolean | For DOWNLOAD links |
| `notify_on_access` | boolean | |
| `notify_on_download` | boolean | |
| `notify_on_upload` | boolean | For UPLOAD links |
| `notification_email` | string | Override owner email |
| `upload_folder_id` | string | Required for UPLOAD links |
| `max_file_size` | integer | For UPLOAD links |
| `allowed_extensions` | array of strings | For UPLOAD links |

## GET /api/v1/external/links/{link_id}

Get link details. Authenticated — the link's creator or a tenant admin.

## GET /api/v1/external/links

List links visible to the caller.

**Query:** `share_id`, `resource_id`, `status`, `limit`, `offset`.

## PATCH /api/v1/external/links/{link_id}

Update a link. Any field except `resource_id`, `resource_type`, `link_type` can change.

## DELETE /api/v1/external/links/{link_id}

Revoke the link. Returns 204. Revocation is permanent.

## GET /api/v1/external/links/{link_id}/sessions

List access events for a link.

**Response:**

```json
{
  "sessions": [
    {
      "id": "gss_01J6C",
      "guest_email": "partner@external.com",
      "ip_address": "203.0.113.7",
      "user_agent": "Mozilla/5.0...",
      "accessed_at": "2026-04-23T11:30:00Z",
      "download_count": 1
    }
  ],
  "total": 1
}
```

## Public endpoints (no auth)

Used by recipients of a link. The recipient-facing URL is `https://<host>/s/{token}` (or `/s/{short_code}`), served by the web frontend — these API endpoints sit underneath.

### GET /api/v1/external/access/{token}/info

Lightweight metadata about a link: the link type, the resource name, and the policy flags a client needs to render the right prompt (`password_required`, `requires_email_verification`, `watermark_enabled`, etc.). Does not require a password.

**Response:**

```json
{
  "link_type": "download",
  "resource_type": "file",
  "resource_name": "report.pdf",
  "resource_id": "fil_01J3K",
  "password_required": true,
  "requires_email_verification": false,
  "allow_preview": true,
  "show_download_button": true,
  "custom_message": null,
  "watermark_enabled": false
}
```

### POST /api/v1/external/access/{token}

Access a link with the appropriate credentials. The request body carries `password` (if required) and `email` (if `require_email`); a successful call returns a session that the web frontend uses to gate the next step (preview, download, or upload UI).

**Body:**

```json
{
  "password": "hunter2",
  "email": "partner@external.com"
}
```

**Response:** `LinkAccessResponse` — `link_type`, `resource_type`, `resource_name`, `resource_id`, `allow_preview`, `show_download_button`, `watermark_enabled`, `email_verified`, and (when `require_email` is set) a `session_token` to use for subsequent calls.

### POST /api/v1/external/verify-email

Email-verification step for links that require it. The frontend calls this between `/access/{token}` and the actual download/upload.

### Direct download / upload paths

The recipient-facing flow continues through the web frontend after `/access/{token}`. There are no separate `/external/download/{token}` or `/external/upload/{token}` API paths today — downloads stream through the frontend's file-serving layer, and `UPLOAD` links use a frontend route that calls the standard upload service with the session established by `/access/{token}`.

For server-to-server scripted access, authenticate as the link creator and use the standard `GET /api/v1/files/{file_id}/content` instead.

## Error codes

| Code | HTTP | When |
|------|------|------|
| `EXTERNAL_LINK_NOT_FOUND` | 404 | Token doesn't match |
| `EXTERNAL_LINK_EXPIRED` | 410 | Past `expires_at` |
| `EXTERNAL_LINK_REVOKED` | 410 | Revoked |
| `EXTERNAL_LINK_PASSWORD_REQUIRED` | 401 | Password needed |
| `EXTERNAL_LINK_PASSWORD_INCORRECT` | 401 | Wrong password |
| `EXTERNAL_LINK_IP_DENIED` | 403 | IP not in allow-list |
| `EXTERNAL_LINK_EMAIL_DENIED` | 403 | Email not in allow-list |
| `EXTERNAL_LINK_MAX_DOWNLOADS` | 429 | Download cap reached |
| `EXTERNAL_LINK_MAX_VIEWS` | 429 | View cap reached |

## Related

- [External Links Guide](/docs/scaidrive/api-guides/external-links)
- [Enterprise Compliance](/docs/scaidrive/advanced/enterprise-compliance) — DLP on outbound links.