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.com/v1/media/publicAll paths prefixed by /v1. Base URL: https://pixelcore-api.wentzel-dev.workers.dev
/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/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/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/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/games/word/startBegin or resume Lettercore daily. Query: ?mode=practice for non-daily./v1/games/word/guessBody: { guess: string }. 5-letter validated./v1/games/word/statsToday's plays/wins/avg-guesses, dedup'd by displayName./v1/games/word/leaderboardTop 20 today. Default limit 20, max 100./v1/games/connections/startBegin / resume Quadcore./v1/games/connections/guessBody: { words: [a,b,c,d] }./v1/games/connections/shuffleReshuffle remaining tiles./v1/games/connections/statsToday's stats./v1/games/connections/leaderboardTop 20 today./v1/games/scoreboard/wsWebSocket. Subscribe to live scoreUpdate events. Query: ?game=lettercore or quadcore./v1/download/healthIs the yt-dlp sidecar online./v1/download/initQueue a download from any URL. Body: { url, audioOnly?, formatId?, audioBitrate? }./v1/download/jobs/:codePoll status. Returns { status: 'pending' | 'done' | 'failed', shareUrl }.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.com/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.com/v1/upload/finalize \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"code\":\"$CODE\"}"
# Share at https://pixelcore.com/p/$CODEWebSocket fires scoreUpdate every time a player anywhere on Earth finishes today's puzzle.
const ws = new WebSocket('wss://api.pixelcore.com/v1/games/scoreboard/ws?game=lettercore');
ws.onmessage = (e) => {
const evt = JSON.parse(e.data);
console.log(evt.displayName, 'finished in', evt.score, 'guesses');
};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.com/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. Subscribe to GitHub releases for changelogs.