REST & WebSocket API · v1

Build on PixelCore

Same 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.

Authentication

#auth

Three credential types depending on what you're building:

Session cookie

Used by the website itself + the SvelteKit app. Set after Google OAuth, sent automatically on same-origin requests.

pc_session

API key (Bearer)

Used by the Chrome extension + your own scripts. View at Settings → API key. Treat like a password.

Authorization: Bearer YOUR_API_KEY

Public (no auth)

Read endpoints for public media + user profiles need nothing. Browser-friendly, CORS-permissive.

curl https://api.pixelcore.com/v1/media/public
No active session Sign in with Google →

Endpoints

#endpoints

All paths prefixed by /v1. Base URL: https://pixelcore-api.wentzel-dev.workers.dev

Auth

GET/v1/auth/googleBegin Google OAuth flow. Browser-only.
GET/v1/auth/google/callbackOAuth callback. Sets session cookie + redirects to APP_URL.
POST/v1/auth/logoutInvalidate the current session.

Me

GET/v1/meCurrent user, or { authenticated: false }.
PATCH/v1/meUpdate bio / theme / username / privacy prefs.
GET/v1/me/statsStorage used, upload count, follower count.

Users + social

GET/v1/usersDiscoverable users. Query: limit, offset.
GET/v1/users/:idPublic profile. Includes upload + follower counts.
POST/v1/users/:id/followToggle follow. Returns { following: bool }.

Media

GET/v1/media/publicPublic feed. Cursor-paginated via ?before=<ISO>, ?limit=N (max 100).
GET/v1/media/mineYour wall. Includes private + hidden items.
GET/v1/media/:codeOne item's metadata.
GET/v1/media/:code/bytesRaw bytes. Honours HTTP Range. Long-cached at edge.
GET/v1/media/:code/thumbThumbnail variant. WebP/AVIF auto-served.
PATCH/v1/media/:codeRename, move folder, change visibility, edit tags, set expiry.
DELETE/v1/media/:codeSoft delete (item recoverable for 30 days).
POST/v1/media/:code/likeToggle like.
POST/v1/media/:code/viewTrack a view (fire-and-forget).
POST/v1/media/:code/passwordVerify a share password. Returns 30-min unlock cookie.

Upload

POST/v1/upload/initMints share code + presigned R2/Stream/Images URL. Reply: uploadUrl + uploadType.
POST/v1/upload/finalizeMarks an upload as ready. Triggers post-processing queue.
POST/v1/upload/streamIn-Worker streaming for files <5 MB. One round-trip.

Folders

GET/v1/foldersAll folders for the authenticated user (flat list - build the tree client-side).
POST/v1/foldersCreate. Body: { name, parentId?, icon?, colorHex? }.
PATCH/v1/folders/:idRename, recolour, move parent.
DELETE/v1/folders/:idDelete. Items inside detach to root (not deleted).

Albums

GET/v1/albums/:codeEvery item in an album, ordered by albumPosition.

Games

POST/v1/games/word/startBegin or resume Lettercore daily. Query: ?mode=practice for non-daily.
POST/v1/games/word/guessBody: { guess: string }. 5-letter validated.
GET/v1/games/word/statsToday's plays/wins/avg-guesses, dedup'd by displayName.
GET/v1/games/word/leaderboardTop 20 today. Default limit 20, max 100.
POST/v1/games/connections/startBegin / resume Quadcore.
POST/v1/games/connections/guessBody: { words: [a,b,c,d] }.
POST/v1/games/connections/shuffleReshuffle remaining tiles.
GET/v1/games/connections/statsToday's stats.
GET/v1/games/connections/leaderboardTop 20 today.
GET/v1/games/scoreboard/wsWebSocket. Subscribe to live scoreUpdate events. Query: ?game=lettercore or quadcore.

Download (yt-dlp bridge)

GET/v1/download/healthIs the yt-dlp sidecar online.
POST/v1/download/initQueue a download from any URL. Body: { url, audioOnly?, formatId?, audioBitrate? }.
GET/v1/download/jobs/:codePoll status. Returns { status: 'pending' | 'done' | 'failed', shareUrl }.

Examples

#examples

1. Upload an image

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/$CODE

2. Subscribe to live scoreboard updates

WebSocket 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');
};

3. Download from any URL via yt-dlp bridge

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'

Webhooks

#webhooks

Subscribe to events on your account. We POST a JSON body to your URL with an HMAC-SHA256 signature.

Events

  • upload - a new media item lands on your wall
  • like - someone likes one of your items
  • follow - someone follows you

Payload + signature

POST 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 */ }

Errors

#errors

Every error response is JSON: { error: 'code', message: 'human-readable', details?: {...} }.

HTTPerror codeWhat happened
400invalid_jsonBody wasn't parseable JSON.
400invalid_url / invalid_oauth_stateParam validation failed.
401unauthenticatedNo session cookie + no Bearer token.
403forbiddenYou're authenticated but don't own this resource.
404not_foundCode/id doesn't exist or was deleted.
410object_missingDB row exists but the bytes are gone (rare; data corruption).
413too_largeBody exceeds size cap. Use /init + presigned upload.
429rate_limitedSee rate limits below.
500internal_errorBug or transient infra issue. Retry.
503service_offlineThe yt-dlp sidecar is sleeping or unhealthy.

Rate limits

#rate-limits

Generous, application-level. Cloudflare's edge protection catches anything pathological before it reaches us.

EndpointAnonymousAuthenticated
Read endpoints (GET)120/min600/min
Mutating endpoints (POST/PATCH/DELETE)20/min120/min
Upload init60/hour
Auth (sign-in flow)10/min
Webhook delivery10K/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.

Versioning

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.