| name | expiring-links |
| display_name | Expiring Share Links |
| description | Create and manage time-limited, count-limited, and password-protected share links for uploaded files. |
| base_url | http://localhost:3000 |
| auth | none |
| version | 1.0 |
Expiring Links Skill
When to use
Use this skill when you need to:
- Create links that automatically stop working after a date
- Create links that stop working after N downloads
- Add password protection to a download link
- Check if a link is still valid without consuming a download
- Understand why a link returned 410 or 401
How expiry works
A share link is checked in this order when accessed via GET /dl/:token:
- Is the link active (
is_active = 1)? If not, return 410.
- Is
expires_at set and in the past? Return 410.
- Is
max_downloads set and download_count >= max_downloads? Return 410.
- Does the link have a password (
password_hash != NULL)? Check X-Link-Password header. If missing or wrong, return 401.
- Stream the file. After the stream completes, atomically increment
download_count and insert a downloads row.
Create a link that expires in 7 days
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"label": "7-day access",
"expires_at": "2026-03-27T00:00:00Z"
}'
Create a link limited to 1 download (one-time link)
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{"max_downloads": 1, "label": "One-time access"}'
After the first download, this link returns 410 Gone.
Create a link with both expiry and download limit
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"expires_at": "2026-04-20T00:00:00Z",
"max_downloads": 10,
"label": "Client share - limited"
}'
Create a password-protected link
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"password": "MySecretPassword",
"label": "Secure access"
}'
The password is hashed with bcrypt (cost 10) before storage. It is never returned in API responses.
Check if a link is still valid
Use HEAD /dl/:token to check validity without downloading the file and without consuming a download slot:
curl -I http://localhost:3000/dl/pq7rn4xyz90a
Responses:
| Status | Meaning |
|---|
| 200 | Link is active and the file is accessible |
| 401 | Link is active but password-protected; provide X-Link-Password |
| 404 | Token does not exist |
| 410 | Link is expired, revoked, or download limit reached |
Check how many downloads are remaining
curl http://localhost:3000/api/links/pq7rn4xyz90a
Response includes:
{
"token": "pq7rn4xyz90a",
"max_downloads": 10,
"download_count": 3,
"expires_at": "2026-04-20T00:00:00Z",
"is_active": true
}
Remaining downloads = max_downloads - download_count = 7.
If max_downloads is null, the link has no download limit.
Extend the expiry of an existing link
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"expires_at": "2026-06-01T00:00:00Z"}'
Remove the expiry from a link (make it permanent)
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"expires_at": null}'
Remove the download limit from a link
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"max_downloads": null}'
Manually revoke a link
curl -X DELETE http://localhost:3000/api/links/pq7rn4xyz90a
This sets is_active = 0. The link immediately returns 410 for all new download attempts. The download history is preserved.
Reactivate a revoked link
PATCH is not supported. To reactivate, use PUT to update is_active:
The admin UI has a "Reactivate" button that calls an internal endpoint. Via the API, revocation is permanent - create a new link if access needs to be restored.
Automatic cleanup
The server runs a cleanup job every CLEANUP_INTERVAL_HOURS (default: 1 hour). This job:
- Sets
is_active = 0 for all links where expires_at < now
- Deletes orphan files (files with no active links older than 7 days)
The cleanup job does not delete download history or the file records themselves.
Troubleshooting
Link returns 410 but it was just created
Check that expires_at is in the future (UTC). The server compares against UTC time.
date -u
Password returns 401 even with the correct password
Ensure you are sending the password in the X-Link-Password request header, not as a query parameter or body field.
download_count is not incrementing
The count only increments after the file stream completes successfully. Cancelled or aborted downloads do not increment the count. Check GET /api/links/:token/downloads to see the log of completed downloads.
Link is active but still returns 410
Check max_downloads. If the count equals the limit, the link is exhausted regardless of is_active.
curl http://localhost:3000/api/links/pq7rn4xyz90a | jq '{max_downloads, download_count, expires_at, is_active}'