# auth.md: Jobreach access for AI agents

You are an agent. Jobreach (https://jobreach.app) finds openings for job seekers in India, makes a one-page resume for each from
their own CV, and drafts the email to the person who posted the job, in their own Gmail; they press Send.

This service supports **agentic registration** with one method, `service_auth`: you name the person by their email,
they open a link on jobreach.app, sign in and type the six-digit code you show them, and you get a key that sends
them hiring posts. Discover, register, hand off the code, poll, then call the API. Follow the steps in order.

- Resource server: https://jobreach.app/api (the intake API)
- Authorization server: https://jobreach.app
- Scope: `intake`: send the person hiring posts and read the key's own status. The key can't read their resume,
  their emails or their Gmail, and it can't send anything.

## Step 1: Discover

- Protected Resource Metadata: https://jobreach.app/.well-known/oauth-protected-resource (also named in the
  `WWW-Authenticate: Bearer resource_metadata="..."` header of the intake API's 401s).
- Authorization Server metadata: https://jobreach.app/.well-known/oauth-authorization-server. Its `agent_auth` block gives
  `identity_endpoint` (https://jobreach.app/api/agent/identity) and `claim_endpoint` (https://jobreach.app/api/agent/identity/claim); `token_endpoint`,
  `revocation_endpoint` and `jwks_uri` are standard OAuth fields.

## Step 2: Pick a method

- You have the person's email: `service_auth`. The only method here.
- An ID-JAG (`identity_assertion` with `urn:ietf:params:oauth:token-type:id-jag`) isn't accepted
  (`identity_assertion_not_enabled`), and neither is `anonymous` (`anonymous_not_enabled`): a key always belongs
  to a person who said yes.
- The protocol's first draft named this path `identity_assertion` with `assertion_type: verified_email` and the
  email as `assertion`. That shape is taken too and handled exactly like `service_auth`.

## Step 3: Register

```http
POST https://jobreach.app/api/agent/identity
Content-Type: application/json

{"type": "service_auth", "login_hint": "person@example.com", "agent_name": "Your name for yourself"}
```

`agent_name` is optional. The person sees it, marked as your own words; at most 60 characters.

```json
{
  "registration_id": "reg_...",
  "registration_type": "service_auth",
  "claim_url": "https://jobreach.app/api/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-10-24T11:00:00Z",
  "post_claim_scopes": ["intake"],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://jobreach.app/agent/claim?attempt=cla_...",
    "interval": 5
  }
}
```

Keep `claim_token` secret. Jobreach emails nobody, and the answer is the same whether or not the address has an
account. At most 5 requests an hour from one network address.

## Step 4: Claim ceremony

### 4a. The ceremony materials

You have them: the `claim` block above. A code lasts 10 minutes and the request an hour. For a fresh code (at most 5
per request), ask; the previous link stops working:

```http
POST https://jobreach.app/api/agent/identity/claim
Content-Type: application/json

{"claim_token": "clm_...", "email": "person@example.com"}
```

The answer carries a `claim_attempt` block shaped like `claim`.

### 4b. Hand off to the person

Show the link and the code in one message, for example: "To let me send hiring posts to your Jobreach, open <link>,
sign in, and type the code <code>." Only the account with that email can answer. Someone who hasn't finished setting
up Jobreach (it includes agreeing to the privacy notice) is asked to do that first; then ask for a fresh code. The
person can also say no.

### 4c. Poll for completion

```http
POST https://jobreach.app/api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
```

At most every 5 seconds. While waiting: `authorization_pending`. Too fast: `slow_down`. The person said no:
`access_denied`. The request ran out: `expired_token` (register again). Once they allow it:

```json
{
  "access_token": "jri_...",
  "token_type": "Bearer",
  "scope": "intake",
  "identity_assertion": "eyJ...",
  "assertion_expires": "2026-11-23T10:00:00Z"
}
```

The access token is a Jobreach intake key: it has no `expires_in` and works until it is turned off. The claim token
works once: keep the answer. If the key is lost, use the assertion (Step 5).

## Step 5: Exchange the assertion

Only to replace a lost key, at most once a minute (`slow_down` otherwise). The assertion is good for 30 days, and
only while the connection lasts.

```http
POST https://jobreach.app/api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJ...
```

You get a new `access_token`; the key you had stops working. Assertions are ES256 JWTs (`typ: oauth-id-jag+jwt`,
`iss` and `aud` https://jobreach.app); the public key is at https://jobreach.app/api/agent/jwks.json.

## Step 6: Use the access token

Send it on every request: `Authorization: Bearer jri_...`

- `POST https://jobreach.app/api/intake/leads`: up to 25 hiring posts per call, 100 a day. Each becomes the person's private lead;
  Jobreach reads it, checks the company, scores it against their work, and for a good fit drafts the email.
- `GET https://jobreach.app/api/intake/me`: whose key it is, and today's emails.
- `DELETE https://jobreach.app/api/intake/key`: turn the key off.

Send only what you saw in the post. Don't extract email addresses, verify companies, write emails or create drafts:
Jobreach does that, and writes only to an address the post or the company published. The emails for posts you send
always wait in the person's Gmail drafts for them to read: Jobreach never sends those on its own, even when the person
has turned on Send for me. Format and limits: https://jobreach.app/agents and https://jobreach.app/openapi/agent-api.json.

## Errors

| Code | Where | What to do |
|---|---|---|
| `invalid_request` | /agent/identity, /agent/identity/claim | Fix the body: `type` service_auth and a valid email as `login_hint`. |
| `anonymous_not_enabled`, `identity_assertion_not_enabled` | /agent/identity | Use service_auth. |
| `rate_limited` (429) | /agent/identity | Wait an hour. |
| `invalid_claim_token` | /agent/identity/claim | Register again. |
| `claimed_or_in_flight` | /agent/identity/claim | The person already answered: poll the token endpoint. |
| `claim_expired` | /agent/identity/claim | Register again. |
| `authorization_pending` | /oauth2/token | Keep polling, every 5 seconds. |
| `slow_down` | /oauth2/token | Poll less often, or trade the assertion at most once a minute. |
| `access_denied` | /oauth2/token | The person said no. Don't ask again unless they tell you to. |
| `expired_token` | /oauth2/token | Register again. |
| `invalid_grant` | /oauth2/token | The connection ended or the claim token was used: register again. |
| `rate_limited` (429) | /oauth2/*, /agent/identity/claim | Too many calls from your address: wait a few minutes. |
| `unsupported_grant_type` | /oauth2/token | Use one of the two grants above. |
| 401 | /intake/* | The key is wrong or turned off: register again. |
| 402 | /intake/leads | The person's free week is over and they have no pass. |
| 429 | /intake/leads | Today's 100 posts are used. |

## Revocation

- You: `POST https://jobreach.app/api/oauth2/revoke` with `token=jri_...` (RFC 7009; always 200), or `DELETE https://jobreach.app/api/intake/key`.
- The person: Profile, Your agent, Turn off; or Start over, or deleting their account.

Either way the connection ends: the assertion and the claim token get nothing more, and you register again to
reconnect. (Stricter than the protocol, where revoking an access token leaves the assertion valid.)

## Without the code

A person can also make a key themselves under Profile, Your agent, and give it to you. It works the same way.

## More

- Guide for people and agents: https://jobreach.app/agents
- API description: https://jobreach.app/openapi/agent-api.json
- API catalog: https://jobreach.app/.well-known/api-catalog
- Pricing (first week free, then Rs 200 for 30 days): https://jobreach.app/pricing
- Privacy: https://jobreach.app/privacy
- Questions: support@jobreach.app
