# Job Monkey API > Clean, open jobs with GUIDs, kept current. One key, JSON, cursors that never skip. The one thing charged is a full job returned by the full feed; events, updates, companies and counts are free. Docs: https://www.jobmonkey.ai/docs/ ; OpenAPI: https://www.jobmonkey.ai/openapi.json ; free key: https://app.jobmonkey.ai/signup ## Routes - GET /v1/jobs: Full jobs by cursor: a first load cut to countries and systems (charged: every full job in the answer, every time). Docs: https://www.jobmonkey.ai/docs/get-v1-jobs - POST /v1/jobs: Full jobs by GUID (up to 1,000); closed or unknown GUIDs come back free in `missing` (charged: every full job in the answer, every time; a GUID in `missing` is free). Docs: https://www.jobmonkey.ai/docs/post-v1-jobs - GET /v1/jobs/count: Open jobs, held closures and companies for a cut (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-jobs-count - GET /v1/events: Job events after a cursor: new, changed (with the new values), closed, reopened (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-events - GET /v1/updates: Changed fields with their new values: the events stream filtered to `changed` (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-updates - GET /v1/events/count: Events after a cursor, by kind: how far behind a buyer is (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-events-count - GET /v1/companies: Companies in GUID order for a cut, or by GUID (`guid=a,b,c`) (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-companies - GET /v1/companies/events: Company changes after a cursor (the same ledger numbers as the job events) (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-companies-events - GET /v1/search: Title search over open jobs: shape=page (the card) or shape=match (GUIDs for a campaign) (charged: nothing). Docs: https://www.jobmonkey.ai/docs/get-v1-search - GET /v1/snapshots: The newest weekly snapshot: its files, their jobs, bytes and sha256 (charged: nothing for the list). Docs: https://www.jobmonkey.ai/docs/get-v1-snapshots - POST /v1/snapshots/{stamp}/{file}: A 24-hour link to one snapshot file (`everything` or a country); the file's jobs are charged once (charged: the file's jobs, once, at link time; a second link for the same file inside 24 hours is free; the companies file comes free). Docs: https://www.jobmonkey.ai/docs/post-v1-snapshots-stamp-file - GET /v1/health: 200 when every database answers (charged: nothing; no key). Docs: https://www.jobmonkey.ai/docs/get-v1-health ## Prices Pay as you go $1.20 per 1,000 jobs; Pro 100k $99 a month for 100,000 jobs, overage $1.20 per 1,000; Scale 500k $349 a month for 500,000 jobs, overage $0.90 per 1,000; Volume 2M $999 a month for 2,000,000 jobs, overage $0.60 per 1,000; free: 250 jobs a month, no card, no end date. --- # Start here Five minutes, three steps, nothing to install beyond `curl`. ## 1. Get a free key [Sign up](https://app.jobmonkey.ai/signup) with your email. Click the link in the mail, set a password, and your first key is on the screen, once. Copy it somewhere safe: we keep only its first twelve characters. The free plan has no end date: 250 full jobs and 25 full-feed calls a month, 10 calls a day on the free routes (events, updates, companies, counts). No card. ## 2. Count before you load ``` curl -s 'https://api.jobmonkey.ai/v1/jobs/count?country=Canada' \ -H 'Authorization: Bearer cb_live_XXXXXXXX' ``` ```json {"jobs": 81137, "held": 1904, "companies": 2874, "as_of": "2026-11-07T19:35:59Z"} ``` Counts are free. `held` are closures we are still checking (see [Held closures](/docs/held-closures)); `jobs` includes them. ## 3. Read your first page ``` curl -s 'https://api.jobmonkey.ai/v1/jobs?country=Canada&limit=2' \ -H 'Authorization: Bearer cb_live_XXXXXXXX' ``` The answer holds two full jobs, `next` (the cursor for the next page), `more`, `ledger_seq` and `charged: 2`. Two of your 250 free jobs are used. Every field of a job is in [Fields](/docs/fields). The same in Python, standard library only: ```python import json, urllib.request KEY = "cb_live_XXXXXXXX" req = urllib.request.Request("https://api.jobmonkey.ai/v1/jobs?country=Canada&limit=2", headers={"Authorization": f"Bearer {KEY}"}) with urllib.request.urlopen(req) as r: page = json.load(r) for job in page["jobs"]: print(job["job_guid"], job["title"], job["company"]) print("next:", page["next"], "ledger_seq:", page["ledger_seq"]) ``` ## What is next - [Your first sync in three calls](/docs/first-sync): load once, then read events, then fetch by GUID. - [The cursor](/docs/cursor): why it never skips and how to keep it. - [Prices](/docs/prices): what is charged (one thing) and what is not (everything else). Every answer carries `X-Request-Id`; quote it when you write to support@jobmonkey.ai. --- # Your first sync in three calls A sync is three calls, and only one of them costs money. | Call | What it does | Charged | |---|---|---| | `GET /v1/jobs` (or a snapshot file) | the first load of your cut, page by page in GUID order | every full job, once per page | | `GET /v1/events` | what changed since your cursor: new, changed (with the new values), closed, reopened | nothing | | `POST /v1/jobs` | the full record of the GUIDs you want after a `new` or `reopened` event | every full job returned | ## 1. The first load Page your cut until `more` is false. Keep the last page's `ledger_seq`: that is your first cursor. ``` curl -s 'https://api.jobmonkey.ai/v1/jobs?country=Canada&system=greenhouse,lever.co&limit=1000' \ -H 'Authorization: Bearer cb_live_XXXXXXXX' ``` A job that changed, closed or was born while you were paging is harmless: the same change reaches you as an event from the cursor, and every event is idempotent (set the fields, upsert, drop). Held closures are in the pages, marked by `closing_since`: keep them out of matching while it is set. For a whole country at once, a [snapshot](/docs/snapshots) file is the same jobs in one download; the price is the same per job. ## 2. Read events from the cursor ``` curl -s 'https://api.jobmonkey.ai/v1/events?country=Canada&system=greenhouse,lever.co&cursor=9648658&limit=10000' \ -H 'Authorization: Bearer cb_live_XXXXXXXX' ``` Apply each event in `seq` order: - `new`: fetch the job by GUID (step 3) and insert it. - `changed`: `fields` holds the served fields that moved with their **new values**; set them. No fetch needed. - `closed`: drop or hide the job; `closed_at` says when it left its board. - `reopened`: fetch it again by GUID. Pass `next` back as `cursor`; when `more` is false you are at `top`. Call as often as you like: events are free and have no daily cap on a paid key. ## 3. Fetch the full records ``` curl -s -X POST https://api.jobmonkey.ai/v1/jobs \ -H 'Authorization: Bearer cb_live_XXXXXXXX' -H 'Content-Type: application/json' \ -d '{"guids": ["21b3928d-c2ea-4b3b-8281-7bdf05adbf1b"]}' ``` Up to 1,000 GUIDs a call. A GUID that closed between the event and your call comes back in `missing`, free. Send an `Idempotency-Key` header so a retry after a timeout costs nothing. ## The loop in Python ```python import json, urllib.request KEY, API = "cb_live_XXXXXXXX", "https://api.jobmonkey.ai" CUT = "country=Canada&system=greenhouse,lever.co" def call(path, body=None): data = json.dumps(body).encode() if body else None req = urllib.request.Request(API + path, data=data, headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}) with urllib.request.urlopen(req) as r: return json.load(r) # 1. the first load cursor, page = None, {"more": True} while page["more"]: page = call(f"/v1/jobs?{CUT}&limit=1000" + (f"&cursor={page['next']}" if page.get("next") else "")) store(page["jobs"]) # your upsert cursor = page["ledger_seq"] # 2 and 3. every few minutes, for ever while True: ev = call(f"/v1/events?{CUT}&cursor={cursor}&limit=10000") want = [e["job_guid"] for e in ev["events"] if e["event"] in ("new", "reopened")] for i in range(0, len(want), 1000): store(call("/v1/jobs", {"guids": want[i:i + 1000]})["jobs"]) for e in ev["events"]: if e["event"] == "changed": update(e["job_guid"], e["fields"]) elif e["event"] == "closed": drop(e["job_guid"]) cursor = ev["next"] if not ev["more"]: sleep(300) ``` Store the cursor with your data, in the same transaction. If you lose it, `GET /v1/events?since=