Skip to content

Jobs 9/9: /api/2.0 jobs + organizations endpoints, job.published webhook, developer docs #936

Description

@wintermeyer

TL;DR Expose jobs and organization pages through /api/2.0 (read for everyone with a token, write for a posting's owner), add a job.published webhook topic, and document all of it in the developer docs — so HR tools can post and manage openings without the browser.

Order: 9/9 in the Jobs & Organizations milestone. Depends on #932 (resource + lifecycle), #933 (board filters), #929 (organizations).
Where: VutuvWeb.ApiV2 (new controllers), router /api/2.0 scope, Vutuv.Webhooks, the /developers documentation pages.

Want — endpoints (additions only, per the API contract):

GET    /api/2.0/jobs                jobs:read   list published postings; same filters
                                                as /jobs (q, tag, near + radius,
                                                country, salary_min, workplace_type,
                                                employment_type); keyset-paginated
GET    /api/2.0/jobs/:id            jobs:read   one posting incl. status + dates
GET    /api/2.0/organizations           jobs:read   verified organizations, paginated, ?q=
GET    /api/2.0/organizations/:slug     jobs:read   page incl. aliases + verified domains
POST   /api/2.0/jobs                jobs:write  create draft or publish
PATCH  /api/2.0/jobs/:id            jobs:write  edit own posting (same rules as the form)
POST   /api/2.0/jobs/:id/closure    jobs:write  close (reason: filled | withdrawn)
DELETE /api/2.0/jobs/:id            jobs:write  discard a draft
  • Two new scopes jobs:read / jobs:write, available to personal access tokens and OAuth apps like the existing scopes.
  • The API is a second door to the same room, never a side channel: writes go through the exact Vutuv.Jobs changesets and policies as the forms — quotas, account-age gate, organization attribution only with a role, the fixed 90-day lifecycle (no renewal endpoint exists because no renewal exists), salary/AGG validations, rate limiter. An API posting is indistinguishable from a form posting.
  • Read responses of expired/closed postings keep working for the owner (their tooling needs the final state); the public token view returns published postings only.
  • Payloads carry the full structured location and pay data the board uses: street_address, zip_code, city, country, resolved coordinates when available, workplace_type, remote_countries, the complete salary block (required on publish, Jobs 5/9: Job postings: resource, 90-day lifecycle, detail page, easy apply #932), and the tags with their required/nice-to-have priority. Organization responses include the postal address. Publishing over the API enforces the same completeness as the form: an onsite/hybrid draft without zip + city + country, a remote draft without applicant countries, or a non-volunteer draft without a salary range gets a clean, documented validation error.
  • Webhooks: new subscribable topic job.published (payload: id, title, URL, employer name, structured location [city, zip code, country, workplace type, applicant countries for remote], salary range, tags) so integrators can mirror new openings with real filter data; delivery through the existing webhook machinery (SSRF-validated URLs, retries).

Documentation (part of the deliverable, not an afterthought):

  • /developers gains a "Jobs" chapter: auth + scopes, every endpoint with request/response examples, the lifecycle state machine and what each status means for API clients (including "expired postings cannot be edited or reopened — repost instead"), error catalogue (quota exceeded, attribution denied, editing an expired posting, incomplete location, missing salary range), webhook payload example.
  • docs/architecture/api.md and jobs.md updated; any new env knob lands in docs/ADMINS.md.

SEO & GEO: the JSON responses reuse the agent-doc structures where they exist (one doc map per page, no drift); machine-opt-out members' data never appears in API people fields beyond what the public page shows.
Smoke test (curl + browser): create a PAT with jobs:write in the browser → POST /api/2.0/jobs a draft → try to publish it without a salary range and without a location → documented validation errors → complete it and publish via PATCH → posting appears on /jobs (browser), expires_on is 90 days out in the GET response and the response carries the structured location + salary → GET /api/2.0/jobs?near=Köln&radius=50&salary_min=60000 finds it → POST …/closure (reason filled) → status closed in the GET; a further PATCH returns a clean JSON error → job.published webhook delivery visible for a subscribed test endpoint → token without jobs:write gets 403 on write and 200 on read.
Out of scope: applications over the API, people search over the API, OAuth scope UI changes beyond listing the new scopes.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions