# Quickstart

From nothing to a real response in one command, and to an authenticated one in about a minute. No card at any point.

**What this covers:** Step one — a real response, with no account; Step two — get a key; Step three — your first authenticated call; Step four — the three things to get right before you ship.

**Assumed knowledge:** A terminal with curl, or any HTTP client. No account.

**Canonical HTML:** https://jobopportunitiesapi.org/docs/quickstart  
**Machine-readable index:** https://jobopportunitiesapi.org/docs/ai/index.md  
**Last verified:** 2026-08-22  
**Superseded by:** the live API at https://api.jobopportunitiesapi.org and its spec at https://jobopportunitiesapi.org/openapi.json — where this file and the API disagree, the API is right.

---

> **Two hosts, and getting them the wrong way round costs people hours** — The API is `api.jobopportunitiesapi.org`. The website is `jobopportunitiesapi.org`, and it is edge-protected — a non-browser client sending a request there may be challenged and read it as a 403. Every example on this site uses the API host. See [Hosts](./api-overview.md#hosts).

---

<a id="qs-keyless"></a>

## 1. Step one — a real response, with no account

Everything under /public/* answers without a key. This is real data, not a sandbox: the same rows the paid endpoints return.

### 1.1 In depth

The keyless surface exists so that evaluating this product costs you nothing and requires no conversation. It serves from the same projection as `/v1`, so a row you see here is a row you would receive on a paid key — the differences are page size, throughput, and the endpoints that do not have a keyless mirror (the delta feed and bulk export).

It is bounded on purpose: about two requests a second sustained, forty a minute, and fifty rows a page. That is comfortable for evaluation and for a small site, and deliberately uncomfortable for extraction. Breaching it earns a ten-minute block, not a permanent one.

### 1.2 Exact contract

`Access-Control-Allow-Origin: *` is set, so this works from a browser console and from client-side JavaScript with no proxy. Responses are edge-cached — `/public/jobs` for 300 seconds, `/public/jobs/{slug}` for 600 — so a repeated request may not reach the origin at all, and `cf-cache-status` in the response headers tells you which happened.

Two parameters behave differently here than on `/v1`: `limit` is capped at 50 rather than 200, and the parameters marked *paid endpoints only* in [the parameter reference](./api-parameters.md) — `status`, `quality`, `include_poster_type` — are ignored rather than honoured.

### 1.3 Worked examples

Three live roles in France

```console
$ curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=FR&limit=3'
{
  "data": [
    {
      "id": "4a879276-f754-4748-8e5f-7aac306c7f1c",
      "slug": "global-digital-communications-manager-f-m-4a879276",
      "title": "Global Digital Communications Manager F/M",
      "company": "betclic-group",
      "company_slug": "betclic-group",
      "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/betclic.png",
      "category_confidence": null,
      "country": "FR",
      "city": "Bordeaux",
      "location": "Bordeaux, FR",
      "remote": "remote",
      "remote_inferred": false,
      "employment_type": "Full-time",
      "posted_at": "2026-09-11T09:36:08Z",
      "first_seen_at": "2026-09-11T09:43:02Z",
      "last_verified_at": "2026-09-11T09:50:45Z",
      "status": "live",
      "closed_at": null,
      "closed_reason": null,
      "apply_url": "https://betclic-group.breezy.hr/p/c7e2f473ddf801-global-digital-communications-manager-f-m",
      "source": "breezy",
      "source_type": "ats",
      "provider_type": "employer_ats",
      "has_description": false,
      "field_sources": {
        "remote": "published",
        "employment_type": "published",
        "category": "absent",
        "seniority": "absent",
        "salary": "absent",
        "location": "inferred",
        "posted_at": "published",
        "description": "absent",
        "source_type": "inferred"
      }
    },
    {
… 77 more lines
```

_Pipe it through `jq` if you have it; the shape is the same either way. Real response, fetched from `/public/jobs?country=FR&limit=3` when this file was built (11 September 2026, 09:54 UTC)._

The same call, readable, if you have jq installed

```bash
curl -s 'https://api.jobopportunitiesapi.org/public/jobs?country=FR&limit=3' \
  | jq '.data[] | {title, company, city, source, apply_url}'
```

**See also**

- [The limits, exactly](./api-keyless.md#keyless-limits)
- [Step two — get a key](./quickstart.md#qs-get-key)

<a id="qs-get-key"></a>

## 2. Step two — get a key

Sign in with your email at /login, click through the link it sends, and create a free key on the dashboard. No password, no card.

### 2.1 In depth

Sign-in is passwordless. You give an email address, the site emails you a one-time link, and following that link signs you in. There is no password to choose, to forget or to leak, and there is nothing in the database that could be stolen and replayed as one.

The free plan (**Explore**) is created from the dashboard with one click and needs no payment details. It gives you every REST endpoint against the live ledger, with a monthly record allowance rather than a trial period — it does not expire. The exact allowance is in [the plans table](./account-plans.md#plans-table), rendered from the live price list rather than typed here.

> **The secret is shown once** — When a key is created the dashboard shows the secret a single time. It is stored as a SHA-256 hash, so nobody — including us — can recover it afterwards. If you lose it, rotate the key; the key keeps its identity and its usage history and gets a new secret. See [Rotating a key](./account-sign-in.md#key-rotation).

### 2.2 Exact contract

1. Open [/login](https://jobopportunitiesapi.org/login) and enter an email address.
2. The site POSTs to its own `/api/auth/request`, which asks the API for a magic link and mails it. Nothing is stored against the address until you follow the link.
3. Following the link hits `/auth/verify`, which exchanges the one-time token for a session cookie and redirects to `/dashboard`.
4. On the dashboard, **Create a free key** calls `POST /public/auth/keys` with that session. The response carries the secret; the page shows it once.
5. Send it as `Authorization: Bearer <secret>` from then on.

The session is a signed value in a cookie, not a row in a session table, and it lasts 30 days. Signing out clears the cookie. Neither the login flow nor the dashboard is part of the API contract — they are the website — but they are documented in full under [Sign-in and the dashboard](./account-sign-in.md).

**See also**

- [Signing in](./account-sign-in.md#signin-magic-link)
- [The plans](./account-plans.md#plans-table)
- [Creating, naming, rotating and revoking](./api-authentication.md#auth-keys-lifecycle)

<a id="qs-first-authenticated"></a>

## 3. Step three — your first authenticated call

Send the key as a bearer token to api.jobopportunitiesapi.org. Everything under /v1 uses exactly this header.

### 3.1 In depth

The first call worth making is `/v1/me`, because it answers the two questions you will ask again later: which plan is this key on, and how much of it have I used. It costs no records.

After that, `/v1/jobs` is the endpoint you will spend most of your time in. It takes about thirty-five parameters; you do not need to learn them, you need to know that [the reference exists](./api-parameters.md) and is generated from the specification rather than written by hand.

### 3.2 Exact contract

Three forms of the header are accepted: `Authorization: Bearer <key>`, `Authorization: bearer <key>` (the scheme token is case-insensitive) and the bare `Authorization: <key>` with no scheme at all. `Bearer` is the canonical form and the one to write; the tolerance exists so that a client library that strips the scheme does not fail confusingly. See [Authentication](./api-authentication.md).

Every metered response carries `X-RateLimit-Records-Remaining`. Watch it rather than waiting for a 402 — the 402 is not something to retry, it means the month's licence is spent.

### 3.3 Worked examples

Who am I, and what is this key allowed to do

```console
# Replace $JOA_KEY with the secret from the dashboard.
$ export JOA_KEY='your-api-key-here'
$ curl -s -H "Authorization: Bearer $JOA_KEY" https://api.jobopportunitiesapi.org/v1/me
{
  "created": "2026-08-22",
  "key_prefix": "joadocs",
  "plan": "signal",
  "status": "active",
  "limits": { "per_day": 400000, "per_minute": 300 },
  "renews_or_expires": null,
  "usage_today": 1
}
```

_Captured 2026-08-22 from a throwaway Signal key created for this build and revoked after it. Your response names your plan and your prefix. The request is re-sent against the live API on every build of this site; if it stopped answering 2xx the site would not ship._

Two Irish roles, with their provenance

```console
$ curl -s -H "Authorization: Bearer $JOA_KEY" \
  'https://api.jobopportunitiesapi.org/v1/jobs?country=IE&limit=2'
{
  "data": [
    {
      "id": "9d16b6bf-207e-45a7-b41b-64507d32dfaf",
      "slug": "afterschool-coordinator-ballincollig-9d16b6bf",
      "title": "Afterschool Coordinator Ballincollig",
      "company": "Junior Adventures Group",
      "company_slug": "junior-adventures-group",
      "company_logo": "https://supabase-erioun.tzekos.eu/storage/v1/object/public/company-logos/logos/junior-adventures.png",
      "category": "Education",
      "category_confidence": 0.65,
      "country": "IE",
      "city": "Cork",
      "location": "Cork, CO, Ireland",
      "remote": "on_site",
      "remote_inferred": false,
      "posted_at": "2026-09-11T08:23:38Z",
      "first_seen_at": "2026-09-11T09:24:19Z",
      "last_verified_at": "2026-09-11T09:50:45Z",
      "status": "live",
      "closed_at": null,
      "closed_reason": null,
      "apply_url": "https://jobs.smartrecruiters.com/JuniorAdventuresGroup/5ba91c7b-92ad-45c2-936f-5ffcee1e3856",
      "source": "smartrecruiters",
      "source_type": "ats",
      "provider_type": "employer_ats",
      "has_description": false,
      "field_sources": {
        "remote": "published",
        "employment_type": "absent",
        "category": "inferred",
        "seniority": "absent",
        "salary": "absent",
        "location": "inferred",
        "posted_at": "published",
        "description": "absent",
        "source_type": "inferred"
      }
… 43 more lines
```

_The response shown is from the keyless mirror of the same query, because this page is built without a key. On `/v1` the rows are identical; what changes is that `limit` may go to 200 and the paid-only parameters are honoured. Real response, fetched from `/public/jobs?country=IE&limit=2` when this file was built (11 September 2026, 09:54 UTC)._

**See also**

- [The header](./api-authentication.md#auth-bearer)
- [GET /v1/me](./endpoints-meta.md#endpoint-me)
- [The response headers](./api-rate-limits.md#limit-headers)

<a id="qs-next"></a>

## 4. Step four — the three things to get right before you ship

Treat next_cursor as opaque, read field_sources before trusting a field, and handle 402 differently from 429. Everything else is detail.

### 4.1 In depth

- **next_cursor is opaque** — It looks like a timestamp and a uuid joined by a pipe, and it is not: it is a token to hand back verbatim. Parsing it, reconstructing it, or storing its halves separately will work until the day the ordering changes, and then silently skip or repeat rows. [Pagination](./api-pagination.md#cursor-opacity).
- **field_sources before the field** — Every row carries, per field, whether the source **published** it, whether we **inferred** it, or whether it is **absent**. A remote flag we inferred from the location text is not the same fact as one the employer ticked, and the row tells you which it is. [Provenance](./ledger-provenance.md#field-sources).
- **402 is not 429** — 429 means slow down and retry. 402 means this month's record allowance is spent and retrying will not help until the first of the month. A client that treats them the same will hammer a wall for three weeks. [Errors](./api-errors.md#error-402).

### 4.2 Exact contract

Two more that cost less but still cost. First, do not hard-code a filter vocabulary: providers, categories and source types are enumerable at run time from `/public/facets` and `/public/providers`, and a value that has left the list is a 422 rather than an empty page — which is a feature, but only if you read it. Second, keep company identity on `company_domain` where you have it: slugs are not currently guaranteed stable across refreshes. [Company slugs](./endpoints-companies.md#slug-instability).

**See also**

- [next_cursor is opaque — this is the rule that bites](./api-pagination.md#cursor-opacity)
- [field_sources — per-field provenance](./ledger-provenance.md#field-sources)
- [402 — the licence, not the throttle](./api-errors.md#error-402)
- [Controlled vocabularies — never hard-code these](./api-parameters.md#filter-vocabularies)

---

## Where to go next

This file is part of **Start here**. Others in the same group:

- [Overview](./overview.md) — What the Job Opportunities API is, what it deliberately is not, and the ten minutes of reading that will save you the most time.
- [If you are a program](./for-agents.md) — This documentation has a plain-Markdown mirror with no JavaScript, no redirects and no browser required. Here is where it is and what is in it.
- [How to read this site](./conventions.md) — Three tiers on every concept, a copyable link on every section, and a rule about numbers that explains why almost nothing here is typed by hand.

Always useful:

- [index.md](./index.md) — the map of every file here
- [BUILD-A-SITE.md](./BUILD-A-SITE.md) — the paste-whole brief for building against this API
- [quickstart.md](./quickstart.md) — zero to a first authenticated response
- [api-errors.md](./api-errors.md) — every status code and whether to retry it
