Session cookie
Used by the website itself + the SvelteKit app. Set after Google OAuth, sent automatically on same-origin requests.
pc_sessionSame edge-rendered, type-safe surface every page on this site uses. Public reads need no auth. Anything that mutates uses an API key. Sub-50ms TTFB globally. Zero egress. Zero rate limits on the free tier.
Three credential types depending on what you're building:
Used by the website itself + the SvelteKit app. Set after Google OAuth, sent automatically on same-origin requests.
pc_sessionUsed by the Chrome extension + your own scripts. View at Settings → API key. Treat like a password.
Authorization: Bearer YOUR_API_KEYRead endpoints for public media + user profiles need nothing. Browser-friendly, CORS-permissive.
curl https://api.pixelcore.co.za/v1/media/publicAll paths prefixed by /v1. Base URL: https://api.pixelcore.co.za
/v1/auth/googleBegin Google OAuth flow. Browser-only./v1/auth/google/callbackOAuth callback. Sets session cookie + redirects to APP_URL./v1/auth/logoutInvalidate the current session./v1/meCurrent user, or { authenticated: false }./v1/meUpdate bio / theme / username / privacy prefs./v1/me/statsStorage used, upload count, follower count./v1/me/api-keyReveal your API key (lazy-creates one on first call)./v1/me/api-key/regenerateRotate the key. Old key stops working immediately./v1/usersDiscoverable users. Query: limit, offset./v1/users/:idPublic profile. Includes upload + follower counts./v1/users/:id/followToggle follow. Returns { following: bool }./v1/media/publicPublic feed. Cursor-paginated via ?before=<ISO>, ?limit=N (max 100)./v1/media/mineYour wall. Includes private + hidden items./v1/media/:codeOne item's metadata./v1/media/:code/bytesRaw bytes. Honours HTTP Range. Long-cached at edge./v1/media/:code/thumbThumbnail variant. WebP/AVIF auto-served./v1/media/:codeRename, move folder, change visibility, edit tags, set expiry./v1/media/:codeSoft delete (item recoverable for 30 days)./v1/media/:code/likeToggle like./v1/media/:code/viewTrack a view (fire-and-forget)./v1/media/:code/passwordVerify a share password. Returns 30-min unlock cookie./v1/media/:code/cloneSave a public item to your library (Save-to-library button)./v1/media/:code/thumbnailUpload a poster image for a video (raw bytes in body, image/jpeg or image/webp)./v1/upload/initMints share code + presigned R2/Stream/Images URL. Reply: uploadUrl + uploadType./v1/upload/finalizeMarks an upload as ready. Triggers post-processing queue./v1/upload/streamIn-Worker streaming for files <5 MB. One round-trip./v1/upload/from-urlWorker fetches a direct media URL, streams to R2. Body: { url, isPublic?, anonymous? }. Used by the Chrome extension./v1/upload/multipartFormData ingestion (file, isPublic, anonymous). Used by the extension for data: / file:// uploads./v1/foldersAll folders for the authenticated user (flat list - build the tree client-side)./v1/foldersCreate. Body: { name, parentId?, icon?, colorHex? }./v1/folders/:idRename, recolour, move parent./v1/folders/:idDelete. Items inside detach to root (not deleted)./v1/albums/:codeEvery item in an album, ordered by albumPosition./v1/download/healthReports which back-ends are live: { direct: true, ytdlp: bool }./v1/download/directDirect-CDN URLs (mp4, jpg, mp3, pdf). Worker streams to R2. Cap 200 MB./v1/download/inityt-dlp bridge for YouTube/Instagram/TikTok-style watch pages. Body: { url, audioOnly?, formatId?, audioBitrate? }./v1/download/jobs/:codePoll yt-dlp job status.Three steps: ask the API for a presigned URL, PUT bytes directly to R2, finalize the metadata.
# Step 1 - get a presigned URL
INIT=$(curl -X POST https://api.pixelcore.co.za/v1/upload/init \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "cat.jpg",
"contentType": "image/jpeg",
"size": 248192,
"isPublic": true,
"tags": ["pets", "cute"]
}')
CODE=$(echo "$INIT" | jq -r .code)
URL=$(echo "$INIT" | jq -r .uploadUrl)
# Step 2 - upload the bytes (browser does this automatically; here for scripts)
curl -X PUT "$URL" --data-binary @cat.jpg -H "Content-Type: image/jpeg"
# Step 3 - finalize
curl -X POST https://api.pixelcore.co.za/v1/upload/finalize \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"code\":\"$CODE\"}"
# Share at https://pixelcore.co.za/p/$CODEOne-shot clone - copies the bytes (or shares the Cloudflare Images / Stream asset) into a fresh row owned by you.
POST /v1/media/abc1234/clone
Authorization: Bearer YOUR_API_KEY
# Response: { "code": "xyz5678", "shareUrl": "https://pixelcore.co.za/p/xyz5678" }POST /v1/download/init
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"audioOnly": false,
"isPublic": false
}
# Response: { "code": "abc1234", "status": "pending", "shareUrl": "..." }
# Poll /v1/download/jobs/abc1234 until status === 'done'Subscribe to events on your account. We POST a JSON body to your URL with an HMAC-SHA256 signature.
upload - a new media item lands on your walllike - someone likes one of your itemsfollow - someone follows youPOST https://your-server.com/webhook
Content-Type: application/json
X-PixelCore-Event: upload
X-PixelCore-Signature: sha256=<hex>
{
"event": "upload",
"code": "abc1234",
"url": "https://pixelcore.co.za/p/abc1234",
"type": 0,
"uploadedAt": "2026-04-29T12:00:00Z"
} Verify the signature server-side:
import crypto from 'node:crypto';
const expected = 'sha256=' + crypto
.createHmac('sha256', YOUR_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (header === expected) { /* trust the payload */ }Every error response is JSON: { error: 'code', message: 'human-readable', details?: {...} }.
| HTTP | error code | What happened |
|---|---|---|
| 400 | invalid_json | Body wasn't parseable JSON. |
| 400 | invalid_url / invalid_oauth_state | Param validation failed. |
| 401 | unauthenticated | No session cookie + no Bearer token. |
| 403 | forbidden | You're authenticated but don't own this resource. |
| 404 | not_found | Code/id doesn't exist or was deleted. |
| 410 | object_missing | DB row exists but the bytes are gone (rare; data corruption). |
| 413 | too_large | Body exceeds size cap. Use /init + presigned upload. |
| 429 | rate_limited | See rate limits below. |
| 500 | internal_error | Bug or transient infra issue. Retry. |
| 503 | service_offline | The yt-dlp sidecar is sleeping or unhealthy. |
Generous, application-level. Cloudflare's edge protection catches anything pathological before it reaches us.
| Endpoint | Anonymous | Authenticated |
|---|---|---|
Read endpoints (GET) | 120/min | 600/min |
Mutating endpoints (POST/PATCH/DELETE) | 20/min | 120/min |
| Upload init | — | 60/hour |
| Auth (sign-in flow) | 10/min | — |
| Webhook delivery | — | 10K/day per hook |
Limits are per IP for anonymous, per user for authenticated. When you're throttled you get a 429 with Retry-After in seconds.
Path-prefixed (/v1/...). Breaking changes go to a new prefix; we keep the old version alive for 6 months minimum so existing integrations keep working.
Non-breaking additions (new fields, new endpoints, new optional params) ship to the existing version. The release notes live on support; subscribe by emailing the address there if you want a heads-up before breaking changes.