> ## Documentation Index
> Fetch the complete documentation index at: https://jobo.world/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Feed prompts for AI assistants

> Copy-paste prompts for your AI coding assistant: build a feed sync into your platform, audit an existing integration, or upgrade an old client library.

Most feed integrations are now written by an AI coding assistant working inside the customer's own codebase. These prompts give the assistant the complete, current API contract, so it builds against what the API actually does instead of what it guesses. Each prompt is self-contained — the assistant needs no other page.

<Note>
  These prompts build against **raw HTTPS**, so the result carries no third-party dependency and the assistant works from the contract on this page. That is a preference, not a requirement — the [official clients](/docs/sdks) cover this whole surface as of 4.0.0, including `updated_after`, the managed feed, and cursor handling. If you would rather use one, the contract below still reads as the reference for what the endpoints do.
</Note>

## Pick your prompt

| Your situation                                                   | Use                                                             |
| ---------------------------------------------------------------- | --------------------------------------------------------------- |
| No integration yet — build the sync into our platform            | [Build a feed sync](#build-a-feed-sync)                         |
| We already sync the feed — check it against the current contract | [Audit an existing integration](#audit-an-existing-integration) |
| We're on a client library older than 4.0.0                       | [Upgrade the client library](#upgrade-the-client-library)       |

<Steps>
  <Step title="Open your assistant at the root of your repo">
    Claude Code, Cursor, Copilot — anything that can read your codebase and edit files.
  </Step>

  <Step title="Copy one prompt below and paste it as your message">
    The copy button on the code block grabs the whole prompt.
  </Step>

  <Step title="Answer its questions, then review the diff">
    Each prompt makes the assistant inspect your stack and report back before writing code, and the build prompt ends with an acceptance checklist you can hold it to.
  </Step>
</Steps>

***

## Build a feed sync

For a platform with **no Jobo integration yet**. Produces a one-time backfill, a scheduled incremental sync, and an expired-job sweep, in your existing stack.

````markdown Build-a-feed-sync prompt [expandable] theme={null}
# Task: integrate the Jobo Enterprise job feed into this codebase

Mirror Jobo's job corpus into this platform's own datastore and keep it current.
Three pieces: (1) a one-time backfill, (2) a scheduled incremental sync, (3) an
expired-job sweep. Everything over raw HTTPS — no vendor SDK.

## First: inspect this repo and confirm the plan

Before writing any code, examine the codebase and report back:
- Language(s) and framework
- The HTTP client already in use
- The database, and the migration tooling for it
- The scheduling mechanism (cron, job queue, hosted scheduler, …)
- How config and secrets are handled
- Logging conventions

Propose where the new code will live, following this repo's conventions, and
wait for my confirmation. Use the existing stack — do not introduce a new HTTP
client, ORM, or scheduler if one exists. Ask me before adding any dependency.

## Hard rules

1. Raw HTTP. Build this against the endpoints below rather than installing a
   client library — we want no third-party dependency in this path. (Official
   clients exist and cover this surface at 4.0.0; we are choosing not to add
   one here.) Do not add an HTTP wrapper of your own beyond what step 4 asks
   for.
2. Read the API key from the `JOBO_API_KEY` environment variable (or this
   repo's existing secret mechanism). Never hardcode it, log it, or commit it.
3. Use only the endpoints, parameters, and values in this prompt. Do not invent
   parameters: the API silently ignores unknown parameters and silently returns
   zero results for unknown filter values, so a guessed name will appear to work.
4. Paginate serially — never fan out cursor requests in parallel.

## API basics

- Base URL: `https://connect.jobo.world`. HTTPS only — plain HTTP gets a `301`
  and most clients drop a POST body on redirect.
- Auth: `X-Api-Key: <key>` header on every request. There is no
  `Authorization: Bearer` form and no query-parameter auth.
- All JSON is snake_case in both directions. No version segment in any path.
- Send `Content-Type: application/json` on POSTs.
- Configure a response timeout of at least 120 seconds — the feed can spend up
  to ~90s server-side before returning a retryable `503`.

## Endpoints

### POST /api/jobs/feed — the bulk feed (metered)

Request body (first request of a scan only — see cursor rules):

```json
{
  "batch_size": 1000,
  "updated_after": "2026-07-27T06:00:00Z",
  "posted_after": null,
  "sources": ["greenhouse", "lever"],
  "work_models": ["remote"],
  "employment_types": ["full-time"],
  "experience_levels": ["senior"],
  "locations": [{ "country": "United States", "region": null, "city": null }],
  "stable_scan": true
}
```

- `batch_size`: 1–1000, default 1000. Always use 1000.
- `updated_after` (ISO 8601, optional): the incremental watermark — returns jobs
  created OR updated on/after it.
- `posted_after` (optional): employer posting date only. NOT a sync watermark —
  it misses edits.
- `sources` (optional): ATS platform ids (`greenhouse`, `lever`, `workday`, …).
  An open set of ~106 values — treat as opaque strings.
- `work_models` (optional): `remote` | `hybrid` | `onsite`
- `employment_types` (optional): `full-time` | `part-time` | `contract` |
  `internship` | `freelance` | `temporary`
- `experience_levels` (optional): `intern` | `entry` | `mid` | `senior` |
  `lead` | `executive`
- `locations` (optional): array of `{country, region, city}` objects.
- `stable_scan`: leave at its default `true`.
- Every field is optional. A full-corpus mirror sends only `{"batch_size": 1000}`.

Response:

```json
{
  "jobs": [ { "...": "Job objects, schema below" } ],
  "next_cursor": "opaque-string-or-null",
  "has_more": true,
  "estimated_total": 123456
}
```

`estimated_total` appears on the first page of a scan only.

### GET /api/jobs/expired — recently expired job ids (free)

Query parameters: `expired_since` (ISO 8601; omitted = last 24h; must be within
the last **7 days**), `batch_size` (1–10000 — use 10000), `cursor`.

```json
{ "job_ids": ["uuid", "uuid"], "next_cursor": "or-null", "has_more": false }
```

### GET /api/jobs/{id} — one job by id (free)

Same job schema. Useful for spot checks and tests.

## Cursor rules (apply to every cursor endpoint)

1. Send filters and `batch_size` on the FIRST request only. The cursor embeds
   them — a continuation request carries ONLY `{"cursor": "..."}` (or the
   `cursor` query param on GET /api/jobs/expired). Anything sent alongside a
   cursor is silently ignored.
2. One in-flight request per cursor. Continue serially until `has_more` is false.
3. Persist `next_cursor` after every successfully processed batch, so a crash
   resumes mid-stream instead of restarting from page one.
4. Never parallelize a scan.

## The job object → our schema

Upsert on `id` — a stable UUID that survives edits to the posting. Store
`updated_at` per row and skip the write when the incoming value is unchanged.
Nullable fields arrive as `null` (never absent); array fields arrive as `[]`
(never null).

| Field | Type | Nullable | Notes |
| --- | --- | --- | --- |
| `id` | uuid | no | Primary key for the upsert |
| `title` | string | no | |
| `normalized_title` | string | yes | snake_case canonical title, e.g. `software_engineer` |
| `company` | object | no | `{id (uuid), name, website?, logo_url?, summary?, industries[], categories[], linkedin_url?, crunchbase_url?, details_url?}` |
| `description` | string | no | **Sanitized HTML, not plain text.** Store as-is; sanitize again (e.g. DOMPurify) if rendered in a web UI |
| `summary` | string | yes | AI summary; null while pending on fresh jobs |
| `listing_url` | string | no | |
| `apply_url` | string | no | |
| `locations` | object[] | no (may be `[]`) | Each: `{location?, city?, region?, country?, latitude?, longitude?}` |
| `compensation` | object | yes | `{min?, max?, currency? (ISO 4217), period?}`; `period` ∈ `yearly` `monthly` `weekly` `daily` `hourly` `per-diem` |
| `employment_type` | string | yes | Display value, e.g. `Full-time` |
| `workplace_type` | string | yes | Display value: `Remote` \| `Hybrid` \| `On-site` |
| `experience_level` | string | yes | Display value, e.g. `Entry Level` |
| `source` | string | no | ATS id. Open set — store as text, never a DB enum or check constraint |
| `created_at`, `updated_at` | datetime | no | |
| `date_posted`, `valid_through` | datetime | yes | |
| `qualifications` | object | no | `{must_have, preferred}`, each `{education[], certifications[], skills[{name, type: "hard"|"soft"}]}` |
| `responsibilities` | string[] | no | |
| `benefits` | string[] | no | |
| `is_work_auth_required` | boolean | yes | |
| `is_h1b_sponsor` | boolean | yes | |
| `is_clearance_required` | boolean | yes | |

Enum gotcha: request filters take the lowercase canonical keys shown under the
endpoint above (`remote`, `full-time`, `senior`), but job objects return display
values (`Remote`, `Full-time`, `Senior`). Never feed a display value back into a
filter — it silently matches nothing. The filter is named `work_models`; the
field on the job is `workplace_type`. There is no `is_remote` parameter or field.

## The sync algorithm

### 1. Backfill (run once)

Record the start time as the first incremental watermark BEFORE the first
request. POST /api/jobs/feed with `{"batch_size": 1000}` (plus any filters we
agree on), loop under the cursor rules, upsert every job by `id`, checkpoint
`next_cursor` after each batch, stop when `has_more` is false.

### 2. Incremental sync (scheduled every 15–60 minutes)

Pick a cadence that fits this repo's scheduler; more often than 15 minutes
wastes credits without surfacing much new data.

a. Read stored `last_run_started_at`. Record now as `this_run_started_at`
   BEFORE the first request.
b. Start a new scan: `{"batch_size": 1000, "updated_after": last_run_started_at
   minus 15 minutes}`. The 15-minute overlap absorbs clock skew and late
   indexing; upserts make it idempotent.
c. Page to completion under the cursor rules. Upsert by `id`; skip rows whose
   stored `updated_at` is unchanged.
d. Only after the loop fully succeeds, persist `this_run_started_at` as the new
   `last_run_started_at`. Never advance the watermark on a failed run.

Use `updated_after`, not `posted_after` — `posted_after` misses edits to jobs we
already hold.

### 3. Expiry sweep (same schedule, same run)

The feed returns ACTIVE jobs only. An expired job never comes back with a flag —
it simply stops appearing. So after each incremental run:

GET /api/jobs/expired?expired_since=<last_run_started_at>&batch_size=10000,
page with the cursor, and mark every returned id expired/inactive in our
datastore (soft-flag rather than delete, unless this repo prefers hard deletes).

Constraint: `expired_since` must be within the last 7 days. If our sync stalls
longer than that, expired jobs become unrecoverable through this endpoint.
Detect the case (watermark older than 7 days) and recover: run a full backfill
again, then mark as expired every stored id that no longer appeared.

## Errors and retries

- Retry ONLY `429`, `500`, `503`. Anything else is a defect in the request —
  fix it, don't retry it.
- `429` / `503`: wait exactly the `Retry-After` header value (seconds). Cap at
  3–5 attempts.
- `500`: exponential backoff 1s → 30s with ±25% jitter, 3–5 attempts.
- `400` with `"code": "invalid_cursor"`: drop the cursor and restart the scan
  from the beginning. Never resend the same cursor.
- `409` with `"code": "feed_cursor_restart_required"`: restart the scan with
  `{"stable_scan": true, "batch_size": 1000}`.
- `401`: key problem — stop and alert. `402`: out of credits
  (`"error": "Insufficient credits"`) or a missing subscription
  (`"Active subscription required"`) — stop and alert; retrying cannot help.
- Branch on the status code and body fields (`code` where present, else the
  `error` string) — NOT on Content-Type, which varies between
  `application/json` and `application/problem+json`.
- Log the `x-correlation-id` response header on every failure — Jobo support
  needs it to trace the request.

## Rate limits and cost

- Feed endpoints share one per-key budget (`X-RateLimit-Group: JobFeed`):
  30 requests/min on the free tier (60/min subscribed), 1,000/hour (10,000
  subscribed), 10,000/day (100,000 subscribed). Read `X-RateLimit-Remaining`
  and slow down below ~15% of `X-RateLimit-Limit` instead of bouncing off 429s.
- The feed meters per delivered job (waived with a Jobs Feed subscription), so
  always use `batch_size: 1000` — larger batches cost no more per job and use a
  tenth of the request budget. `GET /api/jobs/expired` and `GET /api/jobs/{id}`
  are free.
- Surface the `X-Credits-Balance` response header and alert when it runs low.

## Deliverables

1. A migration for the jobs table plus sync state (`last_run_started_at`, the
   in-flight cursor checkpoint), following this repo's migration conventions.
2. The sync module: backfill entrypoint, incremental entrypoint, expiry sweep,
   and one shared HTTP helper implementing the retry rules above.
3. Scheduling wired into this repo's existing mechanism (every 15–60 minutes).
4. Tests with mocked HTTP covering: the cursor loop resuming from a checkpoint,
   watermark-advances-only-on-success, expiry marking, `Retry-After` honored on
   429/503, and `invalid_cursor` causing a restart rather than a retry loop.
5. A short README section: required env vars, how to run the backfill, and how
   the 7-day recovery path works.

## Acceptance checklist — verify every item before calling it done

- [ ] No `jobo-enterprise` / `Jobo.Enterprise.Client` dependency anywhere.
- [ ] Continuation requests carry only the cursor.
- [ ] The watermark advances only after a fully successful run.
- [ ] The expiry sweep runs on the same schedule as the incremental sync, and a
      stall past 7 days triggers (or at minimum flags) a full re-backfill.
- [ ] Upserts key on `id`; unchanged `updated_at` skips the write.
- [ ] Retries cover exactly 429/500/503, honor `Retry-After`, and restart on
      `invalid_cursor` / `feed_cursor_restart_required`.
- [ ] Client timeout ≥ 120s, API key from env, HTTPS URLs only, serial
      pagination.
````

***

## Audit an existing integration

For a platform that **already syncs the feed**. The assistant locates your integration, checks it against the current contract, and reports findings with severities before changing anything.

```markdown Audit-a-feed-integration prompt [expandable] theme={null}
# Task: audit our Jobo job-feed integration against the current API contract

Find our integration, verify it against the contract below, and produce a
findings report. Do not change any code until I approve the report.

## Locate the integration

Search the codebase for: `connect.jobo.world`, `jobs-api.jobo.world`,
`/api/jobs/feed`, `/api/jobs/expired`, `X-Api-Key`, `jobo_enterprise`,
`jobo-enterprise`, `Jobo.Enterprise.Client`. Map every file that touches the
Jobo API before auditing.

## Contract reference (current, condensed)

- Base `https://connect.jobo.world` (HTTPS only; `jobs-api.jobo.world` is a
  legacy alias that still works). Auth header `X-Api-Key`. All JSON snake_case.
- `POST /api/jobs/feed` — body on the FIRST request of a scan:
  `batch_size` (1–1000), optional `updated_after`, `posted_after`, `sources[]`,
  `work_models[]` (`remote|hybrid|onsite`), `employment_types[]`
  (`full-time|part-time|contract|internship|freelance|temporary`),
  `experience_levels[]` (`intern|entry|mid|senior|lead|executive`),
  `locations[]` (`{country, region, city}`), `stable_scan` (default true).
  Response `{jobs[], next_cursor, has_more, estimated_total?}`. Continuation
  requests carry ONLY `{"cursor": ...}` — other values are silently ignored.
- `GET /api/jobs/expired?expired_since=...&batch_size=<=10000&cursor=...` —
  free; `expired_since` max 7 days back, default 24h. Response
  `{job_ids[], next_cursor, has_more}`.
- `GET /api/jobs/{id}` — free single-job lookup.
- Retryable statuses: exactly 429, 500, 503. `Retry-After` (seconds) is
  authoritative on 429/503. `400 invalid_cursor` → restart the scan.
  `409 feed_cursor_restart_required` → restart with
  `{"stable_scan": true, "batch_size": 1000}`.
- Rate limits: `JobFeed` group, 30/min free (60/min subscribed); headers
  `X-RateLimit-Limit/-Remaining/-Used/-Group`. Metered per delivered job;
  `X-Credits-Balance` on debits. The feed's server-side budget is ~90s, so
  client timeouts must be ≥ 120s.
- Job objects: `id` is a stable UUID (the upsert key). `updated_at` changes on
  edits. `description` is sanitized HTML. Filters take lowercase canonical
  values; job fields return display values (`Remote`, `Full-time`, `Senior`).
  `source` is an open set of ~106 ids. Nullable fields arrive as `null`,
  arrays as `[]`.

## Checklist — verify each item, citing file and line

### Critical

1. **Client library below 4.0.0.** `jobo-enterprise` (PyPI/npm) and
   `Jobo.Enterprise.Client` (NuGet) before 4.0.0 have no `updated_after` on
   feed methods, so any sync built on them is re-scanning rather than syncing
   incrementally. Their enums are also missing `freelance` and `intern`. Report
   the pinned version; 4.0.0 fixes both, and the upgrade is the smallest fix
   available.
2. **No expiry sweep.** The feed returns active jobs only; expired jobs simply
   stop appearing. If nothing calls `GET /api/jobs/expired` on the same
   schedule as the sync, our datastore is accumulating dead rows. Quantify if
   possible (jobs in our store not updated in > 14 days).
3. **Wrong watermark.** Incremental sync must use `updated_after` (with a
   ~15-minute overlap), not `posted_after` (misses edits) and not a full
   re-scan every run (wastes credits). The watermark must be recorded BEFORE
   the run's first request and persisted only AFTER the run fully succeeds.
4. **Cursor misuse.** Continuation bodies must carry only the cursor (values
   sent alongside are ignored — if we "change filters mid-scan", it never
   worked). Pagination must be serial, and `next_cursor` checkpointed so a
   crash resumes.
5. **Display values used as filters.** Filters must send canonical lowercase
   values (`remote`, `full-time`, `senior`). Sending `Remote` or
   `Senior` returns 200 with zero results — silently.
6. **The 7-day expiry window unhandled.** `expired_since` older than 7 days is
   rejected. If our sync can stall past 7 days with no full-backfill recovery
   path, dead rows become permanent.

### Important

7. Retry set is exactly {429, 500, 503}; `Retry-After` honored; `invalid_cursor`
   restarts rather than loops; `feed_cursor_restart_required` handled.
8. Client timeout ≥ 120 seconds.
9. Upserts key on `id`; unchanged `updated_at` skips the write.
10. `batch_size` is 1000 on the feed (metering is per delivered job — small
    batches waste the request budget) and up to 10000 on the expired sweep.
11. API key comes from env/secret store, not source; all URLs are `https://`.
12. `description` handled as sanitized HTML — not double-escaped, not rendered
    raw without a sanitizer pass in web UIs.
13. `source` stored as open text — no DB enum or check constraint that a new
    ATS id would violate.

### Minor

14. Base URL is `connect.jobo.world` (flag `jobs-api.jobo.world` as legacy).
15. Pacing reads `X-RateLimit-Remaining` instead of reacting to 429s;
    `x-correlation-id` is logged on failures.
16. Null vs empty-array semantics handled per the contract.

## Report format

A table — severity, finding, file:line, impact, proposed fix — followed by a
short paragraph per critical finding. If historical damage is plausible
(zero-result filters, missing expiry sweeps), say what a recovery run looks
like. Then STOP and wait for my approval before implementing fixes.
```

***

## Upgrade the client library

For a platform already running `jobo-enterprise` or `Jobo.Enterprise.Client` on a
version below 4.0.0. Those versions couldn't express incremental sync, so the
assistant upgrades the dependency, moves the sync onto `updated_after`, and checks
for damage the old enums may have done to stored data.

If you would rather not carry the dependency at all, use [Build a feed
sync](#build-a-feed-sync) instead — it produces a dependency-free integration
against the same contract.

```markdown Upgrade-the-client prompt [expandable] theme={null}
# Task: upgrade the Jobo client library to 4.0.0 in this codebase

We use an official Jobo client — `jobo-enterprise` (PyPI or npm) or
`Jobo.Enterprise.Client` (NuGet) — pinned below 4.0.0. Upgrade it, adopt the
capabilities the old version could not express, and check whether the old
version's enum values corrupted anything we stored.

## Step 1 — inventory

Report before changing anything:

- The pinned version in the manifest and lockfile.
- Every import/usage of `jobo_enterprise`, `jobo-enterprise`, or
  `Jobo.Enterprise.Client`, with the method called and the parameters passed.
- Whether our feed sync re-scans everything each run or syncs incrementally.

## Step 2 — what changes in 4.0.0

Breaking:

- `client.auto_apply` / `client.autoApply` / `client.AutoApply` **no longer
  exists**. It targeted routes the API removed, so nothing on it worked. If we
  call it, remove those call sites — the current Auto Apply contract is
  callback-driven and not yet accepting traffic.
- `EmploymentType` members now serialize `full-time` / `part-time` rather than
  `full_time` / `part_time`. Both spellings match on the wire, so this changes
  what we send, not what we get back.
- `CompensationPeriod` members were renamed to the values the API actually
  emits — `hourly`, `daily`, `weekly`, `monthly`, `yearly`, `per-diem`. The old
  members (`hour`, `day`, `week`, `month`, `year`) matched none of them.
- Node only: if we are on 1.x, the surface moved from flat methods
  (`searchJobs`, `getJobsFeed`) to sub-clients (`client.search.search(...)`,
  `client.feed.getJobs(...)`).

New, and worth adopting:

- Feed: `updated_after`, `stable_scan`, `employment_types`, `experience_levels`.
- Search: `search_description`, `posted_before`, `discovered_after`,
  `discovered_before`, `include_fields`.
- `get_job(id)` for a single job — unmetered, so it costs no credits.
- The managed feed (`get_managed_jobs` / `iter_managed_jobs`), if we use Managed
  Job Scraping.
- `expired_since` is now optional and defaults to a 24-hour lookback.
- `429` and `503` are retried internally with bounded backoff honouring
  `Retry-After`, and `403` / `404` / the `409` cursor restart raise distinct
  typed errors carrying the API's problem `code`.

## Step 3 — check for latent damage

- **Stored compensation periods.** If we mapped API responses through the old
  `CompensationPeriod` members, stored values may be wrong — the old members
  matched nothing the API emits. Check what is actually in our column.
- **Missing filter values.** The old enums had no `freelance` or `intern`. If a
  UI offers those filters, confirm they were passed as raw strings and not
  silently dropped.
- **Re-scanning.** Without `updated_after`, an incremental sync built on the old
  version was re-reading the whole feed every run. Quantify the wallet cost if
  you can.

## Step 4 — implement

1. Bump the dependency to `4.0.0` in the manifest and lockfile.
2. Fix the breaking changes from Step 2 so the project builds and tests pass.
3. Move the feed sync to `updated_after = last run start − 15 minutes`, with the
   watermark persisted only after a fully successful run. Leave `stable_scan` at
   its default.
4. Add a `GET /api/jobs/expired` sweep on the same schedule if one is missing —
   the feed returns active jobs only, so expired rows never disappear on their
   own. `expired_since` looks back 7 days at most.
5. Replace any hand-rolled HTTP that existed only to reach a parameter the old
   client lacked.

## Acceptance

- [ ] The manifest and lockfile pin 4.0.0.
- [ ] No reference to the removed auto-apply surface anywhere.
- [ ] Incremental sync uses `updated_after`, not `posted_after` and not a full
      re-scan.
- [ ] An expiry sweep runs on the same schedule as the sync.
- [ ] A written note on any latent damage found in Step 3.
```

## After the assistant is done

Whichever prompt you used, hold the result against the prose guide — [Sync a database](/docs/guides/sync-a-database) is the human-readable version of the same contract. The things worth checking by hand:

* Continuation requests carry **only** the cursor.
* The watermark advances **only after a successful run**, and there's an expiry sweep on the same schedule.
* Filters send canonical lowercase values, never the display values the API returns.

<CardGroup cols={2}>
  <Card title="Sync a database" icon="database" href="/docs/guides/sync-a-database">
    The same contract as prose — backfill, incremental sync, expiry sweeps, and working Python.
  </Card>

  <Card title="The job object" icon="briefcase" href="/docs/api-reference/jobs/object">
    Every field, its type, nullability, and the enum send/receive table.
  </Card>
</CardGroup>
