# auth.md

You are an agent. **Jobherder** supports **agentic registration** for paid job-search API access: discover → register → buy credits → call API. Follow the steps in order.

Examples use real hosts: `https://jobherder.com/api/v1` (resource / Agent API) and `https://jobherder.com` (authorization server for agent API keys). Browser users also use Clerk OIDC at `https://jobherder.com/__clerk`.

**Searches are paid only. There are no free runs, no trial, and no free first search.** Every search consumes exactly one paid credit. Credits do not expire. Registration grants **0 credits**.

## Two products

| | Full search | Quick match |
|---|---|---|
| Endpoint | `POST /api/v1/searches` | `POST /api/v1/quick-searches` |
| You get | 7 matched roles | the single best current role |
| Per role | tailored CV + cover letter | tailored CV + cover letter |
| Typical time | about an hour | 3 to 8 minutes (up to 15 for a heavily searched profile) |
| Price | EUR 9.99 | 2.50 USDC |
| Paid with | card or USDC | USDC only |

Use the quick match when you want one answer quickly and cheaply; use the full
search when the candidate wants a shortlist to choose from. The quick match
searches a pre-harvested pool plus a small live top-up rather than running the
full discovery pass, which is where the time and the price difference come from.

## Credit packs

| Pack | Buys | Credits | Price | Notes |
|---|---|---|---|---|
| `pack1` | full search | 1 | EUR 9.99 | Single search |
| `pack3` | full search | 3 | EUR 24.99 | **Best value** (~EUR 8.33 per search vs EUR 29.97 for three singles) |
| `quick1` | quick match | 1 | 2.50 USDC | x402 only; no card price |

`pack3` is the recommended default when calling `POST /api/v1/checkout`.

**Credits are per product.** A quick credit cannot pay for a full search and a
full credit cannot pay for a quick match. Each endpoint advertises, and accepts,
only the price of the product it runs: paying the quick price at
`/api/v1/searches` is refused rather than credited.

## Step 1 — Discover

Discovery is two hops — you may have already done this.

A 401 from the Agent API carries:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://jobherder.com/.well-known/oauth-protected-resource"
```

### 1a. Fetch the Protected Resource Metadata

```http
GET https://jobherder.com/.well-known/oauth-protected-resource
```

```json
{
  "resource": "https://jobherder.com/api/v1",
  "resource_name": "Jobherder",
  "authorization_servers": ["https://jobherder.com"],
  "scopes_supported": ["searches:read", "searches:write", "checkout"],
  "bearer_methods_supported": ["header"]
}
```

### 1b. Fetch the Authorization Server metadata

```http
GET https://jobherder.com/.well-known/oauth-authorization-server
```

Read the `agent_auth` block in full. Important fields:

- `agent_auth.skill` — this document
- `agent_auth.register_uri` / `agent_auth.identity_endpoint` — registration
- `agent_auth.identity_types_supported` — `anonymous`, `service_auth`
- `agent_auth.credential_types_supported` — `api_key`
- `agent_auth.revocation_uri` — revoke keys via `DELETE /api/v1/keys`

## Step 2 — Pick a method

1. **You have the user's email** → `service_auth` (recommended).
2. **You have neither a user session nor an email yet** → `anonymous` still requires an email for delivery; use a mailbox you control.

Jobherder does not require ID-JAG / claim ceremonies for API keys. Skip claim unless `claim_endpoint` returns otherwise.

## Step 3 — Register

Surface `Jobherder` and scopes `searches:read`, `searches:write`, `checkout` to the user, then register.

### service_auth

```http
POST https://jobherder.com/api/v1/agent/identity
Content-Type: application/json

{"type":"service_auth","email":"user@example.com","name":"My Agent"}
```

Equivalent shorthand:

```http
POST https://jobherder.com/api/v1/register
Content-Type: application/json

{"email":"user@example.com","name":"My Agent"}
```

Success response (store `api_key` **once** — it is not shown again). Registration grants **0 credits**:

```json
{
  "user_id": "…",
  "email": "user@example.com",
  "api_key": "jbr_live_…",
  "key_id": "…",
  "key_prefix": "jbr_live_…"
}
```

If the email already belongs to a Jobherder web account (someone who signed up on the website), registration returns **409**. Registration is unverified, so issuing a key here would hand over that person's account. The owner creates one instead: sign in, open the dashboard, and use the **API keys** card. They then paste the key to the agent as a Bearer token. The 409 body carries `owner_action_url` pointing at that page.

### anonymous

Same endpoints with `"type":"anonymous"` and an email you control for delivery.

## Step 4 — Claim (not required)

Jobherder issues API keys at registration. `POST https://jobherder.com/api/v1/agent/identity/claim` returns `claim_not_required`. Proceed to payment.

## Step 5 — Buy search credits

```http
POST https://jobherder.com/api/v1/checkout
Authorization: Bearer jbr_live_…
Content-Type: application/json

{"pack":"pack3"}
```

Omit the body to default to `pack3`. Use `"pack":"pack1"` for a single credit. Promotion codes are not accepted on this endpoint.

```json
{
  "checkout_url": "https://checkout.stripe.com/…",
  "session_id": "cs_…",
  "pack": "pack3",
  "credits": 3,
  "amount_cents": 2499,
  "currency": "eur",
  "credits_available": 0
}
```

Open `checkout_url` (or present it to the user). After payment, poll credits:

```http
GET https://jobherder.com/api/v1/checkout
Authorization: Bearer jbr_live_…
```

Expect `credits_available` to increase once Stripe confirms payment. If the webhook is delayed, confirm with:

```http
GET https://jobherder.com/api/v1/checkout/success?session_id=cs_…
Authorization: Bearer jbr_live_…
```

List available packs on `GET /api/v1/checkout` under `packs` (includes `recommended: true` on `pack3`).

## Step 6 — Call the API

Send the API key on every request:

```http
Authorization: Bearer jbr_live_…
```

### Sequential searches

Only one search may run at a time per account. While a search is running, another `POST /api/v1/searches` returns **429**. This is not an error to retry aggressively — poll the running search until `terminal: true`, then start the next one.

### Start a search (consumes one paid credit)

`cfg_email` MUST match the registration email.

Optional header `Idempotency-Key` makes a retry safe: the same key replays the original response instead of consuming a second credit.

```http
POST https://jobherder.com/api/v1/searches
Authorization: Bearer jbr_live_…
Idempotency-Key: my-unique-key-1
Content-Type: multipart/form-data

profile: {"cfg_name":"…","cfg_email":"user@example.com","cfg_location":"Berlin, Germany","cfg_local_country":"Germany","cfg_location_preference":"Fully remote EU","cfg_work_style":"remote_only","cfg_want_remote_eu":"yes","cfg_want_remote_uk":"no","cfg_want_usa":"no","cfg_languages":["english"],"cfg_seniority_level":"Senior","cfg_target_titles":"Staff engineer, Platform engineer","cfg_industries":"technology"}
cv: <pdf|docx|doc|txt>   # or send JSON with cv_json instead of a file
```

Or JSON, with the CV either as structured data or as a file:

```json
{"profile": {…}, "cv_json": {"basics": {"name": "…"}, "work": […], "education": […], "skills": […]}}
```

`cv_json` follows the [JSON Resume](https://jsonresume.org) schema. Prefer it when you hold the CV as
data — you do not need to render a document. The alternative is `cv_base64` with `cv_type` of
`pdf`, `docx` or `txt`; plain text is accepted, so a CV you only have as text needs no conversion.

Whichever form you send, it is reduced to text and is the only source for the tailored CVs, which
never invent experience it does not contain. A CV yielding under 100 characters of text is rejected.

Success (**201**):

```json
{
  "ok": true,
  "run_id": "…",
  "status": "queued",
  "credits_remaining": 2,
  "order_id": "…"
}
```

Without a paid credit the API returns **402**:

```json
{
  "error": "payment_required",
  "message": "…",
  "credits_available": 0,
  "offers": [
    {
      "id": "pack3",
      "credits": 3,
      "amount": 2499,
      "currency": "eur",
      "label": "3 search credits",
      "recommended": true,
      "checkout_path": "/api/v1/checkout",
      "checkout_body": {"pack": "pack3"}
    }
  ],
  "docs": "https://jobherder.com/auth.md"
}
```

### Paying with x402 (USDC)

Instead of Stripe Checkout, you can pay per call with [x402](https://x402.org).
The same 402 carries a challenge in the base64 `PAYMENT-REQUIRED` header
(protocol v2) and, for v1 clients, in the response body under `accepts`.

Sign the authorization and retry the request with `PAYMENT-SIGNATURE` (v2) or
`X-PAYMENT` (v1). We verify and settle through the facilitator, add the credits,
and return a receipt in `PAYMENT-RESPONSE`. An API key is still required, since
credits belong to an account.

Send it to `POST /api/v1/searches` to pay for that one search, to
`POST /api/v1/quick-searches` to pay for one quick match, or to
`POST /api/v1/checkout` to top up credits without starting a run.

Prices: 11.50 USDC for one full-search credit, 28.75 USDC for three, and
2.50 USDC for one quick match.

Each resource accepts only its own price. A 2.50 USDC payment presented to
`/api/v1/searches` is rejected with `wrong_product` — it is a valid payment
for a different thing, and crediting it there would sell a 7-role search for
the price of a 1-role one.

### Start a quick match

Identical request body to `POST /api/v1/searches`, and status is read from the
same `GET /api/v1/searches?run_id=…`.

```http
POST https://jobherder.com/api/v1/quick-searches
Authorization: Bearer jbr_live_…
PAYMENT-SIGNATURE: <base64 x402 payload>
Content-Type: application/json

{"profile": {…}, "cv_base64": "…", "cv_type": "application/pdf"}
```

Success (**201**) reports which product ran, so a poller does not have to assume
a pack size:

```json
{
  "ok": true,
  "run_id": "…",
  "status": "queued",
  "plan": "agent_quick",
  "target_jobs": 1
}
```

> x402 settles on **Base mainnet** in USDC (asset
> `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`). Payments are real.

### Poll status and read matches

```http
GET https://jobherder.com/api/v1/searches?run_id=<run_id>
Authorization: Bearer jbr_live_…
```

Poll until `run.terminal` is `true`. On terminal success, `matches` lists each role:

```json
{
  "run": {
    "run_id": "…",
    "status": "completed",
    "terminal": true,
    "progress": 100,
    "stage": "done",
    "user_message": "…"
  },
  "matches": [
    {
      "title": "…",
      "company": "…",
      "url": "https://…",
      "score": 0.92,
      "rank": 1,
      "source": "…",
      "match_reason": "Why this candidate fits the role",
      "delivered_at": "…"
    }
  ],
  "documents_note": "Each match carries its tailored CV and cover letter as markdown under documents. The same documents are emailed as PDFs to the account address."
}
```

Every match carries `documents.cv_markdown` and `documents.cover_letter_markdown`, each written for that specific role. The same documents are emailed as PDFs to the registered address. An agent never needs the inbox.

## Step 7 — Manage keys

Create an additional key (Bearer API key or signed-in session):

```http
POST https://jobherder.com/api/v1/keys
Authorization: Bearer jbr_live_…
```

Revoke:

```http
DELETE https://jobherder.com/api/v1/keys
Authorization: Bearer jbr_live_…
Content-Type: application/json

{"key_id":"<uuid>"}
```

## Canonical agent flow

1. `POST /api/v1/register` → store `api_key` (0 credits)
2. `POST /api/v1/checkout` with `{"pack":"pack3"}` → human pays at `checkout_url`
3. Poll `GET /api/v1/checkout` until `credits_available > 0` (or call `/api/v1/checkout/success?session_id=…`)
4. `POST /api/v1/searches` — one credit per call; with `pack3` you can run up to 3 searches, **one at a time**
5. Poll `GET /api/v1/searches?run_id=…` until `terminal: true` → read `matches`

With a wallet, steps 2 and 3 disappear: call `POST /api/v1/searches` or
`POST /api/v1/quick-searches` unpaid, sign the challenge it returns, and retry
the same call with `PAYMENT-SIGNATURE`.

## MCP server

Everything above is also available as MCP tools over Streamable HTTP at
`https://jobherder.com/mcp`, for clients that speak MCP rather than raw HTTP.

| Tool | Auth | Does |
|---|---|---|
| `get_pricing` | none | Packs, prices, and x402 terms |
| `register_agent` | none | Creates an account and returns an API key |
| `start_job_search` | API key + credit or x402 | Full search: 7 roles from a profile and CV |
| `quick_job_search` | API key + credit or x402 | Quick match: the single best role, in minutes |
| `get_search_status` | API key | Progress, then the matched roles. Covers both |

Pass the API key as a bearer token on the MCP connection. Example client entry:

```json
{ "mcpServers": { "jobherder": { "url": "https://jobherder.com/mcp", "headers": { "Authorization": "Bearer jbr_live_…" } } } }
```

### Paying without leaving MCP

`start_job_search` and `quick_job_search` speak the x402 MCP transport.
Called with no credit, either returns `isError: true` with the
`PaymentRequired` object in `structuredContent` (and as JSON in
`content[0].text`), naming the resource `mcp://tool/<tool name>` and quoting
only that tool's price. Sign the EIP-3009 authorization and repeat the
same call with the payload in `_meta["x402/payment"]`; the search starts once
settlement succeeds and the receipt comes back in
`_meta["x402/payment-response"]`. No checkout, no separate purchase step.

## Profile fields

See OpenAPI: https://jobherder.com/openapi.json

| Field | Notes |
|---|---|
| cfg_name | Candidate display name |
| cfg_email | Must match registration email |
| cfg_location | e.g. `Berlin, Germany` |
| cfg_location_preference | Location preference (free text) |
| cfg_work_style | `remote_only` \| `mix` |
| cfg_want_remote_eu / _uk / usa | `yes` \| `no` |
| cfg_languages | Array; English always included |
| cfg_seniority_level | Band, e.g. `Director` or `Manager – Head of Department` |
| cfg_target_titles | Comma-separated titles |
| cfg_local_country | ISO country name, e.g. `Germany` |
| cfg_uk_onsite | `yes` \| `no` (default no) |
| cfg_industries | Sectors, not job functions |

## Discovery links

- Protected resource: https://jobherder.com/.well-known/oauth-protected-resource
- Authorization server: https://jobherder.com/.well-known/oauth-authorization-server
- API catalog: https://jobherder.com/.well-known/api-catalog
- MCP server: https://jobherder.com/mcp
- OpenAPI: https://jobherder.com/openapi.json
- Skills: https://jobherder.com/.well-known/agent-skills/index.json
- Agent overview: https://jobherder.com/agents
- Health: https://jobherder.com/api/v1/health
- LLM summary: https://jobherder.com/llms.txt

Support: support@jobherder.com
