Base URL: http://localhost:8000 (development)
API Prefix: /api/v1
GET /healthGET /api/v1/users/public-keyPOST /api/v1/users/
Content-Type: application/json
X-Password-Encrypted: true
{
"username": "newuser",
"email": "user@example.com",
"password": "<encrypted_password>",
"display_name": "Display Name"
}POST /api/v1/users/token
Content-Type: application/x-www-form-urlencoded
X-Password-Encrypted: true
username=user@example.com&password=<encrypted_password>POST /api/v1/users/token/refresh
Content-Type: application/json
{
"refresh_token": "your.refresh.token"
}POST /api/v1/users/logout
Authorization: Bearer <token>
Content-Type: application/json
{
"refresh_token": "your.refresh.token"
}Password Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
POST /api/v1/users/forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}Response:
{
"message": "If an account with that email exists, we've sent password reset instructions.",
"success": true
}Rate Limit: 3 requests per hour per IP
Note: Always returns success to prevent email enumeration attacks.
POST /api/v1/users/verify-reset-token
Content-Type: application/json
{
"token": "your-reset-token-here"
}Response (Valid Token):
{
"message": "Reset token is valid",
"success": true
}Response (Invalid/Expired Token):
{
"detail": "Invalid or expired reset token"
}Use Case: Frontend can verify token validity before showing password reset form.
POST /api/v1/users/reset-password
Content-Type: application/json
{
"token": "your-reset-token-here",
"new_password": "NewSecureP@ssw0rd!"
}Response (Success):
{
"message": "Password has been reset successfully. You can now log in with your new password.",
"success": true
}Response (Invalid Token):
{
"detail": "Invalid or expired reset token"
}curl -X POST "http://localhost:8000/api/v1/users/forgot-password" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com"
}'Response:
{
"message": "If an account with that email exists, we've sent password reset instructions.",
"success": true
}Email Received:
- Subject: "Password Reset Request - Sono"
- Contains reset link:
https://sono.wtf/reset-password?token=abc123... - Token expires in 1 hour
curl -X POST "http://localhost:8000/api/v1/users/verify-reset-token" \
-H "Content-Type: application/json" \
-d '{
"token": "abc123..."
}'Response:
{
"message": "Reset token is valid",
"success": true
}curl -X POST "http://localhost:8000/api/v1/users/reset-password" \
-H "Content-Type: application/json" \
-d '{
"token": "abc123...",
"new_password": "NewSecureP@ssw0rd!"
}'Response:
{
"message": "Password has been reset successfully. You can now log in with your new password.",
"success": true
}curl -X POST "http://localhost:8000/api/v1/users/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=john@example.com&password=NewSecureP@ssw0rd!"Response:
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"refresh_token": "eyJhbGc..."
}{
"detail": "This reset link has already been used"
}{
"detail": "This reset link has expired. Please request a new one."
}{
"detail": "Invalid or expired reset token"
}{
"detail": [
{
"loc": ["body", "new_password"],
"msg": "Password must contain at least one uppercase letter",
"type": "value_error"
}
]
}{
"detail": "Failed to send password reset email. Please try again later."
}GET /api/v1/users/me
Authorization: Bearer <token>PUT /api/v1/users/me
Authorization: Bearer <token>
Content-Type: application/json
{
"display_name": "New Name",
"bio": "Bio text"
}POST /api/v1/users/me/upload-profile-picture
Authorization: Bearer <token>
Content-Type: multipart/form-data
file=@picture.jpgPOST /api/v1/users/me/request-deletion
Authorization: Bearer <token>
Content-Type: application/json
{
"deletion_type": "hard",
"reason": "Optional reason"
}POST /api/v1/users/me/cancel-deletion
Authorization: Bearer <token>DELETE /api/v1/users/me?password=<password>&deletion_type=<soft|hard>
Authorization: Bearer <token>GET /api/v1/users/me/export-data
Authorization: Bearer <token>POST /api/v1/users/me/consent
Authorization: Bearer <token>
Content-Type: application/json
{
"consent_type": "privacy_policy",
"consent_version": "2.0",
"ip_address": "192.168.1.1"
}GET /api/v1/users/me/consents
Authorization: Bearer <token>POST /api/v1/audio/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
file=@audio.mp3
title=Song Title
description=Description
is_public=falseGET /api/v1/audio/stats
Authorization: Bearer <token>GET /api/v1/audio/my-files?skip=0&limit=100
Authorization: Bearer <token>GET /api/v1/audio/public?skip=0&limit=100GET /api/v1/audio/{file_id}
Authorization: Bearer <token>PUT /api/v1/audio/{file_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "New Title",
"description": "New Description",
"is_public": true
}DELETE /api/v1/audio/{file_id}
Authorization: Bearer <token>POST /api/v1/collections/
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Collection Title",
"description": "Description",
"collection_type": "playlist",
"artist": "Artist Name",
"curator_note": "Curator notes",
"is_public": true,
"is_collaborative": false
}POST /api/v1/collections/{collection_id}/cover-art
Authorization: Bearer <token>
Content-Type: multipart/form-data
file=@cover.jpgGET /api/v1/collections/?skip=0&limit=100&collection_type=playlist&public_only=trueGET /api/v1/collections/my-collections?skip=0&limit=100
Authorization: Bearer <token>GET /api/v1/collections/{collection_id}
Authorization: Bearer <token>PUT /api/v1/collections/{collection_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "New Title",
"description": "New Description",
"is_public": true
}DELETE /api/v1/collections/{collection_id}
Authorization: Bearer <token>POST /api/v1/collections/{collection_id}/tracks
Authorization: Bearer <token>
Content-Type: application/json
{
"audio_file_id": 123,
"track_order": 1
}DELETE /api/v1/collections/{collection_id}/tracks/{track_id}
Authorization: Bearer <token>PUT /api/v1/collections/{collection_id}/tracks/{track_id}/reorder
Authorization: Bearer <token>
Content-Type: application/json
{
"new_order": 5
}POST /api/v1/collections/{collection_id}/tracks/bulk-add
Authorization: Bearer <token>
Content-Type: application/json
{
"audio_file_ids": [1, 2, 3, 4, 5]
}POST /api/v1/collections/{collection_id}/collaborators
Authorization: Bearer <token>
Content-Type: application/json
{
"user_id": 123,
"permission_level": "edit"
}PUT /api/v1/collections/{collection_id}/collaborators/{user_id}
Authorization: Bearer <token>
Content-Type: application/json
{
"permission_level": "view"
}DELETE /api/v1/collections/{collection_id}/collaborators/{user_id}
Authorization: Bearer <token>GET /api/v1/announcements?skip=0&limit=20GET /api/v1/announcements/{announcement_id}Data sourced from the Kworb scraper database. Requires KWORB_DATABASE_URL to be configured.
GET /api/v1/kworb/top-streamed-artists?limit=100&offset=0Query Parameters:
limit(optional): Number of results (1-1000, default: 100)offset(optional): Pagination offset (default: 0)
Response:
[
{
"rank": 1,
"artist": "Artist Name",
"streams": 123456789,
"daily": 1234567,
"as_lead": 100000000,
"solo": 80000000,
"as_feature": 23456789,
"scraped_at": "2025-01-01T00:00:00"
}
]GET /api/v1/kworb/monthly-listeners?limit=100&offset=0Query Parameters:
limit(optional): Number of results (1-1000, default: 100)offset(optional): Pagination offset (default: 0)
Response:
[
{
"rank": 1,
"artist": "Artist Name",
"listeners": 98765432,
"peak": 100000000,
"peak_date": "2024-12-15",
"scraped_at": "2025-01-01T00:00:00"
}
]GET /api/v1/kworb/top-songs?limit=100&offset=0Query Parameters:
limit(optional): Number of results (1-1000, default: 100)offset(optional): Pagination offset (default: 0)
Response:
[
{
"rank": 1,
"title": "Song Title",
"artist": "Artist Name",
"streams": 3000000000,
"daily": 5000000,
"scraped_at": "2025-01-01T00:00:00"
}
]GET /api/v1/kworb/artist/{artist_name}Searches for an artist across all Kworb data (top streamed, monthly listeners, and top songs).
Response:
{
"top_streamed": [
{
"rank": 1,
"artist": "Artist Name",
"streams": 123456789,
"daily": 1234567,
"as_lead": 100000000,
"solo": 80000000,
"as_feature": 23456789,
"scraped_at": "2025-01-01T00:00:00"
}
],
"monthly_listeners": [
{
"rank": 1,
"artist": "Artist Name",
"listeners": 98765432,
"peak": 100000000,
"peak_date": "2024-12-15",
"scraped_at": "2025-01-01T00:00:00"
}
],
"top_songs": [
{
"rank": 1,
"title": "Song Title",
"artist": "Artist Name",
"streams": 3000000000,
"daily": 5000000,
"scraped_at": "2025-01-01T00:00:00"
}
]
}Note: Returns up to 10 results per category. Search is case-insensitive and matches partial artist names.
GET /api/v1/admin/stats
Authorization: Bearer <admin_token>GET /api/v1/admin/users?skip=0&limit=100
Authorization: Bearer <admin_token>GET /api/v1/admin/users/{user_id}
Authorization: Bearer <admin_token>PUT /api/v1/admin/users/{user_id}/upload-limit
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"max_audio_uploads": 50
}GET /api/v1/admin/users/{user_id}/upload-stats
Authorization: Bearer <admin_token>POST /api/v1/admin/users/{user_id}/disable
Authorization: Bearer <admin_token>POST /api/v1/admin/users/{user_id}/enable
Authorization: Bearer <admin_token>GET /api/v1/admin/audio-files/all?skip=0&limit=100
Authorization: Bearer <admin_token>DELETE /api/v1/admin/users/{user_id}/audio-files
Authorization: Bearer <admin_token>POST /api/v1/admin/users/{user_id}/reset-uploads
Authorization: Bearer <admin_token>GET /api/v1/admin/collections/stats
Authorization: Bearer <admin_token>GET /api/v1/admin/users/{user_id}/collections/stats
Authorization: Bearer <admin_token>DELETE /api/v1/admin/users/{user_id}/collections/all
Authorization: Bearer <admin_token>GET /api/v1/admin/collections/recent?limit=20
Authorization: Bearer <admin_token>GET /api/v1/admin/collections/summary
Authorization: Bearer <admin_token>POST /api/v1/admin/announcements
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"title": "Important Update",
"content": "Announcement content here...",
"is_published": true
}GET /api/v1/admin/announcements?skip=0&limit=100&published_only=false
Authorization: Bearer <admin_token>GET /api/v1/admin/announcements/{announcement_id}
Authorization: Bearer <admin_token>PUT /api/v1/admin/announcements/{announcement_id}
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"title": "Updated Title",
"content": "Updated content",
"is_published": true
}DELETE /api/v1/admin/announcements/{announcement_id}
Authorization: Bearer <admin_token>GET /api/v1/admin/maintenance/statusPublic endpoint - no authentication required
Response:
{
"enabled": false,
"message": "Service temporarily unavailable for maintenance"
}POST /api/v1/admin/maintenance/enable?message=Custom%20message
Authorization: Bearer <admin_token>Query Parameters:
message(optional): Custom maintenance message (max 200 chars)
Response:
{
"enabled": true,
"message": "Database upgrade in progress - back at 3pm"
}Examples:
Default message:
curl -X POST "http://localhost:8000/api/v1/admin/maintenance/enable" \
-H "Authorization: Bearer $TOKEN"Custom message:
curl -X POST "http://localhost:8000/api/v1/admin/maintenance/enable?message=Scheduled%20maintenance%20-%20back%20at%203pm" \
-H "Authorization: Bearer $TOKEN"POST /api/v1/admin/maintenance/disable
Authorization: Bearer <admin_token>Response:
{
"enabled": false,
"message": "Service temporarily unavailable for maintenance"
}Example:
curl -X POST "http://localhost:8000/api/v1/admin/maintenance/disable" \
-H "Authorization: Bearer $TOKEN"POST /api/v1/admin/maintenance/toggle
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"enabled": true,
"message": "Optional custom message"
}Request Body:
{
"enabled": true,
"message": "Database upgrade - back at 15:00 UTC"
}Response:
{
"enabled": true,
"message": "Database upgrade - back at 15:00 UTC"
}Examples:
Enable with custom message:
curl -X POST "http://localhost:8000/api/v1/admin/maintenance/toggle" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"message": "Server migration in progress"
}'Disable:
curl -X POST "http://localhost:8000/api/v1/admin/maintenance/toggle" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'When maintenance mode is enabled:
/healthendpoint continues to work (200 OK)/api/v1/admin/maintenance/*endpoints continue to work- All other endpoints return
503 Service Unavailable
503 Response Example:
{
"detail": "Database upgrade in progress - back at 3pm",
"status": "maintenance",
"retry_after": 3600
}Response Headers:
HTTP/1.1 503 Service Unavailable
Retry-After: 3600
Content-Type: application/json
{
"id": 1,
"field": "value"
}{
"detail": "Error message"
}{
"items": [...],
"total": 100,
"has_more": true
}All protected endpoints require:
Authorization: Bearer <access_token>
| Endpoint | Limit |
|---|---|
POST /users/ |
5/minute per IP |
POST /users/token |
10/minute per IP |
POST /users/token/refresh |
30/minute per IP |
DELETE /users/me |
3/hour per IP |
POST /users/forgot-password |
3/hour per IP |
POST /users/reset-password |
5/hour per IP |
| Type | Formats | Max Size |
|---|---|---|
| Audio | MP3, WAV, OGG, M4A, AAC, FLAC, WEBM | 50 MB |
| Images | PNG, JPG, WebP | 5 MB |
album- Music album with artistplaylist- User-curated playlistcompilation- Curated compilation with notes
edit- Can add/remove tracksview- Can only view collection