URL Protection API
Protect shortened links with passwords or PINs. Protected links require visitors to verify their identity before being redirected.
Protection Types
Type Value Description None noneDefault — public link, no protection Password passwordFreeform password (4-128 characters) PIN pinNumeric PIN (4 or 6 digits)
Content Classifications
Value Description generalDefault — public content personalPrivate/sentimental content workSecret deployments, internal tools nsfwAdult content
Create a Protected Link
curl -X POST https://api.smo1.io/api/links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/my-secret-deployment",
"custom_slug": "my-staging",
"password": "hunter2",
"protection_hint": "The classic password",
"content_classification": "work"
}'
Create a PIN-Protected Link
curl -X POST https://api.smo1.io/api/links \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/my-dissertation",
"pin": "1234",
"protection_hint": "Year I graduated",
"content_classification": "personal"
}'
Update Link Protection
Add protection to an existing link:
curl -X PATCH https://api.smo1.io/api/links/{id} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"password": "new-secret",
"protection_hint": "Updated hint"
}'
Remove protection:
curl -X PATCH https://api.smo1.io/api/links/{id} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"remove_protection": true
}'
Verify Protection (Public)
This endpoint is public — no authentication required. Visitors use it to unlock protected links.
Password
PIN
Success Response (200)
Failure Response (403)
Rate Limited (403)
curl -X POST https://api.smo1.io/api/links/{id}/verify \
-H "Content-Type: application/json" \
-d '{"password": "hunter2"}'
Rate Limiting
Protection verification is rate-limited per IP address per link:
5 failed attempts per 15-minute window
After exceeding the limit, all attempts are rejected until the window expires
Successful attempts are recorded but do not count toward the limit
Slug Lookup (Edge Workers)
The GET /api/links/slug/:slug endpoint includes protection metadata:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"slug" : "my-staging" ,
"destination" : "" ,
"user_id" : "..." ,
"is_active" : true ,
"expires_at" : null ,
"protection_type" : "password" ,
"protection_hint" : "The classic password" ,
"content_classification" : "work"
}
When a link is protected, the destination field is redacted (empty string) in slug lookup responses. The destination is only revealed through the /verify endpoint after successful authentication.
Security
Passwords and PINs are bcrypt-hashed before storage — never stored in plaintext
Hashes are never returned in any API response
The edge worker gate page uses Cache-Control: no-store to prevent caching
Protection hints are HTML-escaped to prevent XSS