{"openapi":"3.1.0","info":{"title":"EasyTopJobs Public API","version":"dev","description":"The public REST API served from apps/web — the same endpoints we offer to third parties and dogfood in our own web + desk clients. Build commit: dev.","contact":{"name":"EasyTopJobs","url":"https://easytopjobs.com"}},"x-git-sha":"dev","servers":[{"url":"/","description":"Same-origin"}],"tags":[{"name":"Jobs","description":"The public job-search catalogue"},{"name":"Entitlements","description":"Reveal + paid-pass allowances"},{"name":"Auth","description":"Magic-link sign-in"},{"name":"Billing","description":"Stripe checkout"}],"paths":{"/api/jobs/search":{"get":{"tags":["Jobs"],"summary":"Search the public job catalogue","description":"Filters, counts, and facets the full catalogue in Postgres and returns one payload. Anonymous callers receive `total`, `addedToday`, and `facets` but an empty `previews` array. Signed-in free/paid callers receive up to 500 previews. The free tier is gated to 3 reveals/day (`remainingReveals`); a paid pass unlocks the fair-use window (`fairUseRemaining`).","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Full-text query across job title, company, and description"},{"name":"location","in":"query","required":false,"schema":{"type":"string"},"description":"Location filter"},{"name":"workplace","in":"query","required":false,"schema":{"type":"string"},"description":"Workplace filter (e.g. remote / hybrid / onsite)"},{"name":"role","in":"query","required":false,"schema":{"type":"string"},"description":"Role-class filter"},{"name":"seniority","in":"query","required":false,"schema":{"type":"string"},"description":"Seniority filter"},{"name":"company","in":"query","required":false,"schema":{"type":"string"},"description":"Company-name filter"}],"responses":{"200":{"description":"Search results with access + gating metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}}}}},"/api/entitlements":{"get":{"tags":["Entitlements"],"summary":"The caller's current reveal + pass entitlements","description":"Anonymous callers receive the default free allowance (freeRemaining 3, fairUseRemaining 80). Signed-in callers receive the free reveals left for today and their active paid-pass expiry (`paidUntil`), if any. Never errors — falls back to defaults when auth is unavailable.","responses":{"200":{"description":"Entitlement snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}}}}},"/api/auth/email":{"post":{"tags":["Auth"],"summary":"Send a magic-link sign-in email","description":"Sends a Supabase OTP link to the given email. Responds `{ ok: true, demo: true }` when auth is not configured, and `{ ok: true }` on success.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"email":{"type":"string","description":"The email address to send the sign-in link to"}},"required":["email"]}}}},"responses":{"200":{"description":"Link sent (or demo acknowledgement).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkFlag"}}}},"400":{"description":"Invalid email or provider error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/checkout":{"post":{"tags":["Billing"],"summary":"Start a Stripe checkout for the 24h pass","description":"Creates a Stripe Checkout session for the signed-in user and 303-redirects to it. Redirects back to the sign-in modal when unauthenticated, or to the credits modal when a pass is already active or Stripe is not configured (demo mode).","responses":{"303":{"description":"Redirect to Stripe Checkout or an on-site modal state."}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable error message"}},"required":["error"]},"JobFacet":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer","description":"Number of matching jobs for this facet value"}},"required":["value","count"]},"JobPreview":{"type":"object","properties":{"id":{"type":"string"},"company":{"type":"string"},"title":{"type":"string"},"location":{"type":"string"},"workplace":{"type":"string"},"role":{"type":"string"},"seniority":{"type":"string"},"compensation":{"type":["string","null"],"description":"Formatted comp string, or null when unknown"},"addedAt":{"type":"string","description":"ISO-8601 timestamp the job entered the catalogue"},"source":{"type":"string","description":"Where the posting was collected from"}},"required":["id","company","title","location","workplace","role","seniority","addedAt","source"]},"SearchFacets":{"type":"object","properties":{"roles":{"type":"array","items":{"$ref":"#/components/schemas/JobFacet"}},"locations":{"type":"array","items":{"$ref":"#/components/schemas/JobFacet"}},"workplace":{"type":"array","items":{"$ref":"#/components/schemas/JobFacet"}},"seniority":{"type":"array","items":{"$ref":"#/components/schemas/JobFacet"}},"companies":{"type":"array","items":{"$ref":"#/components/schemas/JobFacet"}}},"required":["roles","locations","workplace","seniority","companies"]},"SearchResponse":{"type":"object","properties":{"access":{"type":"string","enum":["anonymous","free","paid"],"description":"Caller access tier resolved from auth + entitlements"},"total":{"type":"integer","description":"Total jobs matching the filters across the whole catalogue"},"addedToday":{"type":"integer","description":"How many matching jobs were added today"},"previews":{"type":"array","items":{"$ref":"#/components/schemas/JobPreview"},"description":"Empty for anonymous callers; up to 500 previews for free/paid callers"},"facets":{"$ref":"#/components/schemas/SearchFacets"},"remainingReveals":{"type":"integer","description":"Reveals left today (free tier caps at 3; paid resets to the daily limit)"},"nextFreeRevealAt":{"type":["string","null"],"description":"ISO timestamp of the next free reveal, or null"},"fairUseRemaining":{"type":"integer","description":"Remaining fair-use reveals in the active window (80 when entitled)"}},"required":["access","total","addedToday","previews","facets","remainingReveals","fairUseRemaining"]},"Entitlements":{"type":"object","properties":{"freeRemaining":{"type":"integer","description":"Free reveals remaining today (defaults to 3 for anonymous callers)"},"paidUntil":{"type":["string","null"],"description":"ISO timestamp the active paid pass expires, or null"},"fairUseRemaining":{"type":"integer","description":"Remaining fair-use reveals (80 when entitled, else 0)"}},"required":["freeRemaining","paidUntil","fairUseRemaining"]},"OkFlag":{"type":"object","properties":{"ok":{"type":"boolean"},"demo":{"type":"boolean","description":"Present when Supabase auth is not configured"}},"required":["ok"]}}}}