# auth.md

You are an agent. **Jobero** 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://jobero.ai/api/v1` (resource / Agent API) and `https://jobero.ai` (authorization server for agent API keys). Browser users also use Clerk OIDC at `https://jobero.ai/__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 |
| 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://jobero.ai/.well-known/oauth-protected-resource"
```

### 1a. Fetch the Protected Resource Metadata

```http
GET https://jobero.ai/.well-known/oauth-protected-resource
```

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

### 1b. Fetch the Authorization Server metadata

```http
GET https://jobero.ai/.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.

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

## Step 3 — Register

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

### service_auth

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

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

Equivalent shorthand:

```http
POST https://jobero.ai/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 Jobero web account (signed up on the website), registration returns **409**. The human must sign in and create a key at `POST /api/v1/keys` instead.

### anonymous

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

## Step 4 — Claim (not required)

Jobero issues API keys at registration. `POST https://jobero.ai/api/v1/agent/identity/claim` returns `claim_not_required`. Proceed to payment.

## Step 5 — Buy search credits

```http
POST https://jobero.ai/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://jobero.ai/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://jobero.ai/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://jobero.ai/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_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":"mid","cfg_industries":"technology"}
cv: <pdf|docx|doc|txt>
```

Or JSON with `cv_base64` + `cv_type`.

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://jobero.ai/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://jobero.ai/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://jobero.ai/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": "Tailored CV and cover letter PDFs are emailed to the account email; document downloads over the API are not available yet."
}
```

Tailored CV and cover letter PDFs are emailed to the registered address. Document downloads over the API are not available yet.

## Step 7 — Manage keys

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

```http
POST https://jobero.ai/api/v1/keys
Authorization: Bearer jbr_live_…
```

Revoke:

```http
DELETE https://jobero.ai/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://jobero.ai/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": { "jobero": { "url": "https://jobero.ai/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://jobero.ai/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` \| `hybrid` |
| cfg_want_remote_eu / _uk / usa | `yes` \| `no` |
| cfg_languages | Array; English always included |
| cfg_seniority | Target seniority |
| cfg_industries | Target industries |

## Discovery links

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

Support: support@jobero.ai
