All platforms

Fountain Jobs API.

Tap high-volume hourly and frontline hiring at the source: pull every active opening, its location, and full HTML description from any Fountain apply directory through one unified endpoint.

Get API access
Fountain
Live
<3haverage discovery time
1hrefresh interval
Developer tools

Try the API.

Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.

What's in every response.

Data fields, real-world applications, and the companies already running on Fountain.

Data fields
  • Full HTML job descriptions
  • Job location & country code
  • Company & brand names
  • Native funnel, account & brand UUIDs
  • Locale & position name
  • Public apply URLs
Use cases
  1. 01Hourly & Frontline Job Aggregation
  2. 02High-Volume Hiring Market Data
  3. 03Real-Time Openings Monitoring
  4. 04Location-Based Job Feeds
DIY GUIDE

How to scrape Fountain.

Step-by-step guide to extracting jobs from Fountain-powered career pages—endpoints, authentication, and working code.

HybridintermediateNo published server limit; crawl politely (1 request at a time)No auth

Fetch the tenant apply directory

Each Fountain customer publishes its complete public opening inventory at /{tenant}/apply as server-rendered HTML. Tenants live on region-specific hosts (web, ap-1, eu-1, us-1), so preserve the exact host you discovered the board on.

Step 1: Fetch the tenant apply directory
import requests
from bs4 import BeautifulSoup

# Tenants live on region-specific hosts: web, ap-1, eu-1, us-1.
HOST = "web.fountain.com"
tenant = "foodja"
origin = f"https://{HOST}"
directory_url = f"{origin}/{tenant}/apply"

resp = requests.get(directory_url, headers={"Accept": "text/html"}, timeout=30)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")

Parse the advertised total and opening links

The directory renders one anchor per active opening plus an advertised "N Openings" total. Read both and fail closed on any mismatch — a shrinking board should never be trusted as complete.

Step 2: Parse the advertised total and opening links
import re

# Fountain-rendered directories end their <title> with "(Fountain)".
title = (soup.title.string or "").strip()
if not title.endswith("(Fountain)"):
    raise RuntimeError("Not a Fountain-rendered apply directory")

# Advertised total, e.g. "5 Openings".
count_match = re.search(r"(\d[\d,]*)\s+Openings?", soup.get_text(), re.IGNORECASE)
advertised_total = int(count_match.group(1).replace(",", "")) if count_match else 0

# One anchor per active opening, each linking to /{tenant}/apply/{slug}.
openings = []
for a in soup.select("ul.positions-published a.app-position-links[href]"):
    slug = a["href"].rstrip("/").split("/")[-1]
    label = a.get_text(strip=True)
    if slug and label:
        openings.append({"slug": slug, "url": f"{origin}/{tenant}/apply/{slug}"})

# Fail closed if the board disagrees with its own advertised total.
if advertised_total and len(openings) != advertised_total:
    raise RuntimeError(f"Advertised {advertised_total} openings but linked {len(openings)}")

Hydrate each opening via the application-form API

Every opening resolves through the anonymous native endpoint /internal_api/portal/{tenant}/application_forms/new. It needs no token — just Accept: application/json and a Referer of the opening URL — and returns the immutable job UUID, title, HTML description, location, and company.

Step 3: Hydrate each opening via the application-form API
import time

jobs = []
for op in openings:
    api_url = f"{origin}/internal_api/portal/{tenant}/application_forms/new"
    r = requests.get(
        api_url,
        params={"funnel_id": op["slug"]},
        headers={"Accept": "application/json", "Referer": op["url"]},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json() or {}
    funnel = data.get("funnel") or {}

    # Skip funnels the directory linked but the API reports inactive.
    if funnel.get("active") is not True:
        continue

    account = data.get("account") or {}
    brand = data.get("brand") or {}
    location = funnel.get("location") or {}
    jobs.append({
        "id": funnel["external_id"],              # immutable Fountain job UUID
        "title": funnel["title"],
        "description_html": funnel["position_description_html"],
        "location": location.get("name"),
        "country_code": funnel.get("country_code"),
        "locale": funnel.get("locale"),
        "company": account.get("name") or brand.get("name"),
        "apply_url": op["url"],
    })
    time.sleep(0.1)  # crawl politely: one opening request at a time

Deduplicate on the native funnel UUID

The human-readable route slug is mutable and can be renamed, so never use it as the persistent job key. Deduplicate and store openings on funnel.external_id, the UUID Fountain guarantees is stable.

Step 4: Deduplicate on the native funnel UUID
# The route slug is mutable; dedupe on the immutable funnel UUID instead.
unique = {job["id"]: job for job in jobs}
print(f"{len(unique)} unique openings for {tenant}")
Common issues
highUsing the URL route slug (e.g. /forkable/apply/chicago-delivery-driver) as the job's persistent ID.

The route slug is mutable and can be renamed, causing churn and duplicates. Hydrate the application-form API and key each job on funnel.external_id, the immutable UUID.

mediumThe advertised "N Openings" total disagrees with the number of linked openings on the directory page.

Treat the mismatch as an incomplete snapshot rather than the real inventory. Compare the advertised count to the app-position-links you parsed and fail closed instead of publishing a partial board.

mediumA directory briefly links a funnel that the API reports as inactive or that has an empty description.

Skip any opening where funnel.active is not true or position_description_html is blank, and mark the snapshot incomplete — never let a missing detail delete an otherwise-valid job.

mediumRequests fail because the tenant is queried on the wrong regional host.

Fountain serves tenants across web-, ap-1-, eu-1-, and us-1.fountain.com. Preserve the exact host from the apply URL you discovered rather than defaulting every tenant to web.fountain.com.

lowAttempting to paginate the directory with a page cursor returns nothing.

Fountain publishes its complete public directory in a single HTML response and does not accept pagination cursors. Read the whole board in one request and iterate the parsed opening links.

Best practices
  1. 1Key and dedupe jobs on funnel.external_id (the native UUID), never the mutable route slug.
  2. 2Preserve the exact regional host (web / ap-1 / eu-1 / us-1) from the source apply URL.
  3. 3Compare the advertised "N Openings" total against linked openings and fail closed on mismatch.
  4. 4Hydrate every directory link before trusting a snapshot; a shrinking board must not authorize deletions.
  5. 5Send Accept: application/json and a Referer of the opening URL when calling the internal application-form API.
  6. 6Throttle to one application-form request at a time with a short (~100 ms) delay between openings.
Or skip the complexity

One endpoint. All Fountain jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=fountain" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access Fountain
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed