Skip to main content
The API is unversioned and changes are additive by default (see Versioning). Anything that breaks an existing integration is announced here ahead of time.
Filter the job feed like the Feed Builder
POST /api/jobs/feed now takes every Feed Builder condition as a request filter, so you can stream a narrow slice without saving a feed first.How they match:
  • Occupation and company filters take {"include": [...], "exclude": [...]}, case-insensitively. Different filters combine with AND.
  • Occupation filters (against each job’s occupation) and companies match as on POST /api/jobs/search. GET /api/occupations lists every value; filter by id, since names can improve between taxonomy releases.
  • company_is_agency: false leaves out jobs that recruitment and staffing agencies post for other employers; true returns only theirs.
  • An exclude_ list leaves out jobs with any listed value. It takes the same values as the include list of the same name; as with languages, language names and locales map to ISO 639-1 codes.
  • Include and exclude lists combine: {"sources": ["greenhouse", "lever"], "exclude_sources": ["lever"]} streams Greenhouse only.
  • Jobs or companies without a value, such as an unknown work model, never match an include list and are kept by an exclude or exclude_ list.
This body streams nursing jobs at large direct employers:
Filters are set on the first request of a scan and kept by its cursor. The request returns 400 for an unknown size or revenue band, more than 1,000 values in one list, or any of these filters with stable_scan: false.companies entries are matched on the first page, and the cursor keeps the matched company ids, so a scan’s companies are fixed when it starts. An entry that matches no company is not a 400. The first page reports it in a new warnings array, and the codes repeat on the X-Jobo-Warnings header:
  • companies_unmatched: an include entry matched no company.
  • companies_exclude_unmatched: an exclude entry matched no company.
  • companies_outside_sources: the matched companies are on none of the requested sources.
When no include entry matches, or every match is outside sources, the first page is empty (has_more: false, estimated_total: 0) and is not billed. warnings appears on the first page only, and only when there is something to report. Requests without the new fields are unchanged.Feed Builder conditions whose request field has another name or shape:The public Feed Builder page lets you pick these filters from lists and writes the JSON body and curl for you.POST /api/jobs/feed/managed does not take the occupation, company, exclude_ or companies filters. To filter your managed companies’ jobs this way, save a Feed Builder feed that starts from your Managed feed and read it with POST /api/jobs/feed/{feed_id}.On the first page of POST /api/jobs/feed, estimated_total is now exact when 1,000 or fewer jobs match. Above that it is an estimate that can run a few percent high, so first pages return sooner.Every job now carries the occupation key, set to null when the job is not classified; before, the key was omitted. This applies to search, GET /api/jobs/{id}, every jobs feed and document outbound feeds (webhook, Algolia, Meilisearch, Elasticsearch, MongoDB). Code that checks whether the key exists should check for null instead.
Outbound feeds from saved feeds
An outbound feed now takes its jobs from one source, chosen under Jobs to deliver: All jobs or one of your saved feeds from the Feed Builder. Every filter now lives in the Feed Builder: location, ATS, work model, employment type, experience level, posted after, company conditions, occupation and excluded companies. Outbound setup and settings no longer offer their own filters or the managed-companies option; save a feed whose source is your Managed feed instead. The saved feed’s rules alone decide what is delivered.Editing a saved feed’s source, conditions or excluded companies re-sends it in full to every outbound feed that delivers it, on that feed’s next sync. Jobs that no longer match are removed. Like any full re-send, the delivered jobs are billed per job, except on the Unlimited plan. Renaming a feed or changing its description re-sends nothing.A feed that an outbound feed delivers can’t be deleted. The Feed Builder names the outbound feeds that use it, so give them another source first. If the feed becomes unavailable anyway, the outbound feed pauses as Source unavailable and keeps everything it already delivered. Choose another source, then resume it.Active outbound feeds set up with their own filters are moved to saved feeds in your Feed Builder with the same rules, and keep syncing without re-sending anything. Other outbound feeds keep their original filters until you choose a saved feed. Those filters still narrow what is delivered, and the feed’s settings show them read-only under Data included. An outbound feed that already delivers your managed companies keeps doing so until you choose another source. Billing is unchanged.Occupation support in outbound feeds:
  • Postgres, Supabase and Bolt: Indexes gains Occupation, a GIN index on the occupation column for filters by job category, subcategory or occupation. See Occupation.
  • Algolia: new outbound feeds facet on occupation.category and occupation.subcategory, and can filter on occupation.id (as filterOnly(occupation.id)).
  • Meilisearch: new outbound feeds can filter on occupation.category, occupation.subcategory and occupation.id.
  • Existing Algolia and Meilisearch feeds keep their settings. To use these, add them to Attributes for Faceting (Algolia) or Filterable Attributes (Meilisearch).
On POST /api/jobs/feed/{feed_id}, feed_revision now changes only when the feed’s source, conditions or excluded companies change. Renaming a feed or changing its description no longer restarts a scan with 409 feed_changed or calls for a full scan. Responses are otherwise unchanged.
Job occupations
Jobs now carry an occupation: one occupation in a category › subcategory › occupation taxonomy (for example Healthcare › Nursing › Registered Nurse). It has a stable id, its ISCO-08 code and its O*NET-SOC codes. It appears on search results, GET /api/jobs/{id} and the jobs feeds. When a job is not classified, the key is omitted (not null); since 2026-11-03 it is present and null.Search can filter on it:
  • GET /api/jobs takes comma-separated job_categories, job_subcategories and occupation_ids. POST /api/jobs/search takes them as include/exclude objects.
  • Three opt-in facets, job_categories, job_subcategories and occupations, return buckets that also carry name and parent, so you can nest category › subcategory › occupation.
  • The free GET /api/occupations lists every value. Filter occupations by id; names can improve between taxonomy releases.
See Filter by occupation. POST /api/jobs/feed and POST /api/jobs/feed/managed return the object but take no occupation filters (POST /api/jobs/feed gained them on 2026-11-03). A saved feed can filter on Job category, Job subcategory and Occupation in the Feed Builder.Postgres, Supabase and Bolt outbound feeds get a JSONB occupation column, added on the next sync to tables Jobo created. Rows written before then are filled when the job is delivered again or on a full resync. Elasticsearch outbound feeds get an occupation field. Occupations come from ESCO and O*NET under CC BY 4.0; see Open data licenses and attribution. Responses are otherwise unchanged.
Saved feeds
Build a feed once in the dashboard’s Feed Builder and pull it with POST /api/jobs/feed/{feed_id}. A feed starts from All jobs or your Managed feed and is narrowed by company and job conditions, such as ATS, industry, size, revenue, work model, location and occupation. It can also exclude companies. Before you save, the builder shows the estimated number of jobs, about how many are new each month, and sample jobs.Pages, cursors and billing are the same as POST /api/jobs/feed. Responses also carry feed_revision, and editing a feed mid-scan returns 409 feed_changed. Available to every account with a customer API key. POST /api/jobs/feed and POST /api/jobs/feed/managed are unchanged.
GET search flags unknown query parameters
GET /api/jobs now reports query parameters it does not recognize, as POST /api/jobs/search already does for unknown body fields. It adds an unknown_request_fields entry to warnings[] and to the X-Jobo-Warnings header. Before, a parameter such as created_after was silently ignored, and the response looked like a filtered result. To filter by first-indexed time, use discovered_after / discovered_before, which compare against each job’s created_at. Responses without unknown parameters are unchanged.
Stable search pagination over a discovered window
POST /api/jobs/search and GET /api/jobs accept a new sort field: relevance (the default, unchanged), discovered_asc or discovered_desc. The discovered orders sort by created_at, the time Jobo first indexed a job, which never changes. With discovered_after / discovered_before, a page-by-page pull no longer skips or repeats jobs that are updated while you page. See Stable pages over a fixed window.
Managed feed pages like the main feed
Breaking (managed feed only): POST /api/jobs/feed/managed now returns jobs oldest first by created_at, the same order as POST /api/jobs/feed with stable_scan. Its first page carries estimated_total. Cursors issued before this change return 400; start a new scan without a cursor.updated_after on the managed feed now also returns every job of a company added to your managed job sources on or after that time. Before, an incremental sync never received a newly added company’s existing jobs, because those jobs had not changed.
HTML job descriptions on request
Set description_format to html to get job descriptions as HTML. The default stays markdown, so existing responses are unchanged.
  • Query parameter on GET /api/jobs, GET /api/jobs/{id} and GET /api/companies/{id}/jobs.
  • Body field on POST /api/jobs/search, POST /api/jobs/feed and POST /api/jobs/feed/managed. On the feed, the value is read from each request, including cursor continuations.
Raw HTML in the source is escaped, and only http(s), mailto and relative URLs are kept. See Description format.
Sandbox keys and Auto Apply live access
Create a Sandbox key (jbe_test_…) in the dashboard under Developers → API Keys to build against fictional jobs and companies in the real response shapes. It uses the same host, https://connect.jobo.world. Sandbox calls are never billed. Auto Apply applications made with a sandbox key reach only Jobo sandbox forms and need no approval. See Sandbox.Live Auto Apply now needs acceptance of the Auto Apply Terms and DPA plus an approved business review. Request it in the dashboard under Auto Apply → Getting started → Request live access. Without them, a live create returns 403 auto_apply_agreement_required or 403 auto_apply_review_required.
Multi-term search pagination fixed
Fixed: between 2026-08-17 and 2026-09-02, POST /api/jobs/search and GET /api/jobs requests with two or more search terms ignored page and page_size. Every page returned the same first 10 results, while total, total_pages, X-Page and X-Page-Size still reflected the request. A client walking total_pages re-fetched, and was billed for, the same 10 jobs on every page. Single-term searches and the feed were unaffected.Delivered jobs on affected multi-page fetches in that window were billed as normal deliveries. Contact support for an adjustment.
Auto Apply fields become structural
Breaking (beta surface): Auto Apply moves to api_version 2026-08-31 and @jobo-ai/autoapply 2.0.
  • Field responses always contain field_id, type, label, required and requires_answer. Options, constraints, format, sensitivity and repeating-group metadata appear only when they apply.
  • The unreliable semantic_key and category hints and the ATS current_value are removed from customer responses.
  • Answer each field from its provider ID, visible label, type, advertised options, constraints and group shape.
  • Sensitive fields stay explicitly marked. Never infer them.
Company name filters explain themselves
The companies include/exclude filter on POST /api/jobs/search now resolves names more forgivingly, and says so when it cannot.Names still match on the whole name, but accents, punctuation and common trailing legal suffixes are normalized first. FedEx Corporation, fedex and FedEx, Inc. now resolve to the same companies; before, only the exact stored spelling did. It is still not a partial match: Glenmark does not find Glenmark Pharmaceuticals, and Fidelity finds the company called Fidelity, not every Fidelity* employer. Widening the match silently would bill you for companies you never asked for.Responses may now carry warnings[] and filters.companies (matched / unmatched / exclude_unmatched), and the warning codes repeat on the X-Jobo-Warnings header. Both fields are omitted when there is nothing to report, so an ordinary successful search is byte-identical to before.An unmatched include name still returns 200 with zero jobs, not 400. Two fixes ship alongside:
  • The zero-result response now sends the X-Total-* pagination headers it used to omit.
  • MCP search now resolves company names. It used to pass raw names into an id filter, so every name-based company query there returned nothing.
Auto Apply goes synchronous
Breaking (beta surface): at api_version 2026-08-12, the Auto Apply webhook mechanism is removed and the whole flow is synchronous.
  • POST /api/auto-apply/applications blocks until the first step’s fields come back in current_step.
  • The new POST /api/auto-apply/applications/{id}/answers validates answers synchronously; a bad value is a free 400 with per-field errors. It then fills the form, auto-continues or auto-submits, and blocks until the next step or the terminal result.
  • GET /applications/{id} gains wait_seconds for long-poll re-attach.
Removed: callback_url, webhook settings and secrets, application.fields_requested and terminal events, HMAC signing, the webhook activity log, and all callback_* error codes. Added: the failure codes answers_timeout and verification_timeout, answers_expire_at on steps, and current_step on applications. Local development no longer needs a tunnel.The @jobo-ai/autoapply SDK is rewritten as 1.0.0 around the new loop, including an applications.run() helper. See The application loop.
Auto Apply beta
Auto Apply opens as an invite-only beta at api_version 2026-07-21. POST /api/auto-apply/applications now returns 202 for granted accounts. Accounts without access get 403 auto_apply_not_enabled instead of the previous 503 auto_apply_coming_soon. Webhook settings, secret rotation, verification and the sandbox are live in the dashboard.The callback docs now state two missing rules. application.fields_requested is a synchronous request whose response body carries the answers. A non-2xx or non-JSON reply fails the application permanently, with no retry. A new local development guide explains that a callback must be a public HTTPS address, so localhost needs a tunnel.
Documentation
The documentation now covers the API only; the Data Platform section moved to jobo.world. Filter enum values are corrected throughout: experience_level is intern | entry | mid | senior | lead | executive, and employment_type includes freelance. New pages cover idempotency, pagination, response headers and the 7-day expiry lookback window. The source catalog reference was updated to 106 providers.
Auto Apply preview
Published the profileless, callback-driven Auto Apply contract as a preview, at api_version 2026-07-21. Application creation was not yet enabled: POST /api/auto-apply/applications returned 503 auto_apply_coming_soon until launch.